[오브젝트] #8. 의존성 관리하기

bien·2024년 11월 5일
0

오브젝트

목록 보기
8/13
  • 좋은 객체지향 APP = 작고 응집도 높은 객체들로 구성
    • 작고 응집도 높은 객체
      • 책임의 초점이 명확 & 한 가지 일을 잘 하는 객체
      • 단독으로 기능 수행이 힘들기 때문에, 다른 객체에게 도움을 요청해야 한다.
        • 이런 요청이 객체 사이의 협력을 낳는다.
  • 과도한 협력은 지식을 요구하고, 이는 객체 사이의 의존성을 낳는다.
    • 협력이 유발하는 지식1. 객체의 존재 여부
    • 협력이 유발하는 지식2. 객체가 수신할 수 있는 메시지
  • 객체지향 설계
    • 객체지향 설계의 핵심
      • 협력에 필요한 의존성은 유지
      • 변경을 방해하는 의존성은 제거
    • 객체지향 설계 기술
      • 의존성을 관리
      • 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술

협력적이면서도 유연한 객체 생성을 위해, 의존성 관리 방법에 대해 알아보자.


01. 의존성 이해하기

변경과 의존성

📖 의존성이란?

  • 의존성
    • 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우, 두 객체 사이의 의존성이 존재하게 된다.
    • 발생 시점에 따라 2가지의 다른 의미를 가진다.
      • 실행 시점의 의존성
        • 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
      • 구현 시점의 의존성
        • 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
    • 의존성은 방향성을 가지며, 항상 단방향이다.
  • 의존성은 변경에 의한 전파 가능성을 암시한다.
    • 두 요소 사이의 의존성은, 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다.[Fowler03b]

💻 예시로 알아보는 의존성; PeriodCondition

PeriodCondition.java

public class PeriodCondition implements DiscountCondition {
	private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
    
    ...
    
    public boolean isSatisfiedBy(Screening screening) {
    	return screening.getStartTime().....
}

PeriodConditionScreening

예시에서 PeriodConditionScreening을 주입받는다.
이 경우, PeriodCondition은 Screening을 의존한다 고 할 수 있다.

  • 실행 시점의 PeriodCondition 정상 동작을 위한 조건
    1. Screening 인스턴스의 존재
    2. ScreeninggetStartTime 메시지를 이해 할 수 있어야 한다.
  • 의존성은 방향성을 가지며, 항상 단방향이다.
    • PeriodConditionScreening에 의존하고 있으며, 그 반대가 아니다.

PeriodCondition의 의존성 종류

  • 인스턴스 변수: DayOfWeek, LocalTime
  • 메서드의 인자: Screening
  • 구현: DiscountCondition
    • 인터페이스에 정의된 오퍼레이션들을 퍼블릭 인터페이스의 일부로 포함시키기 위함

의존성을 표기하는 다양한 방식이 존재하지만, 근본적인 특성은 동일하다.
PeriodCondition은 자신이 의존하는 대상이 변경될 때 함께 변경될 수 있다.

UML과 의존성

  • 실제로 UML에서 다루고 있는 의존성들은 아래와 같다.

  • 이번장에서 다루는 의존성은 UML의 의존관계(dependency)가 아니라, 모든 관계가 가지는 공통적인 특성으로 이야기하고 있다.
  • 의존성은 두 요소 사이에 변경에 의해 영향을 주고받는 힘의 역학관계가 존재한다는 사실에 초점을 맞춘다.

의존성 전이

의존성은 전이될 수 있다.

  • 의존성 전이(Transitive Dependency)
    • 의존하는 존재가 의존대상의 의존에 대해서도 간접적으로 의존하게 되는 것.
    • ex) PeriodConditionScreening에 의존한다.
      • 의존성 전이에 의해 PeriodCondition은 간접적으로 Screening이 의존하는 Movie 의존하게 된다.
  • 의존성의 실제 전이 여부는 변경의 방향캡슐화의 정도에 따라 달라진다.
    • Screening이 의존하고 있는 요소의 구현이나 인터페이스의 변경에 대해 Screening이 효과적으로 캡슐화 하고 있다면, PeriodCondition에게 변경이 전파되지 않을 것이다.
    • 의존성 전이는 변경에 의해 영향이 널리 전파될 수 있다는 경고일 뿐이다.
  • 의존성의 종류
    • 직접 의존성(direct dependency)
      • 한 요소가 다른 요소에 직접 의존
      • 의존여부가 코드에 명시적으로 드러난다.
    • 간접 의존성(indiriect dependency)
      • 직접적인 관계가 존재하지 않지만, 의존성 전이에 의해 영향이 전파되는 경우
      • 코드에 명시적으로 드러나지 않는다.
  • 변경과 관련된 어떤 것에도 의존성 개념을 적용할 수 있다.
    • 의존성의 대상으로 객체, 모듈, 더 큰 규모의 실행 시스템까지 모두 적용 가능하다.
      • 대상을 떠나 의존성의 본질은 변경되지 않는다.
        • 의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.

의존성: 컴파일타임 vs 런타임

컴파일타임 의존성(compie-time depedency)

  • 컴파일 타임; 작성된 코드를 컴파일 하는 시점 or 코드 그 자체.
    • 시간이 아니라 코드의 구조에 초점.
  • 클래스 사이의 의존성을 다룬다.
public class Movie {
	...
    private DiscountPolicy discountPolicy;
    
    public Movie(
    	String title,
        Duration runningTime,
        Money fee,
        DiscountPolicy discountPolicy
    ) {
    	...
        this.discountPolicy = discountPolicy;
    }
    	
    public Money calculateMovieFee(Screening screening) {
    	return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }

}

코드를 작성하는 시점의 Movie에서는 오직 추상 클래스인 DiscountPolicy만 의존한다.
AmountDiscountPolicyPercentDiscountPolicy와 같은 구체적인 클래스에 대해서는 전혀 언급하지 않는다.

런타임 의존성(run-time dependency)

  • 런타임; 애플리케이션 실행 시점
  • 객체 사이의 의존성을 다룬다.

실행 시점의 MovieAmountDiscountPolicyPercentDiscountPolicy 객체와 협력할 수 있어야 한다.

유연하고 재사용 가능한 설계

  • 런타임 의존성과 컴파일타임 의존성은 다를 수 있다.
    • 두 의존성이 다를수록 더 유연하고 재사용 가능한 설계를 만들 수 있다.
  • 동일한 소스코드를 가지고 다양한 실행 구조를 만들 수 있어야 한다.
    • 다양한 인스턴스와 협력하기 위해서는, 협력할 인스턴스의 구체적 클래스를 알아서는 안 된다.
    • 협력할 객체의 클래스를 구체적으로 명시하면 협력의 가능성 자체가 없어진다.

예시에서,

  1. Movie 인스턴스가 두 클래스의 인스턴스와 함께 협력할 수 있게 만드는 방법은, Movie가 둘 중 어느것도 알지 못하게 하는 것이다.
  2. 대신, 둘을 포괄하는 DiscountPolicy에 의존한다.
  3. 런타임에 이 의존이 PercentDiscountPolicy or AmountDiscountPolicy로 대체된다.

객체지향 프로그램의 실행 구조는 소스코드 구조와 일치하지 않는 경우가 종종 있다. 코드 구조는 컴파일 시점에 확정되는 것이고 이 구조에는 고정된 상속 클래스 관계들이 포함된다. 그러나 프로그램의 실행 시점 구조는 협력하는 객체에 따라 달라질 수 있다. 즉, 두 구조는 전혀 다른 별개의 독립성을 갖는다. 하나로부터 다른 하나를 이해하려는 것은 생태계의 동적인 성질을 식물과 동물과 같은 정적 분류 구조를 바탕으로 이해하려는 것과 똑같다. ... 컴파일 시점의 구조와 실행 시점 구조 사이에 차이가 있기 때문에 코드 자체가 시스템의 동작 방법을 모두 보여줄 수 없다. 시스템의 실행 시점 구조는 언어가 아닌 설계자가 만든 타입들 간의 관련성으로 만들어진다. 그러므로 객체와 타입 간의 관계를 잘 정의해야 좋은 실행 구조를 만들어낼 수 있다. [GOF94].

컨텍스트 독립성

핵심 원칙

  • (좋은 설계를 위해) 컴파일타임 의존성과 런타임 의존성이 달라야 한다.
    • 이를 위해, 클래스는 자신이 협력할 객체의 구체적인 클래스에 대해서 알아서는 안 된다.
      • 구체적인 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되기 때문이다.

💻 Movie 클래스의 의존성

나쁜 예시: 컨텍스트 결합도가 높은 경우

public class Movie {
	private PercentDiscountPolicy percentDiscountPolicy; // 구체 클래스에 의존
}
  • 비율할인 정책이라는 특정 컨텍스트에 강하게 결합한다.
    • Movie가 비율 할인 정책이 적용된 영화의 요금 계산을 하는 문맥에서 사용될 것을 가정한다.

좋은 예시: 컨텍스트 독립적인 경우

public class Movie {
	private DiscountPolicy discountPolicy; // 추상 타입에 의존
}
  • 구체적인 할인 정책을 명시하지 않았다.
    • Movie가 할인 정책에 따라 요금을 계산하지만, 구체적으로 어떤 정책을 따르지는 결정하지 않았음을 가정한다.
    • 따라서, 이 경우 구체적인 문맥은 컴파일타임 의존성을 어떤 런타임 의존성으로 대체하느냐에 따라 달라진다.

📖 컨텍스트 독립성

  • 컨텍스트 독립성
    • 클래스가 사용될 특정한 문맥에 최소한의 가정만으로 이뤄져 있는 것.
      • 다른 문맥에서 재사용하기가 더 쉬워진다.
    • 반대로, 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서는 사용하기 어려워진다.
  • 유연한 설계를 위해서는 자신이 실행될 컨텍스트에 대한 정보를 최대한 적게 알아야 한다.
    • 정보가 적으면 적을 수록 더 다양한 컨텍스트에서 재사용될 수 있다.

시스템을 구성하는 객체가 컨텍스트 독립적이라면 해당 시스템은 변경하기 쉽다. 여기서 컨텍스트 독립적이라는 말은 각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못한다는 의미다. 이렇게 되면 행위의 단위(객체)를 가지고 새로운 상황에 적용할 수 있다. ... 컨텍스트 독립성을 따르면 다양한 컨택스트에 적용할 수 있는 응집력있는 객체를 만들 수 있고 객체 구성 방법을 재설정해서 변경 가능한 시스템으로 나아갈 수 있다.[Freeman09].

의존성 해결하기

  • 의존성 해결
    • 컴파일 타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것.
    • 3가지 방법이 있다.
      1. 객체를 생성하는 시점에 생성자를 통해 의존성 해결
      2. 객체 생성 후 setter 메서드를 통해 의존성 해결
      3. 메서드 실행 시 인자를 이용해 의존성 해결

1. by. 생성자

// 클라이언트 코드; AmountDiscountPolicy를 사용하는 경우
Movie avatar = new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(...));

// 클라이언트 코드; PercentDiscountPolicy를 사용하는 경우
Movie avatar = new Movie("스타워즈",
	Duration.ofMinutes(180),
    Money.wons(10000),
    new PercentDiscountPolicy(...));

// Movie의 생성자
// => DiscountPolicy를 받는 생성자를 선언한다.
public class Movie {
	public Movie(
    	String title,
        Duration runningTime,
        Money fee,
        DiscountPolicy discountPolicy
    ){
		...
        this.discountPolicy = discountPolciy;
    } 

2. by. setter 메서드

  • Movie인스턴스를 생성한 후 메서드를 이용해 의존성을 해결한다.
    • Movie에서 DiscountPolicy를 설정할 수 있는 setter 메서드를 제공해야 한다.
  • 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어놓고 싶은 경우에 유용하다.
// 클라이언트 코드
Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmountDisocuntPolciy(...));

// Movie의 setter메서드
public class Movie {
	public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
	}
}
  • 장점
    • 실행 시점에 의존 대상을 변경할 수 있어 설계가 더 유연할 수 있다.
  • 단점
    • 객체 생성 후 협력에 필요한 의존 대상을 설정하므로, 객체 생성과 의존 대상 설정이전까지 객체의 상태가 불완전할 수 있다.
      • NullPointException이 발생 가능하다.
// 장점; 런타임 중 유연한 변경가능
Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
...
avatar.setDiscountPolicy(new PercentDiscountPolicy(...));

// 단점
Movie avatar = new Movie(...);
avatar.calcualteFee(...); // NullPointException 예외 발생
avatar.setDiscountPolciy(new AmountDisocuntPolicy(...));

1+2. 생성자 + setter 혼합

  • 항상 객체 생성 시 의존성 해결해 완전한 상태의 객체를 생성
  • 필요에 따라 setter 메서드를 이용해 의존대상 변경
Movie avatar = new Movie(..., new PercentDiscountPolicy(...));
...
avatar.setDiscountPolicy(new AmountDisocuntPolicy(...));
  • 장점
    • 시스템의 상태 안정적으로 유지
    • 유연성 향상 가능
      • 의존성 해결에 가장 선호되는 방법

3. by. 메서드 인자

  • 협력대상에 대한 지속적인 의존관계가 필요 없고, 메서드 실행 동안에만 일시적으로 의존해도 괜찮은 경우 사용 가능
  • 메서드 실행 시 마다 의존 대상이 매번 달라져야 하는 경우 유용
public class Movie {
	public Money calcualteMovieFee(Screening screening, DisocuntPolicy discountPolicy) {
    	return fee.minus(discountPolicy.calcualteDiscountAmount(screening));
    }
}

대부분의 경우 동일한 객체를 인자로 전달하고 있다면 생성자, setter를 이용하는 방식으로 변경하는 것이 좋다.


02. 유연한 설계하기

📖 용어정리: "의존성" & "결합도"

  • 객체지향 프로그램의 근간은 협력이다.
    • 협력은 2가지 지식을 요구한다.
      1. 서로의 존재
      2. 수행 가능한 책임
        • 지식은 필연적으로 의존성을 낳는다.
  • 의존성의 존재 자체는 협력을 위해 필연적이다.
    • 다만, 의존성의 정도에 따라 협력의 바람직함을 판단할 수 있다.
      • 이 바람직함은 재사용성을 통해 판단 가능하다.
  • 이 의존성의 바람직한 정도는 결합도라는 용어를 통해 표현할 수 있다.
    • 의존성: 두 요소 사이의 관계 유무
      • ex) 의존성이 존재한다, 의존성이 존재하지 않는다
    • 결합도: 두 요소 사이에 존재하는 상대적인 의존성의 정도
      • ex) 결합도가 강하다, 결합도가 느슨하다
  • 용어 정리
    • 바람직한 의존성 = 컨텍스트에 독립적
      • 재사용성이 높다.
      • 느슨한 결합도(loose coupling), 약한 결합도(week coupling)
    • 바람직하지 않은 의존성 = 컨텍스트에 강하게 결합
      • 재사용성이 낮다.
      • 단단한 결합도(high coupling), 강한 결합도(strong coupling)

지식이 결합을 낳는다

  • 서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.
    1. MoviePercentDiscountPolicy에 의존하는 경우
      • Movie비율 할인 정책에 따라 할인 요금을 계산할 것이라는 사실을 알고있다.
    2. MovieDiscountPolicy에 의존하는 경우
      • Movie할인 요금을 계산할 것이라는 사실만 알고있다.
        • 알아야 하는 지식의 양이 더 적다. => 결합도가 더 느슨해진다.
  • 더 많이 알 수록 더 많이 결합된다.
    • 많이 안다는 것은, 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다.
      • 어울리지 않는 컨텍스트에서 사용할 유일한 방법은 수정 뿐이다.
  • 느슨한 결합도 유지
    • 협력하는 대상에 대해 필요한 정보 외에 최대한 감추려는 노력이 필요하다.
      • 이는 추상화를 통해 달성할 수 있다.

추상화에 의존하라

  • 추상화
    • 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법
    • 현재 다루는 문제를 해결하는 데 불필요한 정보를 감출 수 있다.
      • 지식의 양을 줄일 수 있어 결합도를 느슨하게 유지할 수 있다.
  • 아래로 갈 수록 클라이언트가 알아야 하는 지식의 양이 적고, 결합도가 느슨하다.
    1. 구체 클래스 의존성 (concrete class dependency)
    2. 추상 클래스 의존성 (abstract class dependency)
    3. 인터페이스 의존성 (interface dependency)
  • 의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다.
    • 추상적 = 실행 컨텍스트에 대해 알아야하는 정보가 더 낮음을 의미.

명시적인 의존성

🚫 권장하지 않는 의존성

public class Movie {
	...
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee) {
    	...
        this.discountPolicy = new AmountDiscountPolicy(...);
    }
}
  • 문제상황
    • 필드는 추상화된 클래스로 선언했지만, 생성자에서 구체클래스인 AmountDiscountPolicy의 인스턴스를 직접 생성해서 대입하고 있다.
  • 문제상황 분석
    • 느슨한 결합도를 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족하다.
    • 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다.
  • 해결방법
    • 앞서 설명한 의존성 해결방법을 사용한다.
      • 3가지 방식: 생성자, setter메서드, 메서드 인자 사용

✅ 생성자를 이용한 의존성 해결

public class Movie {
	...
    private DiscountPolicy discountPolicy;
    
    public Movie(
    	String title, 
        Duration runningTime, 
        Money fee, 
        DiscountPolicy discountPolicy
    ) {
    	...
        this.discountPolicy = discountPolicy;
    }
}    
  • 생성자의 인자가 추상 클래스 타입으로 선언되었다.
    • 객체 생성 시 생성자의 인자로 DiscountPolicy의 자식 클래스 중 어떤 것이라도 전달 가능하다.

위 두 예시의 차이점은, 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부다.

📖 명시적인 or 숨겨진 의존성

  • 명시적인 의존성(explicit dependency)
    • 의존성을 명시적으로 퍼블릭 인터페이스에 노출시키는 방식
    • 의존성 해결의 3가지 방법(생성자, setter, 메서드 인자) 모두 해당한다.
      • MovieDisocuntPolicy에 의존한다는 사실을 Movie의 퍼블릭 인터페이스에 들어낸다.
    • 의존성이 명시적으로 드러나면 실행 컨텍스트에 적절한 의존성을 선택할 수 있으므로, 코드를 직접 수정해야 하는 위험을 피할 수있다.
  • 숨겨진 의존성(hideen dependency)
    • 의존성이 퍼블릭 인터페이스에 표현되지 않는 방식
      • MovieDiscountPolicy에 의존한다는 사실이 감춰진다.
    • 의존성이 명시적이지 않으므로, 의존성 파악을 위해 내부 구현을 직접 살펴봐야만 한다.
    • 클래스를 재사용하기 위해서는 내부 구현을 직접 변경해야 한다.
      • 코드 수정은 언제나 버그 발생 가능성을 내포하므로, 오류 가능성이 높다.

✒️ 의존성을 명시적으로 표현해라

  • 의존성은 명시적으로 표현
    돼야 한다.
    • 의존성을 구현 내부에 숨겨두지 마라.
    • 명시적으로 의존성을 표현해야지만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 런타임 의존성으로 교체할 수 있다.
  • 경계해야 할 것은 의존성 그 자체가 아니라 의존성을 감추는 것이다.
    • 숨겨져 있는 의존성을 밝은 곳으로 드러내서 널리 알려라.

new는 해롭다

💣 new 연산자 = 결합도 증폭기

  • new 연산자는 2가지 지식을 필수로 요구해 클래스간의 결합도를 높인다.
    1. 구체 클래스의 이름
      • new 연산자는 추상화가 구체 클래스에 의존시킨다.
    2. 생성 시 요구되는 인자 목록
public class Movie {
	...
    private DiscountPolicy discountPolicy
    
    public Movie(String title, Duration runningTime, Money fee) {
    	...
        this.discountPolicy = new AmountDiscountPolicy(Money.wons(800),
        					    new Sequence Condition(1),
                                new Sequence Condition(10),
                                new PeriodCondition(DayOfWeek.MONDAY,
                                	LocalTime.of(10, 0), LocalTime.of(11, 59)),
                                new PeriodCondition(DayOfWeek.THURSDAY,
                                	LocalTime.of(10, 0), LocalTime.of(20, 59)));
	}
}
  • (예시에서 확인 가능한) new가 생성한 불필요한 결합도
    1. 구체 클래스에 대한 결합
    2. 협력한 클래스 인스턴스 생성을 위해 필요한 인자 목록
    3. 인자들의 순서
    4. 인자로 사용되는 구체 클래스의 추가 의존성

🛠️ 해결방법 = 인스턴스 생성과 사용의 분리

인스턴스 생성로직과 생성된 인스턴스를 사용하는 로직을 분리하면 된다.
앞서 언급된 의존성 해결방법을 통해 분리한다.

  • 인스턴스 사용
    • Movie는 입력받은 인스턴스 사용의 책임만 가진다
  • 인스턴스 생성
    • Movie의 클라이언트가 인스턴스를 생성하고 주입한다.

생성자를 통한 의존성 분리

public class Movie {
	...
    private DiscountPolicy discountPolicy
    
    public Movie(String title, Duration runngingTime, Money fee, DiscountPolicy discountPolicy) {
    	...
        this.discountPolicy = discountPolicy;
    }
}

Movie 클라이언트 코드

Movie avatar = new Movie("아바타",
					Duration.ofMinutes(120),
                    Money.wons(10000),
                    new AmountDiscountPolicy(Money.wons(800),
        					new Sequence Condition(1),
                            new Sequence Condition(10),
                            new PeriodCondition(DayOfWeek.MONDAY,
                                LocalTime.of(10, 0), LocalTime.of(11, 59)),
                            new PeriodCondition(DayOfWeek.THURSDAY,
                                LocalTime.of(10, 0), LocalTime.of(20, 59)));

💡 유연한 설계

  1. 사용과 생성의 책임을 분리한다.
  2. 의존성을 생성자에 명시적으로 드러낸다.
  3. 구체 클래스가 아닌 추상 클래스에 의존한다.
    • 이때, 객체를 생성하는 책임은 객체 내부가 아니라 클라이언트가 수행한다.

가끔은 생성해도 무방하다

클라이언트에서 코드가 과도하게 중복되는 것을 막기위해, 객체 인스턴스 기본값을 사용하고 싶은 경우가 있을 수 있다. 이는 결합도와 사용성의 트레이드 오프로, 구체 클래스와의 의존으로 결합도가 증가되더라도 클래스의 사용성이 더 중요하다면, 아래의 방법들을 사용할 수 있다.

1. 생성자 체이닝

public class Movie {
	...
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime) {
    	this(title, runningTime, new AmountDiscountPolicy(...));
    }
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    	...
        this.discountPolicy = discountPolicy;
    }
}

2. 메서드 오버로딩

public class Movie {
	public Money calcualteMovieFee(Screening screening) {
    	return calculateMovieFee(screening, new AmountDiscountPolicy(...)));
    }
    
    puublic Movie calcualteMovieFee(Screening screening, Discount{olicy disscountPolicy) {
    	returh fee.minus(discountPolicy.calculateDiscountAmount(screenig));
    }
}

표준 클래스에 대한 의존은 해롭지 않다

  • 의존성의 위험은 변경 가능성에 의해 발생한다.
    • 따라서 변경 확률이 매우 낮다면, 클래스의 의존성은 크게 문제가 되지 않는다.
      • 특히 JDK에 포함된 표준 클래스가 이 부류에 속한다.
public abstract class DiscountPolicy {
	private List<DiscountCondition> conditions = new ArrayList<>();
}

JDK 표준 컬렉션 라이브러리의 ArrayList의 변경 가능성은 0에 가깝다.
따라서 직접 인스턴스를 생성하더라도 문제가되지 않는다.

이 경우에도 가능한 한 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하다.
예시에서는 List를 사용하여 대체의 범위를 높혔다.
뿐만 아니라, 의존성의 영향이 적은 경우에도 추상화에 의존하고 의존성을 명시적으로 드러내는 것은 좋은 습관이다.

public abstract class Movie {
	private List<DiscountCondition> conditions = new ArrayList<>();
    
    public void switchConditions(List<DiscountCondition> conditions) {
    	this.conditions = conditions;
    }
}

♻️ 컨텍스트 확장하기

지금까지 Movie의 유연한 설계에 대해 알아봤다.
이제 실제 추가적인 요구사항을 기반으로 Movie의 재사용성을 확인해보자.

1. 할인혜택을 제공하지 않는 경우

⛔️ discountPolicy에 null 할당하기

public class Movie {
	public Movie(String title, Duration runningTime, Money fee) {
    	this(title, runningTime, fee, null);
    }
    
	public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    	...
        this.discountPolicy = discountPolicy;
    }
    
    public Money calculateMovieFee(Screening screening) {	
    	if (discountPolicy == null) {
        	retrun fee;
        }
        
        return fee.minus(discountPolicy.calculateDiscountAmont(screening));
    }
}        
  • 구현 방법
    • 생성자 체이닝 기법
    • 할인 정책을 적용하지 않는 경우를 포함하기 위해 코드 내부에서 discountPolicynull인지 체크하는 요소를 추가했다.
  • 💣 문제상황
    • 지금까지의 MovieDiscountPolicy 사이의 협력 방식에 예외 케이스가 추가된다.
    • 이 예외 케이스를 처리하기 위해 Movie 내부 코드를 직접 수정해야 했다.
      • 어떤 경우든 코드 내부를 직접 수정하는 것은 버그의 발생 가능성을 높인다.
  • 🛠️ 해결방법
    • 할인 정책이 존재하지 않는다는 사실을 기존의 협력 방식에 통합시킨다.
    • 즉, 할인 정책이 존재하지 않는다는 사실을 할인 정책의 한 종류로 간주한다.

할인비적용 정책 추가

public class NoneDiscountPolicy extends DiscountPolicy {
	@Overrie
    protected Money getDisocuntAmount(Screening screening) {
    	return Money.ZERO;
    }
}

// 클라이언트 코드
Movie avatar = new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(10000),
    new NoneDiscountPolic());

2. 다수의 할인 정책을 중복해서 적용하는 경우

  • 📋 요구사항
    • 중복할인 = 금액 할인 정책, 비율 할인 정책의 동시 사용
      • 이를 위해서는 Movie가 하나 이상의 DiscountPolicy와 협력할 수 있어야 한다.
  • 구현방법
    1. MovieList<DiscountPolicy>를 인스턴스 변수로 갖게하는 것.
      • ❌ 이 방법은 기존의 할인 정책의 협력 방식과는 다른 예외 케이스를 추가하게 만든다.
    2. 기존의 협력 방식에 중복 할인 정책을 추가한다.
      • ✅ 기존의 협력방식을 수정하지 않고 여러개의 할인 정책을 적용할 수 있다.

OverlappedDiscountPolicy.java

public class OverlappedDiscountPolicy extends DiscountPolicy {
	private List<DiscountPolicy> discountPolicies = new ArrayList<>();
    
    public OverlappedDiscountPolciy(DiscountPolicy ... discountPolicies) {
    	this.discountPolicies = Arrays.asList(discountPolicies);
    }
    
    @Override
    protected Money getDiscountAmount(Screening screening) {
    	Money result = Money.ZERO;
        for (DiscountPolicy each: discountPolicies) {
        	result = result.plus(each.calculateDiscountAmount(screening));
        }
        return result;
   }
}

// 클라이언트 코드
Movie avatar = new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(1000)m
    new OverlappedDiscountPolicy(
    	new AmountDiscountPolicy(...),
        new PercentDiscountPolicy(...)));

🌈 Movie의 유연한 설계

설계가 유연할 수 있었던 이유는

  1. MovieDiscountPolicy라는 추상화에 의존하고
  2. 생성자를 통해 DisocuntPolicy에 대한 의존성을 명시적으로 드러냈으며
  3. new와 같이 구체 클래스를 직접적으로 다뤄야하는 책임을 Movie 외부로 옮겼기 때문이다.

결하도를 낮춤으로써 얻게되는 컨택스트 확장이라는 개념이 유연하고 재사용 가능한 설계의 핵심이다.

조합 가능한 행동

  • 다양한 컨텍스트에서 Movie를 재사용할 수 있었던 것은, 추상화를 기반으로 교체기능을 제공했기 때문이다.
    • 이 기능을 기반으로, 코드의 변경 없이도 다양한 문맥을 제공할 수 있엇다.
    • 협력하는 객체의 종류에 따라 객체의 행동이 달라지는 것
      • PercentDiscountPolicy: 비율 할인 정책
      • AmountDiscountPolicy: 금액 할인 정책
      • OverlappedDiscountPolicy: 중복 할인 정책
      • NoneDiscountPolicy: 할인정책 비적용
  • 유연하고 재사용 가능한 설계
    • ❌ 객체가 어떻게(how)하는지를 장확하게 나열
    • 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들의 조합
      • 따라서, 인스턴스 생성 코드만으로 객체가 하는 일을 빠르게 파악 가능하다.
        • 어떤 객체와 연결됐는지를 보는 것 만으로 객체의 행동을 예상하고 이해하기 쉽기 때문
        • 즉, 선언적으로 객체의 행동을 정의할 수 있다.

생성자를 통한 코드 이해

new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(1000),
    new AmountDiscountPolicy(Money.wons(800),
    	new SequenceCondition(1),
        new SequenceCondition(10),
        new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(12,0)),
        new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(21,0)));
  1. Movie 생성 코드로부터 아래의 사항들을 알 수 있다.
    • 첫번째 상영, 10번째 상영, 월요일 10~12시 사이, 목요일 10시~21시 사이 상영의 경우 800원을 할인한다.
  2. 인자를 변경하는 것 만으로 새로운 조건들을 적용할 수 있다.

📌 결론

  • 유연하고 재사용 가능한 설계는
    • 작은 객체들의 행동을 조합함으로서 새로운 행동을 이끌어낼수 있는 설계다.
    • 객체들의 조합을 선언적으로 표현하여, 객체들이 무엇을 하는지를 표현하는 설계다.
      • 이를 위한 핵심은, 의존성을 관리하는 것이다.

객체지향 시스템은 협력하는 객체들의 네트워크로 구성돼 있다. 시스템은 객체를 생성해 서로 메시지를 주고 받을 수 있게 조립하는 과정을 거쳐 만들어진다. 시스템의 행위객체의 조합(객체의 선택과 연결 방식)을 통해 나타나는 특성이다.

따라서 시스템에 포함된 객체의 구성을 변경해(절차적인 코드를 작성하기보다는 인스턴스 추가나 제거 또는 조합을 달리해서) 시스템의 작동 방식을 변경할 수 있다. 시스템을 이런 방식으로 구축하면 방법(how)이 아니라 목적(what)에 집중할 수 있어 시스템의 행위를 변경하기가 더 쉽다[Freeman09].


Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글