💡 "설계란 의존성을 관리하는 것이고, 객체지향 설계란 객체 간의 의존성을 관리하는 것이다"
협력하는 객체들의 공동체를 구축하기 위해서는 객체들이 서로 의존해야 합니다. 하지만 과도한 의존성은 변경을 어렵게 만들죠. 따라서 필요한 의존성은 유지하되, 변경을 방해하는 의존성은 제거해야 합니다.
// 의존성이 없다면? - 협력 불가능
public class Movie {
// 할인 정책 없이는 영화 예매 시스템이 작동하지 않음
}
// 잘못된 의존성이라면? - 변경 어려움
public class Movie {
private AmountDiscountPolicy discountPolicy = new AmountDiscountPolicy();
// 정책을 바꾸려면 Movie 코드를 수정해야 함
}
// 올바른 의존성이라면? - 협력 가능하면서도 유연함
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
위 코드에서 PeriodCondition은 다음에 의존합니다:
DayOfWeek - 요일 정보LocalTime - 시간 정보 Screening - 상영 정보DiscountCondition - 인터페이스// 직접적인 클래스 참조
public class Movie {
private AmountDiscountPolicy discountPolicy; // 구체 클래스에 의존
}
// 추상화에 의존
public class Movie {
private DiscountPolicy discountPolicy; // 인터페이스에 의존
}
public Money calculateMovieFee(Screening screening) {
// screening에 일시적으로 의존
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
public void someMethod() {
DiscountPolicy policy = new AmountDiscountPolicy(...); // 로컬 변수로 의존
}
public boolean isSatisfiedBy(Screening screening) {
return DayOfWeek.SATURDAY.equals(screening.getWhenScreened().getDayOfWeek());
// DayOfWeek 클래스의 정적 변수에 의존
}
// UML 표기법
Movie -----> DiscountPolicy // 연관관계 (인스턴스 변수)
Movie - - -> Screening // 의존관계 (파라미터, 로컬 변수)
Movie --|> DiscountPolicy // 실체화 관계 (인터페이스 구현)
RegularEmployee --|> Employee // 일반화 관계 (상속)
// 컴파일타임 의존성 - 코드 상의 의존성
public class Movie {
private DiscountPolicy discountPolicy; // 추상 타입에 의존
public Movie(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
// 런타임 의존성 - 실행 시점의 의존성
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800), ...)); // 실제 객체
Movie starWars = new Movie("스타워즈",
Duration.ofMinutes(210),
Money.wons(10000),
new PercentDiscountPolicy(0.1, ...)); // 다른 객체
// 컴파일타임 - Movie는 DiscountPolicy만 안다
public class Movie {
private DiscountPolicy discountPolicy;
}
// 런타임 - 실제로는 구체적인 객체들과 협력
Movie ---> AmountDiscountPolicy (avatar)
Movie ---> PercentDiscountPolicy (starWars)
Movie ---> NoneDiscountPolicy (specialMovie)
🌟 핵심: 유연한 설계를 위해서는 컴파일타임 의존성과 런타임 의존성이 달라야 한다!
객체가 사용되는 특정한 문맥을 의미합니다.
// 컨텍스트에 강하게 결합된 설계 - 나쁜 예
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
// 특정 컨텍스트(10% 할인)에 결합
this.discountPolicy = new PercentDiscountPolicy(0.1,
new PeriodCondition(DayOfWeek.MONDAY, ...),
new SequenceCondition(10));
}
}
// 컨텍스트 독립적인 설계 - 좋은 예
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
// 구체적인 컨텍스트는 외부에서 결정
this.discountPolicy = discountPolicy;
}
}
// 다양한 컨텍스트에서 재사용 가능
Movie movie1 = new Movie("영화1", duration, fee, new AmountDiscountPolicy(...));
Movie movie2 = new Movie("영화2", duration, fee, new PercentDiscountPolicy(...));
Movie movie3 = new Movie("영화3", duration, fee, new OverlappedDiscountPolicy(...));
public class Movie {
private DiscountPolicy discountPolicy;
// 생성자를 통해 의존성 주입
public Movie(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
}
// 사용
Movie avatar = new Movie("아바타", Duration.ofMinutes(120),
Money.wons(10000), new AmountDiscountPolicy(...));
장점:
public class Movie {
private DiscountPolicy discountPolicy;
// setter를 통해 의존성 주입
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
throw new IllegalStateException("discountPolicy가 설정되지 않았습니다");
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
// 사용
Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000));
avatar.setDiscountPolicy(new AmountDiscountPolicy(...)); // 나중에 주입
장점:
단점:
public class Movie {
// 의존성을 저장하지 않고 메서드 실행 시 전달
public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
// 사용
Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000));
Money fee = avatar.calculateMovieFee(screening, new AmountDiscountPolicy(...));
장점:
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
private List<Review> reviews = new ArrayList<>();
// 필수 의존성은 생성자로
public Movie(String title, Duration runningTime, Money fee) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = new NoneDiscountPolicy(); // 기본값
}
// 선택적 의존성은 setter로
public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
// 일시적 의존성은 메서드 파라미터로
public void addReview(Review review, ReviewValidator validator) {
if (validator.validate(review)) {
reviews.add(review);
}
}
}
// 바람직하지 못한 의존성 - 구체 클래스에 의존
public class Movie {
private AmountDiscountPolicy discountPolicy; // 결합도 높음
}
// 바람직한 의존성 - 추상화에 의존
public class Movie {
private DiscountPolicy discountPolicy; // 결합도 낮음
}
// 너무 많이 아는 경우 - 높은 결합도
public class Movie {
private AmountDiscountPolicy discountPolicy;
public Movie(Money discountAmount, DiscountCondition... conditions) {
// Movie가 AmountDiscountPolicy의 생성자 인자까지 알아야 함
this.discountPolicy = new AmountDiscountPolicy(discountAmount, conditions);
}
}
// 적게 아는 경우 - 낮은 결합도
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(DiscountPolicy discountPolicy) {
// Movie는 DiscountPolicy 인터페이스만 알면 됨
this.discountPolicy = discountPolicy;
}
}
// 1. 구체 클래스 의존 - 가장 높은 결합도
class Movie {
private AmountDiscountPolicy discountPolicy;
}
// 2. 추상 클래스 의존 - 중간 결합도
abstract class DiscountPolicy {
private List<DiscountCondition> conditions;
public Money calculateDiscountAmount(Screening screening) {
for(DiscountCondition condition : conditions) {
if (condition.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
// 3. 인터페이스 의존 - 가장 낮은 결합도
interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
// 숨겨진 의존성 - 나쁜 예
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
// 의존성이 내부에 숨겨짐 - 외부에서 알 수 없음!
this.discountPolicy = new AmountDiscountPolicy(
Money.wons(800),
new SequenceCondition(1),
new SequenceCondition(10)
);
}
}
// Movie를 생성할 때 AmountDiscountPolicy에 의존한다는 사실을 알 수 없음
Movie movie = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000));
// 어? 할인 정책은 어떻게 되는거지?
@Test
public void 할인정책_테스트() {
// 특정 할인 정책으로 테스트하고 싶은데...
Movie movie = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000));
// AmountDiscountPolicy로 고정되어 있어서 테스트 불가능!
}
// 다른 할인 정책을 사용하고 싶어도 불가능
Movie movie1 = new Movie(...); // 무조건 AmountDiscountPolicy
Movie movie2 = new Movie(...); // 이것도 AmountDiscountPolicy
// PercentDiscountPolicy를 쓰고 싶다면? Movie 클래스 수정 필요
// 명시적인 의존성 - 좋은 예
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
// 의존성을 명시적으로 드러냄
public Movie(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
}
// 사용하는 쪽에서 의존성을 명확히 알 수 있음
Movie movie1 = new Movie("아바타", duration, fee, new AmountDiscountPolicy(...));
Movie movie2 = new Movie("스타워즈", duration, fee, new PercentDiscountPolicy(...));
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(...) {
// new는 구체 클래스 이름을 직접 사용하게 만든다
this.discountPolicy = new AmountDiscountPolicy(...);
// ^^^^^^^^^^^^^^^^^^^^ 구체 클래스!
}
}
public class Movie {
public Movie(...) {
// AmountDiscountPolicy의 모든 생성자 인자를 알아야 함
this.discountPolicy = new AmountDiscountPolicy(
Money.wons(800), // 할인 금액
new SequenceCondition(1), // 조건 1
new SequenceCondition(10), // 조건 2
new PeriodCondition(...) // 조건 3
);
}
}
// Client - 사용하는 책임
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy) {
// 생성하지 않고 사용만 함
this.discountPolicy = discountPolicy;
}
public Money calculateMovieFee(Screening screening) {
// 사용
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
// Factory - 생성하는 책임
public class MovieFactory {
public Movie createAvatarMovie() {
return new Movie(
"아바타",
Duration.ofMinutes(120),
Money.wons(10000),
// 생성은 Factory가 담당
new AmountDiscountPolicy(
Money.wons(800),
new SequenceCondition(1),
new SequenceCondition(10)
)
);
}
}
public class Movie {
private List<Review> reviews; // 표준 클래스
private Money fee; // 값 객체
public Movie(...) {
this.reviews = new ArrayList<>(); // OK! 표준 클래스
this.fee = Money.ZERO; // OK! 불변 값 객체
}
public LocalDateTime getScreeningTime() {
return LocalDateTime.now(); // OK! 표준 클래스
}
}
// 변경 가능성이 없는 클래스들
public class Order {
private Long id;
private List<OrderItem> items = new ArrayList<>();
private Money totalAmount = Money.ZERO;
private OrderStatus status = OrderStatus.PENDING; // Enum
public void addItem(Product product, int quantity) {
// 값 객체는 직접 생성해도 무방
OrderItem item = new OrderItem(product, quantity);
items.add(item);
totalAmount = totalAmount.plus(item.getAmount());
}
}
// ArrayList를 상속한 경우 - 이제는 표준 클래스가 아님!
public class DiscountConditions extends ArrayList<DiscountCondition> {
// 커스텀 동작 추가
}
// 이런 경우는 추상화해야 함
public class Movie {
// private DiscountConditions conditions; // 구체 클래스 의존 X
private List<DiscountCondition> conditions; // 인터페이스 의존 O
}
// 상위 수준 모듈이 하위 수준 모듈에 의존
// Movie(상위) → AmountDiscountPolicy(하위)
public class Movie { // 상위 수준 (비즈니스 로직)
private AmountDiscountPolicy discountPolicy; // 하위 수준 (구체적 구현)
}
// Movie(상위) → DiscountPolicy(추상) ← AmountDiscountPolicy(하위)
// 추상화 (상위 수준 모듈이 소유)
public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
// 상위 수준 모듈
public class Movie {
private DiscountPolicy discountPolicy; // 추상화에 의존
}
// 하위 수준 모듈
public class AmountDiscountPolicy implements DiscountPolicy {
// 추상화에 의존
}
public class PercentDiscountPolicy implements DiscountPolicy {
// 추상화에 의존
}
// 잘못된 구조 - 순환 의존성
com.theater.movie
├── Movie.java
└── discount
├── DiscountPolicy.java
├── AmountDiscountPolicy.java
└── PercentDiscountPolicy.java
// 올바른 구조 - 의존성 역전
com.theater.domain // 상위 수준
├── Movie.java
└── DiscountPolicy.java // 인터페이스는 상위 수준에 위치
com.theater.infrastructure.discount // 하위 수준
├── AmountDiscountPolicy.java
└── PercentDiscountPolicy.java
// 강한 결합, 숨겨진 의존성
public class OrderService {
private MySQLOrderRepository orderRepository;
private EmailNotificationService notificationService;
public OrderService() {
// new의 남용, 구체 클래스 의존
this.orderRepository = new MySQLOrderRepository(
"localhost", 3306, "user", "password"
);
this.notificationService = new EmailNotificationService(
"smtp.gmail.com", 587
);
}
public void placeOrder(Order order) {
// 구체적인 구현에 의존
orderRepository.save(order);
notificationService.sendEmail(
order.getCustomerEmail(),
"주문 완료",
"주문이 완료되었습니다."
);
}
}
// 인터페이스 정의
public interface OrderRepository {
void save(Order order);
Order findById(Long id);
List<Order> findByCustomer(Customer customer);
}
public interface NotificationService {
void notify(String recipient, String subject, String message);
}
// 구현체들
public class MySQLOrderRepository implements OrderRepository {
private final DataSource dataSource;
public MySQLOrderRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
// 구현...
}
public class MongoOrderRepository implements OrderRepository {
private final MongoClient mongoClient;
public MongoOrderRepository(MongoClient mongoClient) {
this.mongoClient = mongoClient;
}
// 구현...
}
// 명시적 의존성, 생성자 주입
public class OrderService {
private final OrderRepository orderRepository;
private final NotificationService notificationService;
public OrderService(OrderRepository orderRepository,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.notificationService = notificationService;
}
public void placeOrder(Order order) {
orderRepository.save(order);
notificationService.notify(
order.getCustomerEmail(),
"주문 완료",
"주문이 완료되었습니다."
);
}
}
// 설정 (Configuration)
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository(DataSource dataSource) {
return new MySQLOrderRepository(dataSource);
// 또는 프로파일에 따라
// return new MongoOrderRepository(mongoClient);
}
@Bean
public NotificationService notificationService() {
if (isProduction()) {
return new EmailNotificationService(emailConfig());
} else {
return new MockNotificationService(); // 테스트용
}
}
@Bean
public OrderService orderService(OrderRepository orderRepository,
NotificationService notificationService) {
return new OrderService(orderRepository, notificationService);
}
}
// 사용
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
Order order = new Order(request);
orderService.placeOrder(order);
return ResponseEntity.ok(order);
}
}
@Test
class OrderServiceTest {
@Test
void 주문_생성_시_알림을_보낸다() {
// Given - Mock 객체로 쉽게 교체
OrderRepository mockRepository = mock(OrderRepository.class);
NotificationService mockNotification = mock(NotificationService.class);
OrderService orderService = new OrderService(
mockRepository,
mockNotification
);
Order order = new Order(/* ... */);
// When
orderService.placeOrder(order);
// Then
verify(mockRepository).save(order);
verify(mockNotification).notify(
eq(order.getCustomerEmail()),
eq("주문 완료"),
anyString()
);
}
}
추상화에 의존하라
명시적으로 의존하라
생성과 사용을 분리하라
의존성 방향을 관리하라
// ❌ 피해야 할 패턴
public class BadExample {
private ConcreteClass dependency = new ConcreteClass(); // 1. new 직접 사용
private HiddenDependency hidden; // 2. 숨겨진 의존성
public BadExample() {
hidden = ServiceLocator.getDependency(); // 3. 서비스 로케이터
}
}
// ✅ 권장하는 패턴
public class GoodExample {
private final AbstractType dependency; // 1. 추상화 의존
public GoodExample(AbstractType dependency) { // 2. 명시적 주입
this.dependency = dependency; // 3. 생성자 주입
}
}
// Spring을 활용한 의존성 관리
@Component
public class PaymentService {
private final PaymentGateway paymentGateway;
private final OrderRepository orderRepository;
private final NotificationService notificationService;
// Spring이 자동으로 의존성 주입
public PaymentService(PaymentGateway paymentGateway,
OrderRepository orderRepository,
NotificationService notificationService) {
this.paymentGateway = paymentGateway;
this.orderRepository = orderRepository;
this.notificationService = notificationService;
}
}
Chapter 8을 공부하면서 느낀 점은, 의존성 관리가 단순히 "결합도를 낮추는 것"이 아니라 "협력을 가능하게 하면서도 변경을 수용할 수 있게 하는 것" 이라는 점입니다.
마치 레고 블록처럼, 각 객체는 표준화된 인터페이스(블록의 돌기와 홈)를 통해 연결되어야 합니다. 구체적인 모양이나 색깔(구현)은 다를 수 있지만, 연결 방식(인터페이스)은 동일해야 하죠.
특히 스프링 같은 DI 프레임워크가 왜 필요한지, 왜 @Autowired보다 생성자 주입을 권장하는지, 왜 인터페이스를 사용해야 하는지가 명확해집니다.
🚀 핵심: 의존성은 협력을 위해 필요하지만, 올바르게 관리하지 않으면 변경의 족쇄가 된다. 추상화에 의존하고, 명시적으로 표현하며, 방향을 관리하라.