오늘은 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의 장점 🌟
유연성 향상
- 필요한 기능만 구현 가능
- 불필요한 의존성 제거
유지보수성 향상
- 변경의 영향 범위가 축소됨
- 코드 이해도 향상
테스트 용이성
- 각 인터페이스별로 독립적인 테스트 가능
- 목(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
'800===Dev Docs and License > 이론 문서' 카테고리의 다른 글
함수형 프로그래밍 vs 명령형 프로그래밍 비교 가이드 🔄 (0) | 2024.11.03 |
---|---|
객체지향 심화 학습 5편: 의존성 역전 원칙 (DIP) 완전정복 🎯 (0) | 2024.11.03 |
객체지향 심화 학습 1편 SRP : SOLID 원칙 완전정복 🎯 (0) | 2024.11.03 |
객체지향의 핵심 개념 정복하기 🎯 (0) | 2024.11.03 |
What is the Internet (0) | 2024.06.07 |