728x90
CS
상속과 합성
- 프로그래밍을 할 때 가장 신경 써야 할 것 중 하나가 바로 코드 중복을 제거하여 재사용 함으로써 변경, 확장을 용이하게 만드는 것이다.
- 상속과 합성은 객체지향 프로그램에서 가장 널리 사용되는 코드 재사용 기법이다.
상속(Inheritance) 합성(Composition)
부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결 | 두 객체 사이의 의존성은 런타임에 해결 |
is-a 관계 | has-a 관계 |
부모 클래스의 구현에 의존 결합도가 높다 | 구현에 의존하지 않는다. |
내부에 포함되는 객체의 구현이 아닌 인터페이스에 의존한다. | |
클래스 사이의 정적인 관계 | 객체 사이의 동적인 관계 |
부모 클래스 안에 구현된 코드 자체를 물려 받아 재사용 | 포함되는 객체의 퍼블릭 인터페이스를 재사용 |
상속(Inheritance) 이란
- 상속은 객체 지향 4가지 특징 중 하나로서 클래스 기반의 프로그래밍에서 가장 먼저 배우는 개념이다.
- 클래스 상속을 통해 자식 클래스는 부모 클래스의 자원을 물려 받게 되며, 부모 클래스와 다른 부분만 추가하거나 재정의함으로써 기존 코드를 쉽게 확장할 수 있다. 그래서 상속 관계를 is-a 관계라고 표현하기도 한다.
- 상속을 사용하는 경우는 명확한 is-a 관계에 있는 경우, 그리고 상위 클래스가 확장할 목적으로 설계되었고 문서화도 잘되어 있는 경우에 사용하면 좋다.
- 그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다. 또한 상속 관계는 컴파일 타임에 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다. 따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하게 된다면 모든 조합별로 클래스를 하나하나 추가해주어야 한다. 이것을 클래스 폭발 문제라 한다.
합성(Composition) 이란
- 합성은 또 다른 말로 조합이나 그냥 콩글리쉬로 컴포지션이라고 불린다.
- 합성 기법은 기존 클래스를 상속을 통한 확장하는 대신에, 필드로 클래스의 인스턴스를 참조하게 만드는 설계이다. 예를들어 서로 관련없는 이질적인 클래스의 관계에서, 한 클래스가 다른 클래스의 기능을 사용하여 구현해야 한다면 합성의 방식을 사용한다고 보면 된다.
- 이 방식을 포워딩(forwarding)이라고 하며 필드의 인스턴스를 참조해 사용하는 메소드를 포워딩 메소드(forwarding method)라고 부른다.
- 그래서 클래스간의 합성 관계를 사용하는데 다른 말로 Has-A 관계라고도 한다. 객체 지향에서 다른 클래스를 활용하는 기본적인 방법이 바로 합성을 활용하는 것이다.
- 합성을 이용할 때 곡 클래스 뿐만 아니라 추상 클래스, 인터페이스로도 가능하다.
상속의 문제점
- 결합도가 높아진다.
- 결합도는 하나의 모듈이 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 의존 정도를 뜻한다.
- 객체 지향 프로그래밍에서는 결합도는 낮을수록, 응집도는 높을수록 좋다. 그래서 추상화에 의존함으로써 다른 객체에 대한 결합도는 최소화하고 응집도를 최대화하여 변경 가능성을 최소화 할 수 있다.
- 하지만 상속을 하게 되면 부모 클래스와 자식 클래스의 관계가 컴파일 시점에 관계가 결정되어 결합도가 당연히 높아질 수 밖에 없다.
- 컴파일 시점에 결정되는 관계는 유연성을 상당히 떨어뜨리고, 실행 시점에 객체의 종류를 변경하는 것이 불가능하여 유기적인 다형성 및 객체지향 기술을 사용할 수 없다.
- 불필요한 기능 상속
- 부모 클래스에 메소드를 추가했을 때, 자식 클래스에는 적합하지 않는 메소드가 상속되는 문제이다. 물론 메소드를 구현하고 빈칸으로 놔두거나, 클래스를 분리하고 분리하여 해결은 할 수 있지만 결국 복잡해질 뿐이다.
- 물론 이는 인터페이스로 따로 implements하면서 해결할 수 도 있다.
- 부모 클래스에 메소드를 추가했을 때, 자식 클래스에는 적합하지 않는 메소드가 상속되는 문제이다. 물론 메소드를 구현하고 빈칸으로 놔두거나, 클래스를 분리하고 분리하여 해결은 할 수 있지만 결국 복잡해질 뿐이다.
- 부모 클래스의 결함이 그대로 넘어온다.
- 만일 상위 클래스에 결함이 있다고 하면, 이를 상속을 하게 되면 부모 클래스의 결함도 자식 클래스에게 넘어오게 된다.
- 결국 자식 클래스에서 아무리 구조적으로 잘 설계하였다 하더라도 애초에 부모 클래스에서 결함이 있기 때문에 자식 클래스도 문제가 터지게 된다.
- 부모 클래스와 자식 클래스의 동시 수정 문제
- 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해, 부모 클래스를 변경할 때 자식 클래스도 함꼐 변경해야 하는 문제를 말한다.
- 메서드 오버라이딩의 오동작
- 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는 문제이다.
- 부모의 public 메소드는 외부에서 사용하도록 노출한 메소드이다. 그런데 상속을 하게 된다면, 자식 클래스에서도 부모 클래스의 public 메소드를 이용할때 의도하지 않는 동작을 수반할 수 있게 될 수 있다.
- 이는 캡슐화를 위반하였다고 하기도 한다.
- 불필요한 인터페이스 상속 문제
- 클래스 폭발
- 상속을 남용하게 되면, 새롭게 만든 클래스에 하나의 기존의 기능을 연결하기 위해 상속을 하게 될것이고, 또 다시 새롭게 만든 클래스에 기능을 연결하기 위해 상속을 하고, 이렇게 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발(class explosion) 문제 도는 조합의 폭발(combinational explosion) 문제라고 부른다.
- 클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현과 강하게 결합되도록 강요하는 상속의 근본적인 한계 떄문에 발생한다.
- 상속 관계는 컴파일 타임에 결정되고 고정되기 떄문에 코드를 실행하는 도중에는 변경할 수 없다. 그래서 부모와 자식 클래스의 구현이 강하게 결합된 상속 관계 상태에서, 다양한 조합이 필요한 상황이 오면 결국 유일한 해결 방법은 조합의 수 만큼 새로운 클래스를 추가해 상속하는 것 뿐이다.
- 클래스 폭발 문제는 새로운 기능을 추가할 떄 뿐만 아니라 기능을 수정할 떄에도 동일하게 발생한다.
합성을 사용해야 하는 이유
- 합성은 구현에 의존하지 않는 점에서 상속과 다르다.
- 합성을 이용하면 객체의 내부는 공개되지 않고 인터페이스를 통해 코드를 재사용하기 때문에, 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경하여 결합도를 낮출 수 있기 때문이다.
- 상속과 합성은 재사용 기법으로 많이 쓰이는 방법이지만, 대체로 상속을 최소화하고 합성을 사용하기를 권한다.
- 상속은 자식 클래스 정의에 부모 클래스의 이름을 덧붙이는 것(extends)만으로 코드를 재사용할 수 있으며 쉽게 확장할 수 있다. 그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세히 알아야 하기 때문에 자식과 부모 사이의 결합도가 높아질 수 밖에 없다.
- 결과적으로 상속은 코드를 재사용하기 쉬운 방법이기 하지만 결합도가 높아지는 치명적인 단점이 있다.
- 또한 상속 관계는 클래스 사이의 정적인 관계인데 비해 합성 관계는 객체 사이의 동적인 관계이다. 코드 작성 시점에 결정된 상속 관계는 변경이 불가능하지만, 합성 관계는 실행 시점에 동적으로 변경할 수 있기 때문이다. 그래서 합성을 사용하고 인터페이스 타입을 사용한다면 런타임시에 외부에서 필요한 전략에 따라 교체하며 사용할 수있으므로 좀 더 유연한 설계를 할 수 있다.
참조
💠 상속을 자제하고 합성(Composition)을 이용하자
C++
iostream
- C++에서 표준 입출력에 필요한 것들을 포함하고 있다.
- ex) std::cout이나 std::endl
- C언어에서의 stdio.h와 비슷하다고 생각하면된다.
namespace(이름 공간)
- cout 앞에 붙어 있는 std의 정체는 C++ 표준 라이브러리의 모든 함수, 객체등이 정의된 이름 공간(namespace) 입니다.
- 이름 공간은 말 그대로 어떤 정의된 객체에 대해 어디 소속인지 지정해주는 것과 동일하다. 코드의 크기가 늘어남에 따라, 혹은 다른 사람들이 쓴 코드를 가져다 쓰는 경우가 많아지면서 중복된 이름을 가진 함수들이 많아졌다. 따라서 C++에서 이를 구분하기 위해, 같은 이름이라도, 소속된 이름 공간이 다르면 다른것으로 취급한다.
std::cout
- 위의 경우 std 라는 이름 공간에 정의되어 있는 cout 을 의미한다. 만약에 std:: 없이 그냥 cout 이라고 한다면 컴파일러가 cout 을 찾지 못한다.
- using namespace std; 와 같이 어떠한 이름 공간을 사용하겠다라고 선언하는 것은 권장되지 않는다. 왜냐하면 std에 이름이 겹치는 함수를 만들면 오류가 발생하기 때문이다. 권장하는 방식은 using namespace std; 같은 것은 사용하지 않고, std:: 를 직접 앞에 붙여서 std의 이름공간의 함수이다를 명시해주는 것이 좋다. 또한, 여러분이 작성하는 코ㄷ는 여러분 만의 이름 공간에 넣어서 혹시 모를 이름 충돌로 부터 보호하는 것이 좋다.
- cout 은 ostream 클래스의 객체로 표준 출력을 담당하고 있다.
이름 없는 이름 공간
- C++에서는 이름 공간에 굳이 이름을 설정하지 않아도 된다. 이 경우 해당 이름 공간에 정의된 것들은 해당 파일 안에서만 접근할 수 있다. 이 경우 마치 static 키워드를 사용한 것과 같은 효과를 낸다.
728x90
'Study > TIL(Today I Learned)' 카테고리의 다른 글
24.08.26 CS, C++ (0) | 2024.08.26 |
---|---|
24.08.23 CS (0) | 2024.08.23 |
24.08.21 CS (0) | 2024.08.21 |
24.08.20 CS, 디자인 패턴 (0) | 2024.08.20 |
24.08.19 네트워크 (0) | 2024.08.19 |