[리팩토링] 악취 22 : 서로 다른 인터페이스의 대안 클래스 *

 

✍️ 악취 22 : 서로 다른 인터페이스의 대안 클래스

유사한 기능과 목적을 가진 클래스가 서로 다른 인터페이스를 구현하고 있다면 그 목적은 유사할지언정 상호 대체가 불가능한 구조로, 다르게 말하면 다형성을 활용할 수 없는 문제점이 발생한다. 비슷한 일을 서로 다른 규악으로 지원한다면 관리 문제도 발생한다.

 

중복된 인터페이스를 제거하고 '함수 선언 변경하기'와 '함수 옮기기'를 사용해서 하나의 인터페이스만을 구현하도록 구조를 변경할 수 있다. 하지만 인터페이스를 수정할 수 없는 상황도 존재한다. 외부 모듈 혹은 라이브러리를 참조하는 케이스가 그 대표적인 예시이다. 본 포스팅에선 인터페이스의 수정이 불가능한 상황에서 리팩토링을 적용하는 예제를 소개하려 한다.

 

🍊 예제 : 적용 전

가정해 봅시다. '이메일을 전송하는 기능'과 '알람을 전송하는 기능'이 서로 다른 인터페이스로 정의되었음을.

public interface EmailService {

    void sendEmail(EmailMessage emailMessage);
}

public interface AlertService {

    void add(AlertMessage alertMessage);
}

public class EmailMessage {

    private String title;
    private String to;
    private String from;
}


public class AlertMessage {

    private String message;
    private String to;
}

 

비슷하지만 서로 다른 인터페이스로 그 기능을 제공하고 있다면 다형성을 활용할 수 없고 관리가 어렵다는 문제가 있다.

public class OrderManager {

    private AlertService alertService;

    public void alertOrder(Order order) {
        AlertMessage alertMessage = AlertMessage.builder()
                .message(order.getOrder() + "is ordered")
                .to(order.getEmail())
                .build();

        alertService.add(alertMessage);
    }
    
}

public class ShippingManager {

    private EmailService emailService;

    public void notifyShipping(Shipping shipping) {
        EmailMessage emailMessage = EmailMessage.builder()
                .title(shipping.getOrder() + "is shipped")
                .to(shipping.getEmail())
                .from("no-reply@knagworld.co.kr")
                .build();

        emailService.sendEmail(emailMessage);
    }

}

 

🌱 예제 : 적용 후

인터페이스를 변경하는 것이 베스트지만, 그렇지 못한 케이스에선 '어댑터 패턴'을 적용하는 것도 하나의 방법이 될 수 있다.

 

OrderShipping을 감싸는 인터페이스를 정의한다.

public interface Notification {
}

 

각각의 구현체는 AlertMessage EmailMessage를 제공한다.

@AllArgsConstructor
public class AlertNotification implements Notification {

    private Order order;

    public AlertMessage toAlertMessage() {
        return AlertMessage.builder()
                .message(order.getOrder() + " is ordered")
                .to(order.getEmail())
                .build();
    }
}

@AllArgsConstructor
public class EmailNotification implements Notification {

    private Shipping shipping;

    public EmailMessage toEmailMessage() {
        return EmailMessage.builder()
                .title(shipping.getOrder() + " is shipped")
                .from("no-reply@kangworld.co.kr")
                .to(shipping.getEmail())
                .build();
    }
}

 

클라이언트는 추상화된 Notification과 NotificationService 만으로 코드를 전개하고, 구체적인 AlertMessage와 AlertService, EmailMessage와 EmailService는 구현체에서 처리하도록 변경한다. 

public interface NotificationService {

    void sendNotification(Notification notification);
}
public class OrderManager {

    private NotificationService notificationService;

    public void alertOrder(Order order) {
        Notification notification = new AlertNotification(order);

        notificationService.sendNotification(notification);
    }
}

public class AlertNotificationService implements NotificationService {

    private AlertService alertService;

    @Override
    public void sendNotification(Notification notification) {
        AlertNotification alertNotification = (AlertNotification) notification;

        alertService.add(alertNotification.toAlertMessage());
    }

}