이 글에서는 설문조사 웹 애플리케이션 개발 프로젝트에서 서비스 레이어 메서드의 로직이 가독성이 떨어지고, 유지보수가 어려운 상황을 개선하기 위해 Command 디자인 패턴을 적용한 경험을 공유하고자 합니다.
Command 패턴
Command 패턴은 행동 패턴 중 하나로, 요청을 객체의 형태로 캡슐화하여 여러 가지 다른 요청이 나중에 필요할 때 실행되거나 취소될 수 있도록 하는 패턴이다. 이 패턴을 사용하면 요청하는 객체와 요청을 수행하는 객체 사이의 결합을 느슨하게 만들 수 있다.
Command 패턴의 핵심 아이디어는 작업을 요청하는 객체를 독립적으로 처리할 수 있게 하는 것이다.
Command 패턴의 구성 요소
- Command 인터페이스:
- 실행될 동작을 정의하는 인터페이스이다. 일반적으로 execute() 메서드를 포함한다.
- ConcreteCommand 클래스:
- Command 인터페이스를 구현하며, 실제 동작을 수행하는 클래스이다. 여기에는 요청을 처리할 수신자(Receiver)가 포함된다.
- 요청을 처리하는 로직을 execute() 메서드에 구현한다.
- Receiver 클래스:
- 실제 요청을 처리하는 작업을 수행하는 클래스이다. ConcreteCommand 클래스는 Receiver 객체를 호출하여 요청을 처리한다.
- Invoker 클래스:
- Command 객체를 실행하는 클래스이다. 이 클래스는 하나 이상의 Command 객체를 저장하고 있다가 필요할 때 execute() 메서드를 호출한다.
Command 패턴 적용하기
기존 코드의 문제점
설문조사에 응답 데이터를 저장하기 위해서는 대략 이런 순서로 진행이 된다.
- 응답한 설문조사에 해당하는 설문조사 엔티티를 불러온다.
- 응답한 질문에 해당하는 질문 엔티티를 불러온다.
- 응답의 유형(e.g. 단답형, 객관식)에 따라 응답 엔티티를 생성하고 저장한다.
- 응답에 따른 보상 포인트를 계산하고 저장한다.
위 모든 비즈니스 로직을 한 메서드에 직접 구현했다. 한 메서드 내에 너무 많은 작업이 집중되면서 코드가 복잡해졌고 가독성이 떨어졌다.
위 문제를 해결하기 위해 Command 디자인 패턴을 적용했다.
1. Command 인터페이스 정의
public interface Command {
void execute();
}
2. Invoker 클래스 정의
@Service
@RequiredArgsConstructor
public class CommandExecutor {
private final List<Command> commands;
public void executeCommands() {
for (Command command : commands) {
command.execute();
}
}
}
3. Concrete Command 객체들 정의
기존에 한 메서드에서 수행하던 로직을 여러 Command 클래스를 만들어서 분리시켰다.
4. Receiver 메서드 리팩토링
효과
Command 패턴을 적용함으로써 얻은 효과는 다음과 같다:
- 책임 분리: 각 Command 클래스 별 책임을 분리하여, 코드의 가독성이 크게 향상되었다.
- 복잡성 관리: 복잡한 로직을 여러 개의 작은 클래스로 나누어 관리하여 코드의 복잡성을 줄였다.
- 유지보수성 향상: 코드가 명확하게 분리되어 있어 수정 시 영향 범위를 최소화할 수 있다.
마치며
- Command 패턴은 코드의 가독성, 유지보수성, 재사용성을 높이는 데 유용한 디자인 패턴이다.
- 그러나 Command 패턴을 적용하면 클래스와 객체의 수가 증가한다. 과도한 Command 클래스 생성을 피하고, 필요에 따라 적절한 수준으로 적용하는 것이 좋다. 단순한 작업에는 오버헤드가 될 수 있으므로 신중하게 사용한다.