본문 바로가기
Spring/Spring MVC 5

생성자 vs setter vs field 의존성 주입

코동이 2022. 7. 15.

개요


DI를 하는 방법은 생성자 주입과 setter 주입이 있습니다. 2개를 코드로 비교하게 된 계기는, 스프링 공식문서에서 순화참조를 해결하기 위해서는 setter 주입을 사용하라고 했던 내용입니다. 생성자 주입이 훨씬 좋다고 평가되는데, 왜 setter 주입을 사용해서 해결하라고 했을까요? 실제 코드를 통해서 의존성 주입 방식에 따라 빈이 어떤순서로 생성되는지 비교해보겠습니다.

 

 

주입 방식에 따른 호출 순서


setter주입, 필드 주입, 생성자 주입에 따라서 매개변수가 있는 생성자, 매개변수가 없는 생성자, setter 함수 등이 어떤 순서로 실행된는지 알아보겠습니다.

 

 @Component로는 DogsDao, DogsService, DogsController 총 3개를 사용하겠습니다.

 

 

setter 주입


 DogsService는 DogsDao를, DogsController는 DogsService에 의존성을 가지고 있으며 의존성 주입을 set메서드로 해보겠습니다. setter 주입의 경우 매개변수가 없는 생성자를 같이 확인하면 이해가 더 빠릅니다.

 

@Component
public class DogsDao {
    public DogsDao(){
        System.out.println("DogsDao no-arg constructor called");
    }
    public List<Dog> getAllDogs() {
        System.out.println("DogsDao.getAllDogs called");
        return null;
    }
}

 

DogsDao입니다. 어떠한 의존성 주입도 필요하지 않습니다.

 

 

@Component
public class DogsService {
    private DogsDao dao;

    public List<Dog> getDogs() {
        System.out.println("DogsService.getDogs called");
        return dao.getAllDogs();
    }

    @Autowired
    public void setDao(DogsDao dao) {
        System.out.println("DogsService setter called");
        this.dao = dao;
    }

    public DogsService(){
        System.out.println("DogsService no-arg constructor called");
    }
    
    public DogsService(DogsDao dao) {
        System.out.println("DogsService arg constructor called");
        this.dao = dao;
    }
}

 

DogsService입니다. DogsDao에 의존성을 가지며, setDao() 메서드를 통해서 주입을 합니다. 매개변수가 없는 생성자도 만들었습니다. 또한 매개변수가 있는 생성자도 있습니다.

 

 

@RestController
@RequestMapping("/dogs")
public class DogsController {
    private DogsService service;

    @GetMapping
    public List<Dog> getDogs() {
        return service.getDogs();
    }

    @Autowired
    public void setService(DogsService service) {
        System.out.println("DogsController setter called");
        this.service = service;
    }
}

 

마지막으로 DogsController입니다. DogsService에 의존성을 가지며 setService() 메서드를 통해서 의존성을 주입합니다. 스프링 어플리케이션을 시작해서 어떻게 로그가 찍히는지 확인해보겠습니다.

 

 

...
DogsDao no-arg constructor called
DogsService no-arg constructor called
DogsService setter called
DogsController no-arg constructor called
DogsController setter called
...

 

 Controller가 Service에 의존하고, Service가 Dao를 의존하고 있으므로 Dao -> Service -> Controller 순서대로 빈의 의존성이 주입됩니다. 특히 setter의 주입은 매개변수가 있는 생성자를 전혀 호출하지 않습니다. 또한, 먼저 매개변수가 없는 생성자를 호출하고, 이후에 setter 메서드를 호출합니다. 

 

 

Field 주입


@Component
public class DogsService {
    @Autowired
    private DogsDao dao;

    public List<Dog> getDogs() {
        System.out.println("DogsService.getDogs called");
        return dao.getAllDogs();
    }

    public void setDao(DogsDao dao) {
        System.out.println("DogsService setter called");
        this.dao = dao;
    }

    public DogsService(){
        System.out.println("DogsService no-arg constructor called");
    }

    public DogsService(DogsDao dao) {
        System.out.println("DogsService arg constructor called");
        this.dao = dao;
    }
}

 

 

@RestController
@RequestMapping("/dogs")
public class DogsController {
    @Autowired
    private DogsService service;

    @GetMapping
    public List<Dog> getDogs() {
        return service.getDogs();
    }

    public void setService(DogsService service) {
        System.out.println("DogsController setter called");
        this.service = service;
    }

    public DogsController(){
        System.out.println("DogsController no-arg constructor called");
    }

    public DogsController(DogsService service) {
        System.out.println("DogsController arg constructor called");
        this.service = service;
    }
}

 

@Autowired로 필드 주입을 합니다. Controller, Service, Dao 호출 순서를 확인해보겠습니다

 

...
DogsDao no-arg constructor called
DogsService no-arg constructor called
DogsController no-arg constructor called
...

 

setter 주입과 마찬가지로 DogsDao, DogsService, DogsController 가 순서대로 주입됩니다.  하지만 매개변수가 있는 생성자나 혹은 setter 메서드는 전혀 호출되지 않습니다. 단지 매개변수가 없는 생성자만 호출됩니다.

 

 

 

생성자 주입


@Component
public class DogsService {
    private DogsDao dao;

    public List<Dog> getDogs() {
        System.out.println("DogsService.getDogs called");
        return dao.getAllDogs();
    }

    public void setDao(DogsDao dao) {
        System.out.println("DogsService setter called");
        this.dao = dao;
    }

    public DogsService(){
        System.out.println("DogsService no-arg constructor called");
    }

    @Autowired
    public DogsService(DogsDao dao) {
        System.out.println("DogsService arg constructor called");
        this.dao = dao;
    }
}

 

@RestController
@RequestMapping("/dogs")
public class DogsController {
    private DogsService service;

    @GetMapping
    public List<Dog> getDogs() {
        return service.getDogs();
    }

    public void setService(DogsService service) {
        System.out.println("DogsController setter called");
        this.service = service;
    }

    public DogsController(){
        System.out.println("DogsController no-arg constructor called");
    }

    @Autowired
    public DogsController(DogsService service) {
        System.out.println("DogsController arg constructor called");
        this.service = service;
    }
}

 

생성자 주입을 위해서 생성자에 @Autowired를 설정했습니다.

 

...
DogsDao no-arg constructor called
DogsService arg constructor called
DogsController arg constructor called
...

 

DogsDao는 외부에서 주입될 의존성이 없기 때문에 매개변수가 없는 생성자가 호출됩니다. 하지만 DogsService와 DogsController는 의존성 주입이 있기 때문에 매개변수가 있는 생성자가 호출됩니다.

 

 

정리하자면 다음과 같습니다.

 

  setter field constructor
콘솔 로그 ...
DogsDao no-arg constructor

DogsService no-arg constructor

DogsService setter

DogsController no-arg constructor

DogsController setter 
...
...
DogsDao no-arg constructor

DogsService no-arg constructor

DogsController no-arg constructor
...
...
DogsDao no-arg constructor

DogsService arg constructor

DogsController arg constructor
...
매개변수 있는 생성자 호출(1) X X O
매개변수 없는 생성자 호출(2) O O Δ
setter 메서드 호출(3) O X X
호출 순서 2 -> 3 2 1 (1이 없으면 2)

 

(* 콘솔로그에 called 생략)

 

만약에 3개를 동시에 설정한다면, constructor -> field -> setter 순서로 적용됩니다.

 

스프링 5.X 버전 기준으로 공식문서에서는 생성자 주입을 사용 할 것을 권고합니다.

 

 

그 이유는 다음과 같습니다.

 

  • 순환참조 미리 예방
  • 불변성 보장
  • 의존성이 null 아님을 보장
  • 테스트의 편리함

 

1. 순환참조 미리 예방

 

* 순환 참조가 일어나는 경우는?

 

스프링 부트 2.6 릴리즈 기준으로 의존성 주입 방식에 따른 순환참조가 바꼈습니다. 2.6버전 이전에서는 생성자 주입의 경우만 스프링 컨테이너 로딩 시 예외를 발생 시켰습니다. setter주입과 필드 주입은 스프링 컨테이너 로딩 시, 순환참조가 발생하지 않고 실제로 해당 빈이 사용될 때 오류를 발생했습니다.

 

 

 

DogsController, DogsService, DogsDao 예시 코드에서 호출 순서를 다시 확인해보면 이유를 알 수 있습니다. 생성자 주입은 매개변수가 있는 생성자를 먼저 호출하고, 생성자의 매개변수에 있는 객체가 의존성 주입이 되어 있어야하기 때문에 서로 의존성 주입을 무한히 요청해서 순환참조 오류를 발생시킵니다.

 

하지만, setter 주입과 필드 주입은 매개변수가 있는 생성자를 호출하지도 않고, 협력하는 객체의 의존성 주입을 먼저 하지 않고 자신 객체의 의존성 주입을 먼저 합니다. 그 이후에 협려과는 객체들의 의존성 주입을 합니다.

 

 

2.6 버전 이후에서는 어떠한 경우에도 순환참조를 하고 있다면 스프링 컨테이너 로딩 시, 순환참조 오류를 발생시킵니다. 

 

 

 아래는 해당 변경에 릴리즈 내용입니다. 그럼에도 순환참조를 스프링 컨테이너 로딩 시, 발생하지 않도록 하기 위해서는 spring-main.allow-circular-referencestrue로 설정하거나, SpringApplicationSpringApplcationBuilder에 setter 메서드를 설정하면 됩니다.

 

순환참조 금지(기본)

 

 DI 글에도 설명이 있지만, 실제 운영중에 순환참조 문제가 발생하는 것보다, 스프링 컨테이너 로딩 시에 미리 오류를 잡아주는 것이 훨씬 관리가 편합니다. 물론, 모든 빈을 스프링 컨테이너 로딩 시 초기화하기 위한 시간비용과 메모리 비용이 들지만, 실제 운영 환경이 아닌 초기에 오류를 잡을 수 있기 때문입니다. 기본적으로 스프링은 빈을 컨테이너 생성 시, 바로 초기화하는 싱글톤 방식을 사용하고 있기 때문에, 개발자 입장에서는 어떠한 설정을 딱히 하지 않아도 됩니다. 

 

 

순환참조 오류는 다음과 같이 나옵니다. aService와 BService가 서로를 무한히 참조하고 있는 그림으로 표현됩니다.

 

 

이렇게, 스프링 컨테이너가 로딩하면서 오류를 발견하여 시작되지 않습니다. 

 

 

2. 불변성 보장

 

 객체의 불변성이란, 객체의 생성 이후에 변경되지 않는 것입니다. 이는 OOP의 중요한 요소입니다. 불변성은 쓰레드 안정성, 상태 안정성, 클래스에 가독성을 높여줍니다. 이는 생성자 주입을 통해서 보장받으며, setter 주입과 필드 주입에서는 보장되지 않습니다. 

 

@Service
public class Aservice {
	private final BService bService;

	public Aservice(BService bService) {
		this.bService = bService;
	}

	public void a() {
		bService.b();
	}
}

 

 생성자 주입의 경우, final을 설정하여 생성자로 한번 생성되면 이후에는 상태를 변경할 수 없도록 보장할 수 있습니다. 하지만 setter 주입의 경우 처음에 의존성이 주입 된 이후에도, 언제든지 setter로 객체의 상태를 변경할 수 있습니다. 이는 멀티 쓰레드 환경에서, 언제든지 객체의 상태를 변경시킬 수 있다는 점에서 쓰레드에 안전하지 않습니다.  

 

 

3. 의존성이 null 아님을 보장

 

 생성자 주입을 사용하면, 의존성이 null이 아님을 보장합니다. 즉, 의존하고 있는 객체가 무조건 의존성 주입 상태를 가지도록 설정한다는 뜻입니다. 따라서, 의존성이 없는 오류 상황을 신경쓰지 않아도 됩니다.

 

@Service
public class Aservice {
	private BService bService;

	@Autowired(required = false)
	public void setbService(BService bService) {
		this.bService = bService;
	}

	public void a() {
		bService.b();
	}
}

 

 하지만 setter 주입의 경우, @Autowired(required = false) 설정으로 의존성이 없더라도 일단 스프링 컨테이너를 오류없이 로딩 시킬 수 있습니다. 이는, setter 주입이 스프링 컨테이너가 생성된 이후에도 새로운 의존성 주입을 할 수 있다는 특징 때문에 가능합니다. 반대로 말하면, 의존성이 없는 상태로 객체가 호출될 수 있기 때문에 언제든지 NullPointException 예외를 발생할 수 있는 문제점이 있으므로 유의해서 사용해야 합니다. ( 생성자 주입에서는 매개변수에 @Nullable, @Autowired(required = false) 혹은 Optional을 사용하여 설정 할 수도 있습니다. )

 

 

4. 테스트의 편리함

 

생성자 의존성 주입은 불변성을 보장하고, 의존성이 null이 아님을 보장한다고 배웠습니다. 그렇다면, 생성자 의존성 주입으로 의존성이 주입되면, 모든 협력하는 객체들이 정상적으로 의존성이 주입되어, 유효성을 보장합니다. 만약에 setter 주입을 사용한다면, 의존성이 주입이 되지 않아 NullPointException이 발생할 수도 있습니다.

 

 

* 참고

 

https://reflectoring.io/constructor-injection/

https://yeonyeon.tistory.com/220

https://www.amitph.com/spring-constructor-injection-example/

https://www.amitph.com/spring-setter-injection-example/

https://www.amitph.com/spring-field-injection-example/

 

 

 

반응형

'Spring > Spring MVC 5' 카테고리의 다른 글

DispatcherServlet & ContextLoaderListener  (0) 2022.07.12
미니프로젝트  (0) 2020.05.29
Restful API  (0) 2020.05.28
MyBatis  (0) 2020.05.27
예외처리  (0) 2020.05.27