오브젝트 내용 정리
‘오브젝트를 읽고…’
조영호님의 오브젝트를 읽고, 내용을 정리해봤습니다.
1. 절차지향적인 코드의 단점
절차지향적인 프로그래밍에서는 함수와 데이터가 분리되어 있으며, 동작하는 코드는 여러 데이터에 의존합니다.
이로 인해 데이터의 변경이 함수의 변경을 초래하게 되고, 이는 단일 책임 원칙(SRP)에 위배될 수 있습니다.
예를 들어, 하나의 함수가 특정 데이터를 처리하는 로직을 포함하고 있다면, 그 데이터 구조가 변경되면 함수도 변경되어야 합니다.
만약 이 함수가 여러 데이터 구조에 의존한다면, 각 데이터 구조의 변경이 함수의 변경을 요구하게 됩니다.
이렇게 되면 해당 함수는 여러 변경 이유를 가지게 되어 코드의 유지보수성이 떨어지게 됩니다.
2. 객체지향 설계 원칙
절차지향적인 프로그래밍의 단점을 보완하기 위해 등장한 방법론이 객체지향 설계입니다.
객체지향 설계의 목표는 요구사항이 변경될 때 코드를 안전하고 쉽게 수정할 수 있도록 설계하는 것입니다.
객체지향 설계의 기본 원칙은 다음과 같습니다
- 협력에 필요한 행동을 먼저 결정하고, 그 행동에 적합한 객체를 나중에 선택하라.
- 객체의 행동을 먼저 구현하고, 행동에 필요한 데이터를 나중에 선택하라.
이 원칙은 객체들이 어떻게 협력할지를 먼저 결정하고, 그 다음에 데이터를 누가 갖고 있을지를 정하는 것을 의미합니다.
이를 통해 데이터와 행위를 캡슐화하고, 코드의 응집도를 높이며 결합도를 낮출 수 있습니다.
3. 책임 주도 설계
객체지향에서 책임이란, 객체가 협력에 참여하기 위해 수행하는 행동을 의미합니다.
책임 주도 설계는 객체지향 설계의 핵심 원칙 중 하나로, 각 객체가 자신만의 책임을 갖고 그 책임에 따라 행동을 결정하는 것을 목표로 합니다.
객체가 자신의 데이터를 관리하고, 외부에서 그 데이터에 접근할 필요가 없도록 설계함으로써 응집도를 높이고 결합도를 낮출 수 있습니다.
예를 들어, 할인 조건을 관리하는 DiscountCondition
클래스가 있다고 가정해 봅시다. 이 클래스가 단순히 데이터를 보관하는 역할만 하게 되면, 비즈니스 로직이 외부 클래스에 분산되어 코드의 응집도가 낮아지고, 시스템의 결합도가 높아집니다.
예시
기존의 DiscountCondition
클래스는 다음과 같습니다
class DiscountCondition {
public enum ConditionType {
PERIOD_CONDITION, SEQUENCE_CONDITION
}
private Long id;
private Long policyId;
private ConditionType conditionType;
private LocalTime startTime;
private LocalTime endTime;
private Integer sequence;
public boolean isPeriodCondition() {
return ConditionType.PERIOD_CONDITION.equals(conditionType);
}
public boolean isSequenceCondition() {
return ConditionType.SEQUENCE_CONDITION.equals(conditionType);
}
}
여기에 새로운 할인 조건이 추가되면, 다음과 같이 클래스와 외부 로직 모두가 변경되어야 합니다
class DiscountCondition {
public enum ConditionType {
PERIOD_CONDITION, SEQUENCE_CONDITION, COMBINED_CONDITION
}
// 기존 필드들...
public boolean isCombinedCondition() {
return ConditionType.COMBINED_CONDITION.equals(conditionType);
}
}
외부의 ReservationService
클래스도 다음과 같이 수정이 필요합니다.
class ReservationService {
private DiscountCondition findDiscountCondition(Screening screening,
List<DiscountCondition> conditions) {
for(DiscountCondition condition : conditions) {
if (condition.isPeriodCondition()) {
// 로직 처리...
} else if (condition.isSequenceCondition()) {
// 로직 처리...
} else if (condition.isCombinedCondition()) {
// 새로운 로직 처리...
}
}
return null;
}
}
이처럼 데이터와 행동이 분리되어 있으면 코드의 변경 이유가 많아지고, 이는 유지보수를 어렵게 만듭니다.
책임 주도 설계 적용 예시
책임 주도 설계 원칙을 적용하여 DiscountCondition
클래스를 다음과 같이 수정할 수 있습니다.
class DiscountCondition {
public boolean isSatisfiedBy(Screening screening) {
if (isPeriodCondition()) {
return screening.isPlayedIn(startTime, endTime);
} else if (isSequenceCondition()) {
return sequence.equals(screening.getSequence());
} else if (isCombinedCondition()) {
return someAdditionalLogic(screening);
}
return false;
}
}
이렇게 수정된 코드는 DiscountCondition
객체가 스스로 책임을 가지고 할인 조건을 판단하도록 하여, 응집도를 높이고 결합도를 낮출 수 있습니다.
4. 객체지향 설계 순서
객체지향 설계에서는 다음과 같은 순서로 객체를 구성합니다.
- 애플리케이션이 제공할 기능을 파악한다.
- 각 기능에 대한 책임을 담당할 적절한 객체를 선택한다.
- 객체가 자신의 책임을 수행하기 위해 외부의 도움이 필요하다면, 다른 객체에게 도움을 요청한다.
5. 표현적 차이 줄이기
표현적 차이란 도메인 모델과 코드 사이의 차이를 의미합니다.
표현적 차이가 작다는 것은 도메인의 모습과 코드의 모습이 유사하다는 뜻입니다.
표현적 차이가 작으면 코드의 변경이 쉽고, 유지보수가 용이해집니다.
비즈니스의 본질이 크게 변하지 않는 한, 도메인 개념과 관계도 크게 변하지 않습니다.
따라서 도메인 모델과 코드의 구조를 잘 맞추면, 요구사항이 변경되더라도 코드의 수정이 최소화됩니다.
6. 책임 할당을 위한 GRASP 패턴
GRASP 패턴은 General Responsibility Assignment Software Patterns의 약자로, 소프트웨어 설계에서 책임을 효과적으로 할당하기 위한 일반적인 패턴을 나타냅니다.
각 패턴은 특정 상황에서 객체에 책임을 할당하는 방법을 제공합니다.
6.1 정보 전문가 패턴
- 책임 할당의 원칙: 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 객체에게 그 책임을 할당하라.
- 설명: 이 패턴은 객체가 자신이 가진 정보에 따라 책임을 지도록 하여, 정보가 있는 객체가 해당 정보를 기반으로 작업을 수행하게 합니다.
6.2 창조자 패턴
- 인스턴스 생성 책임: 새로운 인스턴스를 생성하는 책임을 어떤 객체에게 할당할 것인가?
- 원칙: 다음 조건 중 하나라도 만족할 경우, 객체 B에게 객체 A의 인스턴스를 생성할 책임을 할당하라
- B가 A를 포함하거나 참조한다.
- B가 A를 기록한다.
- B가 A를 긴밀하게 사용한다.
- B가 A를 초기화하는 데 필요한 정보를 알고 있다.
6.3 낮은 결합도 패턴
- 결합도 최소화: 어떻게 낮은 의존성을 유지하고, 변경에 따른 영향을 줄이면서 재사용을 높일 수 있을까?
- 원칙: 설계의 전체적인 결합도를 낮게 유지할 수 있도록 책임을 할당하라. 이미 결합도가 있는 객체에게 책임을 추가로 할당하여, 새로운 결합도를 도입하지 않도록 한다.
6.4 높은 응집도 패턴
- 응집도 유지: 어떻게 낮은 결합도를 유지하고, 변경에 따른 영향을 줄이면서 재사용을 높일 수 있을까?
- 원칙: 높은 응집도를 유지하도록 책임을 할당하라. 각 객체는 자신이 담당하는 책임과 관련된 작업만 수행하도록 하여, 클래스가 변경될 이유를 최소화한다.