본문 바로가기

Java/객체지향

오브젝트를 읽고

Getter Setter의 캡슐화 위반

 

getFee 메서드와 setFee 메서드는 Movie 내부에 Money 타입의 fee라는 인스턴스 변수가 존재하는 사실을 퍼블릭 인터페이스에 노골적으로  드러낸다.

  • 추측에 의한 설계 전략 - 엘런 홀 업

단일 책임 원칙 (Single Responsiblility Principle)

 

로버트 마틴은 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 단일 책임 원칙이라는 설계 원칙을 제세했다.

단일 책임 원칙을 한마디로 요약하면 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다. (클래스의 응집도를 높일 수 있는 설계 원칙)

 

책임이라는 말은 "변경의 이유"이다. 단일 책임 원칙에서의 책임은 역할, 책임, 협력에서 이야기하는 책임과는 다르며 변경과 관련된 더 큰 개념의 의미

public class Rectangle {
    private int right;
    private int left;


    public int getRight() {
        return right;
    }

    public void setRight(int right) {
        this.right = right;
    }

    public int getLeft() {
        return left;
    }

    public void setLeft(int left) {
        this.left = left;
    }
}
public class AnyClass {

    public int calculateArea(Rectangle rectangle) {

        return rectangle.getLeft()*rectangle.getRight();
    }
}

이러한 식으로 설계가 되어있다면 만약 단순히 명칭 하나가 바뀌더라도 AnyClass를 수정해야 한다.

또한 이러한 방식의 코드가 어디든 산재할 것이므로 코드의 중복이 발생하기도 한다.

 

차라리 Rectangle 클래스 내부에 놓으면 이러한 문제가 발생하지 않는다.

public class Rectangle {
    private int right;
    private int left;

    public Rectangle(int right, int left) {
        this.right = right;
        this.left = left;
    }
    
    public void Area(int addHeight) {
        this.left += addHeight;
        this.right += addHeight;
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "right=" + right +
                ", left=" + left +
                '}';
    }
}

 


GRASP 패턴

INFORMATION EXPERT 패턴

  • 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하자! - INFROMATION EXPERT(정보 전문가)

정보는 데이터와 다르다. 정보를 알고 있다고 해서 그 정보를 저장할 필요는 없다. 

 

상영이라는 도메인 개념이 영화의 상영 시간, 상영 순번과 같은 다양한 정보를 알고 있다.


Screening이 예매하라는 메시지를 접했을 때 일어나는 작업흐름에 대해서 생각 - 너무 세세하게 고민할 필요 X 

  • 예매하라를 처리하기 위해선 예매가격을 계산하는 작업 필요 (한 편의 가격을 알아야 한다.) - Screening은 이에 대해 알지 못함!
  • 외부 객체에게 도움을 요청해야한다. 

 

가격을 계산하는데 필요한 정보를 알고 있는 전문가는 누구인가?  영화

Movie가 예매하라는 메시지를 접했을 때 일어나는 작업흐름에 대해서 생각 - 너무 세세하게 고민할 필요 X 

  • 영화가 할인 가능한지를 판단해야한다.
  • 판단 이후 할인 요금을 제외한 금액을 계산한다.

Movie는 할인 가능한지 여부를 판단할 수 없다. 

  • 외부 객체에게 도움을 요청해야 한다. 

할인 가능한지 여부를 판단할 수 있는 객체는 누구인가?  할인조건

할인 조건은 자체적으로 할인 여부를 판단하는 데 필요한 모든 정보를 알고 있기 때문에 외부의 도움 없이도 스스로 판단 가능!

 

LOW COUPLING(낮은 결합도) HIGH COHESION(높은 응집도) 패턴

몇 가지 설계 중  한 가지를 선택해야 하는 경우가 빈번

  • ex) 영화를 거쳐서 할인 여부를 판단하는 것보다 상영이 직접 할인 여부를 판단하는 것이 어떨까?

LOW COUPLING 관점

다시 도메인을 보면 영화가 할인 조건과 이미 결합되어 있기 때문에 영화를 할인 조건과 협력하게 되면 전체적으로 결합도를 추가하지 않고 협력을 완성할 수 있다.

  • 상영과 할인조건이 협력할 경우에 새로운 결합도가 추가된다.

HIGH COHESION 관점

영화의 주된 책임은 요금을 계산하는 것이다. 영화 요금을 계산하기 위해서 할인 조건과 협력하는 것은 응집도에 아무런 문제가 없다.

  • 상영이 할인조건과 협력한다면 예매 요금 방식이 변경될 경우 상영도 함께 변경되게 된다.


CREATOR 패턴

영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것

  • 누군가는 Reservation 인스턴스를 생성할 책임을 가져야 한다.

 

  • 상영은 예매 정보를 생성하는데 필요한 영화, 상영 시간, 상영 순번에 대한 전문가
  • 예매 요금을 계산하는데 필수적인 Movie와도 협력관계

시그니처

메시지를 전송할 때 수신자의 의도가 아닌 송신자의 의도를 표현해라!

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;

    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
    }

    private Money calculateFee(int audienceCount) {
        return movie.calculateMovieFee(this).times(audienceCount);
    }

    public LocalDateTime getWhenScreened() {
        return whenScreened;
    }

    public int getSequence() {
        return sequence;
    }
}

Screening은 Movie의 내부 구현에 대한 어떤 지식도 없이 전송할 메시지를 결정한다.


하나 이상의 변경 이유를 가지는 클래스 개선 (1)

해당 클래스는 응집도가 낮다.

 

왜냐면 연관성 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐 저 있기 때문에 서로 다른 시점에 변경이 이루어진다.

코드를 통해 변경의 이유를 파악하는 방법

1. 인스턴스 변수가 초기화되는 시점을 파악!

빨간색과 파란색으로 인스턴스 변수의 초기화 시점이 구별됨을 알 수 있다.

 

2. 메서드들이 인스턴스 변수를 사용하는 방식 파악!

isSatisfiedByPeriod 메서드는 dayOfWeek, startTime, endTime을 사용하고

isSatisfiedBySequence 메서드는 sequence를 사용한다.

 

각각 사용하는 인스턴스 변수가 다르다.

 

 

하나 이상의 변경 이유를 가지는 클래스 개선(2) - 다형성

하나 이상의 이유를 가지는 메서드를 클래스 단위로 쪼갰을 때 부작용이 있는가?

 

있다! 왜냐면 이전에 Movie는 DiscountCoundition 만 협력관계가 있었지만

쪼갬으로써 PeriodCondition과 SequenceCondition 과의 추가적인 협력관계가 형성되었다.

 

개선 1. 목록을 따로 유지

Movie

이러한 방식으로 구현하면 부작용은 만약 할인조건이 추가된다면 List 도 추가하고 해당 조건을 판단하는 메서드도 추가하는 작업이 나타난다. 또한 해당 메서드를 호출하도록 isDiscountable 메서드를 수정해야 한다.

 

개선 2. 다형성

구현을 공유해야 할 필요가 있다면 추상 클래스 

구현을 공유해야 할 필요가 없고 책임만을 정의하고 싶다면 인터페이스

 

위와 같이 암시적인 타입에 따라서 행동을 분기해야 한다면 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도를 해결할 수 있다.

 

결합도 낮추기 : 디미터 법칙

디미터 법칙이란 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하자

낯선 자에게 말을 걸지 말고 오직 인접한 이웃에게만 말을 걸자!

아래 다섯 가지 조건에 해당하는 인스턴스에게만 메시지를 전송하자

  1. this 객체
  2. 메서드의 매개변수
  3. this의 속성
  4. this의 속성인 컬렉션의 요소
  5. 메서드 내에서 생성된 지역 객체

아래의 코드는 너무 강 결합이다. ReservationAgenct, Screening, Movie, DiscountCondition 중 한 개라도 변경된다면 다 같이 바뀌기 때문에 강 결합이라는 것!

디미터의 법칙에서 언급하였듯이 메서드의 인자로 전달된 Screening 인스턴스에게만 메시지를 전달하는 것으로 코드를 변경하였다.
Movie의 내부에 접근하는 대신 Screening에게 직접 요금을 계산하도록 요청

 

 

의존성을 관리하기

1. 의존성 구분

의존성은 객체 간의 협력을 위해 어쩔 수 없이 사용되어야 한다.

  • 왜냐면 협력을 하기 위해서는 그러한 객체가 존재한다는 사실과 객체가 수신할 수 있는 메시지에 대해서도 알고 있어야 하기 때문

PeriodCondition 클래스의 의존성

메서드로 Screening 인스턴스를 받고 속성으로 DayOfWeek, LocalTime의 인스턴스를 받고 있다.

2. 의존성 전이

Screening이 가지고 있는 의존성 : Movie, LocalDateTime, Customer

PeriodCondition이 가지고 있는 의존성 : DayOfWeek, LocalTime, Screening

 

Screening이 가지는 의존성이 PeriodCondition에게 전파된다는 말 (반대도 성립)

3. 런타임 의존성 & 컴파일 타임 의존성

Movie 클래스의 코드 어디에도 AmountDiscountPolicy PercentDiscountPolicy를 찾아볼 수 없다.

Movie클래스는 단지 DiscountPolicy와만 협력한다. 하지만 런타임 시점에서는 Movie는 AmountDiscountPolicy PercentDiscountPolicy와 협력하게 된다.

 

코드 작성 시점의 Movie 클래스는 할인 정책을 구현한 두 클래스에 대해서 알지 못하지만 실행 시점의 Movie 클래스는 두 클래스의 인스턴스와 협력할 수 있게 된다. 

(컴파일 시점의 의존성과 런타임 시점의 의존성이 다르다.)

 

이처럼 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다.

 

 

의존성을 해결하는 세 가지 방법

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

1번 방식

생성자에 아래와 같이 추상 클래스 타입을 파라미터로 선언한다.

이러한 방식으로 선언하면 인스턴스 생성 시에 원하는 인스턴스를 타입으로 받을 수 있다.

2번 방식

인스턴스 생성 시에 파라미터로 원하는 타입을 받지 않고 인스턴스 생성 이후에 setter 메서드를 통해 원하는 타입을 받는다.

해당 방식은 setter메서드로 원하는 인스턴스를 주입받기 이전에 해당 인스턴스 변수를 선언하면 NPE가 발생할 수 도 있다는 단점이 존재한다.

제일 좋은 건 1번 방식과 2번 방식을 병행해서 사용하는 것이다.

1번과 2번의 병행

3번 방식

메서드 인자를 사용하는 방식은 메서드를 실행될 때마다 의존 관계가 매번 달라져야 하는 경우에 유용하다.

 

 

 

클래스 내에서 NEW 연산자는 해롭다

클래스 내에서 new 연산자를 사용해서 인스턴스를 선언하는 것은 결합도를 높이기 때문에 올바른 설계라고 할 수 없다.

 

아래 코드를 보면 Movie 클래스 생성자에 인스턴스를 직접 선언하도록 되어있다.

 

이에 대한 파급 효과는 아래와 같다.

Movie 생성 시에 new 연산자를 사용하여 여러 가지 객체를 생성하기 때문에 결합도가 높아지는 것을 볼 수 있다.

 

그러면 객체의 생성을 누구에게 맡겨야 하는가?

클라이언트에게 맡기자!

인스턴스를 직접 생성하지 말고 클라이언트에게 맡겨버리면 결합도가 낮아진다.

하지만 협력하는 기본 객체를 설정하고 싶을 땐 클래스 내에 new를 선언해도 된다. (Feat. 오버 로딩)

예를 들어서 Movie가 대부분의 경우에는 AmountDiscountPolicy 인스턴스와 협력하고 특별한 경우에만 PercentDiscountPolicy 인스턴스와 협력할 때를 예로 들 수 있다.

 

이때는 오버 로딩을 사용하면 된다.

 

 

 

개방 폐쇄 원칙 (OCP)

개방 폐쇄 원칙이란 

를 의미한다.

 

기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있다니 약간 이상하지 않은가?

개방 폐쇄 원칙의 내재된 의미는 런타임 의존성과 컴파일 타임 의존성에 관한 이야기이다.


개방 폐쇄 원칙의 내재된 의미는 런타임 의존성과 컴파일 타임 의존성에 관한 이야기이다.

컴파일 타임 의존성에서 Movie와 각각의 할인 정책이 직접적으로 의존하지 않기 때문에 할인 정책을 추가하더라도 기존의 코드의 수정 없이 확장이 가능한 것이다. 결국은 추상화가 핵심

상속의 문제점 1

Vector와 Stack

Vector는 임의의 위치에서 요소를 조회 추가 삭제 기능을 제공한다.

stack은 맨 마지막 위치에서만 요소를 추가하거나 제거할 수 있다. 

 

 

유감스럽게도 Stack은 Vector를 상속받고 있다. 어떤 문제가 일어나는지 코드를 통해서 살펴보자.

 

 

stack에서 add를 하면 마지막 요소가 아닌 특정 인덱스의 자리고 들어가게 된다. 그러고 나서 pop을 해버리면 맨 마지막 요소를 바라보기에 

"4th" == stack.pop() 은 성립할 수 없다. 

 

안 쓰면 그만이지 라고 생각할 수 도 있겠지만 Stack 개발자 한 사람의 일시적인 편의를 위해 인터페이스를 사용해야 하는 무수한 사람들이 가슴을 졸여야 하는 상황을 초래한 것은 어떠한 경우에도 정당화될 수 없다.

https://jwdeveloper.tistory.com/188?category=823919

 

 

상속의 문제점 2

상속에서 부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결 (has - a)

합성에서 부모 클래스와 자식 클래스 사이의 의존성은 런타임 시점에 해결 (is -a)

  • 상속은 사용하기는 쉽지만 부모 클래스의 내부 구현에 대해서 알아야 한다는 단점이 존재한다.

기본정책 + 부가 정책

기본정책은 필수적인 정책이고 부가 정책은 사용자가 선택적으로 선택할 수 있는 정책이다.

  1. 기본 정책의 계산 결과에 적용
  2. 선택적으로 적용 가능
  3. 조합이 가능
  4. 부가 정책은 임의의 순서로 적용 가능

 

일단 상속을 함으로써 발생하는 일련의 문제점들을 살펴보자

일반 요금제와 세금 정책의 조합을 상속으로 구현

가장 쉬운 방법은 일반 요금제를 세금 정책이 상속받는 것이다.

위의 코드에서 도드라져 보이는 것은 super이다. 즉 자식 클래스가 부모 클래스의 코드를 직접 참조한다는 것이다. 이로 인해서 부모 자식 간의 결합도가 높아져 버리는 문제점이 생긴다.


이를 해결하기 위해서 최상위 부모 클래스의 Phone에게 afterCalculated라는 추상 메서드를 하나 더 선언한다.

이전보다는 나아졌지만 굳이 afterCalculated을 구현할 필요가 없는 자식 클래스들을 구현을 해야 한다는 문제점이 발생하였다.

(hook method)

이렇게 된다면 문제가 무엇일까?

일단은 afterCalculated라는 중복 코드가 발생하였다는 것이다.

또한 더 중요한 문제는 상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이기 때문에 아래와 같은 문제가 발생한다. 꽤 복잡해 보이지 않는가?

더 나아가 기본 정책이 새로 추가된다면?

위에 보이는 바와 같이 기본 정책 하나를 추가하기 위해서 다섯 개의 클래스가 추가된 점을 볼 수 있다. 얼마나 비효율 적인가..?

왜 이런 문제가 발생했을까?

상속 관계는 컴파일 타임에 결정되고 고정되기 때문에 실행 도중에는 변경할 수 없다. 따라서 여러 기능을 조합해야 하는 설계의 경우에는 상속을 이용하면 모든 조합 가능한 경우의 수에 대한 클래스를 추가해야 하기 때문이다. (클래스 폭발)

 

 

합성을 적용시켜보자

  • 기본정책과 부가 정책을 포괄하는 RatePolicy 인터페이스 추가
  • 기본 정책은 전체 처리 로직이 거의 동일하기 때문에 BasicRatePolicy 추상 클래스로 중복 코드를 담는다.
  • Phone 수정

1. RatePolicy 인터페이스 (기본정책 + 부가 정책)

2. 중복 코드를 담기 위한 BasicRatePolicy 추상 클래스 생성

3. BasicRatePolicy를 구현한 일반 요금제 클래스 생성 (또 하나의 기본정책인 심야 요금제도 비슷한 방식으로 생성)

4. Phone 수정 (인터페이스 -> 클래스)

  • RatePolicy 참조자가 포함되어있는 것에 주목
  • Phone이 다양한 요금 정책과 협력할 수 있어야 하므로 요금 정책의 타입이 RatePolicy 인터페이스 타입으로 선언되어있는 것에도 주목

컴파일 시점에는 Phone이 RatePolicy 인터페이스와 협력하지만 런타임 시점에는 RegularPolicy와 협력한다.

부가 정책 적용

  • Phone 입장에서는 부가정책은 RatePolicy의 역할을 수행하기 때문에 해당 인터페이스를 구현하는 AdditionalRatePolicy를 생성 
    • 또다른 요금정책과 조합할 수 있도록 RatePolicy 타입의 next라는 이름의 인스턴스를 포함시킨다.
    • 해당 인스턴스 값은 컴파일 의존성을 런타임 의존성으로 대체할 수 있도록 RatePolicy 타입의 인스턴스를 인자로 받는 생성자를 포함시킨다.

1. 부가 정책에 관한 추상 클래스인 AdditionalPolicy 생성

2. 세금정책 구현 (기본요금 할인 정책도 추가하는 것도 이와 동일)

상속과 합성 최종적으로 비교해보기

상속

합성

상속에서 기본정책을 추가할 때에는 5개의 클래스를 추가했던 것을 기억하는가? 

이와 다르게 합성을 사용하였을 때는 클래스만 추가하면 된다. 

 

다형성

  • 오버로딩 다형성 : 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 다형성
  • 강제 다형성 : 언어가 지원하는 강제 타입변환 방식의 다형성
    • ex) '+' 연산자는 정수의 연산에서는 덧셈 연산자, 문자열의 연산에는 연결 연산자로 사용된다.
  • 매개변수 다형성 : 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 다형성
    • ex) 제네릭 List<T>
  • 포함 다형성 (서브타입 다형성) : 객체지향 프로그래밍에서 가장 널리 알려진 다형성 
    • 메세지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 의미

 


기억에 남는 문장

  1. 행동과 상태를 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서이다.
  2. 시그니처를 외부에 들어내지 말라, 내부 구현을 외부에 들어내지 말라 - 캡슐화란 변하는 어떤 것이든 감추는 것!

퍼블릭 인터페이스란 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 말한다.
오퍼레이션이란 퍼블릭 인터페이스에 포함된 메시지를 말한다. 보통은 추상화된 시그니처를 의미한다.


201페이지 (원칙의 함정) : 가끔은 시키지 말고 물을 때도 있어야 한다. 소프트웨어 설계에 원칙이란 존재하지 않는다. 항상 상황에 맞게 써야 한다.

옆의 코드가 Screening에 물으니깐 객체지향 설계에 어긋나는 것처럼 보이지만 만약 Screening에게 시켜버린다면  Screening은 기간에 따른 할인 조건을 판단하는 책임을 떠안게 된다.


(257P) 컴파일 타임 vs 런타임

런타임은 단순히 애플리케이션이 실행되는 시점을 의미

컴파일 타임은 실제로 코드가 컴파일되는 시점일 수 도 있고 코드 그 자체의 시점이 될 수 도 있다. 하지만 동적 타입 언어의 경우에는 컴파일이 존재하지 않기 대문에 코드를 작성하는 시점으로 이해하는 것이 좋다.

 

Dry 원칙 : Don't Repeat Yourself

모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을 만한 표현 양식을 가져야 한다. - 즉 코드 안에 중복이 존재해서는 안된다.


 

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


References

오브젝트
국내도서
저자 : 조영호
출판 : 위키북스 2019.06.17
상세보기