스프링

[Spring] DI의 3가지 Field주입, Setter주입, 생성자 주입

easy-6 2025. 3. 16. 18:56

작성하게 된 이유 ::  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 필드나 메서드에 접근할 수 있는 기능