로컬에서는 잘 되는데 ☘️

[생성 패턴] 추상팩토리 패턴 적용 예시

by youngjun._.

1. 추상 팩토리 패턴(Abstract Factory Pattern)이란? 

서로 관련있는 여러 객체를 만들어주는 인터페이스

 

구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다.

추상 팩토리 패턴 UML (출처 : 코딩으로 학습하는 GoF의 디자인 패턴- 백기선, 인프런)

팩토리를 추상화된 형태(인터페이스, abstract 등)  

구체적인 펙토리에서 구체적인 인스턴스를 만드는 것은 팩토리 메소드 패턴과 굉장히 유사하다.

 

추상 팩토리 패턴의 목적?

- 클라이언트 코드(팩토리에서 인스턴스를 생성해서 사용하는 코드)를 인터페이스 기반으로 활용할 수 있도록 제공

 

팩토리 쪽에만 집중하면 팩토리 메소드 패턴과 유사하지만, 클라이언트에서 추상 팩토리를 어떻게 사용하는지에 대한 관점으로 접근해야 목적과 차리점을 이해하기 쉽다.

 

2. 코드로 알아보는 추상 팩토리 패턴

요구사항을 구현한 코드와 이를 개선하며 추상 팩토리 패턴이 필요한 경우를 알아보자.

2-1. 여러가지 맞춤 메일 생성 시스템

사람인과 같은 채용사이트에서 사용자 맞춤 추천 메일을 보내는 메일 생성 시스템이 있다고 하자.

 

메일을 받을 사용자를 추출하고, 메일 내용을 채워서 메일을 발송하는 시스템이다. 주요 흐름을 아래와 같다.

1. 발송 대상자 추출
2. 맞춤 메일 내용 생성
3. 메일 발송

맞춤/추천 메일은 여러 케이스로 생성될 수 있다.

- 사용자 검색 데이터 기반 메일 (검색 추천 메일)

- 사용자 정보 기반 메일 (개인 맞춤 메일)

 

여러가지 케이스의 메일을 생성하더라도 메일 생성 시스템 코드(클라이언트) 변경은 최소화하는 방향으로 구성하고 싶은 상황이다.

 

2-2. 추상화 할 수 있는 기능 찾기 (템플릿 메소드 패턴 적용)

추천 메일 종류가 다른 경우, '발송 대상자 모수' 와 '메일 내용' 이 다를 것이다. 

이 중 '메일 내용'을 만들어주는 MailTemplate 객체의 필요한 기능을 정리하면 다음과 같다.

  1. 발송자 / 수신자 설정
  2. 메일 내용 추가
  3. 하단 Footer 추가

기능 중  '2번 메일 내용 추가' 를 제외하면 다른 기능은 동일하다.

먼저 템플릿 메소드 패턴을 적용해서 코드의 중복을 줄여보자.

템플릿 메소드 패턴 적용 Class Diagram

// 메일 템플릿 생성 추상 클래스
public abstract class MailTemplate {
    public Map<String, String> template = new HashMap<>();

    /**
     * 발신자 Setting
     * @param sender 수신자
     */
    public void setSender(String sender) {
        template.put("sender", sender);
    }

    /**
     * 수신자 Setting
     * @param receiver 발신자
     */
    public void setReceiver(String receiver) {
        template.put("receiver", receiver);
    }

    /**
     * 하단 Footer Setting
     * @param footer 하단 내용
     */
    public void setFooter(String footer) {
        template.put("footer", footer);
    }

    /**
     * 메일 내용 Setting
     * @param content 메일 내용
     */
    protected abstract void setContent(String content);
}

// 개인 검색 데이터 기반
public class SearchMailTemplate extends MailTemplate {
    @Override
    protected void setContent(String content) {
        System.out.println("Set Content For Search Mail");
    }
}

// 경력, 직무 기반
public class AvatarMailTemplate extends MailTemplate {
    @Override
    protected void setContent(String content) {
        System.out.println("Set Content For Avatar Mail");
    }
}

2-2-1. 문제점

현재 코드에서 문제점은 메일 생성 시스템의 클라이언트 입장에서, 특정 메일의 객체가 각각 필요하다.

예를 들어 AvatarMailTemplate, AvatarMailValidUser 같은 구체적인 객체가 필요하다는 의미이다.

 

최초에 구성한 디자인과는 다르게 클라이언트 코드 변경이 불가피하기 때문에 확장성이 좋은 코드로 바꿔보자.

 

2-3. 확장성 있는 코드로 변경 (팩토리 메소드 패턴 적용)

팩토리 메소드 패턴 적용 Class Diagram

문제가 있던 '객체 생성'을 서브 클래스로 분리하고 캡슐화하여 더 확장성 있는 코드로 변경할 수 있다.

// Enum 타입으로 Mail 종류 선언
public enum MailType { AVATAR, SEARCH }

// 메일 템플릿 펙토리
public class MailTemplateFactory {

    /**
     * Mail Type에 따라 Search, Avatar 메일 템플릿 객체 생성
     *
     * @param mailType 메일 종류
     * @return MailTemplate 메일 템플릿
     */
    public static MailTemplate createMailTemplate(MailType mailType) {
        MailTemplate mailTemplate = null;

        switch (mailType) {
            case AVATAR:
                mailTemplate = new AvatarMailTemplate();
                break;
            case SEARCH:
                mailTemplate = new SearchMailTemplate();
                break;
        }
        return mailTemplate;
    }
}

// 클라이언트 호출 코드
public class Client {
    public static void main(String[] args) {
        // 팩토리 메소드 호출
        ValidUser avatarValidUser = ValidUserFactory.createValidUser(MailType.AVATAR);
        MailTemplate avatarMailTemplate = MailTemplateFactory.createMailTemplate(MailType.AVATAR);

        // 대상자 추출
        Set<Long> validUser = avatarValidUser.getValidUser();
        
        // 메일 내용 Setting
        avatarMailTemplate.setContent("신입/경력 기반 개인 맞춤 메일 내용");
        String sender = avatarMailTemplate.template.get("sender");
        String receiver = avatarMailTemplate.template.get("receiver");
        // ..생략
        
        // 대상자에게 메일 템플릿 설정하여 메일 발송 로직 추가
    }
}

VaildUserFactory도 동일하기 때문에 생략했다.

 

실행해보면 Avatar 메일 타입으로 수행되는 것을 확인할 수 있다.

// 실행 결과
Valid User For Avatar Data
Set Content For Avatar Mail

2-3-1. 문제점

1. 여전히 클라이언트 코드의 수정이 필요하다.

  • 팩토리 메소드를 만들어서 코드 수정이 줄어들었지만, 다른 타입의 Mail을 발송하려면 모두 수정해야한다.
public class Client {
    public static void main(String[] args) {
        // AS-IS
        ValidUser avatarValidUser = ValidUserFactory.createValidUser(MailType.AVATAR);
        
        // TO-BE
        ValidUser searchValidUser = ValidUserFactory.createValidUser(MailType.SEARCH);
        
        // ...
    }
}

2. 새로운 타입의 메일이 추가되는 경우

  • 커뮤니티 질문 기반의 맞춤 메일 타입이 추가된다면 선언했던 모든 Factory 클래스에 Type을 추가해야한다.
public class MailTemplateFactory {

    public static MailTemplate createMailTemplate(MailType mailType) {
        MailTemplate mailTemplate = null;

        switch (mailType) {
            case AVATAR:
                mailTemplate = new AvatarMailTemplate();
                break;
            case SEARCH:
                mailTemplate = new SearchMailTemplate();
                break;
            // 새로운 타입을 추가
            case POSTING:
                mailTemplate = new PostingMailTemplate();
                break;
        }
        return mailTemplate;
    }
}
결과적으로 팩토리 메소드 패턴을 이용한 객체 생성은 여러 객체를 일관성 있는 방식으로 생성하는 경우, 많은 코드 변경이 발생한다.

2-4. 관련 객체를 일관성 있게 생성 (추상 팩토리 패턴 적용)

관련성이 있는 여러 종류의 객체를 생성할 때 각각 별도의 Factory 클래스를 사용하는 대신, 관련 객체들을 일관성 있게 생성하는 Factory 클래스를 정의해보자.

  • 이전에 정의했던 ValidUserFactory, MailTemplateFactory 클래스와 같이 '기능 단위'로 Factory 클래스를 정의하지 않고, SearchMailFactory, AvatarMailFactory 클래스와 같이 '실제 발송할 메일 타입 단위'로 Factory 클래스를 정의했다.
// 추상 대상자, 추상 메일 템플릿을 생성하는 추상 팩토리 클래스
public abstract class MailFactory {
    public abstract Set<Long> createValidUser();
    public abstract MailTemplate createMailTemplate();
}


// 개인 정보 맞춤 메일 생성 팩토리 클래스
public class AvatarMailFactory extends MailFactory {
    @Override
    public Set<Long> createValidUser() {
        return new AvatarMailValidUser().validUserList;
    }

    @Override
    public MailTemplate createMailTemplate() {
        return new AvatarMailTemplate();
    }
}

// 검색 기반 맞춤 메일 생성 팩토리 클래스
public class SearchMailFactory extends MailFactory {
    @Override
    public Set<Long> createValidUser() {
        return new SearchMailValidUser().validUserList;
    }

    @Override
    public MailTemplate createMailTemplate() {
        return new SearchMailTemplate();
    }
}

public class Client {
    public static void main(String[] args) {
        MailFactory mailFactory;
        // 메일 타입을 런타임에 받음
        String mailType = args[0];

        if (mailType.equals("AVATAR")) {
            mailFactory = new AvatarMailFactory();
        } else {
            mailFactory = new SearchMailFactory();
        }

        // 해당하는 mail 타입에 따른 대상자, 메일 템플릿 생성
        Set<Long> validUser = mailFactory.createValidUser();
        MailTemplate mailTemplate = mailFactory.createMailTemplate();

        // (추가) 발송하는 로직 ...
    }
}
  • 클라이언트 코드에서 런타임에 인자로 받는 'MailType'에 따라 Mail 생성 팩토리를 생성한다. 
    • 다른 메일 타입으로 변경되어도 클라이언트 코드를 변경할 필요가 없다! (첫번째 문제 해결)
  • 메일 타입 별 Factory 클래스로 정의하여, 새로운 메일 타입을 기존 코드의 변경 없이 적용할 수 있다.
    • 새로운 메일 타입 Factory를 선언하고 해당하는 대상자추출, 메일템플릿 클래스만 생성하면 확장이 가능하다.

커뮤니티 게시글 작성 기반 개인 맞춤 메일 Class Diagram

추상 팩토리 패턴은 관련성이 있는 여러 객체를 일관적으로 생성하는 다양한 case가 생길 가능성이 있다면, Client와 객체간의 결합을 피할 수 있고, SRP와 OCP를 지킬 수 있어 유용하게 사용될 것 같다.

 

Reference

블로그의 정보

개발하는만두

youngjun._.

활동하기