ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 스프링에서 의존성 주입을 하는 3가지 방법
    Server/Spring 2021. 11. 29. 01:32
    728x90
    반응형

    다양한 의존관계 주입 방법

    • 생성자 주입
    • 수정자 주입(setter 주입)
    • 필드 주입
    • 일반 메소드 주입

     

    주입 방법은 크게 4가지가 있는데 하나씩 알아보겠습니다.



    생성자 주입

    • 이름 그대로 생성자를 통해 의존 관계를 주입하는 것입니다.
    • 생성자 1개이면 @Autowired를 생략할 수 있습니다.

     

    @Component
    public class TestComponent {
    
        private TestRepository testRepository;
    
        // 생성자 주입
        public TestComponent(TestRepository testRepository) {
            this.testRepository = testRepository;
        }
    
        @PostConstruct
        public void test() {
            System.out.println(testRepository.getClass());
        }
    }

    위와 같이 TestComponent와 TestRepository가 Bean으로 등록이 되었을 때 스프링 IoC 컨테이너가 의존 관계 주입을 해줍니다. 생성자 주입은 setter, 필드 주입과는 다르게 TestComponent 객체가 생길 때 TestRepository와의 의존 관계 주입이 동시에 발생합니다.

     

    TestComponent가 Bean 으로 등록되면 싱글톤으로 관리되기 때문에 생성자는 단 한번만 호출된다는 것을 알 수 있습니다. (불변을 보장할 수 있습니다.) 그리고 필수 의존 관계일 때 사용할 수도 있습니다. 예를들어 필드에 final 키워드를 사용하면 생성자에서 반드시 초기화를 시켜줘야 합니다. (final로 바꾸면 생성자 외에서 다른 곳에서 바꿀 수 없기 때문에 불변도 유지할 수 있습니다.)



    setter 주입

    @Component
    public class TestComponent {
    
        private TestRepository testRepository;
    
        @Autowired
        public void setTestRepository(TestRepository testRepository) {
            this.testRepository = testRepository;
        }
    }
    • setter라 불리는 필드의 값을 변경하는 수정자 메소드를 통해서 의존관계를 주입하는 방법입니다.
    • 선택, 변경 가능성이 있는 의존관계에 사용합니다.(클라이언트에게 setter를 노출하기 때문에 불변을 유지하지 못해 약간의 불안함이 존재합니다.)



    필드 주입

    • 이름 그대로 필드에 바로 주입하는 방법입니다.
    • 코드가 간결해서 많은 개발자들이 편하게 사용할 수는 있지만 변경이 불가능해서 테스트 하기 힘들다면 치명적인 단점이 있습니다.
    • DI 프레임워크가 없다면 아무 것도 할 수 없습니다.
    • 애플리케이션과 관련 없는 테스트 코드, 스프링 설정을 목적으로 하는 @Configuration 같은 곳이 아니라면 사용하지 말자.

     

    스크린샷 2021-04-24 오후 5 47 17

    그리고 필드 주입은 스프링에서 권장하고 있지 않는 방법입니다.



    필드 주입을 지양해야 하는 이유는 무엇일까?

    너무나 대충 만든 예제 코드이지만 지양해야 하는 이유를 코드를 보면서 알아보겠습니다.

    @Component
    public class TestComponent {
    
        @Autowired
        private TestRepository testRepository;
    
        public void createTest() {
            Test test = testRepository.findByTest(1);
    
        }
    }
    public class Test {
        private String name;
        private String address;
    
        public Test(String name, String address) {
            this.name = name;
            this.address = address;
        }
    }
    @Repository
    public class TestRepository {
    
        public Test findByTest(int testId) {
            return new Test("Gyunny", "Korea");
        }
    }

    위와 같이 아주 간단한 코드가 있습니다. 이 코드에 대해서 테스트 코드를 작성해보면 아래와 같습니다.

     

    import org.junit.jupiter.api.Test;
    
    class TestComponentTest {
    
        @Test
        public void fieldInjectionTest() {
            TestComponent testComponent = new TestComponent();
            testComponent.createTest();
        }
    }

    여기서는 TestComponent을 직접 new를 사용해서 객체를 만들었습니다. 이렇게 직접 객체를 만들면 @Autowired가 적용이 되지 않기 때문에 TestComponent의 TestRepository는 주입을 받지 못하게 됩니다.

     

    그리고 testRepository.createTest()를 하면 어떻게 될까요? TestRepository가 주입을 받지 못했기 때문에 당연히 testRepository.findByTest(1); 이 부분에서 NullPointerException이 발생하게 됩니다. 그래서 NullPointerException의 발생을 막고자 setter를 만들 수 밖에 없습니다. 그러면 그냥 setter 인젝션을 쓰는게 낫습니다.



    생성자 주입을 사용하는 것이 좋다.

    public class OrderServiceImpl implements OrderService {
    
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
    
        @Autowired
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository; 
        }
    
        @Autowired
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy; 
        }
    }

    위와 같이 setter 인젝션을 사용한 후에 테스트 코드를 작성해보겠습니다.

     

    @Test
    void createOrder() {
       OrderServiceImpl orderService = new OrderServiceImpl();
       orderService.createOrder(1L, "itemA", 10000); 
    }

    위의 테스트를 실행하면 어떻게 될까요? 위에서 계속 말했던 것처럼 NullPointerException이 발생합니다. 위의 코드로만 봐서는 해당 테스트가 제대로 작동하는지 안하는지 파악하는데 한계가 있습니다. OrderServiceImpl 내부 코드를 봐야 알 수 있습니다. 즉, setter 주입은 이러한 단점을 가지고 있기에, 생성자 주입을 사용해야 합니다.

     

    생성자 주입을 사용하면 어떤 이점이 있을까요?

    @Component
    public class OrderServiceImpl implements OrderService {
    
        private final MemberRepository memberRepository;
        private final DiscountPolicy discountPolicy;
    
        @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
            this.memberRepository = memberRepository; 
        }
    }

    생성자를 이용하면 OrderServiceImpl 객체를 만들 때 어떤 객체가 반드시 필요한지 알 수 있기 때문에 NullPointerException을 예방할 수 있습니다.

    반응형

    댓글

Designed by Tistory.