본문 바로가기

WEB/Spring

의존관계 주입 4가지 방법과 생성자 주입 권장 이유

다양한 의존관계 주입 방법

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

 

(1) 생성자 주입

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

(3) 필드 주입

(4) 일반 메서드 주입

[1] 생성자 주입

  • 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
  • 지금까지 우리가 진행했던 방법이 바로 생성자 주입이다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

@Autowired
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
  • 특징

생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.( 그때가서 한 번 세팅하고 그 다음 세팅 못하게 막을 수 있다)

불변, 필수 의존관계에 사용

불변 = 한 번만 호출하기 때문에 두 번 호출 못하게 된다.

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

@Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		//-> OrderServiceImpl 어떤 구현 객체가 들어올지는 알 수 없다. 의존 관계를 외부에 맡긴 것이다.
		this.memberRepository = memberRepository; //할당 하는 부분
		this.discountPolicy = discountPolicy;
	}

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

위의 코드와 같이 setDiscountPolicy 을 두어서 값을 변경하게 해서는 안 된다.

[좋은 개발은 제약있게 코드를 짜는 것 제약이 없으면 어디서 수정되었는지 모른다]

필수 = final 로 선언을 해주었다. 이는 값이 꼭 있어야 한다는 것이다. (언어적으로 제약을 설정함)

생성자에는 값을 채워넣어야 한다.

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
  • 중요 : 중요! 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다.
@Component
public class OrderServiceImpl implements OrderService{

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;

	@Autowired //생성자가 1개가 있으므로 생략가능 -> 자동으로 의존관계 주입 
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository; //할당 하는 부분
		this.discountPolicy = discountPolicy;
	}

[생성자가 2개 있는 경우]

@Component
public class OrderServiceImpl implements OrderService{

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;

	//생성자 1
	public OrderServiceImpl(){...}

	@Autowired //생성자 2
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository; //할당 하는 부분
		this.discountPolicy = discountPolicy;
	}

[2]수정자 주입(setter 주입)

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

 

특징

=> 선택, 변경 가능성이 있는 의존관계에 사용

 

1) [선택적]

스프링 빈에 등록이 안되어 있을 수 있을 때도 사용할 수 있다. (선택적으로 의존 관계 주입을 한다)

• 참고: @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

 

2) [변경 가능성]

거의 그럴 일은 없지만 배우는 변경하고 싶을 때 외부에서 변경하고싶을 때 가능

자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

@Component
public class OrderServiceImpl implements OrderService{

	private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;
	
	@Autowired
	public void setMemberRepository(MemberRepository memberRepository) //메소드 이름은 set+클래스명으로 하는 것이 관례
	{
		this.memberRepository = memberRepository;
	}
	@Autowired
	public void setDiscountPolicy(DiscountPolicy discountPolicy) {
		this.discountPolicy = discountPolicy;
	}

..
}
  • 생성자 주입과 수정자 주입이 같이 있다고 가정하자 그럼 어떤 순서로 주입이 이뤄질까?

스프링이 orderServiceImpl 객체를 만드려고 한다면 어쩔 수 없이 생성자를 불러야한다.

스프링은 스프링 컨테이너에서 객체를 등록할 때

new OrderServiceImpl(); 를 해야 하는데 Autowired 부분을 보고 스프링 컨테이너에서 memberRepository와 discountPolicy를 각각 찾아서 new OrderServiceImpl(memberRepository, discountPolicy); 이렇게 만들어서 실행해서 OrderServiceImpl을 만드는 것이다.

즉, 빈을 등록하면서 의존성 주입도 같이 일어나게 되는 것이다.

하지만 수정자 주입은 이렇게 하지 않고 빈 의존관계 주입 단계에서 일어난다. 그래서 생성자 주입과 수정자의 순서를 따져보자면 먼저 생성자가 실행되고 그 다음 수정자(set)가 실행된다.

[참고]

• 참고: 자바빈 프로퍼티, 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이다. 더 자세한 내용이 궁금하면 자바빈 프로퍼티로 검색해보자.

객체 안의 필드에 접근할 때는 직접 접근하는 것이 아니라 메소드를 통해서 접근을 하자

class Data {
      private int age;
      public void setAge(int age) {
        this.age = age;
      }
      public int getAge() {
        return age;
} }

 

[3] 필드 주입

이름 그대로 필드에 바로 주입하는 방법이다.

@Component
public class OrderServiceImpl implements OrderService {
        @Autowired
        private MemberRepository memberRepository;
        @Autowired
        private DiscountPolicy discountPolicy;
}

특징 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.

DI 프레임워크가 없으면 아무것도 할 수 없다.(순수한 자바코드로 테스트 코드를 작성하여 테스트 할 수 없음) 사용하지 말자!

  1. 사용할 수 있는 경우는 애플리케이션의 실제 코드와 관계 없는 테스트 코드이다.

(테스트할 때만 임시적으로 사용)

--CoreApplicationTest.java

@SpringBootTest
class CoreApplicationTests
{
		@Autowired OrderService orderService;
		
		@Test
		void contextLoads(){}
}
  1. 스프링 설정을 목적으로 하는 @Configuration 같은 곳(스프링에서만 사용하는 )에서만 특별한 용도로 사용
@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)) //자동으로 스프링 빈으로 등록//자동 스캔 예외 대상 설정 (앞서 한 예시들과의 충돌을 피하기 위해)
public class AutoAppConfig {

	@Autowired ..

	@Bean(name = "memoryMemberRepository")
	MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
}

[4] 일반 메소드

아무 메소드에서 사용가능

  • 생성자 주입, 수정자 주입과 비슷 → 얘네들이 다 해결하기 때문에 거의 사용할 일은 없음

[특징]

  1. 한 번에 여러 필드를 주입 받을 수 있다.
  2. 일반적으로 잘 사용하지 않는다.
private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;

	@Autowired
	public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy)
	{
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}

참고: 어쩌면 당연한 이야기이지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.

스프링 빈이 아닌 Member 같은 클래스(일반 자바 객체)에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

 


생성자 주입을 선택해라!

과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이

생성자 주입을 권장한다. 그 이유는 다음과 같다.

 

[1]불변

대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. (시작할 때부터 배역이 정해져있음)

오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야 한다.)

수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.

누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.

생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다.

따라서 불변하게 설계할 수 있다.

 

[2]누락

프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에

다음과 같이 수정자 의존관계인 경우

public class OrderServiceImpl implements OrderService {
      private MemberRepository memberRepository;
      private DiscountPolicy discountPolicy;
      @Autowired
      public void setMemberRepository(MemberRepository memberRepository) {
          this.memberRepository = memberRepository;
      }
      @Autowired
      public void setDiscountPolicy(DiscountPolicy discountPolicy) {
          this.discountPolicy = discountPolicy;
      }
//...
}

@Autowired 가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 지금은 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행하고 있다.

@Test
	void createOrder() {
		OrderServiceImpl orderService = new OrderServiceImpl(); //NullPointerException오류 -> MemberRepository 와 DiscountPolicy의 값을 정하지 않아서 
		orderService.createOrder(1L, "itemA", 10000);
	}

그런데 막상 실행 결과는 NPE(Null Point Exception)이 발생하는데, memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문이다.

만약 수정자 주입이 아니라 생성자 주입을 사용하면 다음처럼 주입 데이터를 누락 했을 때 컴파일 오류가 발생한다. 그리고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다.

@Test
  void createOrder() {
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
  }

[3]final 키워드

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다는 장점이 있다. (생성자 주입이 아닌 방법으로 주입한다면 final 키워드가 불가능하다 예를 들어 수정자 주입은 객체가 생성된 다음에 주입된다)

그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다. 다음 코드를 보자.

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
          this.memberRepository = memberRepository;
      }
//...
}

잘 보면 필수 필드인 discountPolicy 에 값을 설정해야 하는데, 이 부분이 누락되었다. 자바는 컴파일 시점에 다음 오류를 발생시킨다.

java: variable discountPolicy might not have been initialized

기억하자! 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!

또 다른 이점으로는 final을 한다면 생성자 코드에서만 값을 넣을 수 있음 이다. 다른 부분에서 값을 넣을 수 없다.

참고: 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다.

오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.

 

 

이 글은 김영한님의 스프링 기본편을 듣고 작성한 글입니다(인프런)