생성자 주입이 권장되는 이유를 나름대로 정리해보기

생성자 주입 및 필드 주입


  • 최근? 생성자 주입이 권장되는 이유가 제대로 이해할 수 있었던 생각이 들기 때문에 자신의 생각·이해를 정리해 두려고 생각해, 이 기사를 쓰기로 했습니다.
  • 다음의 코드는 SpringBoot, Java, JPA를 사용해 구현하고 있는 것을 상정하고 있습니다.

  • 현장 주입의 장점과 단점



  • 장점
  • 필드 인젝션의 경우는 필드 위에 @Autowired 를 붙이는 것만이므로 인젝트 하는 것이 편하다.
  • 인젝트 하는 것이 증가해도 필드를 추가해 @Autowired 를 붙이는 것만으로 끝난다.


  • 단점
  • 테스트가 힘들어진다. (후술)


  • 필드 인젝션에서의 구현 예


    @Service
    public class BookService{
    
        @Autowired
        private BookDao bookDao;
    
        @Autowired
        private HogeDao hogeDao;
    
        /**
         * 引数のbookIdと一致したBookを返す
         */
        public Book selectById(int bookId){
            Optional<Book> wrappedBook = bookDao.findById(bookId);
            return wrappedBook.orElseThrow(() -> NotFoundException("無い"));
        }
                              .
                              .
                              .
    }
    

    생성자 주입의 장점과 단점



  • 장점
  • 기술량이 증가한다고 하는 단점은 있습니다만, 필드를 Immutable로 할 수 있다.
  • 테스트가 쉬워진다. (후술)


  • 단점
  • 생성자 인젝션의 경우 필드 인젝션보다 설명량이 증가합니다.
  • 인젝트하는 것이 늘어났을 때, 필드 인젝션과 비교하면 변경을 가하는 것이 조금 번거롭다.


  • 생성자 주입에서의 구현 예


    @Service
    public class BookService{
        private final BookDao bookDao;
        private final HogeDao hogeDao;
    
        @Autowired // → Spring4.3以上ならこのアノテーション省略可
        public BookService(BookDao bookDao, HogeDao hogeDao){
            this.bookDao = bookdao;
            this.hogeDao = hogedao;
        }
    
        /**
         * 引数のbookIdと一致したBookを返す
         */
        public Book selectById(int bookId){
            Optional<Book> wrappedBook = bookDao.findById(bookId);
            return wrappedBook.orElseThrow(() -> NotFoundException("無い"));
        }
                              .
                              .
                              .
    }
    

    생성자 주입으로 테스터빌리티 향상


  • 앞서 언급했듯이 생성자 주입을 사용하면 테스트가 더 쉬워집니다.

  • 필드 인젝션으로 구현된 클래스를 테스트하는 경우


  • @Autowired 로 인젝트 된 필드는 SpringBoot 가 제공하고 있는 @MockBean 를 사용하는 것으로 모의 수 있습니다.
  • SpringBoot가 제공하는 어노테이션을 사용하기 때문에 @ExtendWith(SpringExtension.class)@SpringBootTest가 필요합니다.
  • @ExtendWith(SpringExtension.class)
    @SpringBootTest
    public class BookServiceTests{
    
        @Autowired    
        private BookService bookService;
    
        @MockBean
        private BookDao bookDao;
    
        @MockBean
        private HogeDao hogeDao;
    
    
        @Test
        public void 指定したbookIdの本を取得できること(){
            // setUp
            var expected = new Book(1, "本の名前"); 
            when(bookDao.findById(1)).thenReturn(expected);
            var bookService = new BookService();
    
            // execute
            var actual = bookService.selectById(1);
    
            // verify
            assertThat(...以下省略
        }
    }
    
  • 겉보기, @MockBean 를 붙이는 것만으로 끝나기 때문에 매우 편하게 보입니다만 SpringBootTest는 유닛 테스트에 비해서 꽤 시간이 걸립니다....
  • Failed to create application context 라는 오류가 발생합니다. (경험 완료)
  • application.yml(.properties) 에 기술하고 있는 DB 정보가 잘못되었을 경우에 일어나거나.

  • DB가 움직이지 않으면 원래 테스트가 실행되지 않습니다.
  • DB가 움직이지 않는 것이 원인이 아니고 @Table 로 테이블이 없는 경우는 자동 생성하는 구현으로 하고 있는 것이 원인...?

  • DB와 관련이없는 테스트에서 "DB가 ..."라는 오류가 발생하여 스트레스가 굉장합니다.

    생성자 주입으로 구현된 클래스를 테스트하는 경우


  • @BeforeEach 메소드로 mock 할 필요가 있는 것이 조금 번거로울지도 모릅니다.
  • 필드 인젝션으로 구현된 테스트에는 @ExtendWith(SpringExtension.class)@SpringBootTest 가 필수적이었지만 여기에서는 붙일 필요가 없습니다.
  • 
    public class BookServiceTests{ 
        private BookService bookService;
        private BookDao bookDao;
        private HogeDao hogeDao;
    
        @BeforeEach
        public void setUp(){
            bookDao = Mockito.mock(BookDao.class);
            hogeDao = Mockito.mock(HogeDao.class);
            bookService = new BookService(bookDao, hogeDao);
        }
    
        @Test
        public void 指定したbookIdの本を取得できること(){
            // setUp
            var expected = new Book(1, "本の名前"); 
            when(bookDao.findById(1)).thenReturn(expected);
            var bookService = new BookService();
    
            // execute
            var actual = bookService.selectById(1);
    
            // verify
            assertThat(...以下省略
        }
    }
    
  • SpringBoot 관련 어노테이션을 부여하지 않았으므로 테스트가 원활하게 수행됩니다.
  • Failed to create application context 이나 「DB가...」라고 하는 에러도 괴롭히는 일이 없어져, 스트레스 없이 테스트를 할 수 있습니다
  • @BeforeEach 를 붙인 메소드로 스스로 mock 하는 것이 귀찮은 경우는 Mockito가 제공하고 있는 @Mock 를 부여하는 것으로 간단하게 mock 할 수 있습니다.
  • public class BookServiceTests{ 
        private BookService bookService;
    
        @Mock
        private BookDao bookDao;
        @Mock
        private HogeDao hogeDao;
    
        @BeforeEach
        public void setUp(){
            bookService = new BookService(bookDao, hogeDao);
        }
        // 以下省略
    
  • 주의점으로서는 @RunWith(MockitoJUnitRunner.class) 가 필요하게 되는 것입니다.

  • 참고


  • ぇtps://후 mbぇ-무 mbぇ. 이 m / ck bean-vs - ck / # st-와 ch-1
  • 좋은 웹페이지 즐겨찾기