실무 개발자가 바라보는 Behavioral 디자인 패턴의 모든 것
개발자로 일하다 보면 코드의 구조화와 패턴에 대해 고민하는 시간이 참 많습니다.
오늘은 제가 실무, 개인 프로젝트에서 자주 사용하는 행위(Behavioral) 디자인 패턴에 대해 이야기해보려 합니다.
행위 패턴, 왜 필요한가?
처음 개발을 시작했을 때는 "왜 이렇게 복잡하게 해야 하지?"라는 생각을 많이 했습니다. 단순히 if-else로 처리하면 될 것을, 왜 이런 패턴들이 필요할까요? 하지만 프로젝트의 규모가 커지고, 요구사항이 복잡해질수록 이러한 패턴들의 진가가 드러나기 시작했습니다. 행위 패턴은 크게 세 가지 관점에서 우리의 코드를 개선해줍니다:
- 책임을 적절히 분산시켜 각 클래스가 한 가지 일만 잘하도록 만들어줍니다.
- 객체 간의 커뮤니케이션을 체계화하여 유지보수를 쉽게 만들어줍니다.
- 객체의 상태 관리를 효율적으로 할 수 있게 해줍니다.
행위 패턴 분류
Behavioral Patterns은 다음과 같이 분류할 수 있습니다.
- 책임 분산 패턴: 책임을 여러 클래스나 객체에 분산하여 코드를 단순화하고, 유지 관리를 쉽게 합니다.
- 커뮤니케이션 패턴: 클래스나 객체 간의 통신을 정의하여 코드를 유연하고, 재사용 가능하게 합니다.
- 상태 패턴: 객체의 상태를 관리하여 코드를 일관성 있게 유지하고, 유지 관리를 쉽게 합니다.
1. 책임 분산 패턴에는 다음과 같은 패턴이 있습니다.
- Strategy Pattern: 알고리즘을 캡슐화하여 코드를 단순화하고, 유지 관리를 쉽게 합니다.
- Observer Pattern: 객체의 상태 변화를 다른 객체에 알리는 데 사용됩니다.
- Command Pattern: 요청을 캡슐화하여 코드를 단순화하고, 재사용 가능하게 합니다.
- State Pattern: 객체의 상태를 관리하여 코드를 일관성 있게 유지하고, 유지 관리를 쉽게 합니다.
- Memento Pattern: 객체의 상태를 보존하여 코드를 복원 가능하게 합니다.
2. 커뮤니케이션 패턴에는 다음과 같은 패턴이 있습니다.
- Mediator Pattern: 객체 간의 통신을 중재하여 코드를 유연하고, 재사용 가능하게 합니다.
- Facade Pattern: 복잡한 시스템의 인터페이스를 단순화하여 코드를 이해하고 사용하기 쉽게 합니다.
- Adapter Pattern: 서로 다른 인터페이스를 갖는 클래스나 객체를 연결하여 코드의 호환성을 향상시킵니다.
- Decorator Pattern: 객체에 추가 기능을 제공하여 코드의 확장성을 향상시킵니다.
- Bridge Pattern: 추상 계층과 구현 계층을 분리하여 코드의 재사용성을 향상시킵니다.
3. 상태 패턴에는 다음과 같은 패턴이 있습니다.
- Finite State Machine Pattern: 객체의 상태를 관리하여 코드를 일관성 있게 유지하고, 유지 관리를 쉽게 합니다.
사용하기 가장 무난한 패턴들
가장 무난하게 활용한 예시로, 주문 시스템의 결제 프로세스를 예로 보여드리겠습니다.
카드 결제, 포인트 사용, 쿠폰 적용 등 다양한 결제 수단을 처리해야 했는데,
각각의 로직을 Command 객체로 캡슐화하니 코드가 훨씬 깔끔해졌습니다.
- Command Pattern
- 요청을 캡슐화하여 코드를 단순화해야 하는 경우
- 요청을 다른 클래스나 객체에서 재사용해야 하는 경우
interface Command {
void execute();
void undo();
}
// 실제 결제 처리 예시
class PaymentCommand implements Command {
private Payment payment;
@Override
public void execute() {
payment.process();
}
@Override
public void undo() {
payment.cancel();
}
}
주문 상태 관리에 State 패턴을 적용하면서, 복잡한 상태 전이 로직을 깔끔하게 정리할 수 있었습니다.
'결제 대기' -> '결제 완료' -> '배송 준비' -> '배송 중' -> '배송 완료'와 같은 흐름을 각각의 상태 객체로 분리하니 유지보수가 한결 수월해졌죠.
- State Pattern:
- 객체의 상태를 관리해야 하는 경우
- 객체의 상태 변화에 따라 다른 동작을 수행해야 하는 경우
// State 패턴의 실제 주문 시스템 예제
interface OrderState {
void proceed(Order order);
void cancel(Order order);
String getStatus();
}
class Order {
private OrderState state;
private String orderId;
public Order(String orderId) {
// 초기 상태는 결제 대기
this.state = new PaymentPendingState();
this.orderId = orderId;
}
public void setState(OrderState state) {
this.state = state;
}
public void proceedToNextState() {
state.proceed(this);
}
public void cancelOrder() {
state.cancel(this);
}
}
class PaymentPendingState implements OrderState {
@Override
public void proceed(Order order) {
// 결제 완료 시 배송 준비 상태로 전환
order.setState(new ShippingPrepState());
System.out.println("결제가 완료되어 배송 준비 상태로 전환됩니다.");
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
System.out.println("주문이 취소되었습니다.");
}
@Override
public String getStatus() {
return "결제 대기 중";
}
}
class ShippingPrepState implements OrderState {
@Override
public void proceed(Order order) {
order.setState(new InTransitState());
System.out.println("상품이 발송되어 배송 중 상태로 전환됩니다.");
}
@Override
public void cancel(Order order) {
// 배송 준비 중에는 취소 수수료가 발생할 수 있음
System.out.println("취소 수수료가 발생할 수 있습니다.");
order.setState(new CancelledState());
}
@Override
public String getStatus() {
return "배송 준비 중";
}
}
Observer 패턴
실시간 알림 시스템을 구현할 때 이 패턴이 큰 도움이 되었습니다.
주문 상태가 변경될 때마다 고객에게 알림을 보내야 했는데, Observer 패턴을 활용하니 핵심 비즈니스 로직과 알림 로직을 깔끔하게 분리할 수 있었습니다. 물론 실무에서는 추가로 이벤트 브로커를 사용하거나, 상용 솔루션을 사용하겠죠. :D
- Observer Pattern:
- 요청을 객체로 캡슐화 하는 경우
- 상태 변화를 전파해야 하는 경우
// Observer 패턴의 실제 알림 시스템 예제
interface OrderObserver {
void update(String orderId, String status);
}
class Order {
private List<OrderObserver> observers = new ArrayList<>();
private String orderId;
private String status;
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void setStatus(String status) {
this.status = status;
notifyObservers();
}
private void notifyObservers() {
for (OrderObserver observer : observers) {
observer.update(orderId, status);
}
}
}
// SMS 알림 구현
class SMSNotifier implements OrderObserver {
@Override
public void update(String orderId, String status) {
// SMS 발송 로직
System.out.println("SMS 발송: 주문 " + orderId + "이 " + status + " 상태로 변경되었습니다.");
}
}
// 이메일 알림 구현
class EmailNotifier implements OrderObserver {
@Override
public void update(String orderId, String status) {
// 이메일 발송 로직
System.out.println("이메일 발송: 주문 " + orderId + " 상태가 " + status + "로 업데이트되었습니다.");
}
}
// 관리자 알림 구현
class AdminNotifier implements OrderObserver {
@Override
public void update(String orderId, String status) {
if (status.equals("CANCELLED")) {
System.out.println("관리자 알림: 주문 " + orderId + " 취소 처리 필요");
}
}
}
// 사용 예시
public class OrderSystem {
public static void main(String[] args) {
Order order = new Order("ORDER-001");
// 옵저버 등록
order.addObserver(new SMSNotifier());
order.addObserver(new EmailNotifier());
order.addObserver(new AdminNotifier());
// 주문 상태 변경
order.setStatus("PAYMENT_COMPLETED"); // 모든 옵저버에게 알림 발송
order.setStatus("SHIPPING"); // 모든 옵저버에게 알림 발송
order.setStatus("CANCELLED"); // 취소 시 관리자에게도 특별 알림
}
}
앞으로의 계획
다음 포스팅에서는
실무환경, 특히 MSA 환경에서 이러한 패턴들을 어떻게 효과적으로 활용할 수 있을지, 설계하고 검토해보겠습니다.
패턴을 위한 패턴이 아닌, 실제 문제 해결을 위한 도구로서의 디자인 패턴. 이것이 제가 실무에서도 그리고 블그를 통해서도 전달하고 싶은 핵심 메시지입니다.