개발지식 먹는 하마 님의 블로그

전략 패턴과 팩토리 메서드 패턴 - 메일 발송 방식 변경 본문

TIL

전략 패턴과 팩토리 메서드 패턴 - 메일 발송 방식 변경

devhippo 2025. 7. 22. 16:39

내일부터 프로젝트 고도화 작업을 시작하고자 한다.
가장 먼저 수정할 부분으로 이메일 발송 방식을 디자인 패턴 기반으로 쉽게 변경할 수 있도록 바꾼 부분을 선정하고자 하였다.

그 당시에도 튜터님들의 의견이 서로 다르셔서 약간 애매했던 부분이 있는데 바로,
해당 구조를 전략 패턴과 팩토리 메서드 패턴 중 무엇으로 불러야 할 지에 대한 것이었다.

메일 발송 방식 변경 구조

메일 발송 방식을 변경하는 구조는 다음과 같다.

메일 발송 시, 사용하는 서버가 달라지더라도 공통으로 실행되어야 하는 행위들이 있다.

  • 메일 내용 구성 + 메일 발송 요청 -> sendQuizMail
  • RateLimiter 적용

먼저, 위와 같은 공통된 기능을 상위 객체 인터페이스로 추상화하였다. = MailSenderStrategy

그 후, MailSenderStrategy 인터페이스를 상속받아 각각 Gmail 서버와 SES 서버를 사용하는 클래스를 구현하였다.
(다형성)

마지막으로, MailSenderContext에서 입력받은 전략키 strategyKey에 따라
JavaBatchmailSender 또는 sesMailSender를 호출한다.


팩토리 메서드 패턴

  • 생성 패턴
  • 객체 생성의 인터페이스를 정의하지만, 실제 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정한다.

예를 들어서 어떤 게임의 몬스터 종류로 고블린, 유령, 요정이 있다고 하자.

public interface Monster {
    void attack();
}

public class Goblin implements Monster {
    @Override
    public void attack() {
        System.out.println("고블린이 단검으로 공격합니다!");
    }
}

public class Ghost implements Monster {
    @Override
    public void attack() {
        System.out.println("유령이 공포의 파동을 날립니다!");
    }
}

public class Fairy implements Monster {
    @Override
    public void attack() {
        System.out.println("요정이 마법의 불꽃을 쏩니다!");
    }
}

Monster는 추상화된 공통 인터페이스이고
고블린, 유령, 요정은 각각 인터페이스를 상속받아 서로 다르게 생성된 구현체(Concrete Products)이다.

팩토리 메서드 패턴은 먼저 객체 인터페이스를 정의한다.

public abstract class MonsterFactory {
    public abstract Monster createMonster();
}

위의 코드와 같이 몬스터 객체 생성이 추상 클래스로 구현되어 있을 때,
아래와 같이 실제 어떤 객체를 생성할지는 서브 클래스에서 결정한다.

고블린 팩토리는 고블린을 생성하고, 유령 팩토리는 유령을 생성한다.

public class GoblinFactory extends MonsterFactory {
    @Override
    public Monster createMonster() {
        return new Goblin();
    }
}

public class GhostFactory extends MonsterFactory {
    @Override
    public Monster createMonster() {
        return new Ghost();
    }
}

public class FairyFactory extends MonsterFactory {
    @Override
    public Monster createMonster() {
        return new Fairy();
    }
}

 

실제로는 다음과 같이 호출하게 되는 것이다.

public class Game {
    public static void main(String[] args) {
        MonsterFactory goblinFactory = new GoblinFactory();
        Monster goblin = goblinFactory.createMonster();
        goblin.attack();
    }
}

 

뭐랄까 이름 그대로 공장이 있는데
공장 설계도는 공유하지만
각 공장마다 생산하는 물건이 다른 느낌?


전략 패턴

  • 행위 패턴
  • 런타임 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 한다.
  • 동일한 계열의 알고리즘을 개별적으로 캡슐화하여 상호 교환할 수 있게 정의하는 패턴
    (클라이언트에 영향 없이 알고리즘 변경 가능)
전략 패턴 구현 방식은
공통된 알고리즘이나 행위를 상위 인터페이스로 추상화한다.
이를 상속받은 하위 클래스들이 구체적인 전략을 정의하는 방식이다.

예를 들어서 게임 속 공격이라는 공통된 행위를 추상화한다.
그 후 공격이라는 행위에 대한 구체적인 방식을 검술, 마법, 궁술로 구현한다.

public class SwordAttack implements AttackStrategy {
    @Override
    public void attack() {
        System.out.println("검으로 공격합니다!");
    }
}

public class MagicAttack implements AttackStrategy {
    @Override
    public void attack() {
        System.out.println("마법을 사용해 공격합니다!");
    }
}

public class BowAttack implements AttackStrategy {
    @Override
    public void attack() {
        System.out.println("활로 멀리서 공격합니다!");
    }
}

 

그 후 Context로 전략 등록 및 실행한다.

public class GameCharacter {
    private AttackStrategy strategy;

    public void setStrategy(AttackStrategy strategy) {
        this.strategy = strategy;
    }

    public void performAttack() {
        if (strategy == null) {
            System.out.println("공격 전략이 설정되지 않았습니다!");
        } else {
            strategy.attack();
        }
    }
}

 

public class Game {
    public static void main(String[] args) {
        GameCharacter character = new GameCharacter();

        character.setStrategy(new SwordAttack());
        character.performAttack();  // "검으로 공격합니다!"

        character.setStrategy(new MagicAttack());
        character.performAttack();  // "마법을 사용해 공격합니다!"

        character.setStrategy(new BowAttack());
        character.performAttack();  // "활로 멀리서 공격합니다!"
    }
}

어떤 전략이 입력되었느냐에 따라 그에 해당하는 전략을 실행하는 것이다.


그래서 메일 발송 구조는 어떤 패턴?

두 패턴의 자세한 내용을 살펴본 후, 나는 현재 메일 발송 구조는 전략 패턴이 맞다고 생각한다.

현재 메일 발송 방식의 공통된 행위인 메일 발송과, RateLimiter 반환이 상위 객체로 만들어져 있다. 

public interface MailSenderStrategy {
    void sendQuizMail(MailDto mailDto);

    Bucket getBucket();
}

 

팩토리 메서드 패턴이 아닌 이유) 객체를 생성하지 않음

서로 다른 구현체를 호출한다고 할 때, 새로운 객체가 생성되지 않는다.

그저 메일을 발송하는 서비스 로직을 호출하거나

public class SesMailSenderStrategy implements MailSenderStrategy{   
	@Override
    public void sendQuizMail(MailDto mailDto) {
        sesMailService.sendQuizEmail(mailDto.getSubscription(), mailDto.getQuiz());
    }
}

public class JavaMailSenderStrategy implements MailSenderStrategy{
	@Override
    public void sendQuizMail(MailDto mailDto) {
        javaMailService.sendQuizEmail(mailDto.getSubscription(), mailDto.getQuiz()); 
    }
}

 

이미 특정 전략에 맡게 생성되어 있는 RateLimiter 객체를 가져올 뿐이다.

public class SesMailSenderStrategy implements MailSenderStrategy{
    private final Bucket bucket = Bucket.builder()
            .addLimit(limit ->
                    limit
                            .capacity(14)
                            .refillIntervally(7, Duration.ofMillis(500))
            )
            .build();
    
    @Override
    public Bucket getBucket() {
        return bucket;
    }
}

 

따라서, 객체의 생성의 책임을 하위 클래스에게 넘기는 팩토리 메서드와 구조는 유사할지 몰라도
용도와는 맡지 않기 때문에 전략 패턴이 맞다고 결론 내렸다.

 

전략 패턴이라고 하기에는 약간 아쉬운 점

전략 패턴은 프로그램 실행 도중, 행위가 바뀌어야 한다.
그러나 현재는 메일 발송 도중에 발송 방식이 변경되지는 않는다.

Job을 실행하기 전에, 개발자가 설정해 놓은 발송 방식에 따라 발송된다.
(그렇게 한 이유는 이전에 설명함)

하지만, 현재 구현된 구성을 바탕으로 약간의 로직만 추가한다면 충분히 런타임 중에도
발송 방식을 충분히 바꿀 수 있다는 점에서
전략 패턴을 사용했다고 말해도 무방할 것 같다.

 

동적 바인딩에 대한 내용을 정리하면서 전략 패턴에서 말하는
"런타임 중 전략을 선택하거나 동작을 실시간으로 바뀌도록 할 수 있다"라는 특성에 대해 
다시 생각해 보게 되었다.

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%84%EB%9E%B5Strategy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

 

💠 전략(Strategy) 패턴 - 완벽 마스터하기

Strategy Pattern 전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다. 여기서 '전략'이란 일종의 알고리즘이 될 수

inpa.tistory.com

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%84%EB%9E%B5Strategy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

위의 블로그 내용을 참고하여 전략 패턴에 대해 공부했었기 때문에
프로그램 실행 중에 객체 동작을 실시간으로 바뀌도록 할 수 있게 한다는 문구에 집착(?)하였고
현재 내 구조는 실시간으로 바뀌게 할 수 있지만, 그 부분까지 구현하지는 않았기 때문에
전략 패턴이라고 말하기에는 약간 아쉽다는 생각이 지배적이었다.

그러나 다형성과 동적 바인딩에 대해 복습하면서
"런타임 중에 의존성이 형성된다" 라는 부분에서 문득 전략 패턴에 다시 한번 의구심을 가지게 되었다.

실시간으로 객체의 동작이 바뀌게 된다는 건 결국 동적 바인딩을 이야기 하는게 아닐까?
동적 바인딩 구조로 구현되었다면 중간에 방식이 바뀌던 말던 어쨌든 전략 패턴이라고 할 수 있는것 아닌가?

 

오랜만에 정보처리기사 책을 펴고 전략 패턴에 대한 내용을 다시 살펴보았다.
그 어디에서 실시간으로 변경한다는 얘기는 없었다.

다른 블로그 글들도 찾아보고 GPT에게도 물어봤다.

결론은 내가 떠올린 의구심이 맞다는 것이다.

전략 패턴의 근본은 런타임에 의존성이 생성되는 것이기 때문에 현재 구조만으로 조건을 충족한다.
실시간으로 전략을 변경하는 것은 전략 패턴의 활용 방식 중 하나일 뿐 핵심은 아니다.

따라서, 현재 구조만으로도 충분히 전략 패턴으로 볼 수 있다.