매번 스팸으로 여겨왔던 Jet Brain의 메일.
무슨 바람이 불었는지 갑자기 이번 4월호는 읽어보고 싶었는데요,
이번 4월 Java Annotated Monthly 를 열어보니 흥미로운 컨텐츠가 정말 많았어요.
이번 포스팅은 그중 다음 영상을 보고 학습한 내용을 정리한 내용입니다.
추가적으로 영상 링크 하단의 포스팅도 참고했습니다.
Spring Constructor Injection: Why is it the recommended approach to Dependency Injection?
[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
1. 📘 용어 정리
본격적인 논의에 앞서 먼저 정리가 필요한 용어들을 짚어봅니다
Spring Bean
Bean is just a fancy word for an Object
클래스가 A가 B를 의존하고, A의 생성자에 B를 전달해야 한다면,
보통 코드는 new A(new B()) 와 같은 식으로 작성될 것입니다.
그런데 만약 애플리케이션의 규모가 커지면 어떨까요?
B가 또다른 클래스를 의존할 수도 있고 ( new A(new B(new C()))... )
이러한 의존관계를 개발자가 매번 직접 파악하고 전달하기 어려워질 것입니다.
이러한 문제를 Spring이라는 프레임워크가 해결해줄 수 있는데,
필요한 객체들을 Spring Bean으로 생성해두고 알아서 조립해주는 방식입니다.
즉 Spring Bean은 애플리케이션 동작에 필요한,
인스턴스화 되어 스프링이 관리해주는 객체라고 정리할 수 있습니다.
Application Context
A container that manages created Spring Beans
마치 Connection들이 Connection Pool에서 관리되듯이,
Spring Bean들은 Application Context에서 관리됩니다.
여기서 말하는 관리는 인스턴스화, 설정(configure), 조립(assemble)을 의미합니다.
또 앞서 Connection Pool을 비유했듯이, 우리가 필요로 하는 Spring Bean,
즉 이미 인스턴스화 되어 있는 객체를 Application Context에서 가져올 수 있습니다.
매번 새로 생성하지 않아도 되는 겁니다.
Dependency Injection & Inversion Of Control
제어 권한을 스프링 프레임워크에 위임함으로써 제어의 역전이 발생합니다
앞서 A가 B를 의존하는 경우에 대해 예를 든 바 있습니다.
new A(new B()) 와 같이 말이죠.
이를 스프링이 조립해주도록 위임하면 어떨까요?
1) Class A와 Class B를 Spring Bean으로 등록할게~
2) A는 B가 필요해~
라고 선언만 해두면, Application Context에서 A와 B 를 알아서 조립해주는 거죠.
그러면 개발자는 A가 필요한 곳에서 직접 생성할 필요 없이,
Application Context에 A 클래스를 달라고 요청만 하면 됩니다.
이렇게 관리 역할을 위임하면 매번 새로 생성하지 않고 재사용된다는 장점도 있습니다.
이렇게 애플리케이션이 구동되는 과정에서 필요한 객체들의 생성과 조립 등을
개발자가 하지 않고 프레임워크가 하도록 제어 권한이 역전되는 것을
제어의 역전, Inversion Of Control 이라고 합니다.
의존성 주입, 즉 객체의 조립(assemble)을 프레임워크가 대신 수행하는 거죠.
2. 여러 가지 의존성 주입 방법
생성자 주입, 필드 주입, setter주입에 대해 알아보고 장단점을 확인해보아요~
// 1. Constructor Injection with 'final' declaration
private final BoardDao boardDao;
public ChessServiceImpl(BoardDao boardDao) {
this.boardDao = boardDao;
}
// 2. Field Injection using Autowired Annotation
@Autowired
private BoardDao boardDao;
// 3. setter Injection with Autowired Annotation
private BoardDao boardDao;
@Autowired
public void setBoardDao(BoardDao boardDao) {
this.boardDao = boardDao;
}
위 코드는 순서대로 생성자 주입, 필드 주입, setter 주입의 예시 코드입니다.
ChessServiceImpl 클래스는 내부 로직 수행에 있어 BoardDao 를 필요로 합니다.
즉 BoardDao를 의존하고 있는데요,
ChessServiceImpl 내부에서 BoardDao를 new 생성자로 직접 생성하는 방법은
강한 결합이 발생하는, 명백하게 회피해야 하는 방법이라고만 논하고 넘어가겠습니다.
setter 주입을 먼저 보겠습니다.
객체가 생성된 이후에 setter를 통해 의존성을 주입하는 방식입니다.
따라서 객체의 상태값을 외부에서 변경할 수 있는 여지가 생겨 캡슐화가 깨집니다.
애플리케이션 동작 중에 상태값이 변경될 수 있어 신뢰할 수 없게 됩니다.
당연히 지양되어야 할 의존성 주입 방법입니다.
다음으로 필드 주입을 보겠습니다.
필드 주입을 사용할 경우 final 키워드 사용이 불가합니다.
객체 생성 시점이 아닌 생성 이후에 의존성을 주입하기 때문입니다.
필드 주입은 객체 생성 이후 reflection으로 우회해서 강제로 값을 할당하기 떄문입니다.
private 선언된 필드의 값을 강제로 변경하는 것은
캡슐화를 깨는 것이고, Java의 컨벤션을 우회하는 행위입니다.
인텔리제이어서도 필드 주입은 권장하지 않는다고 이야기해줍니다.
마지막으로 생성자 주입입니다.
생성자 주입을 사용하려면 필드에 final 선언을 하고, 이를 주입받는 생성자를 선언해야 합니다.
final 선언된 필드는 객체의 생성 시점에 값이 할당되어야 하기 때문입니다.
스프링 프레임워크는 ChessServiceImpl 클래스에 선언된 @Service 애너테이션을 읽어서
ChessServiceImpl을 Spring Bean으로 생성해서 Application Context에서 관리하고자 합니다.
이 때, 생성자에 있는 의존성 BoardDao를 확인하고,
Application Context내에 BoardDao에 맞는 Spring Bean을 찾아 내부적으로 조립해줍니다.
이 과정은 필드 주입과 달리 우리가 선언해둔 생성자를 이용하기 때문에,
reflection을 사용하지 않고 진행됩니다.
추가적으로 Autowired 애너테이션 선언을 하지 않아도 됩니다.
3. 필드 주입 🆚 생성자 주입 in 테스트 코드
생성자 주입을 선택하면, 테스트하기 더 용이한 설계가 가능해집니다
필드 주입을 사용할 경우,
생성자를 별도로 선언하지 않습니다.
따라서 매개변수를 받지 않는 기본 생성자 하나만 존재하게 됩니다.
그 결과 ChessServiceImpl 을 new 생성자로 생성할 때,
의존하고 있는 BoardDao를 전달할 방법이 없습니다.
개발자가 외부에서 의존성을 주입할 수 없게 되어 테스트가 어려운 구조가 됩니다.
반면 생성자 주입을 사용할 경우,
final 선언된 필드에 대한 주입을 new 생성자로 생성시에 전달해야만 합니다.
이 때 stub을 사용해 단위테스트를 위한 가짜 객체를 전달하여
로직이 의도대로 수행되는지 테스트가 가능한 구조가 됩니다.
물론 필드 주입의 경우에도 생성자를 선언하면,
생성자 주입의 경우와 마찬가지로 테스트 코드에서
생성자를 통한 의존성 주입이 가능해집니다.
그러나 그렇게 생성자를 선언할 거라면,
불필요한 애너테이션 지우고 final 선언의 장점도 가져가서
아예 생성자 주입을 사용하는게 맞지 않나 생각됩니다.
4. 🔎 Spring Bean 아이콘 살펴보기
귀여운 콩을 눌러봅시다!
스프링 프레임워크를 사용하다보면 코드 라인이 표시된 곳에
Spring Bean 아이콘이 생기기도 합니다.
사실 지금까지 전혀 관심을 갖지 않았는데,
의존성 주입을 학습하는 과정에서 아이콘에 마우스오버도 해보고 클릭도 해봤는데,
꽤 재밌는 기능이어서 적어봅니다.
Bean Dependencies
먼저 Bean Dependencies 입니다.
SpringBootApplication 애너테이션이 선언된 곳의 아이콘 중 하나를 눌러 진입 가능합니다.
Application Context에서 관리되는 Spring Bean들이 어떻게 생성되고 조립되는지 보여줍니다.
Spring에서는 DB접속 정보만 application.properties에 선언하면
JDBC Template이 자동으로 생성되어 사용될 수 있는데요,
DataSource가 어떻게 생성되고 JDBC Template과 조립되는지도 살펴볼 수 있었네요.
Navigate to the autowired dependencies
다음으로 재밌는 기능은 autowired 된 의존성을 찾아가는 기능입니다.
생성자 주입으로 BoardDao, GameDao 두 가지를 주입받도록 선언해둔 상태인데요,
각각을 클릭하면 실제 주입되는 구현체 클래스로 바로 이동됩니다.
애플리케이션이 비대해지면 어떤 Spring Bean이 주입되는지 확실하지 않을 때가 있을 것 같아요.
그때 유용하게 사용할 수 있을 것 같습니다.
어떤 것이 주입되는지 알더라도 해당 파일로 바로 찾아가는데 유용할 것 같고요.
5. 🆗 SpringBoot 통합 테스트시에는 필드 주입이 허용된다
아직 완전한 저의 지식은 아닙니다..
앞서 논한 단점에도 불구하고 @Autowired 를 이용한 필드 주입이 허용되는 때가 있습니다.
바로 SpringBootTest 애너테이션을 이용한 통합 테스트 코드를 작성할 때 입니다.
단위 테스트가 아닌 실제 스프링 기동 환경과 동일하게 Spring Bean들을 등록하고 테스트할 경우,
테스트 클래스의 생성자를 사용하지 않기 때문에 필드 주입을 통해 테스트를 수행하기도 합니다.
사실 이 부분은 학습 출처의 내용을 그대로 옮겨둔 것이라,
적으면서도 뭔가 찝찝하네요 ^^;;
하지만 이번 학습 범위를 넘어서는 것 같아 여기까지만 하겠습니다.
6. 🗜️ 요약
생성자 주입!
스프링 프레임워크는 개발자 대신 의존성 주입을 수행해준다.
개발자는 애플리케이션 구동에 필요한 객체들의 생성과 조립의 책임을 스프링에 위임한다.
이를 제어의 역전, Inversion Of Control, IoC 라고 한다.
스프링은 필요한 객체들을 Spring Bean으로 생성해 Application Context에서 조립, 관리한다.
스프링 프레임워크에게 의존성 주입을 요청하는 방법은 세 가지가 있다.
생성자 주입, 필드 주입, setter주입 등이다.
필드 주입은 객체를 생성한 이후에 reflection으로 우회해 값을 할당한다.
생성자 주입은 필드의 final 선언이 가능하고 애너테이션 선언이 불필요하며
테스트하기 용이한 구조를 가질 수 있게 해준다.
SpringBootTest 애너테이션을 통한 통합 테스트 시에는 필드 주입을 사용할 수 있다.
왜 필드 주입 보다 생성자 주입을 권장하는지 궁금했을 뿐인데,
정말 하나를 공부하려 하면 열의 무지가 발견되는 것 같습니다 ㅋㅋㅋ
참고 자료
Spring Constructor Injection: Why is it the recommended approach to Dependency Injection?
[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
'우아한테크코스 4기' 카테고리의 다른 글
우당탕탕 Repository 제작기 (feat. Reflection) (2) | 2022.05.06 |
---|---|
@Component 🆚 @Service 🆚 @ Repository (0) | 2022.04.23 |
Level 1을 정리하는 레벨 인터뷰 후기 (2) | 2022.04.22 |
다중 행 Insert 최적화 (Level 1 체스 미션 초기 체스판 구성) (2) | 2022.04.09 |
🤔 도메인(domain)은 무슨 뜻일까?! (2) | 2022.03.25 |