ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring과 Factory Pattern
    Spring 2023. 1. 19. 14:29
    반응형

    단순 팩토리

    이번 글은 Spring에서 단순 팩토리 패턴을 사용하는 글을 작성하려고 합니다.

    단순 팩토리에 대한 설명은 팩토리 비교 (refactoring.guru)에서 단순 팩토리 부분을 참고하시면 되겠습니다.

    단순 팩토리는 정말 단순하게 개발자가 요구하는 객체들이 모여져 있는 클래스가 존재하고 필요 할 때마다 해당 클래스에서 반환 받는 형태를 가지게 됩니다.

    이번 주제는 정말 단순해서 바로 본론으로 들어가도록 하겠습니다.

    이번 예제는 이전 글인 팩토리 메서드 패턴에 단순 팩토리를 적용하는 예로 들겠습니다.

    먼저 이전 글인 팩토리 메서드 패턴에는 몇 가지 문제(?)가 있습니다. 바로 의존성에 대한 문제 입니다. 프로세스 로직 담당 클래스는 정보 저장을 위해 Spring Bean에 있는 정보 저장 담당 클래스에 의존하고 있습니다. 그리고 트랜잭션 담당 클래스는 프로세스 로직 담당 클래스를 반환하기 위해 어쩔 수 없이 프로세스 로직 담당 클래스가 Spring Bean에 의존하고 있는 객체를 참조 할 수 밖에 없는 구조 입니다.

    이로 인해 프로세스 로직 담당 클래스를 사용하려면 프로세스 로직 담당 클래스가 의존하고 있는 객체들을 어쩔 수 없이 참조해야 하는 구조 입니다.

    결합도를 낮추기 위해서 사용한 패턴인데 그럼에도 결합도가 상당해집니다. 이 결합도를 단순 팩토리 패턴을 적용해서 낮춰보도록 하겠습니다.

    예제

    예제는 기본 회원정보 가입 하나로만 들도록 하겠습니다. 그럼 바로 단순 팩터리 클래스부터 확인해보겠습니다.

    /**
     * <pre>
     *     회원가입 관련 Repo 팩토리 클래스
     * </pre>
     */
    @Component
    public class MemberJoinFactory {
        private static final Map<String, Object> instance = new HashMap<>();
    
        MemberJoinFactory(MemberJoin memberJoinImpl, KakaoMemberJoin kakaoMemberJoinImpl) {
            instance.put(memberJoinImpl.getClass().getSimpleName(), memberJoinImpl);
            instance.put(kakaoMemberJoinImpl.getClass().getSimpleName(), kakaoMemberJoinImpl);
        }
    
        @SuppressWarnings("unchecked")
        public static <T> T getInstance(Class<T> clazz) {
            return Optional.ofNullable((T) instance.get(clazz.getSimpleName()))
                    .orElseThrow(() -> new IllegalArgumentException("지원되지 않는 타입입니다."));
        }
    }

    네 그렇습니다. 끝입니다. 해당 단순 팩토리 패턴 클래스는 Spring Bean에 의존 하고 있는 객체를 주입 받아야 해서 @Compoent를 선언하고 Spring으로 부터 객체를 주입 받습니다.

    그럼 단순 팩터리 패턴을 사용한 경우와 이전 글인 팩토리 메서드 패턴인 경우를 비교해보겠습니다.

    먼저 팩토리 메서드 패턴이 적용된 기본 회원정보 저장 프로세스 처리 클래스 입니다.

    저장하기 위해 MemberJoin 이 선언되어 있어 해당 클래스를 사용하려면 사용 하려는 클래스에서 MemberJoin 을 참조하고 있다가 객체 생성 시 생성자 매개변수에 주입 시켜줘야 합니다.

    /**
     * <pre>
     *     기본 회원가입 처리 프로세스 클래스
     * </pre>
     */
    public class MemberJoinProcessImpl implements MemberJoinProcess {
        private final MemberJoin memberJoinImpl;
        private final MemberJoinEntity memberJoinEntity;
    
        public MemberJoinProcessImpl(MemberJoin memberJoinImpl, MemberJoinEntity memberJoinEntity) {
            this.memberJoinImpl = memberJoinImpl;
            this.memberJoinEntity = memberJoinEntity;
        }
    
        /**
         * {@link MemberJoinProcess#join()}
         */
        @Override
        public void join() {
            this.memberJoinImpl.join(memberJoinEntity);
        }
    }

    해당 클래스를 사용하는 트랜잭션 처리 담당 클래스를 보면 해당 클래스를 객체화 하기 위해MemberJoin 을 참조하고 있음을 알 수 있습니다.

    /**
     * <pre>
     *     기본 회원가입 팩토리 메서드 패턴 하위 클래스
     * </pre>
     */
    @Service
    public class MemberJoinServiceImpl extends MemberJoinServiceAbs implements MemberJoinService {
        private final MemberJoin memberJoinImpl;
    
        public MemberJoinServiceImpl(MemberJoin memberJoinImpl) {
            this.memberJoinImpl = memberJoinImpl;
        }
    
        /**
         * <pre>
         *     {@link MemberJoinService#join(Object)}
         * </pre>
         *
         * @param joinEntity 전달 받은 객체를 {@link MemberJoinEntity}로 형변환.
         * @param <T> {@link MemberJoinEntity} 타입
         * @return 기본 회원가입 처리를 하는 {@link MemberJoinProcessImpl} 구현체 반환
         */
        @Override
        protected <T> MemberJoinProcess createMemberJoin(T joinEntity) {
            return new MemberJoinProcessImpl(this.memberJoinImpl, (MemberJoinEntity) joinEntity);
        }
    }

    이걸 단순 팩터리 패턴으로 변경하면 필요한 부분에서 팩토리 클래스를 호출하면 되기 때문에 클래스 자체에서 의존성을 없앨 수 있습니다. 적용된 코드를 보면 MemberJoin 이 없어진걸 보실 수 있습니다.

    /**
     * <pre>
     *     기본 회원가입 처리 프로세스 클래스
     * </pre>
     */
    public class MemberJoinProcessImpl implements MemberJoinProcess {
        private final MemberJoinEntity memberJoinEntity;
    
        public MemberJoinProcessImpl(MemberJoinEntity memberJoinEntity) {
            this.memberJoinEntity = memberJoinEntity;
        }
    
        /**
         * {@link MemberJoinProcess#join()}
         */
        @Override
        public void join() {
            MemberJoinFactory.getInstance(MemberJoinImpl.class)
                    .join(memberJoinEntity);
        }
    }

    해당 클래스를 사용하려는 트랜잭션 처리 담당 클래스를 보면 MemberJoin을 참조하지 않고 있음을 알 수 있습니다.

    @Service
    public class MemberJoinFactoryExampleServiceImpl extends MemberJoinFactoryExampleServiceAbs implements MemberJoinFactoryExampleService {
    
        /**
         * <pre>
         *     {@link MemberJoinFactoryExampleService#join(Object)}
         * </pre>
         *
         * @param joinEntity 전달 받은 객체를 {@link MemberJoinEntity}로 형변환.
         * @param <T> {@link MemberJoinEntity} 타입
         * @return 기본 회원가입 처리를 하는 {@link MemberJoinProcessImpl} 구현체 반환
         */
        @Override
        protected <T> MemberJoinProcess createMemberJoin(T joinEntity) {
            return new MemberJoinProcessImpl((MemberJoinEntity) joinEntity);
        }
    }

    그럼으로서 프로세스 처리 담당 클래스와 해당 클래스를 사용하려는 클래스 사이에 결합이 낮아진것을 확인 할 수 있습니다.

    테스트

    정상 실행이 되는지 확인하기 위한 코드 입니다. 본 글에서는 카카오톡_회원가입, 일반_회원가입에 대한 실행결과만 작성하겠습니다.

    @SpringBootTest
    class FactoryPatternTests {
        @Autowired
        MemberJoinFactoryExampleService memberJoinFactoryExampleServiceImpl;
    
        @Autowired
        MemberJoinFactoryExampleService kakaoMemberJoinFactoryExampleServiceImpl;
    
        @Test
        void 카카오톡_회원가입() {
            KakaoMemberJoinEntity kakaoMemberJoinEntity = new KakaoMemberJoinEntity(13248627872L
                    , LocalDateTime.now()
                    , true
                    , false
                    , true
                    , true
                    , "test@kakao.com"
            );
    
            this.kakaoMemberJoinFactoryExampleServiceImpl.join(kakaoMemberJoinEntity);
        }
    
        @Test
        void 카카오톡_회원가입_다중() {
            for(int i = 0; i < 3; i++) {
                this.카카오톡_회원가입();
            }
        }
    
        @Test
        void 일반_회원가입() {
            MemberJoinEntity memberJoinEntity = new MemberJoinEntity(UUID.randomUUID().toString().replace("-", "").substring(0, 5)
                , UUID.randomUUID().toString().replace("-", "").substring(0, 10)
                , MemberJoinEntity.MemberJoinType.DEFAULT);
    
            this.memberJoinFactoryExampleServiceImpl.join(memberJoinEntity);
        }
    
        @Test
        void 일반_회원가입_다중() {
            for(int i = 0; i < 3; i++) {
                this.일반_회원가입();
            }
        }
    }

    먼저 카카오톡_회원가입 실행결과 입니다.

    c.d.c.repo.test.KakaoMemberJoinImpl      : kakaoJoin : [{"id":13248627872,"connectedAt":[2023,1,19,14,6,3,271517500],"kakaoAccountHasEmail":true,"kakaoAccountEmailNeedsAgreement":false,"kakaoAccountIsEmailValid":true,"kakaoAccountIsEmailVerified":true,"kakaoAccountEmail":"test@kakao.com"}]
    c.d.c.repo.test.MemberJoinImpl           : defaultJoin : [{"memberId":"test@kakao.com","password":"16cf42bea7","memberJoinType":"KAKAO"}]

    일반_회원가입 실행결과 입니다.

    c.d.c.repo.test.MemberJoinImpl           : defaultJoin : [{"memberId":"0f20f","password":"cb98414097","memberJoinType":"DEFAULT"}]

    마치며

    Spring으로 개발하면 Spring Bean 객체를 사용하기 위해 무조건 선언을 해야 하는데 자칫 잘못하면 하나의 클래스가 여러 개의 객체를 참조함으로서 강한 결합도가 발생되어 재사용 하려는 클래스에서 모든 객체들을 동일하게 참조 해야 하는 불상사가 발생될 수 있습니다.

    단순 팩터리 패턴를 활용하면 클래스에서 선언해야 하는 객체 참조들을 제거함으로 위와 같이 결합도를 낮출 수 있다고 생각 됩니다.

    그렇지만 팩토리 패턴 클래스 하나에 너무 많은 객체를 참조하는것도 해당 팩토리 패턴 클래스에 의존도가 높아지기 때문에 하나의 팩토리 패턴 클래스에 몰빵(?)하지 말고 기준을 삼고 적절히 팩토리 패턴 클래스를 나눠서 생성하는게 좋은 방향이지 않을까 합니다.

    끝으로 해당 예제 코드는 https://github.com/sungwookkim/spring-designpattern의 designPattern.creationalPattern.pattern.factory 패키지에서 확인 하실 수 있습니다.

    끝까지 긴 글 읽어주셔서 감사하고 하시는 모든 개발에 원하는 성취를 얻을 수 있게 기도드리겠습니다. 감사합니다.

    반응형

    'Spring' 카테고리의 다른 글

    Spring과 Builder Pattern  (0) 2023.04.05
    Spring과 Abstract Factory Pattern  (0) 2023.02.01
    Spring과 Factory Method Pattern  (0) 2023.01.18
    Spring @Sevice 클래스 유연하게 써보기  (0) 2022.07.15
    Spring Mybatis 상속  (0) 2022.05.30

    댓글

Designed by Tistory.