Digging into Collaborators는 협력 객체의 내부 구조를 너무 깊게 파고드는 것을 의미합니다. 즉, 객체가 다른 객체와 협력할 때, 그 객체의 내부 상태나 구현 세부사항에 지나치게 의존하는 경우입니다. 이런 접근은 디미터 법칙을 위반하는 방식으로 간주됩니다.
협력 객체(Collaborator)의 이해
협력 객체는 객체 지향 프로그래밍에서 특정 객체가 자신의 역할을 수행하기 위해 상호작용하는 다른 객체를 의미
객체는 모든 작업을 직접 처리하지 않고 협력 객체에 위임하여 책임을 분산시킵니다.
안티 패턴
Digging into Collaborators는 협력 객체의 내부에 과도하게 접근하는 안티패턴으로, 다음과 같은 문제를 야기합니다:
캡슐화 위반
객체 간 결합도 증가
단일 책임 원칙 위반
아래는 Flutter 코드 예제를 통해 "Digging into Collaborators" 문제를 설명하고 이를 개선하는 방법입니다.
Digging into Collaborators 사례 1
🛑 문제 코드
class Address {
final String street; // Collaborator
final String city; // Collaborator
Address(this.street, this.city);
}
class Order {
final Address address; // Collaborator
Order(this.address);
}
class ShippingService {
void shipOrder(Order order) {
// ❌ 문제: Service가 Order 객체 내부로 파고들어 값 객체(Address)에 직접 접근함
String destination = "${order.address.street}, ${order.address.city}";
print("Shipping to: $destination");
}
}
void main() {
Order order = Order(Address("123 Main St", "Flutter City"));
ShippingService().shipOrder(order);
}
⚠️ 문제점 분석
ShippingService가 Order 객체 내부의 Address 필드에 직접 접근해 데이터를 조합하는 방식은 캡슐화 원칙을 위반합니다.
이는 Order가 자신의 데이터를 관리하지 않고 외부 객체가 이를 조작하도록 만드는 결과를 초래합니다.
✅ 개선된 코드 (Tell, Don't Ask 원칙 적용)
class Address {
final String street;
final String city;
Address(this.street, this.city);
String getFullAddress() {
return "$street, $city";
}
}
class Order {
final Address address;
Order(this.address);
String getShippingAddress() {
// Order 객체가 자신의 데이터를 활용해 필요한 정보를 제공
return address.getFullAddress();
}
}
class ShippingService {
void shipOrder(Order order) {
// ✅ 개선: Service가 Order에 요청하고 필요한 정보를 얻음
String destination = order.getShippingAddress();
print("Shipping to: $destination");
}
}
void main() {
Order order = Order(Address("123 Main St", "Flutter City"));
ShippingService().shipOrder(order);
}
🚀 개선 사항 요약
Order 클래스가 자신에 대한 책임을 가지도록 getShippingAddress 메서드를 추가했습니다.
이제 ShippingService는 객체에 데이터를 요청하기만 하고 내부 구현에 관여하지 않습니다.
Digging into Collaborators 사례 2
🛑 Before: 테스트하기 어려운 설계
class TaxTable {
double getTaxRate(Address address) {
// 단순히 예제를 위해 주소에 따른 세율 반환
return address.city == "Flutter City" ? 0.09 : 0.07;
}
}
class Address {
final String street; // Collaborator
final String city; // Collaborator
Address(this.street, this.city);
}
class User {
final Address address; // Collaborator
User(this.address);
}
class Invoice {
final double subtotal; // Collaborator
Invoice(this.subtotal);
}
class SalesTaxCalculator {
final TaxTable taxTable; // Collaborator
SalesTaxCalculator(this.taxTable);
double computeSalesTax(User user, Invoice invoice) {
// 문제점:
// - User 객체 내부의 Address ,Invoice 내부 subtotal에 직접 접근 (캡슐화 위반)
// - SRP(Single Responsibility Principle) 위반: 계산 외에 데이터 추출 책임 포함
Address address = user.address;
double amount = invoice.subtotal;
return amount * taxTable.getTaxRate(address);
}
}
User, Address, Invoice 객체를 모두 생성해야 하는 불필요한 초기화 과정이 테스트 코드에 포함되어 있어 복잡도가 증가합니다.
강한 결합 (Tight Coupling):
SalesTaxCalculator가 User와 Invoice 객체에 직접 의존하면서 불필요한 객체 참조가 발생합니다.
테스트 유지보수 어려움:
요구사항이 변경되면 객체 생성 로직이 수정되어야 하므로 테스트 코드의 유지보수가 어려워집니다.
SRP 위반:
SalesTaxCalculator가 본래 책임인 세금 계산 외에 데이터 추출 로직도 수행하여 역할이 복잡해집니다.
✅ After: 테스트하기 쉬운 설계
class SalesTaxCalculator {
final TaxTable taxTable;
SalesTaxCalculator(this.taxTable);
// 위 다른 코드 생략
double computeSalesTax(Address address, double amount) {
return amount * taxTable.getTaxRate(address);
}
}