생성자 주입 final - saengseongja ju-ib final

[Spring, OOP] 생성자 주입이 좋은 이유와 스프링을 이용한 다양한 DI

  • 2021.02.10 22:45
  • Spring

생성자 주입 final - saengseongja ju-ib final

GOAL

  • 다양한 의존성 주입 방법을 알 수 있습니다.
  • 각 의존성 주입의 장단점을 비교할 수 있습니다.
  • 생성자 주입이 권장되는 이유를 알 수 있습니다.

실험을 위한 클래스 의존관계

생성자 주입 final - saengseongja ju-ib final

우선 의존관계 주입은 크게 3가지 방법으로 나눌 수 있습니다.

  • 수정자 주입(setter 주입)
  • 필드 주입
  • 생성자 주입

메소드 주입 방법은 수정자 주입과 비슷하니까 설명은 따로 생략했습니다.


수정자 주입(Setter)

@Service
public class OrderService {

  private DiscountPolicy discountPolicy;

  @Autowired
  public void setDiscountPolicy(DiscountPolicy discountPolicy) {
  	this.discountPolicy = discountPolicy;
  }
}

단점

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 거의 없습니다.
    하지만 setter는 언제든 변경되게 할 위험이 있습니다. 
  • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 합니다.
    즉 언제 어디서든 변경이 가능하다는 뜻입니다.

필드 주입

@Service
public class OrderService {

  @Autowired
  private DiscountPolicy discountPolicy;
}

단점

  • 코드가 간결하지만, 외부에서 변경하기 힘들다. 프레임워크에 의존적이고 객체지향적으로 좋지 않다.
  •  순수 자바 코드로 테스트를 작성하기 힘들다.

생성자 주입

@Service
public class OrderService {

  private final DiscountPolicy discountPolicy;
  
  // @Autowired // spring framework 4.3부터 생성자가 하나일 경우 생략 가능 
  public OrderService(DiscountPolicy discountPolicy) {
  	this.discountPolicy = discountPolicy;
  }
}

불변

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장됩니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없습니다.
    따라서 불변하게 설계할 수 있습니다.

생성자 주입을 사용하면 다음과 같이 의존성을 주입을 누락하는 것을 방지할 수 있습니다.

컴파일 오류로 누락을 방지할 수 있다.

@Test
void createOrder() {
  // 생성자에 값을 넣지 않을 경우
  OrderService orderService = new OrderService(); // 컴파일 오류 발생
}

final 키워드

생성자 주입을 활용할 경우 값의 불변을 보장할 수 있으므로 final을 활용할 수 있습니다. 

생성자를 통해 의존성을 설정하고 변경할 일이 없으므로 final으로 안전하게 불변을 보장할 수 있습니다.

setter방식으로는 final 키워드를 이용할 수 없습니다.
왜냐면 final은 assign reference를 변경할 수 없는데, setter는 assign reference를 변경할 수 있기 때문에 상충됩니다.

결국 생성자 주입을 이용하면 원하는 구현체를 주입할 수 있으며, 순수 자바 코드로 테스트를 실행할 수 있습니다.

여기서 착각할 수 있는것이, 의존 관계 주입은 스프링 공부랑은 관련이 없습니다

OOP랑 관련이 있는 것입니다. 흔히 생성자로 의존 관계를 주입하는 것이, 스프링이랑 관련지어서 얘기하는 경우가 있는데, 이는 잘못된 사실입니다. 

객체지향이랑 관련이 있는것입니다. 순수 객체지향적으로 의존 관계는 생성자를 통해서 주입하는 것이 좋습니다.

@Test
void creatOrder() {
//  OrderService orderService = new OrderService(new FixDiscountPolicy()); 
	OrderService orderService = new OrderService(new RateDiscountPolicy()); // 구현체 변경
}

정리

  • 생성자를 통해서 의존 관계를 주입하면, 프레임워크를 떠나서 순수 객체지향적으로 좋다.

References

스프링 핵심 원리

다양한 의존관계 주입 방법

의존관계 주입은 크게 4가지 방법이 있다.

1. 생정자 주입

2. 수정자 주입(setter 주입)

3. 필드 주입

4. 일반 메서드 주입

생성자 주입

  • 생성자 주입은 이름 그대로 생성자를 통해 의존관계를 주입받는 방법
  • 생성자 호출 시점에 딱 한 번만 호출되는 것이 보장된다
  • 주로 불변, 필수 의존 관계에 사용한다

불변 

처음에 세팅한 값을 변경하는 것을 허용하지 않는 것을 불변이라고 한다. 

필수

변수에 final 키워드를 적용하면 무조건 값이 초기화되어야 한다. 따라서 해당 필드가 초기화되어있지 않으면 컴파일 오류를 발생시킨다. 생성자로 해당 필드를 필수로 초기화해야 한다. 

@Component
public class PizzaService {
    
    private final PizzaRepository pizzaRepository;
    
    //@Autowired 를 보고 스프링 컨테이너에서 매개변수에 해당하는 스프링 빈을 꺼내 주입해준다.
    @Autowired
    public PizzaService(PizzaRepository pizzaRepository) {
        this.pizzaRepository = pizzaRepository;
    }
}

위와 같이 생성자에 @Autowired 어노테이션을 붙이면 스프링 컨테이너가 매개변수에 해당하는 스프링 빈을 꺼내 주입해준다. 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다.

수정자 주입(setter 주입)

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입하는 방법
  • 주로 선택, 변경 가능성이 있는 의존관계에서 사용한다
@Component
public class PizzaService {
    
    private PizzaRepository pizzaRepository;
    
    @Autowired
    public setPizzaRepository(PizzaRepository pizzaRepository) {
        this.pizzaRepository = pizzaRepository;
    }
    
}

위와 같이 setter에 @Autowired 어노테이션을 붙이면 스프링 컨테이너가 연관관계를 주입해준다. 

필드 주입

  • 이름 그대로 필드에 바로 주입하는 방법
  • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 힘들다
  • DI 프레임워크(스프링)가 없으면 아무것도 할 수 없다.
  • 애플리케이션의 실제 코드와 관련 없는 테스트 코드 등에서 사용된다
@Component
public class PizzaService {

	@Autowired	
	private PizzaRepository pizzaRepository;	
    
    ```
 
}

위와 같이 필드에서 바로 의존관계를 주입하는 방식이다. 위 방식은 순수한 자바 테스트 코드를 이용할 수 없고, 외부에서 변경도 불가능하므로 웬만해선 사용하지 않는 것이 좋다.

일반 메서드 주입

@Component
public class PizzaService {
    
    private PizzaRepository pizzaRepository;
    
    @Autowired
    public init(PizzaRepository pizzaRepository) {
        this.pizzaRepository = pizzaRepository;
    }
    
}

위와 같이 일반 메서드에서 의존관계를 주입하는 방식이다. 일반적으로 잘 사용하지 않는 방법이다.

어떤 방법을 사용해야 할까?

스프링 공식 문서에 따르면, 생성자 주입을 적극 권장하고 있다. 생성자 주입을 권장하는 이유는 다음과 같다.

불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 불변해야 한다.
  • 수정자 주입을 사용하면, setter를 public으로 열어둬야 한다.
  • 누군가 실수로 변경할 수도 있고, 변경하면 안 되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 한 번만 호출되므로 이후에 호출되지 않는다.

누락

프레임워크 없이 순수한 자바 코드를 단위 테스트하는 경우는 굉장히 많다.

이때 수정자 의존관계 등을 사용한다면, 만약 실수로 setter를 호출하여 의존관계를 주입하는 것을 누락한다면, Null Point Exception 등의 런타임 에러가 발생할 수 있다. 

하지만 생성자 주입을 사용한다면 실수로 주입 데이터를 누락했을 때 런타임 에러가 아닌 컴파일 오류가 발생한다.

즉, IDE에서 바로 어떤 값을 누락했는지 쉽게 파악할 수 있다. 따라서 수정자 주입보다 누락을 쉽게 잡을 수 있다.

final 키워드

뿐만 아니라 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 따라서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다. 수정자 주입 등의 나머지 주입 방식은 모두 생성자 호출 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다.

결론을 내리자면, 최대한 생성자 주입 방식을 선택하는 것이 좋다.

@RequiredArgsConstructor

lombok을 이용하면 생성자 주입을 훨씬 간단하게 할 수 있다.

@Component
@RequiredArgsConstructor
public class PizzaService {

	private final PizzaRepository pizzaRepository;	
   
}

위와 같이 롬복 라이브러리가 제공하는 @RequiredArgsConstructor 어노테이션을 적용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. 매우 편리한 기능이다.


참고

1. https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

2. https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans