8. 강한 결합 - 복잡하게 얽혀서 풀 수 없는 구조

내용


  • 결합도?

    • 클래스 사이의 의존도를 나타내는 지표

  • 강한 결합

  • 느슨한 결합

    • 느슨한 결합을 가지고 있으면, 코드 변경이 쉬워진다.

  • 책무

    • 책임과 의무

    • 의무를 다 해야 하는 책임

결합도와 책무

  • 소프트웨어의 책임

    • 자신의 관심사와 관련해서, 정상적으로 동작하도록 제어하는 것

    • 단일 책임 원칙

      • 클래스가 담당하는 책임은 하나로 제한해야 한다는 설계원칙

  • DRY 원칙

    • Don't Repeat Yourself

    • 모든 지식은 시스템 내에서 단 한번만, 애매하지 않고, 권위 있게 표현되어야한다.

      • 지식 -> 비즈니스 지식

      • e.g. '할인', '관심 상품', '크리스마스 캠페인', '공격력', '매직포인트'

    • 일반 할인과 여름 할인은 서로 다른 개념

      • 같은 로직, 비슷한 로직이라도 개념 (비즈니스 로직) 이 다르면 중복을 허용해야 한다.

다양한 강한 결함 사례와 대처 방법

  • 상속 가능한 사용하지 말자

    • 왜?

      • 서브 클래스는 슈퍼 클래스의 구조를 하나하나 신경 써야함.

      • 슈퍼 클래스의 변화를 놓치는 순간, 버그 생성

  • 상속보다는 Composition을 이용하자

  • public 이유없이 사용하지 말자

    • public -> 모든 클래스에서 접근 가능

    • protected -> 같은 클래스, 서브 클래스에서만

    • 없음 -> package private

    • private -> 같은 클래스에서만

      • 기본적으로 package private으로 만들자

      • 외부 공개가 필요하면 public으로 만들자

  • private 메서드가 너무 많다는 건 책임이 많다는 것

  • 응집도가 높다는 것은 같은 개념 (비즈니스 로직) 이 모여있는 것을 의미.

    • 다른 개념의 변수들이 모여있으면 강한 결합 (지양하는 바)

  • 트랜잭션 스크립트 패턴

    • 데이터를 보유하고 있는 클래스 (데이터 클래스) 와 데이터를 처리하는 클래스가 나누어 구현하고 있을 때.

  • 트랜잭션 스크립트 패턴 예시

public class OrderService {
    public void processOrder(int orderId, int productId, int quantity, String paymentMethod) {
        // 주문 정보 조회
        Order order = orderRepository.getOrder(orderId);
        
        // 상품 정보 조회
        Product product = productRepository.getProduct(productId);
        
        // 재고 확인
        if (product.getStock() < quantity) {
            throw new InsufficientStockException("재고가 부족합니다");
        }
        
        // 결제 처리
        double totalAmount = product.getPrice() * quantity;
        boolean paymentResult = paymentGateway.processPayment(paymentMethod, totalAmount);
        
        if (!paymentResult) {
            throw new PaymentFailedException("결제에 실패했습니다");
        }
        
        // 재고 감소
        product.setStock(product.getStock() - quantity);
        productRepository.updateProduct(product);
        
        // 주문 상태 업데이트
        order.setStatus("PAID");
        order.setTotalAmount(totalAmount);
        orderRepository.updateOrder(order);
    }
}
  • 모든 비즈니스 로직이 단일 메서드 내에 절차적으로 작성

  • Order와 Product는 단순한 데이터 컨테이너로, 실제 동작은 포함하지 않음

  • 모든 작업이 서비스 계층에서 조율됨.

  • 도메인 모델 패턴 예시

// 도메인 모델 - Order 클래스
public class Order {
    private int id;
    private List<OrderItem> items;
    private String status;
    private double totalAmount;
    
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
        recalculateTotal();
    }
    
    public void checkout() {
        validateOrder();
        reserveInventory();
        status = "PENDING_PAYMENT";
    }
    
    public void pay(PaymentMethod paymentMethod) {
        if (status != "PENDING_PAYMENT") {
            throw new IllegalStateException("주문이 결제 대기 상태가 아닙니다");
        }
        
        paymentMethod.process(totalAmount);
        status = "PAID";
    }
    
    private void validateOrder() {
        for (OrderItem item : items) {
            item.validate();
        }
    }
    
    private void reserveInventory() {
        for (OrderItem item : items) {
            item.reserveInventory();
        }
    }
    
    private void recalculateTotal() {
        totalAmount = items.stream()
            .mapToDouble(OrderItem::getSubtotal)
            .sum();
    }
}

// 도메인 모델 - OrderItem 클래스
public class OrderItem {
    private Product product;
    private int quantity;
    
    public OrderItem(Product product, int quantity) {
        this.product = product;
        this.quantity = quantity;
    }
    
    public double getSubtotal() {
        return product.getPrice() * quantity;
    }
    
    public void validate() {
        if (product.getStock() < quantity) {
            throw new InsufficientStockException("재고가 부족합니다: " + product.getName());
        }
    }
    
    public void reserveInventory() {
        product.decreaseStock(quantity);
    }
}

// 애플리케이션 서비스
public class OrderService {
    public void processOrder(int orderId, int productId, int quantity, PaymentMethod paymentMethod) {
        Order order = orderRepository.getOrder(orderId);
        Product product = productRepository.getProduct(productId);
        
        order.addItem(product, quantity);
        order.checkout();
        order.pay(paymentMethod);
        
        orderRepository.saveOrder(order);
    }
}
  • 비즈니스 로직이 도메인 객체(Order, OrderItem 등)에 분산되어 있습니다.

  • 각 객체는 자신의 상태와 행동을 책임집니다.

  • 서비스 계층은 도메인 객체들을 조율하는 역할만 합니다.

  • 비즈니스 규칙과 로직이 도메인 객체에 캡슐화되어 있습니다.

  • 갓 클래스

    • 하나의 클래스 내부에 수천, 수만줄의 로직을 담고 있음..

정리


  • 단일 책임 원칙

    • 클래스가 각자 적절한 책임을 가지도록 해야함

    • 그 책임이 가능한 하나로.

  • DRY

    • 비즈니스 개념이 다르면, 코드 중복을 허용해야한다.

  • 느슨한 결합, 높은 응집도.

    • 동일한 개념끼리만 응집도를 가지게 하고,

    • 다른 개념이면 클래스를 나눠 관리하여 느슨한 결합을 유지하도록 한다.

참고


  • https://product.kyobobook.co.kr/detail/S000202521361

Last updated

Was this helpful?