Code/OOP

[오브젝트] 9. 유연한 설계

noahkim_ 2025. 4. 5. 14:30

조영호 님의 "오브젝트" 책을 정리한 글입니다.

 

1. 개방-폐쇄 원칙

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

개방-폐쇄 원칙
  • 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계
  • 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조

 

추상화가 핵심이다

항목 내용
핵심
추상화에 의존하는 것
확장 방식
상속을 통해 생략된 부분을 구체화
확장의 기반
추상화는 생략된 부분을 통해 확장의 여지를 남김
폐쇄를 가능하게 하는 요소
의존성의 방향
의존성 방향이 향해야 할 곳
구현이 아닌 추상화 계층

 

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

    public DefaultDiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    public Money calculateDiscountAmount(Screening screening) {
        for (DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }

        return screening.getMovieFee();
    }

    abstract protected Money getDiscountAmount(Screening screening);
}
  • 변하지 않는 부분: 할인 여부를 판단하는 로직
  • 변하는 부분: 할인된 요금을 계산하는 방법

 

2. 생성 사용 분리

개방-폐쇄원칙을 따르는 설계를 하기 어려운 경우

  • 알아야 하는 지식이 많아야 할 경우
  • 한번에 생성과 사용이란 두가지 책임을 가질 경우

 

해결 방법
항목 설명
대안 생성 책임을 클라이언트에게 옮기기
(클라이언트는 객체 생성 책임을 가지므로 특정 컨텍스트에 묶여도 무방)
문제점 - 객체 생성에 대한 구체적인 지식이 클라이언트에 노출됨
- 클라이언트가 구현에에 의존하게 됨 (결합도 증가)
- 전달할 인자가 많아지면 객체 생성 실수가 일어날 수 있음

 

FACTORY 추가하기

FACTORY 패턴
  • 생성 책임 모두를 FACTORY에 옮기기
  • 클라이언트는 사용과 관련된 책임만 지고 어떤 관련지식도 가지지 않을 수 있음
더보기
public class Factory {
    public Movie createAvatarMovie() {
        return new Movie(
                "아바타", 
                Duration.ofMinutes(120), 
                Money.wons(10000), 
                new AmountDiscountPolicy(new Money(new BigDecimal(1000)), new DiscountCondition())
        );
    }
}
public class Client {
    private Factory factory;

    public Client(Factory factory) {
        this.factory = factory;
    }

    public Money getAvatarFee() {
        Movie avatar = factory.createAvatarMovie();
        return avatar.getFee();
    }
}

 

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

개념 설명
표현적 분해 도메인 모델을 구성하는 개념 중 적절한 후보 객체를 찾아 그들에게 책임을 위임함
행위적 분해
(순수한 가공물에게 책임 할당)
도메인 객체가 없거나 어울리지 않는 경우, 도메인과 무관한 인공적 객체에게 책임을 할당함
(ex: Helper, Policy, Strategy 등)
  • 도메인 추상화를 기반으로 부족한 점은 컴퓨터 내의 인공적인 객체를 이용해 보충하여 시스템을 설계하기
    • 우리의 목표는 요구사항을 효과적으로 응답할 수 있는 좋은 설계를 만드는 것
    • 현실세계를 그대로 모방하는 것이 아님 
  • 도메인 개념만 고집할 경우 오히려 낮은 응집도, 높은 결합도, 낮은 재사용성 문제가 발생할 수 있음

 

3. 의존성 주입

숨겨진 의존성은 나쁘다

  • 의존성 해결 방법은 두가지로 나뉨

 

의존성 주입
  • 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달하는 방법
  • 생성자 주입 / setter 주입 / 메서드 주입

 

service locator
  • 의존성을 해결할 객체들을 보관하는 일종의 저장소
  • 객체가 직접 service locator에게 의존성을 요청함
항목 설명
단점
- 의존성을 감춤: 코드상에서 어떤 의존성을 사용하는지 명확히 보이지 않음
- 디버깅 어려움: 의존성 해결 문제가 컴파일 타임이 아닌 런타임에 발생함
- 테스트 어려움: Locator는 전역 상태 공유 → 테스트 간 고립이 어려움 (테스트 독립성 위반)
원인 - 숨겨진 의존성: 클래스 내부에서 의존 객체를 요청하므로, 외부에서는 어떤 의존성이 필요한지 알기 어려움 (캡슐화 위반)

 

더보기
public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;

    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;

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

    public static DiscountPolicy discountPolicy() {
        return soleInstance.discountPolicy;
    }

    public static void provide(DiscountPolicy discountPolicy) {
        soleInstance.discountPolicy = discountPolicy;
    }

    private ServiceLocator() {}
}
ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie(
    	            "아바타",
	                Duration.ofMinutes(120),
                	Money.wons(10000)
                );

 

4. 의존성 역전 원칙

추상화와 의존성 역전

  • 의존성은 변경의 전파와 관련된 것
  • 설계는 변경의 영향을 최소화하도록 의존성을 관리해야 함

 

의존성 역전 법칙
항목 설명
정의 구체적인 사항이 추상화에 의존해야 함. (하위 모듈이 상위 모듈을 의존하게 만들어야 함)
핵심 설계 원칙 추상화에 의존해야 함 (구현 클래스 X)
장점 - 변경 용이: 하위 수준의 변경으로 인해 상위 수준이 영향 받는 것을 방지
- 재사용성: 상위 수준 모듈을 다양한 컨텍스트에서 재사용 가능

 

의존성 역전 원칙과 패키지

  • 인터페이스의 소유권도 역전이 해당됨

 

SEPARATED INTERFACE 패턴
항목 설명
정의 추상화와 구현체를 서로 다른 패키지에 분리해서 배치하는 설계 패턴
구현 방식 1. 인터페이스의 소유권을 클라이언트측으로 옮기기
2. 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모으기
장점 - 재사용성: 재사용하기 위해서 단지 구현체 패키지만 import하면 됨
- 컴파일 시간 단축: 새로운 구현체 추가 시, 구현체 패키지만 컴파일해서 배포하면 됨
Client: Movie, DiscountPolicy
Server: AmountDiscountPolicy, PercentDiscountPolicy

 

5. 유연성에 대한 조언

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

  • 설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어짐


협력과 책임이 중요하다