주요글: 도커 시작하기
반응형
기능을 추가하는 과정에서 Composite 패턴을 적용한 사례를 공유한다.

추가하려던 기능과 최초 구현

대량 배치 작업을 처리하기 위해 하루에 한번씩 정해진 시간에 동작하는 어플리케이션을 개발한 적이 있다. 이 어플리케이션은 전체 작업이 완료되면 처리 결과를 담당자들에게 이메일로 보내주고 있다. 작업 처리기와 이메일로 결과를 발송해주는 부분의 최초 설계는 다음과 같았다.


결과를 리포트 해 주는 역할을 정의한 ReportSender 인터페이스를 정의했고, 메일을 이용해서 리포팅 해 주는 MailReportSender 구현 클래스를 만들었다. Processor는 처리가 완료되면 ReportSender 모듈을 사용해서 리포트를 발송하도록 구현했다. 아래 코드는 Processor의 일부를 보여주고 있다.

public class Processor {
    private ReportSender sender;
    
    public void process() {
        ...
        sender.sendReport(result);
    }
    ...
    public void setReportSender(ReportSender sender) {
        this.sender = sender;
    }
}

Processor와 ReportSender의 조립은 스프링 DI를 이용해서 처리해서, 요구 기능이 변경될 경우 ReportSender 구현체를 쉽게 교체할 수 있도록 하였다.

개발된 어플리케이션이 실제로 서비스에 투입된 후 몇 달 정도 지나서 새로운 요구 사항이 들어왔는데, 그 요구사항은 아래와 같았다.

  • 처리가 올바르게 되었는 지의 여부를 모니터링 시스템에 알려줄 것
즉, 메일로 담당자들에게 리포팅 하는 것 뿐만 아니라 모니터링 시스템에 처리 결과를 알려주어야 했다. 가장 먼저 모니터링 시스템과의 연동을 위한 인터페이스 및 구현 클래스를 개발하였다.

public interface MonitorSystemClient {
    public void sendMessage(String serviceId, String result);
}

public class HttpMonitorSystemClient implements MonitorSystemClient {
    ...
}

MonitorSystemClient 인터페이스를 작성한 뒤 Processor에 다음과 같이 MonitorSystemClient 필드를 추가하고, 처리 결과를 모니터링 시스템에 보낼 수 있도록 하였다.

public class Processor {

    private ReportSender sender;
    private MonitorSystemClient monitorSystemClient;
    
    public void process() {
        ...
        sender.sendReport(result);
        monitorSystemClient.sendMessage(serviceId, result);
    }
    ...
    public void setMonitorSystemClient(MonitorSystemClient monitorSystemClient) {
        this.monitorSystemClient = monitorSystemClient;
    }
}

새로 추가된 인터페이스에 대한 Mock을 만들어 Processor를 테스트 하고, 통합 테스트로 기능이 정상적으로 동작한 것을 확인하였다. 그리고 실 서비스에 적용해서 정상 동작하는 것을 확인하였다. 이 시점에서 클래스 간의 관계를 다음과 같다.


모든 게 잘 동작했는데, 코드가 마음에 들지 않았다. 모니터링 시스템에 메시지를 알려주는 것 역시 리포팅의 한 종류인데, Processor가 굳이 MonitorSystemClient에 의존해야 하나? 리포팅 해 주어야 하는 새로운 요구사항이 추가되면 새로운 의존을 Processor에 또 추가해야 하나? 이런 질문 끝에, 추후 새로운 리포팅 요구사항이 추가되도 Processor의 코드에 영향을 주지 않기 위해 Composite 패턴을 적용하기로 결정했다.

Composite 패턴을 사용하도록 리팩토링

Composite 패턴을 적용하기 위해 아래 코드와 같이 CompositeReportSender 클래스를 작성하였다.

public class CompositeReportSender implements ReportSender {
    private List<ProcessorResultReportSender> senderList;

    public void sendReport(String serviceId, String result) {
        if (senderList != null && !senderList.isEmpty()) {
            for (ProcessorResultReportSender reportSender : senderList) {
                reportSender.sendReport(serviceId, result);
            }
        }
    }

    public void setSenderList(List<ProcessorResultReportSender> senderList) {
        this.senderList = senderList;
    }

}

추가로 MonitorSystemClient를 이용해서 리포트를 전송해주는 MonitorSystemReportSender를 다음과 같이 구현하였다.

public class MonitorSystemReportSender implements ReportSender {
    private MonitorSystemClient monitorSystemClient;
    
    public void sendReport(String serviceId, String result) {
        monitorSystemClient.sendMessage(serviceId, result);
    }
    
    public void setMonitorSystemClient(MonitorSystemClient monitorSystemClient) {
        this.monitorSystemClient = monitorSystemClient;
    }

}

CompositeReportSender가 1개 이상의 ReportSender에 리포트 발송 요청을 할 수 있고 모니터링 시스템에 리포트를 발송해주는 MonitorSystemReportSender를 구현했기 때문에, Processor는 더 이상 MonitorSystemClient에 의존할 필요가 없어졌다. 그래서 Processor에 추가한 MonitorSystemClient에 대한 의존을 제거하였다.

// 다시 원래대로 돌아옴
public class Processor {
    private ReportSender sender;
    
    public void process() {
        ...
        sender.sendReport(result);
    }
    ...
    public void setReportSender(ReportSender sender) {
        this.sender = sender;
    }
}

변경된 결과를 클래스 다이어그램으로 표현하면 다음과 같다.


이제 남은 작업은 두 ReportSender를 (MailReportSender와 MonitorSystemReportSender를) 포함하는 CompositeReportSender를 생성한 뒤 Processor에 전달하는 것이다. 이 프로젝트에서는 스프링을 사용하고 있으므로 다음과 같은 설정 파일을 이용해서 이들 객체를 조립하였다.

<bean id="reportSender" class="CompositeReportSender">
    <property name="senderList">
        <list>
            <ref bean="mailReportSender" />
            <ref bean="monitorSystemReportSender" />
        </list>
    </property>
</bean>

<bean id="mailReportSender" class="MailReportSender" />

<bean id="monitorSystemReportSender" class="MonitorReportSender"  ... />

<bean id="processor" class="Processor">
    <proerty name="reportSender" ref="reportSender" />
</bean>

만약 새로운 ReportSender에게도 리포트 해 주어야 할 경우, 이제는 다음과 같이 신규 ReportSender 구현체만 CompositeReportSender에 추가해 주면 된다.

<bean id="reportSender" class="CompositeReportSender">
    <property name="senderList">
        <list>
            <ref bean="mailReportSender" />
            <ref bean="monitorSystemReportSender" />
            <ref bean="someNewReportSender" />
        </list>
    </property>
</bean>
...
<bean id="someNewReportSender" class="SomeNewReportSender" />
...

지금까지 리팩토링 과정에서 Composite 패턴을 적용한 과정을 설명하였다. 하나의 문제를 해결하는 방법은 다양하게 존재할 수 있는데, 필자는 다양한 방법 중에 기존 코드(더 나아가 설계)들을 최대한 유지하기 위해 Composite 패턴을 적용하였다. (실제로 이 예제의 경우 기능 추가 전과 Composite 패턴 적용 후 변경된 코드는 없고, 새롭게 추가된 클래스만 존재한다.)

+ Recent posts