냄새 21. 서로 다른 인터페이스의 대안 클래스들

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다.


    냄새 21. 서로 다른 인터페이스의 대안 클래스들 (Alternative Classes with Different Interfaces)

    • 서로 다른 인터페이스의 대안 클래스
      • 비슷한 일을 여러 클래스에서 서로 다른 규약을 사용해 지원하고 있는 경우를 가리키는 코드 냄새.
      • 하나의 클래스 대신 다른 대안 클래스를 사용하려면, 두 클래스는 동일한 인터페이스를 구현하고 있어야 한다.
    • 리팩토링
      • '함수 선언 변경하기'와 '함수 옮기기'를 사용해서 서로 동일한 인터페이스를 구현하게끔 코드를 수정할 수 있다.
      • 두 클래스에서 일부 코드가 중복되는 경우에는 '슈퍼 클래스 추출하기'를 사용해 중복된 코드를 슈퍼클래스로 옮기고, 두 클래스를 새로운 슈퍼클래스의 서브 클래스로 만들 수 있다.  (같은 인터페이스 제공)
      • 위의 방법을 적용할 수 없는 서드파티인 경우라면, 추상 계층을 하나 더 추가해서 통일할 수 있다. 

     

    대안 클래스는 '서로 교체해서 쓸 수 있을 것 같은 클래스들'을 의미한다. 그래서 '비슷한 기능'을 지원하는 클래스라고 볼 수 있다. 한 가지 예를 들면 다음과 같다.

    • JDBC API : DB 접근
    • JPA API : DB 접근 

    이 녀석들은 동일하게 DB에 접근해서 작업을 하기 때문에 비슷한 기능을 하는 녀석들이다. 그렇지만 서로 다른 API 인터페이스를 제공한다. 이런 것들이 '서로 다른 인터페이스의 대안 클래스들'이란 냄새를 의미한다.  '서로 다른 인터페이스의 대안 클래스 냄새'는 다음 방법을 통해 해결할 수 있다.

    • 이런 경우에 '함수 선언 변경하기'를 통해서 함수 시그니처를 맞추어서 동일한 인터페이스를 구현하게끔 바꿀 수 있다. 
    • '함수 옮기기'를 통해서 적절한 위치로 옮겨줄 수 있을 것이다.
    • 일부 코드만 중복되는 경우 '슈퍼 클래스 추출하기'를 사용해 중복된 코드를 슈퍼클래스로 옮길 수 있다. 

    코드 살펴보기

    아래의 AlertService / EmailService는 둘다 Notification을 해준다. 그렇지만 인터페이스는 Notification을 할 때, add() / sendEmail()이라는 서로 다른 API를 사용하고 있다. 비슷한 기능을 제공하지만, 서로 다른 인터페이스를 제공하는 대안 클래스라고 볼 수 있는 상황이다. 

    위에서는 함수 선언 변경하기, 슈퍼 클래스 추출하기 등으로 해결할 수 있다고 했지만, 만약 서드 파티인 경우에 직접적인 수정을 할 수 없다. 이 때는 추상화를 통해서 해결할 수 있다. 

    // AlertService / EmailService는 외부에 뭔가를 전송해준다는 것에서 비슷한 일을 한다.
    // 하지만 서로 다른 API를 제공(add, sendEmail)하기 때문에 대안 클래스처럼 동작하고 있다.
    // 슈퍼 클래스로 추출하면 좋지만, 서드 파티라서 추출할 수 없는 경우라면 하나의 추상화 계층을 추가해서 처리할 수 있다.
    public interface AlertService {
        void add(AlertMessage alertMessage);
    }
    
    public interface EmailService {
        void sendEmail(EmailMessage emailMessage);
    }

    다음과 같은 방식으로 리팩토링을 진행 할 수 있다.

    1. 필요한 클래스들을 추상화하여 하나씩 묶어준다.
      • AlertMessage, EmailMessage를 Notification으로 추상화한다. 
      • AlerService / EmailService를 추상화한 NotificationService 인터페이스를 제공한다. Notification은 sendNotification()으로 공통된 인터페이스를 제공한다.
    2. NotificationService의 구현체 AlertNotificationService, EmailNotificationService 클래스를 제공한다. 
    3. OrderAlerts, OrderProcessor에서 각 메서드를 sendNotification() 메서드로 추출한다. 그리고 sendNotification() 메서드는 Notification을 매개변수로 받도록 한다.
    4. 받은 Shipping, Order 매개변수로 Notification 객체를 생성하고 sendNotification() 메서드에 전달해준다.
    5. 기존에 alertShipped(), notifyShipping()에 있던 메서드는 각각 AlertNotificationService / EmailNotificationService로 이동시켜준다. 

    결과적으로 아래처럼 코드가 작성된다.

    public class AlertNotificationService implements NotificationService{
    
        private final AlertService alertService;
    
        public AlertNotificationService(AlertService alertService) {
            this.alertService = alertService;
        }
    
        @Override
        public void sendNotification(Notification notification) {
            AlertMessage alertMessage = new AlertMessage();
            alertMessage.setMessage(notification.getTitle());
            alertMessage.setFor(notification.getSender());
            alertService.add(alertMessage);
        }
    }
    
    public class EmailNotificationService implements NotificationService{
    
        private final EmailService emailService;
    
        public EmailNotificationService(EmailService emailService) {
            this.emailService = emailService;
        }
    
        @Override
        public void sendNotification(Notification notification) {
            EmailMessage emailMessage = new EmailMessage();
            emailMessage.setTitle(notification.getTitle());
            emailMessage.setTo(notification.getSender());
            emailMessage.setFrom(notification.getReceiver());
            emailService.sendEmail(emailMessage);
        }
    }
    
    
    public class OrderAlerts {
    
        private NotificationService notificationService;
        
        public void alertShipped(Order order) {
            Notification notification = Notification.newNotification(order.toString() + " is shipped")
                    .sender(order.getEmail());
            sendNotification(notification);
        }
    
        private void sendNotification(Notification notification) {
            notificationService.sendNotification(notification);
        }
    }
    
    public class OrderProcessor {
    
        private NotificationService notificationService;
    
        public void notifyShipping(Shipping shipping) {
            Notification notification = Notification.newNotification(shipping.getOrder() + " is shipped.")
                    .receiver(shipping.getEmail())
                    .sender("no-reply@whiteship.com");
            sendNotification(notification);
        }
    
        private void sendNotification(Notification notification) {
            notificationService.sendNotification(notification);
    }
    
    }

    코드 수정 결론

    • NotificationService 라는 추상 계층을 하나 추가해서, 서로 다른 대안 인터페이스를 동일한 인터페이스를 제공하도록 했다. 
    • NotificationService는 Notification 매개변수를 받아서, 각 도메인에 맞는 Notification을 보낸다. 이 때, notifyShipping() 등에 있던 구체적인 코드들(Order 도메인)은 각 NotificationService의 구현체의 sendNotification() 메서드로 이동되었다. 
    • 이 작업의 의미는 '추상화를 하나 더 올린거다'. 기존은 다른 인터페이스 / 비슷한 기능을 제공했었다. 이것을 하나의 추상화된 계층인 인터페이스를 추상화해서 API를 동일하게 바꿔준 것이다. 

    'etc > 리팩토링' 카테고리의 다른 글

    리팩토링 42. 레코드 캡슐화 하기  (0) 2023.05.10
    냄새 22. 데이터 클래스  (0) 2023.05.10
    리팩토링 41. 슈퍼클래스 추출하기  (0) 2023.05.10
    냄새 20. 거대한 클래스  (0) 2023.05.10
    냄새 19. 내부자 거래  (0) 2023.05.10

    댓글

    Designed by JB FACTORY