저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
기능을 추가하는 과정에서 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 패턴 적용 후 변경된 코드는 없고, 새롭게 추가된 클래스만 존재한다.)

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 토비 2008.12.29 18:40 신고  댓글주소  수정/삭제  댓글쓰기

    흥미로운 내용입니다. 스프링을 이용해서 디자인패턴을 자연스럽게 적용시키는 좋은 샘플인 것 같습니다.
    한가지 궁금한 것은 모니터리포트는 그 이전 메일리포트의 처리가 올바르게 되었는지를 확인하는 리포트이니, 메일리포트 처리가 실패했더라도(런타임익셉션 발생등) 모니터리포트가 발송되야 하지 않을까 하는 것입니다. 그런경우 동등한 레벨의 컴포넌트를 그룹핑해서 개별대상처럼 취급한다는 컴포짓패턴은 그리 적합해보이지는 않습니다. 기존 클라이언트(process)와 인터페이스를 유지한 채로라면 그 경우에는 메일모니터에 대해서 AOP를 이용해 일종의 모니터링 애스팩트를 적용하는 것이 더 나아보입니다.

  2. 최범균 madvirus 2008.12.30 09:20 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 토비님. 물론 에러에 대한 부분은 CompositeReportSender에서 처리하고 있습니다. 그리고 위 작업을 수행할 때는 두 Actor-즉, 메일 수신자와 모니터 시스템-에 대한 보고라는 관점으로 접근을 했습니다. 만약 모니터 시스템에 대해 보고라는 관점이 아닌 작업 요청이라는 관점으로 접근하게 된다면 또 다른 인터페이스를 도출했을 것 같긴 합니다.

    AOP에 대해서는 전 이견이 있는데, 개인적으로 (이건 어디까지나 개인적인 생각인데) 뭔가 투명하게 적용되어야 할 기능(예를 들어 캐시 처리, 실행 추적 로그)이나 선언적 트랜잭션처럼 편리함이 명확한 경우가 아니면 AOP가 주는 이로움보다 복잡도가 더 커진다고 생각하고 있습니다.