14장 정리

객체는 협력을 위해 존재하며, 객체지향 설계의 목표는 적절한 책임을 수행하는 객체들의 협력을 기반으로

결합도가 낮고 재사용 가능한 코드 구조를 창조하는 것이다.
객체지향 패러다임의 장점은 설계를 재사용할 수 있다는 것이다.
하지만 재사용을 공짜로 얻어지지 않으며, 객체들의 협력 방식을 일관성 있게 만들어야 한다.
일관성 있는 협력 패턴을 적용하면 코드 전체를 이해하기 쉽고 직관적이며 유연해진다.


설계에 일관성 부여하기

유사한 기능을 서로 다른 방식으로 구현해서는 안된다.
객체지향에서 기능을 구현하는 유일한 방법은 객체 사이의 협력을 만드는 것이다.
따라서 유지보수 가능한 시스템을 구축하는 첫걸음은 협력을 일관성 있게 만드는 것이다.
협력을 일관성 있게 만들기 위해 다음과 같은 기본 지침을 따르는 것이 도움이 된다.

변하는 개념을 변하지 않는 개념으로부터 분리하라.

변하는 개념들을 개별적인 객체로 분리하고, 이 객체들과 일관성 있게 협력하기 위해 타입 계층을 구성한다.
타입 계층을 클라이언트로 분리하기 위해 역할을 도입하고, 최종적으로 역할을 추상 클래스와 인터페이스로 구현한다.
이렇게 하면 결과적으로 변하는 개념을 별도의 서브타입으로 분리한 후 이 서브타입들을 클라이언트로부터 캡슐화할 수 있다.

변하는 개념을 캡슐화하라.

핵심은 훌륭한 추상화를 찾아 추상화에 의존하도록 만드는 것이다.
타입을 캡슐화하고 낮은 의존성을 유지하기 위해서는 이전 장들에서 살펴본 설계 원칙들을 적용하면 도움이 된다.

  • 6장의 인터페이스 설계 원칙들을 적용하면 구현을 효과적으로 캡슐화하는 코드를 구현할 수 있다.
  • 8, 9장의 의존성 관리 기법을 적용하면 타입을 캡슐화하기 위해 낮은 결합도를 유지할 수 있는 방법을 보여준다.
  • 10장을 통해 타입을 캡슐화 하는게 아닌, 코드 재사용을 위해 상속을 사용하는 것의 단점을 보여준다.
  • 11장에서 배운 상속 대신 합성을 사용하면 훌륭한 캡슐화를 구현할 수 있다.
  • 13장에서 설명한 원칙을 따르면 리스코프 치환 원칙을 준수하는 타입 계층을 구현하는 데 상속을 이용할 수 있다.

변경에 초점을 맞추고 캡슐화의 관점에서 설계를 바라보면 일관성 있는 협력 패턴을 얻을 수 있다.

캡슐화 다시 살펴보기

많은 사람들은 객체의 캡슐화를 들으면 데이터 은닉을 떠올리지만, 캡슐화는 데이터 은닉 그 이상의 개념이다.
단순히 데이터를 감추는것이 아닌, 소프트웨어 안에서 변할 수 있는 모든 '개념'을 감추는 것이다.
변경을 캡슐화 할 수 있는 다양한 방법이 존재하지만 협력을 일관성 있게 만들기 위해 가장 일반적으로 사용하는 방법은 서브타입 캡슐화와 객체 캡슐화를 조합하는 것이다.
서브타입 캡슐화는 인터페이스 상속을 사용하고, 객체 캡슐화는 합성을 사용한다.
서브타입 캡슐화와 객체 캡슐화를 적용하는 방법은 다음과 같다.

변하는 부분을 분리해서 타입 계층을 만든다.

변하지 않는 부분으로부터 변하는 부분을 분리한다.
변하는 부분들의 공통적인 행동을 추상화하여 추상 클래스나 인터페이스로 추상화한 후 이를 타입 계층화한다.

변하지 않는 부분의 일부로 타입 계층을 합성한다.

앞에서 구현한 변하는 부분을 변하지 않는 부분에 합성한다.
의존성 주입과 같이 결합도를 느슨하게 유지할 수 있는 방법을 이용해 오직 추상화에만 의존하게 만든다.

지속적으로 개선하라

처음에는 일관성을 유지하는 것처럼 보이던 협력 패턴이 시간이 흐르면서 새로운 요구사항이 추가되는 과정에서 일관성의 벽에 조금씩 금이 가는 경우를 자주 보게 된다.
협력을 설계하는 초기 단계에서 모든 요구사항을 미리 예상할 수 없기 때문에 이는 자연스러운 현상이다.
협력은 고정된것이 아니므로 현재의 협력 패턴이 변경의 무게를 지탱하기 어렵다면 변경을 수용할 수 있는 협력 패턴으로 과감하게 리팩터링하라.
요구사항의 변경에 따라 협력 역시 지속적으로 개선해야 한다.
현재의 설계에 맹목적으로 일관성을 맞추는 것이 아니라 달라지는 변경의 방향에 맞춰 지속적으로 코드를 개선해야 한다.


인상깊었던 점

"유지보수 가능한 시스템을 구축하는 첫걸음은 협력을 일관성 있게 만드는 것이다."
이번 장에서는 일관성 있는 협력의 중요함을 말하고 있는것 같다.
이전 장들에서 공부했던 인터페이스 분리원칙, 서브타입/슈퍼타입 관계와 리스코프 치환 원칙 등을 따라서

변하는 부분과 변하지 않는 부분을 분리하는 방법을 설명해준 부분을 통해 이전 장의 내용과 연관지어 생각할 수 있었다.
또한 시간이 지나면서 협력의 일관성이 깨질 수 있지 않을까? 라고 생각했는데

마지막 장에서 이에 대한 가능성을 언급하며 현재의 설계에 맹목적으로 협력의 형태를 맞추는게 아닌,

지속적으로 코드를 개선해야 함을 언급한 점도 좋았다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 13장  (0) 2024.04.08
[오브젝트] 12장  (0) 2024.03.26
[오브젝트] 11장  (0) 2024.03.18
[오브젝트] 10장  (0) 2024.03.04
[오브젝트] 9장  (0) 2024.02.20

13장 정리

상속의 첫번째 용도는 타입 계층을 구현하는 것이고, 두번째 용도는 코드 재사용이다.
상속은 코드를 쉽게 재사용할 수 있는 방법을 제공하지만 부모 클래스와 자식 클래스를 강하게 결합시킨다.
타입 계층을 목표로 상속을 사용하면 다형적으로 동작하는 객체들의 관계에 기반해 확장 가능하고 유연한 설계를 얻을 수 있게 된다.
따라서 상속을 사용하는 일차적인 목표는 코드 재사용이 아닌 타입 계층을 구현하는 것이어야 한다.
동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다.
상속의 가치는 이러한 타입 계층을 구현할 수 있는 쉽고 편안한 방법을 제공한다는데 있다.
타입 사이의 관계를 고려하지 않은 채 단순히 코드를 재사용하기 위해 상속을 사용해서는 안된다.


타입 

프로그래밍 언어 관점의 타입

프로그래밍 언어 관점에서 타입은 두 가지 목적을 위해 사용된다.

  • 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의한다.
  • 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공한다.
    타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용된다.

객체지향 패러다임 관점의 타입

프로그래밍 언어 관점에서 타입은 호출 가능한 오퍼레이션의 집합을 정의한다면,

객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미한다.
따라서 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
우리는 이미 퍼블릭 인터페이스라는 객체가 수신할 수 있는 메시지의 집합을 정의한 단어를 알고 있다.
객체지향에서는 객체가 수신할 수 있는 메시지를 기준으로 타입을 분류한다.
따라서 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.
즉, 객체에게 중요한 것은 속성이 아니라 행동이다.
어떤 객체들이 동일한 상태를 가지고 있더라도 퍼블릭 인터페이스가 다르다면 이들은 서로 다른 타입으로 분류된다.

타입 계층

위 그림처럼 타입은 집합의 관점에서 더 세분화된 타입의 집합을 부분집합으로 포함할 수 있다.
다른 타입을 포함하는 타입은 다른 타입에 속해 있는 타입보다 좀 더 일반화된 의미를 표현할 수 있다.
반면 다른 타입에 속해 있는 타입은 좀 더 특수하고 구체적이다.
이것은 포함 관계로 연결된 타입 사이에 개념적으로 일반화와 특수화 관계가 존재한다는 것을 의미한다.
이를 그림으로 표현하면 아래와 같다.


타입 계층을 구성하는 두 타입 간의 관계에서 더 일반적인 타입을 슈퍼타입이라고 부르고 더 특수한 타입을 서브타입이라고 부른다.
이를 객체의 관점에서 보자면 일반화란 추상화를 의미하며, 특수화란 구체화를 의미한다고 할 수 있다.

객체지향 프로그래밍과 타입 계층

일반화와 특수화를 객체지향 프로그래밍 관점에서 보자면 더 일반적인 퍼블릭 인터페이스를 갖는 객체는 슈퍼타입이고 더 특수한 퍼블릭 인터페이스를 갖는 객체는 서브타입이라고 할 수 있다.
서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.
이는 상속과 다형성의 관계를 이해하기 위한 출발점이며, 이 장의 핵심이다.

서브클래싱과 서브타이핑

그렇다면 퍼블릭 인터페이스가 더 특수하다는 것은 어떤 의미일까?
이제부터 타입 계층을 구현할 때 지켜야하는 제약사항을 클래스와 상속 관점에서 살펴보자.

언제 상속을 사용해야 하는가?

반복하지만 상속의 올바른 용도는 타입 계층을 구현하는 것이다.
마틴 오더스키는 다음 두 질문을 해보고 두 질문에 모두 '예'라고 답할 수 잇는 경우에만 상속을 사용하라고 조언한다.

  • 상속 관계가 is-a 관계를 모델링하는가?
  • 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?

슈퍼타입과 서브타입 관계에서는 is-a보다 행동 호환성이 더 중요하므로 설계 관점에서는 첫번째보다는 두번째 질문에 초점을 맞추는게 중요하다.

행동 호환성

행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.
클라이언트가 기대하는 행동에 따라 두 타입을 타입 계층으로 묶을지 여부를 판단해야 한다.

클라이언트의 기대에 따라 계층 분리하기

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있게 된다.
요구 사항이 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있다.
이처럼 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙이라고 부른다.

리스코프 치환 원칙

리스코프 치환 원칙을 한마디로 정의하면 아래와 같다.

  • 서브타입은 그것을 기반하는 슈퍼타입에 대해 대체 가능해야한다
    즉 자식 클래스가 부모 클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야한다.

클라이언트와 대체 가능성

어떤 모델의 유효성은 클라이언트의 관점에서만 검증 가능하다.

따라서 상속 관계에 있는 두 클래스 사이의 관계를 클라이언트와 떨어뜨려 놓고 판단해서는 안된다.
대체 가능성을 결정하는 것은 클라이언트다.

계약에 의한 설계와 서브타이핑

리스코프 치환 원칙과 계약에 의한 설계 사이의 관계를 다음과 같은 한 문장으로 요약할 수 있다.

  • 서브타입이 리스코프 치환 원칙을만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 '계약'을 준수해야 한다.

인상깊었던 점

"상속에 있어서 어떤 모델의 유효성은 클라이언트의 관점에서만 검증 가능하다."
이번 장에서는 타입을 프로그래밍 언어와 객체지향적 관점에서 바라보며 일반화와 특수화 관계를 설명하였다.
이후 일반화와 특수화를 추상화와 구체화로 서브타입과 슈퍼타입을 정의하고, 올바른 서브타입 슈퍼타입 관계인지 아닌지는 클라이언트의 기대에 따라 달라진다고 설명하면서 빌드업 과정을 거쳐서 설명한게 인상깊었다.
상속은 단순히 코드 재사용을 위해 사용하는게 아니라는걸 행동 호환성과 클라이언트의 연관성을 설명하면서 인터페이스 분리 원칙과 리스코프 치환 원칙과 함께 설명해 주는 부분이 좋았다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 14장  (0) 2024.04.15
[오브젝트] 12장  (0) 2024.03.26
[오브젝트] 11장  (0) 2024.03.18
[오브젝트] 10장  (0) 2024.03.04
[오브젝트] 9장  (0) 2024.02.20

12장 정리

코드 재사용을 목적으로 상속을 사용하면 변경하기 얼벼고 유연하지 못한 설계에 이를 확률이 높아진다.
상속의 목적은 코드 재사용이 아니다. 상속은 타입 계층을 구조화하기 위해 사용해야한다.
클라이언트 관점에서 인스턴스들을 동일한 행동 그룹군으로 묶기 위해서 사용해야한다.
이번 장에서는 상속의 관점에서 다형성이 구현되는 기술적인 메커니즘을 살펴본다.
다형성이 런타임에 메시지를 처리하기에 적합한 메서드를 동적으로 탐색하는 과정을 통해 구현되며, 상속이 이런 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현하기 위한 방법임을 이해할 수 있을 것이다.


다형성

다형성이라는 단어는 '많은 형태를 가질 수 있는 능력'을 의미한다.

  • 오버로딩 다형성: 메서드 오버로드
  • 강제 다형성: + 연산자의 피연산자에 따른 타입 강제
  • 매개변수 다형성: 제네릭
  • 포함 다형성: 인터페이스 -> 가장 일반적으로 의미하는다형성 = 서브타입 다형성

상속의 진정한 목적은 코드 재사용이 아닌 다형성을 위한 서브타입 계층을 구축하는 것이다.

상속의 양면성

객체지향의 패러다임에서는 데이터와 행동을 객체라는 하나의 실행 단위 안으로 통합한다. 

따라서 객체 지향 프로그램을 작성하기 위해서는 항상 데이터와 행동이라는 두가지 관점을 함께 고려해야 한다.
단순히 데이터와 행동의 관점에서 보면 상속은 부모 클래스에서 정의한 데이터와 행동을 자식 클래스에서 자동으로 공유할 수있는 재사용 메커니즘으로 보인다.
하지만 상속의 목적은 재사용이 아니라 프로그램을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하는 것이다.
타입 계층에 대한 고민 없이 코드를 재사용하기 위해 상속을 사용하면 이해하기 어렵고 유지보수하기 버거운 코드가 만들어질 확률이 높다.

데이터 관점의 상속

데이터 관점에서 상속은 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 볼 수 있다.
따라서 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하는 것이다.

행동 관점의 상속

행동 관점의 상속은 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미한다.
부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다.
런타임 시점에 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문에 외부의 객체가 부모 클래스의 인스턴스에게 전송할 수 있는 모든 메시지는 자식 클래스의 인스턴스에게도 전송할 수 있다.
위의 그림에서 보듯 메시지를 수신한 객체는 class 포인터로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지를 찾고, 메서드가 존재하지 않으면 클래스의 parent 포인터를 따라 부모 클래스를 차례대로 훑어가면서 적절한 메서드가 존재하는지 탐색한다.

업캐스팅과 동적 바인딩

같은 메시지, 다른 메서드

  • 업캐스팅: 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것
  • 동적 바인딩: 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정되는 것

동일한 수신자에게 동일한 메시지를 전송하는 동일한 코드로 서로 다른 메서드를 실행할 수 있는 이유는 업캐스팅과 동적 메서드 탐색이라는 기반 메커니즘이 존재하기 때문이다.
이 두가지를 통해 코드를 변경하지 않더라도 실행되는 메서드를 변경할 수 있다.

업캐스팅

컴파일러의 관점에서 자식 클래스는 아무런 제약 없이 부모 클래스를 대체할 수 있기 때문에 부모 클래스와 협력하는 클라이언트는 현재 상속 계층에 존재하는 자식 클래스 뿐 아니라 앞으로 추가 될 미래의 자식 클래스의 인스턴스와도 협력이 가능하다.
이를 통해 유연하며 확장 용이한 설계가 가능하다.

동적 바인딩

함수를 호출하는 전통적인 언어에서는 컴파일 타임에 호출될 함수를 결정하지만 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다.

동적 메서드 탐색과 다형성

객체지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택한다.

  1. 메시지를 수신한 객체는 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검색한다. 존재하면 메서드를 실행하고 탐색을 종료한다.
  2. 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다. 이 과정은 적합한 메서드를 찾을 때까지 상속 계층을 따라 올라가며 계속된다.
  3. 상속 계층의 가장 최상위 클래스에 이르러도 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 중단한다.

메시지 탐색에서 self 참조란 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다.
동적 메서드 탐색을 self가 가리키는 객체의 클래스에서 시작해서 상속 계층을 타고 올라가면서 이루어지고, 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다.
동적 메서드 탐색은 자동적인 메시지 위임동적인 문맥 사용의 두가지 원리로 구성된다.

자동적인 메시지 위임

메시지를 수신한 객체가 자신이 이해할 수 없는 메시지인 경우 부모 클래스에 처리를 위임한다.
상속을 이용할 경우 프로그래머가 메시지 위임과 관련된 코드를 명시적으로 작성하지 않아도 자동으로 위임된다.

동적인 문맥

메시지를 수신학 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다. 그리고 이 문맥을 결정하는 것은 메시지를 수신한 객체를 가리키는 self 참조다.
동일한 코드라고 하더라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변하므로 self 참조가 가리키는 객체의 타입을 변경함으로써 객체가 실행될 문맥을 동적으로 바꿀 수 있다.
self 전송은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동시킨다.
따라서 self 전송으로 인해 극단적으로 이해하기 어려운 코드가 만들어질 수 있다.

self와 super

self 참조는 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥을 실행 시점에 결정하며, 어떤 클래스에서 메시지 탐색이 시작될지 알지 못한다.
super 참조는 항상 해당 클래스의 부모 클래스에서부터 메서드 탐색을 시작한다.

상속 대 위임

위임과 self 참조

self 참조가 동적인 문맥을 결정한다는 사실을 이해하고 나면 상속을 자식 클래스에서 부모 클래스로 self 참조를 전달하는 메커니즘으로 바라볼 수 있다.
self 참조는 항상 메시지를 수신한 객체를 가리키므로 메서드 탐색 중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유하는 것으로 볼 수 있다.


인상깊었던 점

"상속은 타입 계층을 구조화하기 위해 사용해야한다."
이번 장에서 계속 강조했던 문장이었던것 같다.
상속의 관점에서 다형성이 구현되는 메커니즘을 살펴보면서 self 참조와 super 참조에 대해 알 수 있었다.
또한 self 전송의 설명을 통해 상속을 통해 다형성을 구현할 때는 내부 구현을 알아야 하기 때문에 단순히 코드 재사용 측면에서 상속을 이용하기에는 위험할 수 있다는 생각이 들었다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 14장  (0) 2024.04.15
[오브젝트] 13장  (0) 2024.04.08
[오브젝트] 11장  (0) 2024.03.18
[오브젝트] 10장  (0) 2024.03.04
[오브젝트] 9장  (0) 2024.02.20

11장 정리

상속은 부모 클래스의 정의 대부분을 물려받으므로 제대로 사용하기 위해서는 부모 클래스의 내부 구현에 대해 상세히 알아야한다.
따라서 코드를 재사용할 수 있는 쉬운 방법이지만, 우아한 방법이라고는 할 수 없다.
합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존한다.
그러므로 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화할 수 있기 때문에 변경에 더 안정적인 코드를 얻을 수 있다.

상속을 합성으로 변경하기

10장에서 상속의 문제점들은 합성을 이용하면 구현이 아닌 인터페이스에 의존하기 때문에 보완이 가능하다.

상속으로 인한 조합의 폭발적인 증가

기능을 추가하면서 부모 클래스에 추상 메서드를 추가하면 상속받은 자식 클래스를 모두 수정해야한다.
하지만 여기서 수정하는 부분은 모두 구조가 비슷하지만 내부 구현이 조금씩만 다른 코드이므로 중복 코드의 양이 늘어난다.
이처럼 상속으로 인해 결합도가 높아지면 코드를 수정하는데 필요한 작업량이 과도하게 늘어날 수 있다.
이 또한 아래에서 설명할 합성을 이용하면 중복 코드 문제를 간단하게 해결할 수 있다.

합성 관계로 변경하기

상속 관계는 컴파일타임에 결정되고 고정되므로 코드를 실행하는 도중에는 변경할 수 없다.
따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하면 조합 가능한 경우별로 클래스를 추가해야하는 클래스 폭발 문제를 겪는다.
내부 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존하면 런타임에 객체의 관계를 변경할 수 있다.
합성은 이렇게 컴파일 타임 관계를 런타임 관계로 변경함으로써 이 문제를 해결한다.
물론 컴파일 타임과 런타임 의존성의 거리가 멀면 멀수록 설계의 복잡도가 증가하고 코드를 이해하기 어려워지지만 유지보수를 위한 트레이드 오프이다.

믹스인

믹스인은 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리킨다.
합성이 실행 시점에 객체를 조합하는 재사용 방법이라면, 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다.
믹스인은 상속과는 다르게 is-a 관계가 아닌, 그냥 코드 조각을 다른 코드 안에 섞어 넣는 방법이다.
따라서 클래스와 클래스 사이의 관계를 고정시키지 않고 유연하게 관계를 재구성 할 수 있다.
상속처럼 상속 관계의 순서가 정해진게 아닌, 필요한 시점에 차례로 추가할 수 있는 쌓을 수 있는 변경의 특징을 가진다.

# 인상깊었던 점

"최선의 방법은 상속을 포기하는 것이다"
이번 장에서는 이 문장이 인상깊었다. 
합성이 왜 좋은지 알 수 있었고, 단순히 코드를 재사용하는거라면 합성이 상속보다 좋다는걸 알 수 있었다.
하지만 늘 그러하듯 합성을 사용하면 일종의 DSL처럼 자기만의 문법을 만드는것 같아서

변경 가능성이 낮은 단순한 기능에는 상속을 쓰고, 유연함이 필요한 부분에서는 합성을 써야할것 같다.
역시 좋은 설계를 위해서는 경험이 필요하다는 생각이 들었다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 13장  (0) 2024.04.08
[오브젝트] 12장  (0) 2024.03.26
[오브젝트] 10장  (0) 2024.03.04
[오브젝트] 9장  (0) 2024.02.20
[오브젝트] 8장  (0) 2024.02.06

10장 정리

전통적인 패러다임에서 코드를 재사용하는 방법은 코드를 복사한 후 수정하는 것이다.
객체지향에서는 일반적으로 클래스 안에서 코드를 작성하며, 코드를 재사용하기 위해 새로운 클래스를 추가해서 코드를 추가한다. 
이 때 상속을 이용하며, 재사용 관점에서 상속이란 클래스 안에 정의된 인스턴스 변수와 메서드를 자동으로 새로운 클래스에 추가하는 구현 기법이다.
객체 지향에서는 상속 외에도 합성이라는 새로운 클래스의 인스턴스 안에 기존 클래스의 인스턴스를 포함시키는 방법으로 코드를 효과적으로 재사용할 수 있다.


상속과 중복 코드

DRY 원칙 (Don't Repeat Yourself)

중복 코드는 코드를 수정시 중복된 모든 코드를 수정해야하므로 수정에 드는 노력을 몇 배로 증가시킨다.
따라서 신뢰할 수 있고 수정하기 쉬운 소프트웨어를 만드는 효과적인 방법 중 하나는 중복을 제거하는 것이다.

중복과 변경

많은 코드 더미 속에서 어떤 코드가 중복인지를 파악하는 일은 쉬운 일이 아니다.
중복 코드는 항상 함께 수정돼야 하기 때문에 수정할 때 하나라도 빠트린다면 버그로 이어질 수 있다.

상속을 이용해서 중복 코드 제거하기

상속의 기본 아이디어는 매우 간단하다. 이미 존재하는 클래스와 유사한 클래스가 필요하다면 코드를 복사하지 말고 상속을 이용해 코드를 재사용한다.
하지만 상속을 염두에 두고 설계되지 않은 클래스를 상속해 재사용하는 것은 생각보다 쉽지 않다.
개발자는 재사용을 위해 상속 계층 사이에 무수히 많은 가정을 세웠을 수도 있고, 이는 코드를 이해하기 어렵게 만들 뿐 아니라 직관에도 어긋날 수 있다.
상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 하므로 부모 클래스와 자식 클래스 사이의 강한 결합을 갖게 한다.

취약한 기반 클래스 문제

상속은 자식 클래스와 부모 클래스의 결합도를 높이며, 이로 인해 자식 클래스는 부모 클래스의 구현 세부사항에 엮이게 된다.
이처럼 강하게 결합됨으로써 부모 클래스의 변경에 의해 자식 클래스가 영향을 받을 수 있다. 이 현상을 취약한 기반 클래스 문제라고 부른다.
취약한 기반 클래스 문제는 상속이라는 문맥 안에서 결합도가 초래하는 문제점을 가리킨다. 
상속은 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문에 캡슐화를 약화시키며, 이는 상속을 피해야하는 이유이다.

불필요한 인터페이스 상속 문제

인터페이스는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 만들어야 한다.
상속을 이용하면 규칙을 무너뜨릴 여지가 있는 퍼블릭 인터페이스까지도 함께 상속받으므로 주의해야 한다.

메서드 오버라이딩의 오작용 문제

자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 자식 클래스는 부모 클래스의 메서드를 사용하는 방법까지 고려해야 한다.

부모 클래스와 자식 클래스의 동시 수정 문제

클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나, 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택해야 한다.

상속으로 인한 피해를 최소화할 수 있는 방법

추상화에 의존하자

자식 클래스가 부모 클래스의 구현이 아닌 추상화에 의존하도록 만들면 부모의 변경에 영향을 덜 받을 수 있다.
"변하는 것으로부터 변하지 않는 것을 분리하라", 또는 "변하는 부분을 찾고 이를 캡슐화하라" 라는 조언을 메서드 수준에서 적용한다.
변하는 부분을 메서드로 추출하여 격리시키고, 변하지 않는 부분을 부모 클래스로 올린다.

추상화가 핵심이다

공통 코드를 부모 클래스로 이동시키고 변하는 부분을 추상화하면 자식 클래스들은 각각 하나의 서로 다른 변경의 이유만을 가진다. 따라서 단일 책임 원칙을 준수하므로 응집도가 높다.

합성

상속은 강력한 도구지만 코드를 재사용하기 위해 맹목적으로 상속을 사용하는 것은 위험하다.
상속의 오용과 남용은 애플리케이션을 이해하고 확장하기 어렵게 만드므로 반드시 필요한 경우에만 상속을 사용해야 한다.
코드 재사용 측면에서 대부분의 경우에 상속은 우아한 해결 방법이 아니다.
상속의 단점을 피하면서도 코드를 재사용할 수 있는 더 좋은 방법은 바로 합성이다.


인상깊었던 점

"상속은 강력한 도구지만 코드를 재사용하기 위해 맹목적으로 상속을 사용하는 것은 위험하다."
이번 장에서 말하고 싶었던건 위의 문장인거 같다.
사실상 이번 장은 상속의 문제점을 설명하고, 다음 장의 내용인 합성을 위한 빌드업이었던것 같다.
자바 라이브러리의 예시를 들어서 무분별한 상속의 단점을 잘 설명해줘서 이해에 도움이 되었다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 12장  (0) 2024.03.26
[오브젝트] 11장  (0) 2024.03.18
[오브젝트] 9장  (0) 2024.02.20
[오브젝트] 8장  (0) 2024.02.06
[오브젝트] 7장  (0) 2024.01.29

9장 정리

이름을 가진 설계 원칙을 통해 기법들을 정리한다.

1. 개방-폐쇄 원칙

확장에 대해 열려 있고, 수정에 대해 닫혀 있는 설계
유연한 설계란 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계이다.

컴파일 타임 의존성을 고정시키고 런타임 의존성을 변경하라

의존성 관점에서 OCP를 따르는 설계란 컴파일 타임 의존성은 유지하면서 런타임 의존성을 확장하고 수정할 수 있는 구조이다.

추상화가 핵심이다

OCP의 핵심은 추상화에 의존하는 것이다.
생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다.
공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 즉, 수정할 필요가 없어야 한다.
추상화 부분은 수정에 닫혀 있고, 추상화 과정 속에서 생략된 부분은 확장의 여지를 남긴다.
이를 통해 개방-폐쇄 원칙을 가능하게 만든다.
하지만 OCP가 가능한 설계는 공짜로 얻어지지 않는다.
추상화가 수정에 대해 닫혀 있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 했기 때문이다.
변경에 의한 파급효과를 최대한 피하기 위해 변하는 것과 변하지 않는 것이 무엇인지 이해하고, 이를 추상화의 목적으로 삼아야한다.

2. 생성 사용 분리

결합도가 높아질수록 OCP를 따르는 구조를 설계하기 어려워진다.
특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.
객체 생성을 피할 수는 없고, 어딘가에서는 반드시 객체를 생성해야 한다.
동일한 클래스 안에서 객체 생성과 사용이라는 두 가지를 모두 하기 때문에 문제가 생긴다.
유연하고 재사용 가능한 설계를 원한다면 객체를 **생성**하는 책임과 객체를 **사용**하는 책임을 분리해야 한다.

FACTORY 추가하기

생성의 책임을 Client로 옮겼을 때 Client는 특정 컨텍스트에 묶이게 된다.
만약 Client가 특정한 컨텍스트에 묶이지 않기를 원한다면 Client도 생성과 사용의 책임을 분리한다.
이때 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 사용하는데, 이를 FACTORY라고 부른다.

순수한 가공물에게 책임 할당하기

객체지향 애플리케이션은 도메인 개념뿐만 아니라 설계자들이 임의적으로 창조한 인공적인 추상화들을 포함하고 있다.
오히려 인공적으로 창조한 객체들이 도메인 개념을 반영하는 객체들보다 더 많은 비중을 차지하는 것이 일반적이다.
먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하고, 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라.
객체지향이 실세계를 모방해야 한다는 주장에 현혹될 필요가 없다.

3. 의존성 주입

생성과 사용을 분리할 때, 사용의 책임을 지는 객체의 의존성은 어떡해야할까?
이 때는 사용의 책임을 지는 객체에 외부에서 생성된 인스턴스를 전달하는 방법으로 의존성을 해결해야 한다.
이를 의존성 주입(Dependency Injection)이라고 부른다.
의존성 주입의 방법에는 아래 세 가지 방법이 있다.

  • 생성자 주입: 객체를 생성하는 시점에 생성자를 통한 의존성 해결
  • setter 주입: 객체 생성 후 setter 메서드를 통한 의존성 해결
  • 메서드 주입: 메서드 실행 시 인자를 이용한 의존성 해결

setter 주입은 런타임 시점에 의존성을 변경할 수 있지만, 객체가 올바로 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다는 단점이 있다. 
따라서 객체 생성 후 setter 메서드로 의존성 주입을 누락하면 비정상적인 상태의 객체가 생성된다.

숨겨진 의존성은 나쁘다

의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다.
하지만 의존성을 구현 내부로 감추는것을 주의해야 한다.
의존성을 구현 내부로 감출 경우 의존성과 관련된 문제는 컴파일 타임이 아닌 런타임에서야 발견할 수 있다.
숨겨진 의존성은 코드의 내부 구현을 이해할 것을 강요하므로 캡슐화를 위반하게 된다.
가능하다면 의존성을 명시적으로 표현할 수 있는 기법을 사용하라.

4. 의존성 역전 원칙

어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다.
이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 된다.
따라서 상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안 된다.
요약하자면 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.


5. 유연성에 대한 조언

유연한 설계는 유연성이 필요할 때만 옳다

유연하고 재사용 가능한 설계란 런타임 의존성과 컴파일 타임 의존성의 차이를 인식하고 
동일한 컴파일 타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지를 설계이다.
하지만 유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다.
변경하기 쉽고 확장하기 쉬운 구조를 만들기 위해서는 단순함과 명확함의 미덕을 버려야할 가능성이 높다.
유연성은 항상 복잡성을 수반하고, 유연하지 않은 설계는 단순하고 명확하다.
복잡성이 필요한 이유와 합리적인 근거가 있을 때 유연하고 재사용 가능한 설계를 해야한다.

협력과 책임이 중요하다

설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.
초보자가 자주 저지르는 실수 중 하나는 객체의 역할과 책임이 자리를 잡기 전에 너무 성급하게 객체 생성에 집중하는 것이다.
의존성을 관리해야 하는 이유는 역할, 책임, 협력의 관점에서 설계가 유연하고 재사용 가능해야 하기 때문이다.
따라서 역할, 책임, 협력에 먼저 집중하여 먼저 다양한 컨텍스트에서 협력을 재사용할 필요가 있는지 판단해야 한다.


인상깊었던 점

"유연한 설계는 유연성이 필요할 때만 옳다"
이번 장에서 이 표현이 가장 와닿았다.
얼마 전 객체지향적으로 로그 분석기를 만들 때 오랜 시간 고민하고, 신경을 많이 썼던 부분이었어서 더 와닿았던것 같다.
모든 코드를 유연하게 설계하려면 구현체가 하나 뿐인 클래스라도 인터페이스를 만들어야 하는 등의 불필요한 작업이 많아질 수 있다.
YAGNI 라는 말도 있듯 과한 설계는 결국 한 번도 쓰지 않을 수 있고, 고스란히 시간을 날리는 셈이 될 수 있다는걸 생각해봐야한다.
어떤 부분에서 유연한 설계가 필요하고, 어떤 부분에서 단순함이 필요한지 판단하는 능력이 중요한것 같다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 11장  (0) 2024.03.18
[오브젝트] 10장  (0) 2024.03.04
[오브젝트] 8장  (0) 2024.02.06
[오브젝트] 7장  (0) 2024.01.29
[오브젝트] 6장  (0) 2024.01.23

8장 정리

객체지향 애플리케이션은 객체들의 역할, 책임, 협력을 통해 이루어진다.
협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만들어 개발자를 곤경에 빠트린다.
객체지향 설계의 핵심은 협력을 위해 필욯나 의존성은 유지하면서도 변경을 방해앟는 의존성은 제거하는데 있다.
이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다.

의존성 이해하기

변경과 의존성

지금까지 설계와 관련된 대부분의 용어들은 변경과 관련이 있었다.
두 요소 사이의 의존성 역시 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다.
따라서 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.

런타임 의존성과 컴파일타임 의존성

의존성은 런타임 의존성과 컴파일타임 의존성으로 나눌 수 있다.
유연하고 재사용 가능한 코드를 설계하기 위해서는 런타임 시점과 컴파일 시점의 의존성을 서로 다르게 만들어야 한다.

컨텍스트 독립성

클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 알고 있는 것을 컨텍스트 독립성이라고 부른다.
클래스가 자신과 협력할 객체의 구체적인 클래스를 알고 있다면 그 클래스는 특정한 문맥에 강하게 결합된다.
반면 협력할 객체의 추상적인 클래스를 알고 있다면 구체적인 문맥은 컴파일 타임 의존성을 어떤 런타임 의존성으로 대체하느냐에 따라 달라진다.
클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워진다.
클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 사용하기 더 수월해진다.
따라서 유연한 설계를 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다.

의존성 해결하기

컴파일 시점에 추상적인 클래스를 의존하면 런타임 시점에 구체적인 클래스로 교체해야하며, 이를 의존성 해결이라고 부른다.
의존성 해결의 방법에는 다음과 같은 세 가지 방법이 있다.

  • 객체를 생성하는 시점에 생성자를 통해 의존성 해결
  • 객체 생성 후 setter 메서드를 통해 의존성 해결
  • 메서드 실행 시 인자를 이용해 의존성 해결

유연한 설계

의존성을 관리하는데 유용한 몇 가지 원칙과 기법을 알아본다.

의존성과 결합도

객체지향 프로그래밍에서는 객체들 간의 협력을 통해 기능을 구현한다.
객체들이 협력 위해서는 서로를 알아야하고, 이는 객체들 간의 의존성을 낳는다.
따라서 모든 의존성이 나쁜 것은 아니지만, 의존성이 과하면 문제가 될 수 있다.
바람직한 의존성을 판단하는 기준은 재사용성이다.
어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한하면 그 의존성은 바람직하지 못한 것이다.
반대로 다양한 환경에서 클래스를 재사용할 수 있도록 컨텍스트에 독립적인 의존성은 바람직한 의존성이다.
결합도의 측면에서 본다면 의존성이 재사용을 쉽게 허용한다면 결합도가 느슨하다고 하며, 재사용을 방해한다면 결합도가 강하다고 표현한다.

추상화에 의존하라

추상화란 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 감추는것이다.
추상화를 통해 대상에 대해 알아야 하는 지식의 양을 줄일 수 있고, 따라서 결합도를 느슨하게 유지할 수 있다.
의존성을 객체의 퍼블릭 인터페이스에 드러내는것을 명시적인 의존성이라고 하며, 퍼블릭 인터페이스에 노출하지 않고 내부 구현으로 주입받은 의존성을 숨겨진 의존성이라고 부른다.
의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴봐야하므로 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현의 수정을 야기할 수 있다.
수정은 곧 버그 발생의 가능성을 내포하므로 의존성은 명시적으로 표현돼야 한다.

new는 해롭다

new를 쓰면 구체적인 클래스의 인스턴스를 만들어야 하므로 결합도가 높아진다.
따라서 new 를 쓸 수록 더 많은 것에 의존하게 되고, 변경에 취약해진다.

인상깊었던 점

"모든 의존성이 나쁜 것은 아니지만, 의존성이 과하면 문제가 될 수 있다."
이 문장이 이번 장을 정리하는 문장인것 같습니다.
의존성이 생기는건 피할 수 없지만 생각없이 이것 저것 의존시키지 말고 적절하게 유지하는것의 중요함을 느꼈습니다.
최근 과제 전형을 해보면서 스프링 없이 순수 자바 코드로 요구사항을 만족시키는 애플리케이션을 만들고 있어요.
스프링이 너무 당연하다보니까 간과했었는데 스프링이 의존성 관리를 정말 편하게 해주고 있다는걸 느끼고 있습니다.
과제와 관련지어서 보니까 또 새로 보이는게 있었던거 같습니다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 10장  (0) 2024.03.04
[오브젝트] 9장  (0) 2024.02.20
[오브젝트] 7장  (0) 2024.01.29
[오브젝트] 6장  (0) 2024.01.23
[오브젝트] 5장  (0) 2024.01.15

7장 정리

사람은 문제 해결에 필요한 요소의 수가 단기 기억의 용량을 초과하는 순간 문제 해결 능력이 급격하게 떨어진다. 이를 인지 과부하라고 부른다.
이런 인지 과부하를 피하기 위해 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업을 추상화라고 한다.
맞닥뜨린 문제가 한 번에 해결하기 어려울 정도로 크다면 해결 가능한 작은 문제로 나누는 작업을 분해라고 한다.
인류는 이런 추상화와 분해를 통해 복잡한 분야의 문제를 해결해왔다.

프로시저 추상화와 데이터 추상화

어셈블리어, 고수준 언어 같은 프로그래밍 언어의 발전은 효과적인 추상화를 이용해 복잡성을 극복하려는 개발자의 노력에서 출발했다.
프로그래밍 언어를 통해 표현되는 추상화의 발전은 다양한 프로그래밍 패러다임의 탄생으로 이어졌다.
프로그래밍 패러다임은 프로그래밍을 구성하기 위해 사용되는 추상화의 종류와 이 추상화를 이용해 소프트웨어를 분해하는 방법의 두 가지 요소로 결정되며, 따라서 추상화와 분해의 관점에서 설명할 수 있다.

현대적인 프로그래밍 언어를 특징 짓는 중요한 두 가지 추상화 메커니즘은 프로시저 추상화와 데이터 추상화이다.

 

프로시저 추상화

  • 소프트웨어가 무엇을 해야 하는지를 추상화한다.

데이터 추상화

  • 소프트웨어가 무엇을 알아야 하는지를 추상화한다.

프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌 것인지를 결정하는 원칙과 방법의 집합이다.
시스템을 분해하는 방법을 결정하려면 프로시저 추상화를 중심으로 할 것인지, 데이터 추상화를 중심으로 할 것인지를 결정해야 한다.

 

프로시저 추상화 중심

  • 기능분해

데이터 추상화 중심

  • 데이터를 중심으로 타입을 추상화 하면 추상 데이터 타입
  • 데이터를 중심으로 프로시저를 추상화하면 객체지향

객체지향 패러다임은 역할과 책임을 수행하는 자율적인 객체들의 협력 공동체를 구축하는 것이다.
객체지향 패러다임에서 '역할과 책임을 수행하는 객체'가 추상화를 의미하고, 기능을 '협력하는 공동체'를 구성하도록 객체들로 나누는 과정이 분해를 의미한다.

하향식 기능 분해의 문제점

전통적인 기능 분해 방법은 시스템을 구성하는 가장 최상위 기능을 정의하고, 여기서부터 좀 더 작은 단계의 하위 기능으로 분해하는 하향식 접근법을 따른다.
분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될 때 까지 계속되며, 각 세분화 단계는 바로 위 단계보다 더 구체적이어야 한다.
하향식 기능 분해는 시스템을 최상위의 가장 추상적인 메인 함수로 정의하고, 메인 함수를 구현 가능한 수준까지 세부적인 단계로 분해한다.
이상적인 방법으로 보일 수 있지만 우리가 사는 세계는 그렇게 체계적이지도, 이상적이지도 않기 때문에 아래와 같은 다양한 문제가 발생한다.

  • 시스템은 하나의 메인 함수로 구성돼 있지 않다.
  • 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
  • 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
  • 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.
  • 데이터 형식이 변경될 경우 파급 효과를 예측할 수 없다.

데이터 추상화와 추상 데이터 타입 / 클래스

타입을 사용했지만
초기 절차형 프로그래밍 언어에서는 적은 수의 내장타입만을 제공했고, 새로운 타입을 추가하는 것이 불가능하거나 제한적이었다.
프로시저 추상화로는 프로그램의 표현력을 향상시키는데 한계가 있었고, 이를 보완하기 위해 데이터 추상화의 개념이 나왔다.

추상 데이터 타입과 클래스의 차이는 아래와 같다.

  • 추상 데이터 타입은 오퍼레이션을 기준으로 타입을 묶는다.
  • 클래스는 타입을 명시적으로 정의하고 타입의 유형을 기준으로 오퍼레이션을 지정한다.
    클래스는 타입을 기준으로 절차들을 추상화 함으로써 다형성을 이용할 수 있다.

객체지향은 단순히 클래스를 구현 단위로 사용하는 것을 의미하지 않는다.

클래스를 사용하더라도 타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아니다.
객체지향에서는 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라, 객체가 메시지를 처리할 적절한 메서드를 선택한다.
객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체하고, 이로써 새로운 로직을 추가해도 클라이언트 코드를 수정하지 않아도 된다.
하지만 항상 절차를 추상화하는 객체지향 설계 방식을 따라야하는 것은 아니다.
설계는 변경과 관련된 것이므로 변경의 방향성과 발생 빈도에 따라 결정해야한다.
변경의 방향성이 타입 추가인지, 오퍼레이션 추가인지에 따라 추상 데이터 타입과 클래스를 적절하게 선택해야 한다.
타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며, 그 자체가 목적이 되어서는 안 된다.

인상깊었던 점

"다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며, 그 자체가 목적이 되어서는 안 된다."
이번 주에는 마지막 쪽에 있던 이 말이 가장 와닿았다.
항상 코드를 짤 때 "재사용 할 수 있고 변경에 더 좋은 설계를 고민해야 한다"는 생각을 하는데, 최근 들어 필요해서 객체지향 설계를 적용하는게 아닌, 객체지향 자체가 목적이 된 것 같다는 생각이 자주 들었다.
객체지향적으로 코드를 짤 땐 과연 쓸데 없는 노력을 쓰는건 아닌지 다시 생각을 해보면서 주의해야겠다는 생각이 들었다.

'공부방 > 북스터디' 카테고리의 다른 글

[오브젝트] 9장  (0) 2024.02.20
[오브젝트] 8장  (0) 2024.02.06
[오브젝트] 6장  (0) 2024.01.23
[오브젝트] 5장  (0) 2024.01.15
[오브젝트] 4장  (0) 2024.01.05

+ Recent posts