의존성 주입에는 4가지의 방법이 존재한다.
- 필드 주입(Field Injection)
- 수정자 주입(Setter Injection)
- 생성자 주입(Constructor Injection)
- 일반 메서드 주입(Method Injection)
필드 주입(Field Injection)
필드에 @Autowired
를 붙여 바로 주입하는 방법
특징
- 코드가 간결해진다.
- 그러나 외부에서 변경이 불가능 하여 테스트하기 어렵다.
- DI 프레임워크 없이 아무것도 할 수 없다.
- 애플리케이션의 실제 코드와 상관없는 특정 테스트를 하고싶을 때 사용
- 정상적으로 작동되게 하려면 결국 setter가 필요
- @Autowired(required=false) 옵션 처리를 통해 의존관계가 필수가 아님을 명시할 수 있음
@Service
public class DashboardService {
@Autowired
private final MemberRepository memberRepository;
@Autowired
private final MenuRepository menuRepository;
}
수정자 주입(Setter Injection)
setter를 통해 의존 관계를 주입하는 방법. @Autowired가 있는 setter
에 자동으로 의존관계를 주입한다.
특징
- 선택과 변경 가능성이 있는 의존 관계에 사용
- 자바 빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법
- @Autowired를 입력하지 않으면 실행이 되지 않음
- 생성자 호출 이후 필드 변수에 변경이 일어나야 하므로 final 제어자를 붙일 수 없음
@Service
public class DashboardService {
private MemberRepository memberRepository;
private MenuRepository menuRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setMemberRepository(MenuRepository menuRepository) {
this.menuRepository = menuRepository;
}
}
생성자 주입(Constructor Injection)
생성자를 통해 의존관계를 주입받는 방법으로, 생성자에 @Autowired
어노테이션을 작성해 주입한다.
특징
- 생성자 호출 시점에 1번만 호출되는 것을 보장
- 불변과 필수 의존 관계에 사용
- 생성자가 1개만 존재하는 경우 @Autowired를 생략해도 자동 주입 가능
- NPE(NullPointerException)를 방지할 수 있음
- 주입받을 변수를 final로 선언할 수 있음
@Service
public class DashboardService {
private final MemberRepository memberRepository;
private final MenuRepository menuRepository;
@Autowired
public DashboardService(MemberRepository memberRepository, MenuRepository menuRepository) {
this.memberRepository = memberRepository;
this.menuRepository = menuRepository;
}
}
일반 메서드 주입(Method Injection)
@Autowired 어노테이션은 모든 메서드에서 사용할 수 있기에 일반 메서드를 통해 의존관계를 주입하는 방법이다.
특징
- 한 번에 여러 필드를 주입받을 수 있다.
- 일반적으로 사용하지는 않는다.
@Service
public class DashboardService {
private MemberRepository memberRepository;
private MenuRepository menuRepository;
@Autowired
public void method(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
가장 좋은 방식이 무엇일까?
위의 코드를 읽어봤다면, 가장 간단하고 편해보이는것이 바로 필드 주입인데 왜 사용하지 않을까? 그 이유로는 아래 2가지로 정리할 수 있다.
- NPE(Null Point Exception) 발생 가능성
- 테스트 코드 작성의 어려움
NPE(Null Point Exception) 발생 가능성
public class MyComponent{
@Autowired
MyDependency myDependency;
public void doSth(){
myDependency.doSth();
}
}
public class TestFieldNPE{
MyComponent myComponent = new MyComponent();
myComponent.doSth(); // -> Null Point Exception 발생
}
위의 코드를 들여다보면, NPE가 발생하기 쉬운 구조인 것을 알 수 있다. 심지어 해당 에러는 컴파일시가 아닌 런타임에러로만 잡을 수 있기 때문에 귀찮아진다.
테스트 코드 작성의 어려움
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
public String doSomething() {
return memberRepository.doSomethingElse();
}
}
public class MemberServiceTest {
private MemberService memberService;
@Test
public void testDoSomething() {
String greeting = "Hello";
/*
memberRepository를 주입해줄 수 있는 방법이 없지만 귀찮다
mock해서, set하는 것
memberService = new MemberService();
MemberRepository memberRepository = mock(MemberRepository.class);
Field field = MemberService.class.getDeclaredField("memberRepository");
field.setAccessible(true);
field.set(memberService, memberRepository);
*/
String actual = memberService.doSomething(); // error 발생
assertEquals(greeting, actual);
}
}
이에 반해 생성자 방식을 이용하면, DI 프레임워크에 의존하지 않고 순수 자바 언어로도 잘 작동하므로 테스트 코드에서도 DI를 해줄 수 있다.
@Service
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public String doSomething() {
return memberRepository.doSomethingElse();
}
}
public class MemberServiceTest {
// 생성자를 이용해 바로 DI 가능
private MemberService memberService = new MemberService(new MemberRepository());
@Test
public void testDoSomething() {
String greeting = "Hello";
String actual = memberService.doSomething();
assertEquals(greeting, actual);
}
}
추가로 생성자 주입을 사용하는경우 final 키워드를 사용 가능하므로 필드에 final 키워드를 사용할 수 있다. 고로 생성자에서 값이 설정되지 않으면 컴파일 시점 오류를 확인할 수 있다.
java: variable (데이터 이름) might not have been initialized
또, 개발시 여러 컴포넌트간에 의존성이 생기기 마련이다. 필드 주입과 수정자 주입은 빈이 생성된 후에 참조를 하므로 애플리케이션이 어떤 오류와 경고 없이 구동된다. 따라서 런타임시점까지 문제를 알 수 없다.
그러나 생성자 주입시 BeanCurrentlyInCreationException
이 발생하여 문제를 알 수 있다.
정리
대부분의 의존 관계를 애플리케이션 종료까지 변할 일이 거의 없는 경우가 많다. 생성자 주입은 불변으로 설계할 수 있고, 생성자의 파라미터중 하나라도 누락된 경우 컴파일 예외가 발생하므로 문제점을 바로 발견할 수 있다.
DI 프레임워크에 의존없이 순수 자바 언어로도 잘 작동하기 때문에 테스트 코드 작성에도 용이하다.
생성자 주입 방식을 사용하자!
https://engineerinsight.tistory.com/46
https://ittrue.tistory.com/227
https://marklee1117.tistory.com/118
'Backend > Spring' 카테고리의 다른 글
[Spring] Spring AOP란? Spring AOP는 CTW, PCW, LTW, RTW 중에 무엇일까? (1) | 2024.01.03 |
---|---|
[Spring] 리플렉션(Reflaction)이란? 생성자 주입은 빈 생성때의 리플렉션 외에 추가적인 리플렉션을 진행하는가? (0) | 2024.01.02 |
Configuration은 어떻게 Bean을 등록하고 관리할까? (0) | 2023.12.28 |
[Spring] 스프링 입문 - 코드로 배우는 스프링 부트 #4 스프링 빈과 의존관계 (0) | 2023.11.21 |
[Spring] 스프링 입문 - 코드로 배우는 스프링 부트 #3 회원 관리 예제(백엔드) (0) | 2023.09.12 |