| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- blockingqueue
- rdb
- OOP
- 임베딩
- redisstreams
- jedis
- aof
- Kafka
- redis
- retry
- 레디스
- god object
- 장애복구
- DLT
- springboot
- redissearch
- 레디스스트림
- 데이터유실방지
- 테스트코드
- 메시지브로커
- 비동기처리
- 시맨틱캐싱
- 배치처리
- 코사인
- 메세지브로커
- 객체지향적사고
- 자연어캐싱
- SaaS
- 백엔드
- 마케팅 #퍼플카우 #새스고딘 #혁신 #독서 #이북
- Today
- Total
pandaterry's 개발로그
[책 | 오브젝트] CH5. 책임 할당하기 본문

CH5. 책임 할당하기
도입부
공감이 되는 문장
책임 할당하는 것의 가장 어려운 지점은 어떤 객체에게 어떤 책임을 할당할지 정하는 것이다.
객체지향 설계에서 가장 중요한 것은 올바른 책임 할당입니다. 하지만 어떤 객체에게 어떤 책임을 할당해야 할지 결정하는 것은 쉽지 않습니다. 책임 할당 작업 자체가 트레이드오프 과정이며, 다양한 관점에서 설계를 평가할 수 있어야 합니다.
이번 장에서는 GRASP 패턴을 사용하여 책임을 올바르게 할당하는 방법을 학습합니다. GRASP는 General Responsibility Assignment Software Pattern의 약자로, 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합입니다.
1. 책임 주도 설계를 향해
데이터보단 행동을 먼저 결정
초보자들은 종종 행동보다 상태에만 초점을 맞춥니다. 하지만 데이터에 초점을 맞추면 객체의 캡슐화가 약화되기 때문에 낮은 응집도와 높은 결합도를 가진 객체들로 넘쳐나게 됩니다.
객체지향 설계에서는 데이터가 아닌 행동을 먼저 결정해야 합니다.
협력이라는 문맥 안에서 책임을 결정
협력에 적합한 책임이란 전송자에게 적합한 책임을 의미합니다. 협력은 참고로 전송자와 수신자가 있습니다. 클라이언트 의도에 맞는 책임을 할당해야 합니다.
협력에 적합한 책임을 할당하려면 메시지를 선택한 후에 객체를 선택해야 합니다. 클라이언트(전송자)는 어떤 객체가 메시지를 수신할지 모릅니다. 단지 누가 받을지는 모르지만 의도를 전달할 뿐입니다.
수신자에 대한 어떠한 것도 가정할 수 없기에 메시지 전송자의 관점에서는 수신자가 완전히 캡슐화됩니다.
책임 주도 설계
책임 주도 설계는 다음과 같은 프로세스를 따릅니다:
- 시스템의 책임을 파악합니다.
- 시스템 책임을 더 작은 책임으로 분할합니다.
- 책임을 수행할 수 있는 객체 또는 역할에 할당합니다.
- 책임을 수행하던 도중, 다른 객체의 도움이 필요하다면 또 적절한 객체 혹은 역할을 찾아 할당합니다.
- 이렇게 할당함으로써 두 객체가 협력하게 됩니다.
2. 책임 할당을 위한 GRASP 패턴
GRASP 패턴은 General Responsibility Assignment Software Pattern의 약자로 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합입니다.
도메인 개념에서 출발하기
설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려보는 것도 유용합니다. 영화 예매 시스템의 도메인 모델은 다음과 같습니다:
Screening "1" -- "0..n" Reservation : 예매
Screening "n" -- "1" Movie : 영화
Movie <|-- 금액할인영화
Movie <|-- 비율할인영화
Movie "1" -- "1..n" DiscountCondition : 할인조건
DiscountCondition <|-- 순번조건
DiscountCondition <|-- 기간조건
여기서 중요한 것은 완벽하게 도메인을 정리하는 것이 아닌, 이렇게 시각화해보는 작업입니다. 너무 많은 시간을 들일 필요는 없습니다.
정보 전문가에게 책임을 할당
1. 시스템의 책임 파악
애플리케이션(시스템)이 제공해야 하는 기능을 앱의 책임으로 생각하고 이걸
책임질 첫 번째 객체를 선택해야 합니다. 영화를 예매하는 것이 시스템의
책임입니다. 그럼 첫 메시지는 '예매하라'가 됩니다.
2. 예매하라 - Screening에게 책임 할당
이 메시지를 수신하는 객체는 무엇이 되어야 할까요? 이 메시지를 수행할 정보를 가장 잘 알고 있는 객체, 즉 정보 전문가에게 할당해야 합니다.
중요한 포인트: '정보 == 데이터'가 아닙니다. 책임을 수행하는 정보를 알고 있다고 해서 그 정보를 저장할 필요가 없습니다. 왜냐하면 그런 다른 객체를 알고 있을 수도 있기 때문입니다.
'상영'이 그 후보가 될 텐데, 상태와 별개로 '상영'은 영화에 대한 정보와 상영 시간, 상영 순번처럼 영화 예매에 필요한 다양한 정보를 알고 있습니다. 이것은 상태를 정의하기 전에 '상영'에 대해 개념적으로 알고 있는 정보입니다.
그래서 상영에 예매를 위한 책임을 할당합니다. 여기서 중요한 포인트는 Screening이 책임을 수행하는데 스스로 처리할 수 없는 작업이 무엇인지 가릴 정도의 수준이면 됩니다. 모두 다 알 필요는 없고 많이 아는 전문가면 되기 때문입니다. 혼자 못하면 외부에 도움을 요청해서 협력하면 됩니다.
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));
}
private Money calculateFee(int audienceCount) {
return movie.calculateMovieFee(this);
}
}
Screening은 예매 책임을 수행하기 위해 예매 가격을 계산해야
합니다. 하지만 Screening은 가격 계산을 위해 필요한 정보를 모릅니다.
그래서 외부에 요청해야 합니다.
3. 예매 가격을 계산하라 - Movie에게 책임 할당
영화 가격을 가장 잘 알고 있는 객체는 Movie입니다. 영화별로
가격이 다른 경우를 가정한다고 보면 됩니다. 그런데 가격을 계산하려고 하니
할인 가격을 고려해야 한다는 사실을 알게 되었습니다. 할인 정보는 Movie는
알지 못합니다. 할인 여부를 판단하지도 못합니다.
4. 할인 여부를 판단하라 - DiscountCondition에게 책임 할당
그래서 할인 여부 판단의 정보 전문가인 DiscountCondition에
책임을 할당합니다.
높은 응집도와 낮은 결합도
동일 기능을 구현하는 방법은 무수히 많습니다. 이 많은 방법 중에 올바른 방법으로 가기 위해 정보 전문가 패턴이 있는 것입니다.
Screening이 DiscountCondition과 협력하는 경우
Screening이 Movie 대신 DiscountCondition과 협력하는 방법도 있습니다. 하지만 이 경우 결합도가 높아집니다. 왜냐하면 Movie와 DiscountCondition과의 결합이 있고, Screening과 DiscountCondition이 추가되는 거라 결합도가 높아집니다. 결국 결합도가 높아져서 좋지 않은 설계가 됩니다.
창조자에게 객체 생성 책임을 할당
Screening이 Reservation이라는 예매를 생성하도록 하는 것을 의미합니다. Screening은 예매에 대한 정보를 가장 잘 알고 있으므로, Reservation 객체를 생성하는 책임을 가집니다.
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
// 생성자 및 getter...
}
3. 구현을 통한 검증
이 부분은 chapter05 디렉토리에 있는 코드를 참고하여 실제 구현을 통해 설계를 검증해보겠습니다.
코드의 변경 이유를 찾기
1. 인스턴스 변수가 초기화되는 시점을 살펴보자
응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께
초기화합니다. 하지만 DiscountCondition_legacy는 type에 따라
부분적으로 초기화를 합니다. 이는 응집도가 낮다는 것입니다.
public class DiscountCondition_legacy {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public boolean isSatisfiedBy(Screening screening) {
if (type == DiscountConditionType.PERIOD) {
return isSatisfiedByPeriod(screening);
}
return isSatisfiedBySequence(screening);
}
private boolean isSatisfiedByPeriod(Screening screening) {
return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
}
private boolean isSatisfiedBySequence(Screening screening) {
return sequence == screening.getSequence();
}
}
함께 초기화되는 속성을 기준으로 코드를 분리해야 합니다. 순번 조건일 때는
sequence만 사용하고, 기간 조건일 때는
dayOfWeek, startTime,
endTime만 사용합니다.
2. 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보자
모든 메서드가 객체의 모든 속성을 사용한다면 응집도가 높습니다. 하지만
메서드들 간에 사용하는 속성이 나뉜다면 응집도가 낮습니다.
isSatisfiedByPeriod, isSatisfiedBySequence가
바로 그 예입니다.
타입 분리하기
DiscountCondition을 각각 SequenceCondition과
PeriodCondition으로 분리하는 것입니다.
interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition extends DiscountCondition {
private int sequence;
public boolean isSatisfiedBy(Screening screening) {
return sequence == screening.getSequence();
}
}
public class PeriodCondition extends DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public boolean isSatisfiedBy(Screening screening) {
return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
}
}
이제 각 클래스는 자신이 필요한 속성만 가지고 있으며, 모든 메서드가 모든 속성을 사용합니다. 응집도가 높아졌습니다.
변경으로부터 보호하기
PeriodCondition은 기간 조건이 변경되면 변경이 이뤄지고,
SequenceCondition은 순번 조건이 변경되면 변경이 가해집니다.
서로 변경의 이유가 다른 것입니다.
그럼 만약에 새로운 할인 조건이 추가된다면?
-
어차피
DiscountCondition으로 캡슐화되어 있기 때문에 상관이 없습니다. 이를 상속받는 추가 조건을 만들면 됩니다. - 이렇게 변경으로부터 보호하는 것을 GRASP 패턴에서 변경 보호라고 합니다.
-
DiscountCondition처럼 다형성을 통해 하나의 클래스가 여러 타입의 행동을 하는 것을 분해하여 책임을 분산시킬 수 있다는 것을 알게 되었습니다.
Movie 클래스 개선하기
chapter05 디렉토리의 Movie는 추상 클래스로 변경되었고,
AmountDiscountMovie, PercentDiscountMovie,
NoneDiscountMovie로 분리되었습니다.
public abstract class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
public Money calculateMovieFee(Screening screening) {
if (isDiscountable(screening)) {
return fee.minus(calculateDiscountAmount());
}
return fee;
}
private boolean isDiscountable(Screening screening) {
return discountConditions.stream()
.anyMatch(condition -> condition.isSatisfiedBy(screening));
}
abstract protected Money calculateDiscountAmount();
}
Movie는 이제 추상 클래스가 되었고, 할인 금액 계산은 추상
메서드로 위임했습니다. 각 할인 정책별로 구체적인 구현을 제공합니다.
public class AmountDiscountMovie extends Movie {
private Money discountAmount;
public AmountDiscountMovie(String title, Duration runningTime,
Money fee, Money discountAmount,
DiscountCondition... discountConditions) {
super(title, runningTime, fee, discountConditions);
this.discountAmount = discountAmount;
}
@Override
protected Money calculateDiscountAmount() {
return discountAmount;
}
}
public class PercentDiscountMovie extends Movie {
private double percent;
public PercentDiscountMovie(String title, Duration runningTime,
Money fee, double percent,
DiscountCondition... discountConditions) {
super(title, runningTime, fee, discountConditions);
this.percent = percent;
}
@Override
protected Money calculateDiscountAmount() {
return getFee().times(percent);
}
}
public class NoneDiscountMovie extends Movie {
public NoneDiscountMovie(String title, Duration runningTime,
Money fee, DiscountCondition[] discountConditions) {
super(title, runningTime, fee, discountConditions);
}
@Override
protected Money calculateDiscountAmount() {
return Money.ZERO;
}
}
이제 각 할인 정책별로 클래스가 분리되었고, 각 클래스는 자신이 필요한 속성만 가지고 있습니다. 응집도가 높아졌고, 새로운 할인 정책을 추가할 때도 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하기만 하면 됩니다.
변경과 유연성
변경에 대응하기
- 코드를 이해하기 쉽게 최대한 단순하게 설계합니다.
- 코드를 수정하지 않고도 변경을 수용할 수 있게 코드를 유연하게 짜기 → 유사한 변경이 반복적으로 발생한다면 유연성을 추가합니다!
상속 vs 합성
현재 영화에 할인 정책을 실행 중에 선택적으로 변경이 가능해야 한다면? 새로운 인스턴스를 생성한 후 필요한 정보를 복사해야 합니다. 그럼 추가될 때마다 복사해야 하는가?
이럴 땐 상속보다 합성이 낫습니다!
Movie와 DiscountCondition 사이에
DiscountPolicy를 추가하여 동적으로 변경이 가능하게 해줄 수
있습니다.
// 예시: 실행 중 할인 정책 변경
movie.changeDiscountPolicy(new PercentDiscountPolicy())
4. 책임 주도 설계의 대안
아무것도 없는 상태에서 책임과 협력을 고민하기보다는 일단 실행되는 코드를 작성하고, 코드 상에 명확히 드러나는 책임들을 올바른 위치로 이동시키는 게 낫습니다.
주의할 점: 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌면 안 됩니다. 이것을 리팩토링이라고 합니다.
메서드 응집도
메서드가 명령문들의 그룹으로 구성되고 각 그룹에 주석을 달아야 한다면, 그 메서드는 응집도가 낮은 것입니다. 차라리 메서드를 작게 분해해서 응집도를 높여야 합니다.
클래스도 동일합니다. 클래스도 작게 분해하여 응집도를 높이는 방향이 더 좋습니다. 그래서 리팩토링의 순서는 아래와 같습니다:
- 우선 응집도가 높게끔 작은 메서드로 분리합니다.
- 이제 각 응집도가 높은 메서드를 적절한 객체에 위치시킵니다.
객체를 자율적으로 만들기
메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키면 됩니다.
메서드가 사용하고 있는 데이터를 가진 클래스로 해당 메서드를 옮기면 캡슐화, 높은 응집도, 낮은 결합도를 가지게 됩니다.
느낀점 및 인사이트
학습을 통해 얻은 인사이트
이번 장을 통해 객체지향 설계에서 책임 할당의 중요성을 깊이 이해할 수 있었습니다. 단순히 클래스를 만들고 메서드를 추가하는 것이 아니라, 어떤 객체에게 어떤 책임을 할당할지 신중하게 결정해야 한다는 것을 배웠습니다.
특히 인상 깊었던 점은 정보 전문가 패턴이었습니다. '정보 == 데이터'가 아니라는 점이 매우 중요했습니다. 책임을 수행하는데 필요한 정보를 알고 있다는 것과 그 정보를 직접 저장하고 있다는 것은 다른 개념입니다. 이는 객체 간의 협력을 통해 정보를 얻을 수 있다는 것을 의미합니다.
또한 GRASP 패턴을 통해 체계적으로 책임을 할당하는 방법을 배울 수 있었습니다. 정보 전문가, 창조자, 변경 보호 등의 패턴들은 실제 개발에서 매우 유용한 가이드라인이 될 것입니다.
코드를 통한 검증 과정에서 응집도가 낮은 코드의 문제점을 직접 확인할
수 있었습니다. DiscountCondition_legacy처럼 type에 따라
부분적으로 초기화되는 클래스는 응집도가 낮고, 이를
SequenceCondition과 PeriodCondition으로
분리함으로써 응집도를 높일 수 있다는 것을 실제 코드로 확인했습니다.
Movie 클래스를 추상 클래스로 만들고 각 할인 정책별로
클래스를 분리한 것도 매우 인상적이었습니다. 이렇게 하면 새로운 할인
정책을 추가할 때 기존 코드를 수정할 필요 없이 새로운 클래스만 추가하면
되므로, 변경에 강한 설계가 됩니다.
책임 주도 설계의 대안으로 제시된 리팩토링 접근법도 실용적이었습니다. 처음부터 완벽한 설계를 하기보다는, 일단 동작하는 코드를 작성한 후 점진적으로 개선해 나가는 것이 현실적인 접근 방법이라는 것을 알게 되었습니다.
앞으로 코드를 작성할 때는 "이 객체가 무엇을 해야 하는가?"라는 질문을 먼저 던지고, 데이터가 아닌 책임을 중심으로 설계를 시작해야겠다는 다짐을 하게 되었습니다. 또한 GRASP 패턴을 활용하여 더 체계적으로 책임을 할당할 수 있을 것 같습니다.
결론
객체지향 설계에서 가장 중요한 것은 올바른 책임 할당입니다. 책임 할당 작업 자체가 트레이드오프 과정이며, 다양한 관점에서 설계를 평가할 수 있어야 합니다.
GRASP 패턴은 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합입니다. 정보 전문가 패턴을 통해 책임을 수행하는데 필요한 정보를 가장 잘 알고 있는 객체에게 책임을 할당하고, 창조자 패턴을 통해 객체 생성 책임을 적절한 객체에게 할당할 수 있습니다.
책임 주도 설계는 데이터가 아닌 행동을 먼저 결정하고, 협력이라는 문맥 안에서 책임을 결정합니다. 시스템의 책임을 파악하고, 이를 더 작은 책임으로 분할한 후, 책임을 수행할 수 있는 객체 또는 역할에 할당하는 프로세스를 따릅니다.
코드를 통한 검증 과정에서 응집도가 낮은 코드의 문제점을 확인하고, 타입을 분리함으로써 응집도를 높일 수 있다는 것을 배웠습니다. 또한 상속을 통한 다형성을 활용하여 변경에 강한 설계를 만들 수 있다는 것도 확인했습니다.
책임 주도 설계의 대안으로 리팩토링 접근법이 있습니다. 처음부터 완벽한 설계를 하기보다는, 일단 동작하는 코드를 작성한 후 점진적으로 개선해 나가는 것이 현실적인 접근 방법입니다. 메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키면 캡슐화, 높은 응집도, 낮은 결합도를 가지게 됩니다.
앞으로 코드를 작성할 때는 책임과 협력에 집중하고, GRASP 패턴을 활용하여 더 체계적으로 책임을 할당할 수 있을 것입니다. 이를 통해 변경에 강하고 유지보수가 용이한 설계를 만들 수 있을 것입니다.
'개발 > OOP' 카테고리의 다른 글
| 모던 자바 안티패턴: 설계 의도를 벗어나는 API 사용 사례 (1) | 2025.11.21 |
|---|---|
| JPA와 도메인 모델: 복잡한 조회 쿼리의 딜레마와 실전 해결책 (0) | 2025.11.20 |
| [책 | 오브젝트] CH3. 역할/책임/협력 (0) | 2025.11.09 |
| [OOP 안티패턴] God Object? 이 정도는 괜찮지 않나요 (2) | 2025.06.23 |