ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 팩토리 메소드 패턴을 적용하여 bean을 동적으로 사용하기
    웹 개발/Spring Framework 2020. 4. 19. 22:58

    객체를 만들어서 내보내주는 팩토리 패턴

     

    하나의 인터페이스를 상속받는 구현체들이 많을 경우, 팩토리 메소드 패턴을 이용하면 동적으로 필요한 객체를 받아와서 사용할 수 있다.

    Factory 패턴이란?

    Factory method는 부모(상위) 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며. 자식(하위) 클래스가 어떤 객체를 생성할지를 결정하도록 하는 패턴이기도 하다. 부모(상위) 클래스 코드에 구체 클래스 이름을 감추기 위한 방법으로도 사용한다. - 위키백과

     

    Head First 디자인 패턴을 참고하여 팩토리 패턴을 정리한 포스팅을 보면 팩토리 패턴에 대해서 학습할 수 있다.

     

    팩토리 메소드 패턴을 이용하면 인터페이스의 구현클래스를 매번 선언해서 가져올 필요없이 가져오려는 인터페이스의 구현체의 타입을 이용해서 가져올 수 있다.

     

    Spring에서는 Bean을 Collection으로 주입할(Injection) 수 있다. 다시 말해, 하나의 인터페이스를 상속받고 있는 구현체들인 bean들을 List로 주입할 수 있다.

     

    예제 코드를 살펴보자.

     

    음식의 종류별로 배달하려는 Service가 있다.

    
    // 음식 배달의 상위 인터페이스인 FoodService
    public interface FoodService {
    
        FoodType getFoodType();
    
        void deliverItem();
    }
    

     

    배달하는 음식의 종류.

    // 사탕, 과자, 라면, 초콜릿
    public enum FoodType {
        CANDY, SNACK, NOODLE, CHOCOLATE
    }
    

     

    음식 종류별로 배달하는 deliverItem을 구현하고, 자신의 타입을 알려주는 getFoodType 메서드를 지닌 구현체들.

    
    // 사탕 ServiceImpl
    @Service
    public class CandyServiceImpl implements FoodService {
    
        @Override
        public FoodType getFoodType() {
            return FoodType.CANDY;
        }
    
        @Override
        public void deliverItem() {
            System.out.println("사탕 배달 완료!");
        }
    }
    
    // 과자 ServiceImpl
    @Service
    public class SnackServiceImpl implements FoodService {
    
        @Override
        public FoodType getFoodType() {
            return FoodType.SNACK;
        }
    
        @Override
        public void deliverItem() {
            System.out.println("과자 배달 완료!");
        }
    }
    
    // 라면 ServiceImpl
    @Service
    public class NoodleServiceImpl implements FoodService {
    
        @Override
        public FoodType getFoodType() {
            return FoodType.NOODLE;
        }
    
        @Override
        public void deliverItem() {
            System.out.println("라면 배달 완료!");
        }
    }
    
    // 초콜릿 ServiceImpl
    @Service
    public class ChocolateServiceImpl implements FoodService {
    
        @Override
        public FoodType getFoodType() {
            return FoodType.CHOCOLATE;
        }
    
        @Override
        public void deliverItem() {
            System.out.println("초콜릿 배달 완료!");
        }
    }
    

    이제 이 포스팅의 주제인 팩토리 메서드를 만들어보자.

     

    예시는 생성자 패턴으로 bean을 주입했을 경우로 작성했다.

    Spring 4.3 버전 이상부터는 생성자 주입을 사용할 경우, @Autowired를 붙이지 않아도 된다.

    필드 주입(Field Injection) 대신 생성자 주입(Constructor Injection)을 사용해야 하는 이유

     

    
    @Component
    public class FoodServiceFactory {
        // foodService를 담고있어줄 Map
        private final Map<FoodType, FoodService> foodServices = new HashMap<>();
    
        // 생성자 주입으로 FoodService를 상속하고 있는 bean들을 주입받는다.
        public FoodServiceFactory(List<FoodService> foodServices) {
            // foodService를 상속받는 bean이 없을 경우 IllegalArguemntException을 던진다.
            if(CollectionUtils.isEmpty(foodServices)) {
                throw new IllegalArgumentException("존재하는 foodService가 없음");
            }
    
            // foodService의 구현체인 bean들을 for문을 돌리면서 key는 음식 종류의 타입, value는 해당 동일한 bean을 map에 담아준다.
            for (FoodService foodService : foodServices) {
                this.foodServices.put(foodService.getFoodType(), foodService);
            }
        }
    
        public FoodService getService(FoodType foodType) {
            // 인자로 넘겨준 타입에 맞는 foodService의 bean을 넘겨준다.
            return foodServices.get(foodType);
        }
    }
    

     

    실제로 인자로 넘겨준 타입에 맞는 bean을 가져오는지 확인해보자.

    
    @SpringBootTest
    class FoodServiceFactoryTest {
    
        @Autowired
        private FoodServiceFactory foodServiceFactory;
    
        @Test
        void 음식_타입별로_서비스_가져오기() {
            // given
            FoodType candy = FoodType.CANDY;
            FoodType chocolate = FoodType.CHOCOLATE;
            FoodType snack = FoodType.SNACK;
            FoodType noodle = FoodType.NOODLE;
    
            // when
            FoodService candyService = foodServiceFactory.getService(candy);
            FoodService chocolateService = foodServiceFactory.getService(chocolate);
            FoodService snackService = foodServiceFactory.getService(snack);
            FoodService noodleService = foodServiceFactory.getService(noodle);
    
            // then
            assertThat(candyService.getFoodType(), is(FoodType.CANDY));
            assertThat(chocolateService.getFoodType(), is(FoodType.CHOCOLATE));
            assertThat(snackService.getFoodType(), is(FoodType.SNACK));
            assertThat(noodleService.getFoodType(), is(FoodType.NOODLE));
    
            // print
            candyService.deliverItem();
            chocolateService.deliverItem();
            snackService.deliverItem();
            noodleService.deliverItem();
        }
    }

     

    사탕, 초콜릿, 과자, 라면 타입을 인자로 넘겨주어서 실제로 이 타입들을 가진 서비스들을 넘겨주는지 확인한 테스트 코드이다.

     

    테스트가 모두 통과하면서, 순서대로 배달 완료했다는 안내 내용이 찍히고 있다.


    팩토리 메소드 패턴을 활용한 코드

    팩토리 메소드 패턴을 사용하면 외부에서 들어오는 타입마다 일일이 다른 서비스를 if, else를 이용하거나 switch문을 이용해서 bean을 가려내줄 필요가 없어진다.

     

    팩토리 메소드 패턴을 사용하지 않았을 때, 극단적으로 모든 bean들을 모두 주입 받아야 할 것이다.


    그리고 인자로 보내오는 타입을 모두 판별하여 그에 맞는 bean을 내어주어야 할 것이다.

     

    팩토리 메소드 패턴을 사용하지 않고 주문을 받고 음식을 배달하려는 Service 코드.

    
    @Service
    public class FoodOrderService {
    
        private final FoodService candyService;
        private final FoodService chocolateService;
        private final FoodService noodleService;
        private final FoodService snackService;
    
        public FoodOrderService(
                CandyServiceImpl candyService,
                ChocolateServiceImpl chocolateService,
                NoodleServiceImpl noodleService,
                SnackServiceImpl snackService
        ) {
            this.candyService = candyService;
            this.chocolateService = chocolateService;
            this.noodleService = noodleService;
            this.snackService = snackService;
        }
        public void receiveOrder(OrderContent orderContent) {
            // other receiver Logic....
    
            switch (orderContent.getFoodType()) {
                case CANDY:
                    candyService.deliverItem();
                    break;
                case CHOCOLATE:
                    chocolateService.deliverItem();
                    break;
                case NOODLE:
                    noodleService.deliverItem();
                    break;
                case SNACK:
                    snackService.deliverItem();
                    break;
                default:
                    throw new IllegalArgumentException("존재하는 foodType이 없음");
            }
        }
    }

     

    팩토리 메소드 패턴을 적용하여 활용한 코드.

    @Service
    public class FoodOrderService {
    
        private final FoodServiceFactory foodServiceFactory;
    
        public FoodOrderService(
                FoodServiceFactory foodServiceFactory
        ) {
            this.foodServiceFactory = foodServiceFactory;
        }
        public void receiveOrder(OrderContent orderContent) {
            // other receiver Logic....
    
            foodServiceFactory.getService(orderContent.getFoodType()).deliverItem();
        }
    }

    긴 switch문이 코드 한 줄로 줄어들었다!!

     

    또한, 주입받는 bean도 음식 타입 때마다 달라지므로 의존성이 크게 줄어들었다.

     

    앞으로 해당 service 클래스는 OrderContent(주문 양식)에 담긴 음식 타입에 따라 동적으로 음식을 배달할 것이다.

Designed by Tistory.