800===Dev Docs and License/이론 문서

객체지향 심화 학습 4편: 인터페이스 분리 원칙 (ISP) 완전정복 🎯

블로글러 2024. 11. 3. 21:13

오늘은 SOLID의 네 번째 원칙인 인터페이스 분리 원칙(ISP)을 자세히 알아볼게요!

1. 인터페이스 분리 원칙이란? 💡

핵심: "클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다"

Bad Case: ISP 위반 사례

// ❌ 이렇게 하면 안돼요!
interface Worker {
    void work();
    void eat();
    void sleep();
}

// 로봇은 먹지도 자지도 않는데 구현해야 함
class Robot implements Worker {
    @Override
    public void work() {
        // 실제 작업 수행
    }

    @Override
    public void eat() {
        // 불필요한 구현
        throw new UnsupportedOperationException();
    }

    @Override
    public void sleep() {
        // 불필요한 구현
        throw new UnsupportedOperationException();
    }
}

Good Case: ISP 준수 사례

// ✅ 이렇게 인터페이스를 분리하면 좋아요!
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

class Human implements Workable, Eatable, Sleepable {
    @Override
    public void work() {
        // 일하기
    }

    @Override
    public void eat() {
        // 식사하기
    }

    @Override
    public void sleep() {
        // 잠자기
    }
}

class Robot implements Workable {
    @Override
    public void work() {
        // 작업만 수행
    }
}

2. 실전 적용 예시: 주문 시스템 🛍

2-1. 인터페이스 분리 전

// ❌ 너무 많은 책임이 한 인터페이스에 있는 경우
interface OrderService {
    Order createOrder(OrderRequest request);
    void cancelOrder(String orderId);
    void refundOrder(String orderId);
    OrderStatus getOrderStatus(String orderId);
    List<Order> getOrderHistory(String userId);
    void sendOrderNotification(String orderId);
    void processPayment(String orderId);
    DeliveryStatus trackDelivery(String orderId);
}

2-2. 인터페이스 분리 후

// ✅ 책임별로 인터페이스 분리
public interface OrderCreator {
    Order createOrder(OrderRequest request);
}

public interface OrderManager {
    void cancelOrder(String orderId);
    void refundOrder(String orderId);
    OrderStatus getOrderStatus(String orderId);
}

public interface OrderQueryService {
    List<Order> getOrderHistory(String userId);
}

public interface OrderNotifier {
    void sendOrderNotification(String orderId);
}

public interface PaymentProcessor {
    void processPayment(String orderId);
}

public interface DeliveryTracker {
    DeliveryStatus trackDelivery(String orderId);
}

// 각 구현체는 필요한 인터페이스만 구현
@Service
public class SimpleOrderService implements OrderCreator, OrderManager {
    @Override
    public Order createOrder(OrderRequest request) {
        // 주문 생성 로직
    }

    @Override
    public void cancelOrder(String orderId) {
        // 주문 취소 로직
    }

    @Override
    public void refundOrder(String orderId) {
        // 환불 처리 로직
    }

    @Override
    public OrderStatus getOrderStatus(String orderId) {
        // 주문 상태 조회 로직
    }
}

@Service
public class OrderHistoryService implements OrderQueryService {
    @Override
    public List<Order> getOrderHistory(String userId) {
        // 주문 이력 조회 로직
    }
}

3. 실제 활용 예시: 결제 시스템 💳

3-1. 결제 처리 인터페이스 분리

// 기본 결제 처리
public interface PaymentProcessor {
    PaymentResult process(Payment payment);
}

// 결제 취소 기능
public interface Refundable {
    RefundResult refund(String paymentId);
}

// 결제 상태 조회
public interface PaymentQueryable {
    PaymentStatus getStatus(String paymentId);
}

// 부분 환불 기능
public interface PartialRefundable {
    RefundResult partialRefund(String paymentId, Money amount);
}

// 카드 결제는 모든 기능 지원
@Component
public class CreditCardPayment implements 
    PaymentProcessor, 
    Refundable, 
    PaymentQueryable,
    PartialRefundable {

    @Override
    public PaymentResult process(Payment payment) {
        // 카드 결제 처리
    }

    @Override
    public RefundResult refund(String paymentId) {
        // 전체 환불 처리
    }

    @Override
    public PaymentStatus getStatus(String paymentId) {
        // 결제 상태 조회
    }

    @Override
    public RefundResult partialRefund(String paymentId, Money amount) {
        // 부분 환불 처리
    }
}

// 가상계좌는 부분 환불 미지원
@Component
public class VirtualAccountPayment implements 
    PaymentProcessor,
    Refundable,
    PaymentQueryable {

    @Override
    public PaymentResult process(Payment payment) {
        // 가상계좌 발급 처리
    }

    @Override
    public RefundResult refund(String paymentId) {
        // 전체 환불 처리
    }

    @Override
    public PaymentStatus getStatus(String paymentId) {
        // 입금 상태 조회
    }
}

3-2. 클라이언트 코드

@Service
@RequiredArgsConstructor
public class OrderPaymentService {
    private final Map<String, PaymentProcessor> paymentProcessors;

    public PaymentResult processPayment(Order order) {
        PaymentProcessor processor = paymentProcessors.get(
            order.getPaymentType()
        );
        return processor.process(createPayment(order));
    }

    public RefundResult refundPayment(Order order) {
        // Refundable 기능이 있는 경우만 환불 처리
        PaymentProcessor processor = paymentProcessors.get(
            order.getPaymentType()
        );

        if (processor instanceof Refundable) {
            return ((Refundable) processor).refund(
                order.getPaymentId()
            );
        }

        throw new UnsupportedOperationException(
            "이 결제 수단은 환불을 지원하지 않습니다."
        );
    }
}

4. 인터페이스 분리 시 고려사항 ⚠️

4-1. 너무 잘게 쪼개지 않기

// ❌ 과도한 분리
interface OrderCreator {
    Order create(OrderRequest request);
}

interface OrderValidator {
    void validate(OrderRequest request);
}

interface OrderSaver {
    void save(Order order);
}

// ✅ 응집도 있는 분리
interface OrderService {
    Order createOrder(OrderRequest request);
    void cancelOrder(String orderId);
}

interface OrderQueryService {
    List<Order> getOrderHistory(String userId);
}

4-2. 역할에 따른 분리

// 명령(Command) 작업을 위한 인터페이스
interface OrderCommand {
    void createOrder(OrderRequest request);
    void cancelOrder(String orderId);
    void updateOrderStatus(String orderId, OrderStatus status);
}

// 조회(Query) 작업을 위한 인터페이스
interface OrderQuery {
    Order getOrder(String orderId);
    List<Order> getOrdersByUser(String userId);
    OrderStatus getOrderStatus(String orderId);
}

5. ISP의 장점 🌟

  1. 유연성 향상

    • 필요한 기능만 구현 가능
    • 불필요한 의존성 제거
  2. 유지보수성 향상

    • 변경의 영향 범위가 축소됨
    • 코드 이해도 향상
  3. 테스트 용이성

    • 각 인터페이스별로 독립적인 테스트 가능
    • 목(Mock) 객체 생성이 쉬워짐

6. 실전 적용 전략 💪

6-1. 인터페이스 설계 가이드

// 1. 인터페이스 응집도 확인
interface PaymentProcessor {
    // 결제 처리와 직접적으로 관련된 메서드만 포함
    PaymentResult process(Payment payment);
    void validate(Payment payment);
}

// 2. 클라이언트 관점에서 설계
interface OrderProcessor {
    // 주문 처리에 필요한 핵심 기능만 노출
    OrderResult process(OrderRequest request);
}

// 3. 확장성 고려
interface PaymentProcessor {
    // 미래의 결제 방식 추가를 고려한 설계
    PaymentResult process(Payment payment);
    boolean supports(PaymentMethod method);
}

6-2. 테스트 전략

public interface PaymentTestSupport {
    // 테스트에 필요한 공통 기능 정의
    Payment createTestPayment();
    void verifyPaymentResult(PaymentResult result);
}

@Test
class CreditCardPaymentTest implements PaymentTestSupport {
    private CreditCardPayment payment;

    @Override
    public Payment createTestPayment() {
        return Payment.builder()
            .amount(Money.wons(10000))
            .method(PaymentMethod.CREDIT_CARD)
            .build();
    }

    @Override
    public void verifyPaymentResult(PaymentResult result) {
        assertThat(result.isSuccess()).isTrue();
        assertThat(result.getTransactionId()).isNotNull();
    }

    @Test
    void processPayment_Success() {
        // Given
        Payment payment = createTestPayment();

        // When
        PaymentResult result = payment.process(payment);

        // Then
        verifyPaymentResult(result);
    }
}

7. 실무에서 자주 발생하는 문제와 해결책 🔨

7-1. 인터페이스 분리 시점

// 처음부터 과도하게 분리하지 않기
public interface OrderService {
    Order createOrder(OrderRequest request);
    void cancelOrder(String orderId);
}

// 클라이언트 요구사항에 따라 점진적으로 분리
public interface OrderCreator {
    Order createOrder(OrderRequest request);
}

public interface OrderManager {
    void cancelOrder(String orderId);
}

7-2. 기존 코드 리팩토링

// 단계적인 인터페이스 분리 적용
// 1. 새로운 인터페이스 생성
public interface NewOrderService {
    Order createOrder(OrderRequest request);
}

// 2. 어댑터를 통한 점진적 전환
@Component
public class OrderServiceAdapter implements NewOrderService {
    private final LegacyOrderService legacyService;

    @Override
    public Order createOrder(OrderRequest request) {
        return legacyService.processOrder(request);
    }
}

// 3. 클라이언트 코드 순차적 수정
@Service
public class OrderFacade {
    private final NewOrderService orderService;  // 새 인터페이스 사용

    public Order createOrder(OrderRequest request) {
        return orderService.createOrder(request);
    }
}

마치며 🎁

ISP는 결국 클라이언트 입장에서 필요한 기능만을 제공하자는 원칙입니다.
이를 통해 불필요한 의존성을 제거하고, 더 유연하고 관리하기 쉬운 코드를 만들 수 있습니다.


다음 시간에는 의존성 역전 원칙(DIP)에 대해 알아볼게요!
궁금한 점이 있다면 댓글로 남겨주세요! 😊

728x90