본문 바로가기

:: 프로그래밍 ::/수업 내용

2024.03.06 C#) 구조체/클래스 차이, OOP, 객체지향 설계 5대원칙, 다형성, 추상화, 인터페이스

 

구조체와 클래스의 차이(C#)

 

이전 C++에서 구조체와 클래스의 차이는 디폴트로 구성 되어있는 접근자(public/private)의 차이였다.(24.02.27 참조)

그렇다면 C#에서는 어떤 식으로 차이가 날까?

 

클래스가 값을 다루는 방식을 보면서 접근하면 이해하기 편할 것이다.

class RefType
{
    public int value;
    public RefType(int v) { value = v; }
}

static void Main(string[] args)
{
    RefType refType1 = new RefType(10);
    RefType refType2 = refType1;
    refType2.value = 20;

    Console.WriteLine(refType1.value);
    Console.WriteLine(refType1.value);
}

 

A클래스를 생성하여 B클래스에 A클래스를 복사한 뒤, B클래스의 값을 변경하면 어떻게 될까?

 

정답은 해당 A클래스의 값과 B클래스의 값은 동시에 B클래스의 값으로 바뀐다.

그렇다. 얕은 복사관계이다.(2024.03.05)

클래스는 복사를 할 때 참조 형식을 통해 값을 전달한다.

 

그렇다면 구조체는... 역시나 깊은 복사관계이다.

이것이 구조체와 클래스의 차이이다.

 

구조체는 값의 형식으로 복사가 되고, 클래스는 참조형식으로 복사가 된다.

 


 

 

객체지향프로그래밍 (OOP)

 

캡슐화(Encapsulation) : 객체를 상태와 기능으로 묶는다. 객체의 내부상태와 기능을 숨기고 허용한 상태와 기능만을 엑세스 허용.

상속(Inheritance) : 부모클래스의 모든 기능을 가지는 자식클래스를 만든다.

추상화(Abstract) : 관련 특성 및 엔티티의 상호작용을 클래스를 모델링하여 시스템의 추상적 표현을 정의.

다형성(Polymorphism) : 부모클래스의 함수를 자식클래스에서 재정의하여 자식클래스의 다른 반응을 구현.

 

캡슐화는 이전에 배웠던 정보은닉과 매치가 된다. 하나로 묶으면 해당 클래스의 특성도 드러나게 되지만, 내부적인 기능을 숨기기 위하여 사용하는 것이 중요하다.

상속은 직접 서브클래스를 생성한다면 어떤 느낌인지 알 것이다. 이미 어제 다루었기에 해당 부분은 패스한다.


추상화

 

클래스를 정의 할 때, 구체화 시킬 수 없는 기능을 추상화를 하여 정의하는 것이다.

 

하나 이상의 추상함수를 포함하는 클래스이며, 추상적인 표현을 정의하는 경우 자식클래스에서 구체화시켜 구현할 것을 염두해두고 추상화시킨다.

 

추상클래스를 상속하는 자식클래스가 추상함수를 재정의하여 구체화 한 경우 사용가능하다.

abstract class Item     
{
    public abstract void Use(); 
}

class Potion : Item
{
    public override void Use()
    {
        Console.WriteLine("포션 사용");
    }
}

 

특히 abstract는 함수를 반드시 서브클래스에서 구현을 해주어야 한다. 이건 필수사항이다.

구체화한 서브클래스에서 만든 함수는 인스턴스 생성 후 사용이 가능하다.

 

그리고 추상클래스로는 인스턴스를 생성 할 수 없다! 그래서 해당 클래스로 만들 경우 업캐스팅이 강제가 된다.

업캐스팅 : 서브클래스에 있는 객체가 슈퍼클래스 타입으로 형변환 되는 것.

다운캐스팅에 관련해서는 시간이 나면 다시 정리하는 걸로 하고, 추상클래스는 인스턴스를 생성하기 위해 업캐스팅이 강제된다는 것까지만 기억해두자.


다형성

 

객체나 기능이 상황에 따라서 여러가지 다른 형태로 가질 수 있는 성질을 말한다.

부모의 함수는 물려받을 수 있으나, 그걸로 다른 함수처럼 사용하는 것이 가능하다는 것이다.

다형성은 가상함수와 오버라이딩의 개념을 가지고 있는데

 

가상(virtual) 함수 : 슈퍼클래스의 함수 중 서브클래스에 의해서 재정의할 수 잇는 함수를 지정

오버라이딩(override) : 부모클래스의 가상함수를 같은 함수이름과 같은 매개변수로 재정의해서 자식만의 반응을 구현

 

class Skill
{
    public virtual void Excute()           // virtual 가상
    {
        Console.WriteLine("스킬 재사용 대기시간");
    }
}
class FireBall : Skill
{
    public override void Excute()  //오버라이딩
    {
        base.Excute();                     //base : 부모클래스를 가리킴.
        Console.WriteLine("화염구 발사.");
    }
}

 

추상형(abstract) 클래스 와는 달리 가상(virtual)형 클래스의 경우에는 서브클래스를 반드시 구현해야할 필요는 없다.

 


 

객체지향설계의 5대원칙

 

단일책임의 원칙(Single Responsiblity Principle) : 모든 클래스는 각각 하나의 책임만을 가져야 한다. 클래스는 그 책임을 완전히 그 책임을 캡슐화해야 한다.

개방-폐쇄의 원칙(Open-Closed Principle) : 코드 개발에 있어서 확장에는 열려있고, 수정에는 닫혀있어야 한다.

리스코프 치환 원칙(Liskov Substitution Principle) : 자식 클래스는 언제나 자신의 부모클래스를 대체할 수 있어야 한다.

따라서, 부모클래스 자리에 자식클래스를 넣어도 계획대로 동작해야 한다.

인터페이스 분리 원칙(Interface Segregation Principle) : 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스가 낫다.

의존 역전 원칙(Dependency Inversion Principle) : 의존관계를 맺을 때는 변화가 잦은 것 보다, 변화가 없는 것에 의존하라.

구체적인 클래스/인터페이스 보단 추상클래스와 관계를 맺는 것이다.

 

 

통상 SOLID로 불리는 5대원칙은 객체지향설계를 할 때 지켜주어야할 규범이다.

 

예로 들자면, 사회규범에 비유하자면 관습 정도라 생각한다.

'도덕 규범'보다는 엄중하고 '법 규범'보단 가벼운 느낌 정도로 이 원칙을 지켜주지 못하면 업계에서 퇴출까진 당하진 않겠지만, 오랫동안 지켜주지 않으면 많은 규탄을 받을지 모르니 빠르게 이해하고 최대한 지켜주도록 하자.

 

S. 단일책임의 원칙.

한 클래스가 여러가지를 책임져서는 안된다. 클래스에 수정을 요하는 경우가 빈번하게 생길텐데, 해당 클래스에게 책임을 점점 더 쥐어주게 되는 상황이 충분히 발생할 수 있다. 이 점을 염두해두고 클래스를 작성해주자.

 

O. 개방-폐쇄의 원칙.

수정을 할 때 가능하면 기존에 작성했던 클래스는 건드리지 말아햐 한다는 것이다. 해당 클래스는 다른 클래스와 연동이 이미 일어나고 있을 가능성이 크기 때문에, 수정을 하지 말아야하는 것이다. 그렇다면 어떻게 해결하느냐? 확장을 통해 변경하는 것이다. 그래서 확장에는 개방적(Open), 수정에는 폐쇄적(Close)이어야 한다는 것이다.

 

L. 리스코프 치환 원칙.

서브클래스는 언제나 자신의 부모클래스를 대체할 수 있어야 한다. 따라서 자식클래스로 인스턴스를 생성한다면 부모클래스의 역할도 충실히 이행되어야 하는 것이다.

 

I. 인터페이스 분리의 원칙

하나의 일반적인 인터페이스보단 여러가지의 구체적인 인터페이스가 좋다는 거다. 다시 말해 사용하지 않는 인터페이스는 구현하지 말라는 이야기이다. 사실 '단일책임의 원칙'과 같은 맥락의 이야기이기도 하다.

다시 말해 '단일책임은 클래스에게 단일책임'을, '리스코프치환은 인터페이스에게 단일책임'을 씌우는 것이다.

 

D. 의존역전의 원칙. 

클래스/인터페이스간에 의존이 크다보면 빈번하게 수정되는 클래스에서 문제가 발생할 수 밖에 없다. 그래서 변화가 없거나 적은 추상클래스와 관계를 맺어주는 것이 좋다는 것이다. 클래스가 레이어별로 의존하게 되어있는 상태라면 레이어 사이에 추상클래스를 넣어 의존관계에 안정성을 가져다 주는 것도 좋은 선택이다.

 


 

인터페이스

 

추상클래스의 일종으로 특징이 동일하다. 함수에 대한 선언만 하고 이를 포함하는 클래스에서 구체화한 다음에 사용한다.

 

그렇다면 추상클래스와 무엇이 다른걸까?

인터페이스는 무려 다중상속을 허용한다. 그리고 변수는 인터페이스에 선언 될 수 없다.

클래스는 단일책임의 원칙에 따라 책임을 하나씩 지게 되는데, 인터페이스가 나머지 역할을 행해준다.

 

클래스가 '종류'에 해당한다면, 인터페이스는 가볍게 접근하면 해당하는 것에 '특성'이라고 볼 수 있다.

public interface ISleep
{
    void Sleep();
}

abstract class Animal
{
    public abstract void Cry();
}

abstract class Mechine
{
    public abstract void PowerOn();
}
class Cat : Animal, ISleep
{
    public override void Cry()
    {
        Console.WriteLine("야옹야옹");
    }
    
    void Sleep();
}

class Robot : Mechine, ISleep
{
    public override void PowerOn()
    {
        Console.WriteLine("전원이 켜졌습니다.");
    }
    void Sleep();
}

 

 

우는 것은 동물만 한다 가정한다면, 고양이는 동물은 우는 것에 상속받아 고양이는 울 수 있게 된다.

로봇은 전원은 켤 수 있지만 울지는 못하는 것이다. 반대로 고양이는 전원을 키지 못한다.

 

하지만, 고양이와 로봇 모두 잠들 수 있다는 '특성'을 나타내는 것이다.

그래서 둘 다 '잠드는 것'을 상속받아 사용할 수 있는 것이다.

 

 

다시 돌아와서 클래스는 부모클래스를 상속받은 이후, 인터페이스의 추가적인 상속을 받을 수 있다.

물론 부모클래스를 상속받지 않더라도 인터페이스만으로 상속이 가능하다.

 

결국 인터페이스의 목적은 상속만으로 해결하지 못하는 상황들을 인터페이스 상속을 통해 해결할 수 있음을 의미한다.