ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring과 Factory Method Pattern
    Spring 2023. 1. 18. 16:26
    반응형

    팩토리 메서드

    안녕하세요 글쓴이 입니다.

    이번 주제는 디자인 패턴에 대해서 작성하려고 합니다.

    디자인 패턴을 학습하긴 했는데 Spring에 디자인 패턴을 적목 시키기가 애매한 경우가 많습니다.

    적목 시키기 애매하다고 생각하는 개인적인 견해로는

    • MVC 패턴
      • Spring을 사용하면 MVC 패턴에 의한 구조적 설계가 바탕이 되는데 이 부분에서 어느 정도의 강제성이 부여.
    • Bean 활용을 위한 객체의 싱글톤
      • Spring의 강점인 Bean 객체는 기본이 싱글톤 객체이기 때문에 객체에 대한 핸들링이 어느 정도 강제성 부여.

    이지 않을까 합니다.

    물론 하고자 한다면 할 수 있고 혹은 하고 계시고 있다고 생각합니다. 그래서 이 글의 목적은 하고자 하시려는 분들에게 조금이나마 도움을 그리고 하고 계신 분들한텐 이런 방식도 있구나라는 것을 공유하고자 작성합니다.

    서론이 길었는데 지금부터 본론으로 들어가도록 하겠습니다.

    팩토리 메서드란?

    약간의 검색을 통하면 잘 작성된 글들이 많기 때문에 패턴에 대한 설명은 생략하도록 하겠습니다. 그러나 개인적으로 추천 드리는 곳은 팩토리 메서드 패턴 (refactoring.guru)을 추천 드립니다.

    그렇지만 간단하게 설명을 드리자면

    • 부모 클래스(추상 클래스)에서 프로세스 처리 구현체 객체를 반환하는 메서드(추상 메서드)를 생성.

    • 상속 받은 자식 클래스(구현 클래스)에서 프로세스 처리 구현체를 반환. (@Override)

    • 부모 클래스(추상 클래스)에서 자식 클래스(구현 클래스)가 반환한 프로세스 처리 구현체에 의존해서 프로세스 로직 구현.

    하는 간단한 형태를 가지게 됩니다.

    예제

    예제로는 회원가입 입니다. 회원가입 할 때 기본 회원가입과 카카오톡을 통한 회원가입 2가지 방식이 있다는 가정입니다.

    예제 프로세스 기능은

    • 기본 회원가입은 계정을 직접 생성.
    • 카카오톡을 통한 회원가입은 카카오톡으로 로그인한 정보 저장과 해당 정보를 토대로 임의 계정을 생성.

    하는 식의 기능을 구현합니다.

    그리고 예제에선 저장 시 DB를 사용하지 않고 객체 변수에 저장하는 식으로 되어 있습니다.

    큰 구조로는

    • 프로세스 로직 담당 클래스
      • Bean에 의존하지 않는 클래스 입니다. 즉, new 연산자로 객체를 생성 합니다.
    • 정보 저장 담당 클래스
      • @Repository이 선언된 클래스 입니다. 즉, new 연산자로 인한 객체 생성이 불가하고 Spring Bean에 의존합니다.
    • 트랜잭션 담당 클래스
      • @Service이 선언된 클래스 입니다. 즉, new 연산자로 인한 객체 생성이 불가하고 Spring Bean에 의존합니다.

    로 되어 있습니다.

    프로세스 로직 담당 클래스

    클래스 다이어그램으론 아래 사진을 참고하시면 되겠습니다.

    https://i.ibb.co/RSh1BrG/Member-Join-Process.png

    카카오톡 회원가입 구현체와 기본 회원가입 구현체 이렇게 2개의 구현체가 존재 합니다.

    코드

    구현체들의 인터페이스 입니다.

    /**
     * <pre>
     *     회원가입 관련 인터페이스
     * </pre>
     */
    public interface MemberJoinProcess {
    
        /**
         * <pre>
         *     회원가입에 대한 프로세스 로직을 작성하는 메서드
         * </pre>
         */
        void join();
    }

    기본 회원가입 프로세스 로직을 처리하는 구현체 입니다. 변수 중 memberJoinImpl 있는데 해당 객체가 정보 저장 담당 클래스(Spring Bean 객체)의 객체 입니다.

    즉, 해당 클래스는 Spring Bean 객체에 의존하게 됩니다.

    /**
     * <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);
        }
    }

    카카오톡 회원가입 프로세스 로직을 처리하는 구현체 입니다. 해당 클래스도 카카오톡 정보를 저장하기 위해 Spring Bean에 의존하는 kakaoMemberJoinImpl이 선언되어 있습니다.

    /**
     * <pre>
     *     카카오톡 회원가입 처리 프로세스 클래스
     * </pre>
     */
    public class KakaoMemberJoinProcessImpl implements MemberJoinProcess {
        private final KakaoMemberJoin kakaoMemberJoinImpl;
        private final KakaoMemberJoinEntity kakaoMemberJoinEntity;
    
        public KakaoMemberJoinProcessImpl(KakaoMemberJoin kakaoMemberJoinImpl, KakaoMemberJoinEntity kakaoMemberJoinEntity) {
            this.kakaoMemberJoinImpl = kakaoMemberJoinImpl;
            this.kakaoMemberJoinEntity = kakaoMemberJoinEntity;
        }
    
        /**
         * {@link MemberJoinProcess#join()}
         * 카카오톡 회원가입인 경우엔 응답 받은 카카오톡 전문에서 특정 데이터만 추출하여
         * 기본 회원가입 처리도 동시 처리 진행한다.
         */
        @Override
        public void join() {
            MemberJoinEntity memberJoinEntity = new MemberJoinEntity(this.kakaoMemberJoinEntity.getKakaoAccountEmail()
                    , UUID.randomUUID().toString().replace("-", "").substring(0, 10)
                    , MemberJoinEntity.MemberJoinType.KAKAO);
    
            this.kakaoMemberJoinImpl.kakaoJoin(this.kakaoMemberJoinEntity);
            this.kakaoMemberJoinImpl.join(memberJoinEntity);
        }
    }

    정보 저장 담당 클래스

    카카오톡은 카카오톡 정보 저장과 임의 계정 생성을 위해 별도의 인터페이스(KakaoMemberJoin)와 구현체(KakaoMemberJoinImpl)가 존재하며 해당 구현체에서는 임의로 생성한 계정 저장을 위해 MemberJoin을 참조하고 있습니다.

    https://i.ibb.co/GtN45JX/Member-Join.png

    코드

    기본 회원 정보 저장을 다루기 위한 인터페이스 입니다.

    /**
     * <pre>
     *     가입 회원 정보를 다루기 위한 인터페이스
     * </pre>
     */
    public interface MemberJoin {
    
        /**
         * <pre>
         *     기본 회원 정보를 저장하는 메서드
         * </pre>
         *
         * @param memberJoinEntity 기본 회원정보 엔티티
         */
        void join(MemberJoinEntity memberJoinEntity);
    }

    구현체에 @Repository를 선언함으로서 Spring Bean에 의존 시키게 합니다. 코드를 보시면 아시겠지만 해당 클래스는 별도 물리적인 공간에 저장하지 않고 변수에 저장 시킵니다.

    /**
     * <pre>
     *     기본으로 회원가입 정보를 테스트 용으로 다루기 위한 클래스
     *     해당 클래스는 테스트이기 때문에 변수에 저장
     * </pre>
     */
    @Repository
    public class MemberJoinImpl implements MemberJoin {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private final List<MemberJoinEntity> memberJoinEntities = new ArrayList<>();
    
        /**
         * <pre>
         *     {@link MemberJoin#join(MemberJoinEntity)}
         * </pre>
         *
         * @param memberJoinEntity 기본 회원정보 엔티티
         */
        @Override
        public void join(MemberJoinEntity memberJoinEntity) {
            this.memberJoinEntities.add(memberJoinEntity);
            this.logger.info("defaultJoin : {}", this);
        }
    
        @Override
        public String toString() {
            try {
                return new ObjectMapper().writeValueAsString(this.memberJoinEntities);
            } catch (Exception e) {
                return this.memberJoinEntities.toString();
            }
        }
    }

    카카오톡 회원 정보를 다루기 위한 인터페이스 입니다. 앞서 설명했듯이 카카오톡은 카카오톡 로그인 정보 저장과 임의 계정 생성 저장을 해야 하기 때문에 MemberJoin 인터페이스를 상속 받았습니다.

    /**
     * <pre>
     *     카카오톡으로 가입하는 회원 정보를 다루기 위한 인터페이스
     * </pre>
     */
    public interface KakaoMemberJoin extends MemberJoin {
    
        /**
         * <pre>
         *     카카오톡으로 가입하는 회원 정보를 저장하는 메서드
         * </pre>
         *
         * @param kakaoMemberJoinEntity 카카오톡 회원정보 엔티티
         */
        void kakaoJoin(KakaoMemberJoinEntity kakaoMemberJoinEntity);
    }

    카카오톡 정보 저장 구현체도 @Repository를 선언함으로서 Spring Bean에 의존 시킵니다. 그리고 MemberJoin구현체를 주입 받아 임의로 생성한 계정 정보를 저장하게 합니다.

    /**
     * <pre>
     *     카카오톡으로 가입하는 회원 정보를 테스트 용으로 다루기 위한 클래스
     *     해당 클래스는 테스트이기 때문에 변수에 저장
     * </pre>
     */
    @Repository
    public class KakaoMemberJoinImpl implements KakaoMemberJoin {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private final List<KakaoMemberJoinEntity> memberJoinEntities = new ArrayList<>();
    
        private final MemberJoin memberJoinImpl;
    
        public KakaoMemberJoinImpl(MemberJoin memberJoinImpl) {
            this.memberJoinImpl = memberJoinImpl;
        }
    
        /**
         * <pre>
         *     {@link KakaoMemberJoin#kakaoJoin(KakaoMemberJoinEntity)}
         * </pre>
         *
         * @param kakaoMemberJoinEntity 카카오톡 회원정보 엔티티
         */
        @Override
        public void kakaoJoin(KakaoMemberJoinEntity kakaoMemberJoinEntity) {
            this.memberJoinEntities.add(kakaoMemberJoinEntity);
            logger.info("kakaoJoin : {}", this);
        }
    
        /**
         * <pre>
         *     {@link MemberJoin#join(MemberJoinEntity)}
         * </pre>
         *
         * @param memberJoinEntity 기본 회원정보 엔티티
         */
        @Override
        public void join(MemberJoinEntity memberJoinEntity) {
            this.memberJoinImpl.join(memberJoinEntity);
        }
    
        @Override
        public String toString() {
            try {
                return new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(this.memberJoinEntities);
            } catch (Exception e) {
                return this.memberJoinEntities.toString();
            }
        }
    }

    트랜잭션 담당 클래스

    이번 글의 주제인 팩토리 메서드 패턴이 적용된 클래스 입니다.

    일반적으로 Spring에선 트랜잭션 처리를 위해 @Service 어노테이션이 선언된 클래스를 사용하는데 추상 클래스를 제외한 구현 클래스에 @Service 어노테이션이 선언되어 있습니다.

    https://i.ibb.co/3SQtnxJ/Member-Join-Service.png

    코드

    회원가입 정보 저장 트랜잭션 처리를 다루기 위한 인터페이스 입니다.

    회원 정보의 유형이 다르기 때문에 join 메서드의 매개변수 타입은 제네릭으로 선언되어 있습니다.

    /**
     * <pre>
     *     회원가입 처리 시 트랜잭션 처리를 위한 인터페이스
     * </pre>
     */
    public interface MemberJoinService {
    
        /**
         * <pre>
         *     회원가입 저장 트랜잭션 처리 메서드.
         *     해당 메서드는 팩토리 메서드 패턴을 활용한다.
         *     이때 저장 시 전달 받은 회원 정보 객체 타입이 다양할 수 있음으로 제네릭으로 선언.
         * </pre>
         *
         * @param joinEntity 저정할 회원정보 엔티티
         * @param <T> 저장 시 전달 받을 엔티티 타입
         */
        <T> void join(T joinEntity);
    }

    회원가입 프로세스 로직을 처리하는 추상 클래스 입니다. createMemberJoin 추상 메서드를 보시면 아시겠지만 실제 프로세스 로직 처리 구현체를 자식 클래스(구현 클래스)에 위임한 것을 알 수 있습니다.

    그리고 join 메서드에서 createMemberJoin 메서드를 호출해서 프로세스 로직 처리 구현체를 실행 시키는것을 볼 수 있습니다.

    /**
     * <pre>
     *     회원가입 처리 팩토리 메서드 패턴을 활용한 추상 클래스
     * </pre>
     */
    public abstract class MemberJoinServiceAbs implements MemberJoinService {
    
        /**
         * {@link MemberJoinService#join(Object)}
         */
        @Override
        public <T> void join(T joinEntity) {
            MemberJoinProcess memberJoinProcessFactoryMethod = this.createMemberJoin(joinEntity);
            memberJoinProcessFactoryMethod.join();
        }
    
        /**
         * <pre>
         *     팩토리 메서드 패턴 메서드.
         *     자식 클래스에서 회원가입 처리를 하는 {@link MemberJoinProcess} 구현체를 반환.
         * </pre>
         *
         * @param joinEntity 하위 클래스에서 회원가입 저장 시 사용될 엔티티
         * @param <T> 회원가입 저장 시 사용될 객체 타입
         * @return {@link MemberJoinProcess} 구현체
         */
        protected abstract <T> MemberJoinProcess createMemberJoin(T joinEntity);
    }

    기본 회원가입 구현 클래스 입니다. 해당 클래스에서 실제로 회원가입 처리 프로세스를 처리하는 MemberJoinProcessImpl 객체를 반환하는것을 확인 할 수 있습니다.

    /**
     * <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);
        }
    }

    카카오톡 회원가입 구현 클래스 입니다. 해당 클래스에서 실제로 회원가입 처리 프로세스를 처리하는 KakaoMemberJoinProcessImpl 객체를 반환하는것을 확인 할 수 있습니다.

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

    전체 클래스 다이어그램

    신기하게도 클래스 다이어그램을 나눠서 볼 땐 쉬워보였는데 전체로 보니깐 복잡해보이네요.

    https://i.ibb.co/NCGgznS/Member-Join-Service.png

    테스트

    @SpringBootTest
    class CreationalPatternTests {
    
        @Autowired
        MemberJoinService kakaoMemberJoinServiceImpl;
    
        @Autowired
        MemberJoinService memberJoinServiceImpl;
    
        @Test
        void 카카오톡_회원가입() {
            KakaoMemberJoinEntity kakaoMemberJoinEntity = new KakaoMemberJoinEntity(13248627872L
                    , LocalDateTime.now()
                    , true
                    , false
                    , true
                    , true
                    , "test@kakao.com"
            );
    
            this.kakaoMemberJoinServiceImpl.join(kakaoMemberJoinEntity);
        }
    
        @Test
        void 일반_회원가입() {
            MemberJoinEntity memberJoinEntity = new MemberJoinEntity(UUID.randomUUID().toString().replace("-", "").substring(0, 5)
                , UUID.randomUUID().toString().replace("-", "").substring(0, 10)
                , MemberJoinEntity.MemberJoinType.DEFAULT);
    
            this.memberJoinServiceImpl.join(memberJoinEntity);
        }
    }

    카카오톡 회원가입 테스트 코드를 실행하면 카카오톡 정보 저장 및 임의 계정 생성 저장이 정상적으로 실행된것을 확인 할 수 있습니다.

    c.d.c.f.repo.test.KakaoMemberJoinImpl    : kakaoJoin : [{"id":13248627872,"connectedAt":[2023,1,18,10,43,40,254728500],"kakaoAccountHasEmail":true,"kakaoAccountEmailNeedsAgreement":false,"kakaoAccountIsEmailValid":true,"kakaoAccountIsEmailVerified":true,"kakaoAccountEmail":"test@kakao.com"}]
    c.d.c.f.repo.test.MemberJoinImpl         : defaultJoin : [{"memberId":"test@kakao.com","password":"651bc21dba","memberJoinType":"KAKAO"}]

    기본 회원가입 테스트 코드도 실행하면 정상적으로 저장되는것을 확인 할 수 있습니다.

    c.d.c.f.repo.test.MemberJoinImpl         : defaultJoin : [{"memberId":"5cbfa","password":"abb9a357b1","memberJoinType":"DEFAULT"}]

    부록

    DB 저장 하는 경우

    해당 예제는 정보를 객체 변수에 저장 하는 방식이였습니다. 이 부분을 DB 저장으로 변경 하게 된다면 몇 가지 부분만 추가/수정하면 됩니다.

    정보 저장 담당 클래스

    MemberJoin, KakaoMemberJoin 인터페이스 기준으로 DB 저장 구현체를 생성합니다. 이때 생성은 Mybatis, JPA 등 어떤 기술을 쓰던 상관 없습니다.

    단 Mybatis는 구현체 기반이 아니라 인터페이스 기반이기 때문에 구현체가 아닌 인터페이스를 생성 한 후 상속 받고 해당 인터페이스에 @Mapper 어노테이션을 선언합니다.

    프로세스 로직 담당 클래스

    Spring Bean에 등록된 객체를 각 프로세스 로직 클래스에 맞게 주입 받을 수 있게 합니다.

    예로 MyBatis 기반 카카오톡 회원가입을 한다고 가정하면 KakaoMemberJoin 인터페이스를 상속 받은 KakaoMemberJoinMybatis 인터페이스를 생성하고 해당 인터페이스에 @Mapper 어노테이션을 선언한 후 아래와 같이 변경 합니다.

    public class KakaoMemberJoinProcessImpl implements MemberJoinProcess {
            // Spring Bean에 등록된 MyBatis 기반 카카오톡 회원가입
        private final KakaoMemberJoinMybatis kakaoMemberJoinMybatis;
        private final KakaoMemberJoinEntity kakaoMemberJoinEntity;
    
        public KakaoMemberJoinProcessImpl(KakaoMemberJoinMybatis kakaoMemberJoinMybatis, KakaoMemberJoinEntity kakaoMemberJoinEntity) {
            this.kakaoMemberJoinMybatis = kakaoMemberJoinMybatis;
            this.kakaoMemberJoinEntity = kakaoMemberJoinEntity;
        }
    
            .
            .
            .
    }

    트랜잭션 담당 클래스

    DB 트랜잭션 처리를 해야 하기 때문에 MemberJoinServiceAbs 추상 클래스의 join 메서드에 @Transactions 어노테이션을 선언합니다.

    public abstract class MemberJoinServiceAbs implements MemberJoinService {
    
            @Transactional // 트랜잭션 처리를 위해 @Transactional 선언
        @Override
        public <T> void join(T joinEntity) {
            MemberJoinProcess memberJoinProcessFactoryMethod = this.createMemberJoin(joinEntity);
            memberJoinProcessFactoryMethod.join();
        }
    
        protected abstract <T> MemberJoinProcess createMemberJoin(T joinEntity);
    }

    그리고 해당 추상 클래스를 상속 받은 구현체도 정보 저장 담당 클래스에서 새롭게 등록한 Spring Bean 객체로 변경 합니다. 변경 해야 하는 이유는 프로세스 로직 담당 클래스에서 변경되었기 때문입니다.

    위 정보 저장 담당 클래스의 예처럼 MyBatis 기반 카카오톡 회원가입이라고 한다면 아래와 같이 변경합니다.

    @Service
    public class KakaoMemberJoinServiceImpl extends MemberJoinServiceAbs implements MemberJoinService {
            // Spring Bean에 등록된 Mybatis 기반 객체 주입
        private final KakaoMemberJoinMybatis kakaoMemberJoinMybatis;
    
        public KakaoMemberJoinServiceImpl(KakaoMemberJoinMybatis kakaoMemberJoinMybatis) {
            this.kakaoMemberJoinMybatis= kakaoMemberJoinMybatis;
        }
    
        /*
                프로세스 로직 담당 클래스에서 참조 할 수 있게 Spring Bean에 등록된 Mybatis 기반 객체 주입
         */
        @Override
        protected <T> MemberJoinProcess createMemberJoin(T joinEntity) {
            return new KakaoMemberJoinProcessImpl(this.kakaoMemberJoinMybatis, (KakaoMemberJoinEntity) joinEntity);
        }
    }

    끝으로 예제 코드는 https://github.com/sungwookkim/spring-designpattern에서 확인하실 수 있습니다.

    반응형

    댓글

Designed by Tistory.