ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring @Sevice 클래스 유연하게 써보기
    Spring 2022. 7. 15. 08:16
    반응형

    Spring Multi Datasource(다중 데이터소스)를 활용한 Service 구현 :: 신나게의 개발썰

    Spring Multi Datasource(다중 데이터소스)를 활용한 Service 구현-2 :: 신나게의 개발썰

    Service 관련 작성하였지만 개인적으로 아쉬운게 있어 추가 글(또 작성하네..)을 작성하려고 합니다.(이전 글 홍보 양해 부탁드립니다^^)

    Spring Framework를 활용하면 MVC 패턴을 사용하게 됩니다.

    MVC 패턴에 의거(?)해서

    • @Controller 어노테이션으로 선언된 Controller 클래스 생성
    • 비지니스 로직 처리를 위해 @Service 어노테이션으로 선언된 Service 클래스 생성

    여기서 이번 글의 목적은 저 Servie 클래스를 유연하게 써보기 위한 목적 입니다.

    Spring에서 지원하는 어노테이션을 클래스 및 메서드에 선언하며 Spring 컨테이너가 객체를 관리 하게 됩니다. 그렇게 생성된 객체들은 싱글톤 객체가(별다른 설정을 하지 않으면) 됩니다.

    그래서 일반적으로 아래와 같은 형태로 구현합니다.(예시에선 생성자 DI를 사용하였습니다.)

    @Service
    public class SingletonService {
        private final SingletonInterface singletonInterfaceImpl;
    
        public SingletonService(SingletonInterface singletonInterfaceImpl) {
            this.singletonInterfaceImpl = singletonInterfaceImpl;
        }
    }

    별 문제가 없어보이는 코드입니다.

    그런데 말입니다. 만약에 SingletonInterface 인터페이스의 구현체를 다른걸로 변경하고 싶다면? 우린 자바로 개발하고 자바는 다양성을 강조(?)하기 때문에 우리의 구현체는 언제든지 달라질 수 있습니다.

    그래서 아래와 같이 구현체를 변경하는 changeSingletonInterfaceObj 메서드를 만들었습니다.

    @Service
    public class SingletonService {
        private SingletonInterface singletonInterfaceImpl;
    
        public SingletonService(SingletonInterface singletonInterfaceImpl) {
            this.singletonInterfaceImpl = singletonInterfaceImpl;
        }
    
        public void changeSingletonInterfaceObj(SingletonInterface singletonInterfaceImpl) {
            this.singletonInterfaceImpl = singletonInterfaceImpl;
        }
    }

    그러나 우리는 저렇게 하지 않습니다. 싱글톤 객체이기에 참조하고 있는 객체를 변경하게 되면 참조해서 사용하고 있는 다른 객체에 심각한 문제가 발생될 수 있기 때문입니다.

    그리고 해당 클래스를 New 연산자로 객체를 생성해도 문제가 되는게 해당 객체는 Spring 컨테이너가 관리하는 객체가 아니기 때문에 Spring에서 지원하는 기능들을 올바르게 사용할 수 없습니다.

    그래서 또 아래와 같이 한번 변경해보았습니다.

    @Service
    public class SingletonService {
        private final SingletonInterface singletonInterfaceImpl;
        private final SingletonInterface twoSingletonInterfaceImpl;
    
        public SingletonService(SingletonInterface singletonInterfaceImpl, SingletonInterface twoSingletonInterfaceImpl) {
            this.singletonInterfaceImpl = singletonInterfaceImpl;
            this.twoSingletonInterfaceImpl = twoSingletonInterfaceImpl;
        }
    }

    SingletonInterface 인터페이스 구현체를 모조리 선언하는 것입니다.(…)

    나쁘지(?) 않는 방법 같지만 과연 저게 다양성을 위한 코드일까하는 생각이 듭니다. SingletonInterface 인터페이스 입장에선 다양한 구현체를 가지고 있기 때문에 다양성이 맞겠지만 그걸 사용하는 SingletonService 클래스는 다양성을 올바르게 사용하는 구조는 아니라고 생각됩니다.

    그래도 포기하지 않고 또또 아래와 같이 변경해보았습니다.

    @Service
    public class SingletonService {
        public SingletonService() {}
    
        public void singletonExecute(SingletonInterface singletonInterface) {
            singletonInterface.execute();
        }
    
        public void twoSingletonExecute(SingletonInterface twoSingletonExecute) {
            twoSingletonExecute.execute();
        }
    }

    이번에는 메서드가 SingletonInterface 인터페이스를 참조하는 형식으로 바꿔보았습니다.(…?)

    역시 나쁘지(?) 않는 방법인듯 하나 만약 비즈니스 메서드가 참조해야 하는 객체가 여러 개라면?.. 아마도 괴로운 상황이 많이 생길듯 합니다.

    자 이쯤에서 그만 하도록 하겠습니다..(너무 힘들어서..)

    Spring 컨테이너가 객체를 관리해주기 때문에 저희는 비즈니스 개발에만 집중 할 수 있는건 사실 입니다.

    그렇지만 객체를 싱글톤(별다른 설정을 하지 않으면)으로 관리하기 때문에 다양성에 맞게 개발하기 어려운것 또한 사실입니다.

    사실 위 방법들이 나쁘다고 생각하지 않습니다. 우리들이 개발하는 환경은 각기 다르기 때문에 어떤 환경에선 저 방법들을 사용해야 할 수 있기 때문입니다. 뭐든 두루두루 알아두면 환경에 맞춰서 개발 할 수 있기 때문이죠!

    이번 글에서 작성하고자 하는 방법은

    • Service 클래스 메소드의 프로세스 구현이 아닌 매개변수 참조로 프로세스 실행 입니다.

    Service 클래스의 사용 목적은 대부분 @Transactional 활용을 위해서 일 것 입니다. 트랜잭션 관리를 스프링에 위임함으로서 비즈니스 로직에만 집중해서 개발 할 수 있게 해주는 고마운 어노테이션입니다.

    그러나 개인적으론 장점만 있는건 아닌거 같습니다. 트랜잭션 처리를 위해 코드들은 반드시 @Transactional 을 사용해야 하고 그로 인해 비즈니스 로직 코드를 메서드에 다 작성하는 식으로 구현이 되는 약간의 편의(?)와 강제성이 있지 않나 싶습니다.

    저희 개발자들은 프레임워크를 사용하지만 프레임워크에 복속되지 않고 우리 의사를 자유롭게 작성할 권리가 있기 때문에 조금이나마 우리 자유 의사를 표현하기 위한 방법 중 하나라고 생각해주시면 감사하겠습니다.

    구현은 매우 간단합니다.

    • 인터페이스 1개
    • @Service 어노테이션 선언된 클래스 1개
    • 해당 클래스에 @Transactional 어노테이션 선언된 메서드 1개

    예제는

    게시판 예제를 활용하였습니다. 해당 예제에서는 자유, 게임 게시판을 저장/읽기 하는 기능 밖에 없습니다. 여기에 게시판을 분류해서 저장하는 기능을 만드는것으로 예를 들겠습니다.

    인터페이스

    /**
     * <pre>
     *     프로세스 실행 공용 인터페이스
     * </pre>
     */
    public interface ServiceProcessExecute {
    
        /**
         * <pre>
         *     프로세스 실행 메서드
         * </pre>
         */
        void execute();
    }

    먼저 위와 같은 인터페이스를 하나 생성하였습니다. 해당 인터페이스는 @Service 클래스의 @Transactional 메서드에 주입 받을 인터페이스 입니다.

    Service 클래스

    /**
     * <pre>
     *     {@link Service} 어노테이션을 선언하는 클래스
     *     {@link org.springframework.transaction.annotation.Transactional} 처리를 위한 목적으로 생성
     * </pre>
     */
    @Service
    public class ServiceProcessExecuteImpl {
    
        /**
         * <pre>
         *     Master 트랜잭션을 사용하는 메서드
         * </pre>
         * @param serviceProcessExecute
         */
        @Transactional(rollbackFor = Exception.class)
        public void masterTransactionalProcess(ServiceProcessExecute serviceProcessExecute) {
            serviceProcessExecute.execute();
        }
    }

    위와 같은 Service 클래스를 하나 생성하고 @Transactional 메서드를 하나 생성하였습니다. 해당 메서드는 위 설명 처럼 ServiceProcessExecute 인터페이스를 주입 받고 메서드를 실행하는 역할만 합니다.

    네 끝입니다! 인제 우리는 ServiceProcessExecute 구현체를 생성하고 해당 메서드에 매개변수로 넣어주기만 하면 됩니다. 그러면 인제 게시판 분류에 따라 저장하는 기능의 클래스를 만들어보겠습니다.

    구현체

    /**
     * <pre>
     *     게시물 유형에 맞춰 저장하는 클래스
     * </pre>
     */
    public final class NoticeBoardSaveProcessImpl implements ServiceProcessExecute {
        private NoticeBoardTypeEnum noticeBoardTypeEnum;
        private Posts posts;
    
        private final GameBoardWriteMapper gameBoardWriteMapper;
        private final FreeBoardWriteMapper freeBoardWriteMapper;
    
        public NoticeBoardSaveProcessImpl() {
            this(NoticeBoardWriteMapperFactory.getInstance(GameBoardWriteMapper.class)
                    , NoticeBoardWriteMapperFactory.getInstance(FreeBoardWriteMapper.class));
        }
    
        public NoticeBoardSaveProcessImpl(GameBoardWriteMapper gameBoardWriteMapper
                , FreeBoardWriteMapper freeBoardWriteMapper) {
            this.gameBoardWriteMapper = gameBoardWriteMapper;
            this.freeBoardWriteMapper = freeBoardWriteMapper;
        }
    
        /**
         * <pre>
         *     게시물 저장에 필요한 매개변수 초기화
         * </pre>
         *
         * @param noticeBoardTypeEnum
         * @param posts
         * @return
         */
        public ServiceProcessExecute execute(NoticeBoardTypeEnum noticeBoardTypeEnum, Posts posts) {
            this.noticeBoardTypeEnum = noticeBoardTypeEnum;
            this.posts = posts;
    
            return this;
        }
    
        /**
         * <pre>
         *     {@link ServiceProcessExecute#execute()}
         * </pre>
         */
        @Override
        public void execute() {
            Objects.requireNonNull(this.noticeBoardTypeEnum
                    , "execute(NoticeBoardTypeEnum noticeBoardTypeEnum, Posts posts) 메서드 선행 호출필요");
            Objects.requireNonNull(this.posts
                    , "execute(NoticeBoardTypeEnum noticeBoardTypeEnum, Posts posts) 메서드 선행 호출필요");
    
            NoticeBoardWriteService noticeBoardWriteService;
    
            switch (this.noticeBoardTypeEnum) {
                case GAME_BOARD: noticeBoardWriteService = new GameBoardWriteService(this.gameBoardWriteMapper);
                break;
                case FREE_BOARD: noticeBoardWriteService = new FreeBoardWriteService(this.freeBoardWriteMapper);
                break;
                default: throw new IllegalStateException("지원하지 않는 게시물 유형 입니다.");
            }
    
            noticeBoardWriteService.insertPosts(this.posts);
        }
    }

    해당 구현체는 게시물 저장을 위해

    • GameBoardWriteMapper
    • FreeBoardWriteMapper

    Mapper 인터페이스를 생성자로 받고 있습니다. 해당 인터페이스는 Spring 컨테이너가 관리하는 인터페이스로 주입 받을 예정 입니다.

    그리고 해당 인터페이스를

    • GameBoardWriteService
    • FreeBoardWriteService

    클래스에 생성자로 넣어 객체를 생성합니다. 예제의 게시글 내용을 보면 해당 클래스는 @Service 어노테이션으로 선언된 클래스 입니다.

    즉, Spring 컨테이너가 관리하는 싱글톤 객체가 존재하기도 하며 동시에 New 연산자를 사용하였습니다.

    빈 생성자의 NoticeBoardWriteMapperFactory.getInstance 는 Mapper를 편리하게 제공 받기 위해 생성한 Static 메서드 입니다. 자세한 코드는 아래를 참고 하시면 되겠습니다.

    /**
     * <pre>
     *     게시물 Write Mapper 팩토리
     * </pre>
     */
    @Component
    public final class NoticeBoardWriteMapperFactory {
        private static final Map<String, NoticeBoardWriteMapper> noticeBoardWriteMapperMap = new HashMap<>();
    
        public NoticeBoardWriteMapperFactory(FreeBoardWriteMapper freeBoardWriteMapper
                , GameBoardWriteMapper gameBoardWriteMapper
                , FreeBoardControlWriteMapper freeBoardControlWriteMapper) {
            noticeBoardWriteMapperMap.put(FreeBoardWriteMapper.class.getSimpleName()
                    , freeBoardWriteMapper);
            noticeBoardWriteMapperMap.put(GameBoardWriteMapper.class.getSimpleName()
                    , gameBoardWriteMapper);
            noticeBoardWriteMapperMap.put(FreeBoardControlWriteMapper.class.getSimpleName()
                    , freeBoardControlWriteMapper);
        }
    
        /**
         * <pre>
         *     게시물 Write Mapper 반환
         * </pre>
         * @param clazz
         * @param <T>
         * @return
         */
        @SuppressWarnings("unchecked")
        public static <T> T getInstance(Class<? extends NoticeBoardWriteMapper> clazz) {
            return Optional.ofNullable((T) noticeBoardWriteMapperMap.get(clazz.getSimpleName()))
                    .orElseThrow(() -> new IllegalStateException("존재하지 않는 맵퍼 유형 입니다."));
        }
    }

    자 그럼 테스트 코드를 확인해보겠습니다. 먼제 테스트를 위해서 Spring으로 부터 주입 받는 Bean은 아래와 같습니다.

    // 게임 게시판 저장 Mapper 인터페이스
    @Autowired
    GameBoardWriteMapper gameBoardWriteMapper;
    
    // 자유 게시판 저장 Mapper 인터페이스
    @Autowired
    FreeBoardWriteMapper freeBoardWriteMapper;
    
    // 자유 게시판 전용 관리 테이블 저장 Mapper 인터페이스
    @Autowired
    FreeBoardControlWriteMapper freeBoardControlWriteMapper;
    
    // 자유 게시판 전용 관리 테이블 읽기 Mapper 인터페이스
    @Autowired
    FreeBoardControlReadMapper freeBoardControlReadMapper;
    
    @Autowired
    NoticeBoardMapperReadService freeBoardDecoratorReadService;
    
    // 이번 예제의 핵심 클래스
    @Autowired
    ServiceProcessExecuteImpl serviceProcessExecuteImpl;

    그러면 먼저 생성자에 Mapper 인터페이스를 주입 받아 처리하는 테스트 코드 입니다.

    @Test
    void serviceProcessExecute_게시물저장(){
        Posts insertFreeBoardPosts = new Posts("freeMemberId_1", "freeMemberId_1 이건 serviceProcessExecuteImpl 본문 내용 입니다.");
        this.serviceProcessExecuteImpl.masterTransactionalProcess(new NoticeBoardSaveProcessImpl(this.gameBoardWriteMapper, this.freeBoardWriteMapper)
                .execute(NoticeBoardTypeEnum.FREE_BOARD, insertFreeBoardPosts));
    
        Posts insertGameBoardPosts = new Posts("gameMemberId_1", "gameMemberId_1 이건 serviceProcessExecuteImpl 본문 내용 입니다.");
        this.serviceProcessExecuteImpl.masterTransactionalProcess(new NoticeBoardSaveProcessImpl(this.gameBoardWriteMapper, this.freeBoardWriteMapper)
                .execute(NoticeBoardTypeEnum.GAME_BOARD, insertGameBoardPosts));
    
        Assertions.assertTrue(insertFreeBoardPosts.getSeq() > 0 && insertGameBoardPosts.getSeq() > 0);
    }

    그리고 빈 생성자의 NoticeBoardWriteMapperFactory.getInstance 를 활용한 테스트 코드 입니다.

    @Test
    void serviceProcessExecute_게시물저장_NoticeBoardWriteMapperFactory_생성자() {
        Posts insertFreeBoardPosts = new Posts("freeMemberId_1", "freeMemberId_1 이건 serviceProcessExecuteImpl 본문 내용 입니다.");
        this.serviceProcessExecuteImpl.masterTransactionalProcess(new NoticeBoardSaveProcessImpl()
                .execute(NoticeBoardTypeEnum.FREE_BOARD, insertFreeBoardPosts));
    
        Posts insertGameBoardPosts = new Posts("gameMemberId_1", "gameMemberId_1 이건 serviceProcessExecuteImpl 본문 내용 입니다.");
        this.serviceProcessExecuteImpl.masterTransactionalProcess(new NoticeBoardSaveProcessImpl()
                .execute(NoticeBoardTypeEnum.GAME_BOARD, insertGameBoardPosts));
    
        Assertions.assertTrue(insertFreeBoardPosts.getSeq() > 0 && insertGameBoardPosts.getSeq() > 0);
    }

    그리고 마지막으로 자유 게시판 전용 관리 테이블에 저장하는 테스트 코드 입니다. NoticeBoardSaveProcessImpl 객체를 생성 할 때 두번째 매개변수가 FreeBoardWriteMapper Mapper 인터페이스가 아닌 FreeBoardControlWriteMapper Mapper 인터페이스임을 알 수 있습니다.

    @Test
    void serviceProcessExecute_freeboard_control_게시물저장(){
        Posts insertFreeBoardControlPosts = new Posts("freeMemberId_1", "freeMemberId_1 이건 serviceProcessExecuteImpl 본문 내용 입니다.");
        this.serviceProcessExecuteImpl.masterTransactionalProcess(new NoticeBoardSaveProcessImpl(this.gameBoardWriteMapper, this.freeBoardControlWriteMapper)
                .execute(NoticeBoardTypeEnum.FREE_BOARD, insertFreeBoardControlPosts));
    
        NoticeBoardControl noticeBoardControl = this.freeBoardDecoratorReadService.findNoticeBoardControl(this.freeBoardControlReadMapper, insertFreeBoardControlPosts.getSeq());
    
        Assertions.assertTrue(insertFreeBoardControlPosts.getSeq() > 0 && noticeBoardControl.getSeq() > 0);
    }

    마치며

    이번엔 왠지 급 마무리를 짓는거 같네요ㅎㅎ;;

    사실 Spring에선 이와 같은 사태를 위해 @Scope 어노테이션을 이용해서 객체의 생명주기를 조절 할 수 있습니다. Singleton으로 하거나 매번 객체를 새로 생성 할 수도 있습니다. 그 외 생명주기를 유연하게 사용할 수 있습니다.

    이번 글은 @Scope 말고 순수한 자바 형태로 개발 하는 방법을 적어 보고 싶었습니다.

    Spring의 기능을 아예 안 쓸수는 없지만 모든 걸 Spring에 종속 되서 개발 하기 보단 최소한 비즈니스 로직 만큼은 개발자들 손에 유연하게 작성하고 개발 되었으면 하는 바램으로 작성하였습니다. 그러니 이렇게도 할 수 있구나 라고 느껴주시면 감사하겠습니다.^^

    그럼 긴 글 마지막까지 읽어주셔서 감사드리며 언제나 하시는 모든 개발에 좋은 성과가 있길 기도드리겠습니다.!

    그리고 해당 게시글의 예제는

    에서 확인하실 수 있습니다.

    반응형

    댓글

Designed by Tistory.