작성하게 된 이유 :: DI의 3가지 주입 중에서 공부를 하다가 의문점이 들어서 정리하게 되었다.
본인의 프로젝트에서는 DI의 주입 중 Field 주입을 자주 사용했는데, 어째서DI에서는 필드 주입보다 생성자 주입을 자주 사용하며, 어째서 Field 주입을 사용할 수 있는데 어째서 생성자 주입을 주로 사용하는가?
1. 각 주입에 대한 코드 및 설명
2. field 주입보다 setter주입이 더 안전한지 갖게된 의문에 대한 해결 정리
1. 각 주입에 대한 코드 및 설명
1 - 1). field 주입
@Controller // @Controller : @Component의 확장형이며, @Controller 어노테이션을 사용해서 Spring이 자동으로 빈을 생성
public class FieldClass { // 클래스의 이름이기 때문에 class 키워드가 들어감
@Autowired
private AService aService; // 필드 주입을 통한 service 의존성 주입
@PostMapping("/list") // POST 요청을 처리
public List<Map<String, Object>> exList(String abc) { // abc 파라미터를 직접 받음
List<Map<String, Object>> exList = new ArrayList<>(); // 리스트 생성
Map<String, Object> insideMap = aService.findID(abc); // service 메소드 실행
exList.add(insideMap); // 결과를 리스트에 추가
return exList;
/*
반환 타입이 String이 아니고 List<Map<String, Object>>이므로 JSON 응답이 반환됨.
따라서 ViewResolver를 거치지 않고, Spring의 HttpMessageConverter를 통해 JSON 변환 후 클라이언트에 전달됨.
*/
}
}
1 - 2). setter 주입
@Controller // @Controller : @Component의 확장형이며, @Controller 어노테이션을 사용해서 Spring이 자동으로 빈을 생성
public class FieldClass { // 클래스의 이름이기 때문에 class 키워드가 들어감
private AService aService; // Service 의존성 주입
@Autowired
public void setAService(AService aService) { // Setter 주입 , 값을 주입하는 setter이기 때문에 return타입이 void이다.
this.aService = aService;
}
@PostMapping("/list") // POST 요청을 처리
public List<Map<String, Object>> exList(@RequestParam("abc") String abc) { // @RequestParam을 통해 abc 파라미터를 요청에서 받아옴
List<Map<String, Object>> exList = new ArrayList<>(); // 리스트 생성
Map<String, Object> insideMap = aService.findID(abc); // service 메소드 실행
exList.add(insideMap); // 결과를 리스트에 추가
return exList;
/*
ViewResolver는 반환 타입이 String일 때 JSP를 찾아 렌더링하지만,
현재 반환 타입이 List<Map<String, Object>>이므로 ViewResolver를 거치지 않고 JSON 응답이 됨.
즉, @RequestParam 때문이 아니라 반환 타입이 객체 타입이기 때문에 JSON 변환됨.
*/
}
}
1 - 3). 생성자 주입
@Controller // @Controller : @Component의 확장형이며, @Controller 어노테이션을 사용해서 Spring이 자동으로 빈을 생성
public class FieldClass { // 클래스의 이름이기 때문에 class 키워드가 들어감
private AService aService; // Service 의존성 주입
@Autowired
public FieldClass(AService aService){ // 생성자 주입 방식 생성자에는 class 들어가지 않음
this.aService = aService;
}
@PostMapping("/list") // POST 요청을 처리
public List<Map<String, Object>> exList(@RequestParam("abc") String abc) {
// @RequestParam을 통해 abc 파라미터를 요청에서 받아옴
List<Map<String, Object>> exList = new ArrayList<>(); // 리스트 생성
Map<String, Object> insideMap = aService.findID(abc); // service 메소드 실행
exList.add(insideMap); // 결과를 리스트에 추가
return exList;
/*
ViewResolver는 반환 타입이 String일 때 JSP를 찾아 렌더링하지만,
현재 반환 타입이 List<Map<String, Object>>이므로 ViewResolver를 거치지 않고 JSON 응답이 됨.
즉, @RequestParam 때문이 아니라 반환 타입이 객체 타입이기 때문에 JSON 변환됨.
*/
}
}
한 눈에 보기 쉽게 최종 정리 (필드 주입 vs Setter 주입 vs 생성자 주입)
| 서비스 주입 방식 | @Autowired 필드 선언 | @Autowired Setter 메서드 사용 | @Autowired 생성자 사용 |
| 코드 예시 | @Autowired private AService aService; |
@Autowired public void setAService(AService aService) { this.aService = aService; } |
@Autowired public FieldClass(AService aService) { this.aService = aService; } |
| 객체 변경 가능 여부 | ✔️ 가능 (리플렉션 사용시 final 키워드를 사용할 수 없기에 private 필드에 직접 주입되므로 변경이 쉬움) |
✔️ 가능 (Setter 메서드가 있어 외부에서 변경 가능) |
❌불가능 (final 키워드 사용 가능, 불변성 유지) |
| 테스트 용이성 | 낮음 (의존성 주입을 직접 할 수 없어 테스트 어려움) |
중간 (Setter를 통해 Mock 객체 주입 가능) |
높음 (생성자 주입이므로 명확한 객체 주입 가능) |
| 권장 여부 | 비추천 | 가능하지만 권장되지 않음 | 가장 권장됨 |
| Spring 공식 권장 | 비권장 | 비권장 | 권장 |
2. field 주입보다 setter주입이 더 안전한지 갖게된 의문에 대한 해결 정리
위의 코드를 직접 작성하면서 생성자 주입에서는 final이 가능한데 field주입에서 field에 왜 final을 사용할 수 없는지가 너무 궁금하였다.
구글 검색을 해도 잘 나오지 않았으며, 내가 모르는 것이 무언가 있는데 무엇인지 알 수 없어서 너무 답답하였다.
=> 의문에 대한 정리
○ 생성자 주입의 특징
○ setter 주입이 field 주입 보다 더 안전한 이유
○ 생성자 주입의 특징
1. 불변성 보장 : 의존 객체를 생성 시점에 주입 받기 때문에 해당 의존성이 변경되지 않고 항상 초기화된 상태가 보장됨.
2. 테스트 용이 : 필수 의존성이 생성자 매개변수로 명시되어, 테스트 시에 모의 객체를 주입하기 쉽다.
3. 순환 의존성 감지 : 순환 의존성이 발생하면 컨테이너에서 오류를 감지
* 순환 의존성 : 두 개 이상의 Bean이 서로를 의존하고 있어, 스프링이 주입을 완료할 수 없는 상태
예시 A와B가 존재시, 서로가 서로를 무한 루프 처럼 의존하고 있어 Bean을 생성할 수 없음
예시 코드 ::
@Controller // @Controller : @Component의 확장형이며, @Controller 어노테이션을 사용해서 Spring이 자동으로 빈을 생성
public class AService {
private final BService bService;
@Autowired
public AService(BService bService) {
this.bService = bService;
}
}
@Controller // @Controller : @Component의 확장형이며, @Controller 어노테이션을 사용해서 Spring이 자동으로 빈을 생성
public class BService {
private final AService aService;
@Autowired
public BService(AService aService) {
this.aService = aService;
}
}
*스프링 컨테이너 : 클래스의 객체를 주입받아 사용할 수 있는 컨테이너
개발자가 객체 생성,설정,관리에 관여하지 않으며 객체를 직접 생성, 설정, 관리하며 의존성을 주입한다.
이로 인해서 개발자는 스프링에서 제공하는 객체를 받아와서 사용하여, 개발에 집중할 수 있음
* 제어의 역전
개발자가 객체 생성,설정,관리에 관여하지 않으며 객체를 직접 생성, 설정, 관리하며 의존성을 주입하는 것 .
○ setter 주입이 field 주입 보다 더 안전한 이유
대부분의 의존성 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된다.
이때, 생성자 주입은 setter주입, field 주입과 다르게 불변성을 보장하여, 변하질 않기 때문에 spring에서 공식적으로 권장되는 것이였다.
=> 그렇다면 왜 field 주입에는 final을 적용하여 불변성을 보장할 수 없는가???
예시 : 생성자 주입
// 해당 코드
public class MyClass {
private final Dependency dependency;
// 생성자에서 final 필드 초기화
public MyClass(Dependency dependency) {
this.dependency = dependency;
}
}
// 해당 코드의 순서
[객체 생성 요청]
│
▼
[생성자 호출 → dependency 초기화 (final 필드 할당)]
│
▼
[객체 생성 완료]
위의 코드와 순서를 보고 알 수 있는 것은 생성자 호출 후 의존 주입 후 해당 필드를 final을 통해 불변함을 갖게 하는 것을 볼 수 있다.
예시 : field 주입
// 해당 코드
public class MyClass {
@Autowired
private Dependency dependency; // final 없이 선언됨
}
// 해당 코드 순서
[객체 생성 요청]
│
▼
[생성자 호출 → dependency 아직 초기화되지 않음]
│
▼
[스프링 컨테이너가 리플렉션을 통해 dependency 주입]
│
▼
[의존성 주입 완료 후 객체 사용]
해당 코드를 보면 field 주입에서 생성자 호출을 한 뒤 리플렉션을 통해 dependency 주입하는 것을 보고 의존이 주입되는 것을 볼 수 있다.
그러나 만약 final이 선언 되어있다고 가정
// 해당 코드
public class MyClass {
@Autowired
private final Dependency dependency; // final 있이 선언됨
}
위의 2번째 순서 후 3번째 [스프링 컨테이너가 리플렉션을 통해 dependency 주입] 순서에서 final의 특징인 "초기값 설정 및 할당 후 변경이 불가능하다" 로 인하여 리플렉션을 통한 의존성 주입이 불가능해진다.
=> 따라서 의존성을 주입할 필드에 final을 선언한다면 리플렉션을 통한 의존성 주입이 불가능해진다.
* 리플렉션 : 실행 시점에 클래스 내부의 private 필드나 메서드에 접근할 수 있는 기능
'스프링' 카테고리의 다른 글
| [SpringBoot] IntelliJ를 통한 스프링 설치 (0) | 2025.05.13 |
|---|---|
| [Spring]RESTful 환경에서 Controller과 RestController (0) | 2025.05.04 |
| [Spring] ctrl + space 작동되지 않을 때 (0) | 2025.02.17 |
| [Spring] file 업로드 시에 발생하는 오류 (0) | 2025.02.16 |
| [Spring] Logger 오류 시도 (0) | 2025.02.16 |