Code/OOP

[오브젝트] 4. 설계 품질과 트레이드오프

noahkim_ 2025. 4. 3. 15:59

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


1. 데이터 중심의 영화 예매 시스템

데이터를 준비하자

데이터 중심의 설계
  • 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할하는 방법
  • 객체 내부에 저장해야 하는 "데이터가 무엇인가"를 묻는 것으로 시작됨

 

예제
더보기
public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;
    
    private MovieType movieType;    
    private Money discountAmount;
    private double discountPercent;
    
    public getFee() { return fee; }
    public setFee(Money fee) { this.fee = fee; }
    
    // getter, setter
}
public class DiscountCondition {
    private DiscountConditionType type;
    
    private int sequence;
    
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;    
}
항목 Movie DiscountCondition
역할 영화 정보 및 할인 조건 포함 할인 조건을 정의하는 클래스
구성 요소 discountConditions 리스트 포함 type 변수(할인 조건의 종류) 포함
할인 정책 관리 할인 금액과 할인 비율을 직접 정의 할인 조건을 저장하고 관리

 

영화를 예매하자

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        Movie movie = screening.getMovie();

        boolean discountable = false;
        for (DiscountCondition condition : movie.getDiscountConditions()) {
            if (condition.getType() == DiscountConditionType.PERIOD) {
                discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek()) && 
                        condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                        condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
            } else {
                discountable = condition.getSequence() == screening.getSequence();
            }
            
            if (discountable) break;
        }
        
        Money fee;
        if (discountable) {
            Money discountAmount = Money.ZERO;
            switch (movie.getMovieType()) {
                case AMOUNT_DISCOUNT:
                    discountAmount = movie.getDiscountAmount();
                    break;
                case PERCENT_DISCOUNT:
                    discountAmount = movie.getFee().times(movie.getDiscountPercent());
                    break;
                case NONE_DISCOUNT:
                    discountAmount = Money.ZERO;
                    break;
            }
            
            fee = movie.getFee().minus(discountAmount);
        } else {
            fee = movie.getFee();
        }
        
        return new Reservation(customer, screening, fee, audienceCount);
    }
}
  • 할인 가능 여부 확인
  • 적절한 할인 정책에 따라 예매 요금 계산

 

2. 설계 트레이드오프

캡슐화

개념 설명
정의
외부에서 알 필요 없는 부분을 감추고, 필요한 부분만 공개하는 것
목적
- 내부 구현을 숨기고 외부에서 접근할 수 없도록 보호
- 외부에서는 인터페이스만 사용하여 내부 구현 변경 영향을 최소화
구성 요소
- 상태(State): 객체가 관리하는 데이터 (필드, 변수)
- 행동(Behavior): 객체가 수행하는 기능 (메서드)
이점
- 내부 구현 변경 가능 (외부 영향 최소화)
- 코드 유지보수 용이
- 인터페이스 안정성 확보 (외부와의 의존성 감소)
설계 원칙
- 내부 구현은 자주 변경됨 → 숨기는 것이 좋음
- 외부 인터페이스는 변경 가능성이 적음 → 신중하게 설계

 

응집도와 결합도

개념 설명
응집도 (Cohesion)
모듈 내부 요소들이 얼마나 밀접하게 연관되어 있는지
결합도 (Coupling) 한 모듈이 다른 모듈에 얼마나 의존하는지 (다른 모듈에 대해 얼마나 많은 지식을 가지고 있는가)

 

좋은 설계 원칙
  • 변경이 용이하고 유지보수성이 뛰어남
  • 높은 응집도: 오직 하나의 모듈만 수정하면 됨 (코드 변경 용이)
  • 낮은 결합도: 독립적으로 변경이 가능함 (영향 범위 최소화)

 

캡슐화와의 관계
  • 캡슐화를 잘 지키면
    • 응집도가 높아지고: 모듈 내부의 관련 기능이 강하게 결합됨
    • 결합도가 낮아짐: 모듈 간 의존성이 줄어 변경에 강해짐

 

3. 데이터 중심의 영화 예매 시스템의 문제점

캡슐화 위반

  • Movie의 getter, setter 메서드를 외부에 노출
    • 내부 데이터(Movie)의 존재 여부를 드러냄
    • 원인은 책임이 아닌 데이터에 초점을 맞추었기 때문
  • 캡슐화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있음

 

높은 결합도

개념 설명
정의
한 객체가 다른 객체의 내부 구현에 의존하여 강하게 연결된 상태
특징
- 내부 구현이 노출됨 → 클라이언트가 내부 구현을 직접 참조
- 변경 영향이 큼 → 하나의 변경이 여러 곳에 영향을 미침
예시
- ReservationAgency.reserver()가 Movie.getFee()를 직접 호출하여 의존함
- ReservationAgency가 DiscountCondition 등 여러 데이터 객체에 직접 의존
문제점
🚨 변경이 어려움 → 특정 객체를 변경하면 관련된 모든 객체도 변경해야 함
🚨 유지보수 비용 증가 → 수정할 때 영향 범위를 파악해야 함
해결 방법
✅ 캡슐화 강화 → 내부 구현을 숨기고 인터페이스만 공개
✅ 의존성 줄이기 → 역할 기반 설계 및 인터페이스 활용 (다형성 사용)

 

낮은 응집도

개념 설명
정의
모듈 내부의 요소들이 서로 관련성이 낮은 상태
특징
- 코드 수정 이유가 다양함 → 여러 책임이 한 모듈에 섞여 있음
- 불필요한 영향 발생 → 변경이 필요 없는 코드도 영향을 받음
예시
- ReservationAgency가 할인 정책과 할인 조건을 동시에 관리
→ 새로운 할인 정책 추가 시, 할인 조건 코드에 영향을 미칠 수 있음

- MovieType 열거형에 새로운 할인 정책 추가 시
 관련된 분기문을 추가해야 함
문제점
🚨 확장성 저하 → 새로운 기능 추가 시, 여러 모듈을 수정해야 함
🚨 유지보수 어려움 → 하나의 변경이 연관 없는 코드까지 영향을 미침
해결 방법
✅ 단일 책임 원칙 (SRP) 준수 → 각 모듈이 하나의 역할만 수행하도록 분리
✅ 책임 기반 분리 → 할인 정책과 할인 조건을 별도 클래스로 분리

 

 

4. 자율적인 객체를 향해

캡슐화를 지켜라

  • 낮은 응집도와 높은 결합도를 막기 위해서는 캡슐화를 지켜야 함

 

스스로 자신의 데이터를 책임지는 객체

  • 객체라는 단위에 상태와 행동을 하나로 묶는것은 자신의 상태를 스스로 처리할 수 있게 하기 위함
    • 내부 데이터보다 객체의 책임을 정의하는 것이 더 중요

 

  • 객체를 설계할 때 이 객체가 어떤 데이터를 포함해야 하는 가라는 질문은 두가지 질문으로 분리해야 함
    • 이 객체가 어떤 데이터를 포함해야 하는가?
    • 이 객체가 데이터에 대해 수행해야 하는 행동은 무엇인가?

 

"이 객체가 데이터에 대해 수행해야 하는 행동은 무엇인가?" 라는 질문에 대해 메서드를 추가
더보기
public class DiscountCondition {
    private DiscountConditionType type;
    private int sequence;

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
    
    // getter..
    
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
        if (type != DiscountConditionType.PERIOD) throw new IllegalArgumentException();
        
        return this.dayOfWeek.equals(dayOfWeek) &&
               this.startTime.compareTo(time) <= 0 &&
               this.endTime.compareTo(time) >= 0; 
    }
    
    public boolean isDiscountable(int sequence) {
        if (type != DiscountConditionType.SEQUENCE) throw new IllegalArgumentException();
        
        return this.sequence == sequence;
    }
}
public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;

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

    // getter...

    public Money calculateAmountDiscountedFee() {
        if (movieType != MovieType.AMOUNT_DISCOUNT) throw new IllegalArgumentException();

        return fee.minus(discountAmount);
    }

    public Money calculatePercentDiscountedFee() {
        if (movieType != MovieType.PERCENT_DISCOUNT) throw new IllegalArgumentException();

        return fee.minus(fee.times(discountPercent));
    }

    public Money calculateNoneDiscountedFee() {
        if (movieType != MovieType.NONE_DISCOUNT) throw new IllegalArgumentException();

        return fee;
    }
}
  • Screening, ReservationAgency의 결합도 감소

 

5. 하지만 여전히 부족하다

캡슐화 위반

예시 문제점
DiscountCondition.isDiscountable(DayOfWeek, LocalDate) 객체 내부에 해당 인스턴수 변수가 포함되어 있는것을 알 수 있음
내부 상태가 변경되면 메서드도 수정되어 클라이언트도 함께 수정해야 함
Movie.calculateXXXDiscountFee() 할인 정책이 세가지 존재하는 것을 드러냄 (금액, 비율, 미적용)
새로운 정책이 추가되거나 제거된다면 클라이언트도 함께 수정해야 함

 

 

높은 결합도

낮은 응집도

 

6. 데이터 중심 설계의 문제점

데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다

  • 데이터 주도 설계는 너무 이른 시기에 내부 구현에 초점을 맞추게 함 (설계 시작부터 데이터에 관해 결정하도록 강요)
  • 이러한 순서의 캡슐화는 데이터에 대한 지식이 인터페이스에 고스란히 드러나게 함

 

데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다

  • 협력이라는 문맥안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 중요
  • 객체지향 설계의 무게중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 함
  • 중요한 것은 다른 객체와 협력하는 방법