본문 바로가기
Java

템플릿 메소드(Template Method) 패턴으로 로직 중복 제거하기

by 코더 제이콥 2024. 6. 14.

이번 포스팅은 실무에서 템플릿 메소드 패턴을 사용하여 중복되는 '행위'를 해결한 일지를 작성해보고자 합니다.

개요

실무에서 Google의 Firebase Cloud Messaging을 활용하여 푸시를 전송하는 로직을 개편했다. FCM을 활용하여 푸시를 전송할 때는 FCM에서 발급하는 '토큰'을 전송해야 한다. 이때 FCM 서버는 토큰을 최대 500개까지 허용한다. 만약 회원과 토큰이 일대일이라고 가정하자. 푸시를 전송해야 하는 회원이 2천명이라면, 한 번에 2천개의 토큰을 Request Body에 담아 FCM에 요청하면 예외가 터지는 것이다.

따라서 프로젝트 설계를 아래와 같이 구성했다.

public class ListExtractor<R> {
  ...
  public ListExtractor(final long extractingSize, final List<R> list) {
      ...
  }


  public List<R> getExtractedList(final int iterationIndex) {
      ...
  }
}

ListExtractor는 제너릭 을 extractingSize개로 분할하여 제공하는 역할을 담당한다. 생성자로 리스트를 받아 초기화하고, getExtractedList 메소드를 통해 분할되는 리스트를 제공받는다.

public interface Notifier {
  void send(Payload payload);
}

Notifier 인터페이스는 인프라스트럭처 레이어로 FCM에 푸시 요청을 하는 역할을 담당한다. Notifier에서는 구글의 토큰 500개의 제한을 적용하기 위해 ListExtractor를 다음과 같이 사용한다.

void send() {
    List<String> tokens = payload.tokens();

    ListExtractor<String> extractor = new ListExtractor<>(MAX_TOKENS_PER_REQUEST, tokens);
    long iterationCount = extractor.getIterationCount();

    for (int i = 0; i < iterationCount; i++) {
      List<String> extractedTokens = extractor.getExtractedList(i);

      MulticastMessage message = MulticastMessage.builder()
        .setNotification(notification)
        .addAllTokens(partitionedTokens)
        .build();

      firebaseMessaging.sendEachForMulticastAsync(message);
    }
}

token을 가져와 ListExtractor를 초기화시킨다. MAX_TOKENS_PER_REQUEST 값은 500이다. 그렇다면, ListExtractor 내부에서 생성자를 통해 반복 회수(iterationCount)와 원본 리스트 등을 초기화시킨다. 예를 들어 토큰의 size가 2001이라면 반복 회수는 5가 나온다.

반복문에서는 getExtractedList 메소드를 호출하여 extractedTokens를 얻고 이를 FCM 서버에 요청한다.

문제점

과정을 요약하자면,

  1. extractor.getIterationCount()를 호출하여 반복 회수를 구해야 한다.
  2. 반복문의 조건문으로 반복 회수를 넣어야 한다.
  3. 반복문 내부에서는 extractor.getExtractedList(i) 메소드를 호출하여 extractedTokens 토큰을 요청해야 한다.

인 것이다.

여기서 문제가 있다고 느꼈다. Notifier의 send API를 직접 호출하는 개발자라면 상관하지 않아도 된다. API 내부에 토큰을 500개씩 짜르는 로직이 있기 때문이다. 하지만, 만약 ListExtractor를 직접 사용하고자 한다면 어떻게 할까?

예를 들어 JDBCTemplate의 bulk insert와 같은 기능을 사용할 때 한 번에 너무 많은 저장을 하지 않기 위해 1000천개만 insert하고자 할 때도 ListExtractor를 사용할 수 있을 것이다. 그럴 때 이러한 1번에서 3번의 과정을 강제한다는 히스토리를 모른다면 ListExtractor를 사용조차 할 수 없을 것이다.

  long iterationCount = extractor.getIterationCount();

  for (int i = 0; i < iterationCount; i++) {
      List<String> extractedTokens = extractor.getExtractedList(i);
  }
  ...

위의 코드를 하나로 묶어 API를 호출하는 개발자가 편리하게 사용하게 할 수는 없을까? 그래서 생각한 것이 템플릿 메소드 패턴이다.

탬플릿 메소드 패턴 정의

템플릿 메서드는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드​(재정의)​할 수 있도록 하는 행동 디자인 패턴입니다.
템플릿 메서드 패턴

탬플릿 메소드 패턴을 사용하여 부모 클래스에 반복되는 알고리즘을 정의하여 코드를 개선해보자.

  public abstract class AbstractListExtractorTemplate<T> {

  public void execute(Consumer<List<T>> consumer) {
    ListExtractor<T> extractor = get();
    long iterationCount = extractor.getIterationCount();

    for(int i = 0; i < iterationCount; i++) {
      List<T> extractedList = extractor.getExtractedList(i);
      consumer.accept(extractedList);
    }
  }


  protected abstract ListPartitioner<T> get();
}

이를 구현하여 사용해보자.

public class TokenExtractorTemplate<T> extends AbstractListExtractorTemplate<T> {
    private final ListExtractor<T> element;
      ...

    public TokenExtractorTemplate(final List<T> tokens) {
        element = new ListExtractor<>(MAX_TOKENS_PER_REQUEST, tokens);
          ...
    }

    @Override
    protected ListExtractor<T> get() {
        return element;
    }
}

이를 호출하는 클라이언트는 다음과 같이 사용할 수 있다.

AbstractListExtractorTemplate<String> tempalte = new TokenExtractorTemplate(tokens);

tempalte.execute(extractedList -> {
    MulticastMessage message = MulticastMessage.builder()
        .setNotification(notification)
        .addAllTokens(extractedList)
        .build();

      firebaseMessaging.sendEachForMulticastAsync(message, dryRun);
});