업데이트:

📌 참고 소스는 GitHub에 있습니다.

디자인 패턴을 학습 도중 이해와 사용성에 대해 어려운 몇 가지 패턴이 있어 이를 포스팅하려 합니다.

그 중 첫 번째로 템플릿 메소드 패턴을 포스팅해보겠습니다.

Template 메소드 패턴


Template의 단어 뜻을 보면 “견본”, “본보기”라는 뜻입니다.

인터페이스를 견본으로 하고 어떤 알고리즘(혹은 프로세스)를 진행함에 있어 특정 부분을 하위 클래스에서 구현하여 사용하는 패턴입니다.

예시 코드


어려운 얘기보다 바로 코드를 보도록 하겠습니다.

일반 Service 로직 코드

다음 코드는 주문에 대한 비즈니스 로직이며, 주문을 하기 위해 결제, 재고차감, 주문승인 총 3개의 프로세스를 진행합니다.

제가 만든 Application은 다양한 쇼핑몰을 연동하는 서비스이며 쇼핑몰마다 전체 주문 로직은 변함 없으나 쇼핑몰마다 결제 로직이 다르다는 것은 전제로 하겠습니다. (제가 이런 서비스를 만들고 운영한다면 좋겠네요 🤣ㅎㅎ)

public class ShoppingOrderService {

    public OrderResponseDto order(ShoppingMallType type) {
        payment(type);
        decreaseStock();
        confirmOrder();
        return new OrderResponseDto();
    }

    private void payment(ShoppingMallType type) {

        if (type == ShoppingMallType.AUCTION) {
            // 옥션 결제 코드
        }

        if (type == ShoppingMallType.COUPANG) {
            // 쿠팡 결제 코드
        }

        if (type == ShoppingMallType.GMARKET) {
            // 지마켓 결제 코드
        }

    }

    private void decreaseStock() {}

    private void confirmOrder() {}
}

위의 코드를 보면 order() 메소드에 결제, 재고차감, 주문완료 프로세스가 이어져 있습니다.

그중 결제에서는 ShoppingMallType이라는 enum을 인자로 넘기면서 if문으로 각 쇼핑몰마다의 결제 로직을 분기처리하고 있습니다.

문제점

새로운 쇼핑몰이 생긴다면 어떻게 될까요?

결제 부분에 또 다른 if문을 추가해야함은 물론이고, 만약 재고차감이나 주문 완료 부분마저 다른 쇼핑몰과 다르다면 해당 메소드에 또 if문을 추가하면서 수정 작업을 반복해야 할 것입니다.

결국 가독성이 떨어지게 되고, 코드의 관리가 어려워고, 수정 작업에 Side-Effect가 발생활 확률이 높아지게 됩니다.

이럴 때 쓰기 유용한 패턴이 템플릿 패턴입니다.

다음은 템플릿 패턴으로 변경한 코드를 보도록 하겠습니다.

템플릿 메소드 패턴 코드

Interface

먼저 “견본”이 되는 Interface를 보도록 하겠습니다. 공통으로 동작하는 order(), decreaseStock(), confirmOrder()default로 메소드로 선언하고, 쇼핑몰마다 다르게 동작하는 payment()만을 추상 메소드로 선언하였습니다.

public interface ShoppingMall {

    default OrderResponseDto order(ShoppingMallType type) {
        payment();
        decreaseStock();
        confirmOrder();

        return new OrderResponseDto();
    }

    void payment();

    default void decreaseStock() {
        // 재고 차감 로직
    }
    
    default void confirmOrder() {
        // 주문 완료 로직
    }
}

하위 구현 클래스

다음은 결제 로직 구현 클래스 중 하나인 CoupangOrder 입니다.

여기서 추상 메소드인 payment()를 쿠팡 비즈니스에 맞춰 코드를 작성합니다.

public class CoupangOrder implements ShoppingMall {

    @Override
    public void payment() {
        // 쿠팡 결제 로직
    }
}

Factory 메소드

다음은 ShoppingMallType enum에 따라 하위 클래스를 반환하는 Factory 클래스입니다.

public class ShoppingMallFactory {

    public static ShoppingMall getInstance(ShoppingMallType type) {
        switch (type) {
            case AUCTION:
                return new AuctionOrder();
            case GMARKET:
                return new GmarketOrder();
            case COUPANG:
                return new CoupangOrder();
            default:
                throw new IllegalArgumentException("적합한 쇼핑몰이 아닙니다.");
        }
    }
}

Service

다음은 주문 메소드를 호출하는 Service 클래스입니다.

Factory 클래스에서 ShoppingMallType에 따라 구현 클래스를 반환하고, 쇼핑몰(구현 클래스)에 따라 결제 로직이 자연스럽게 다르게 동작하게 될 것입니다.

public class ShoppingOrderService {

    private ShoppingMall shoppingMall;

    public OrderResponseDto order(ShoppingMallType type) {
        shoppingMall = ShoppingMallFactory.getInstance(type);
        return shoppingMall.order();
    }
}

템플릿 메소드 패턴의 장점

템플릿 메소드 패턴의 장점은 중복코드를 줄이고, 객체 지향스럽게 코드를 작성하듯 여러가지가 있지만 가장 큰 장점은 결합도를 낮추는 것입니다.

위의 예제 코드를 보면 Service는 더 이상 주문 로직에 대해 신경 쓸일이 없어집니다.

새로운 쇼핑몰이 생기게 되면 Factory에서의 추가 작업과 구현 클래스를 새로 생성하면 될 것이고, 주문 로직이 변경되면 ShoppingMall 인터페이스나 각 쇼핑몰 구현 클래스에서 작업이 이뤄질 것입니다.

기존의 if문 수정처럼 하나의 메소드 안에서 수정하고 Side-Effect를 걱정해야하는 것이 아닌 각 쇼핑몰마다 본인의 것만 신경쓰면 되기 때문에 확장 및 수정 작업을 쉽게 할 수 있습니다.

❗주의할 점 하지만 템플릿 메소드 패턴에도 주의할 점이 있습니다. 바로 전체적인 로직 자체가 수정작업이 이뤄지면 하위 구현 클래스 모두 영향을 받게 됩니다. 따라서, 전체적인 로직의 변경이 없거나 적다라고 판단될 때 템플릿 메소드 패턴을 사용하는 것이 좋습니다.

마무리


오늘은 템플릿 메소드 패턴에 대해 알아보았는데요.

처음에는 저도 단순 추상화처럼 알고 실무에서 크게 쓸일이 없겠구나라고 생각했지만, “정해진 공통된 알고리즘 로직을 변경하지 않고 특정 부분만 다르게 동작하도록 한다.”라는 목적에 결합도를 낮추고 객체 지향스럽고 우아하게 코드를 작성하기 위한 패턴임을 알게 되어 실무에서도 적합한 곳에 사용해보려 합니다.

특히 이미 추상화를 진행한 코드라면 이미 인터페이스에 의존하고 있기 때문에 템플릿 메소드 패턴의 주의할 점도 이미 있다고 판단하여 적극적으로 사용해봐야겠습니다.

오늘도 제 포스팅을 읽어 주셔서 감사합니다. 😄

댓글남기기