[스프링] 의존관계 자동 주입

업데이트:



✅ 의존관계 주입 방법

📌 생성자 주입

생성자를 통해 의존관계를 주입받는 방식으로 생성자 호출 시점에 딱 1번 호출되는 것이 보장된다.

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

  • 불변한 외부 의존성 : 의존하는 객체가 외부에서 생성되고 한번 주입된 후에는 변경되지 않아야 하는 경우
  • 의존성 주입 후 변경이 불필요한 의존성 : 읽기 전용 데이터베이스 연결, 로깅 구성

  • 필수적인 의존성 : 해당 의존성이 없으면 어플리케이션의 실행이나 기능이 제대로 동작하지 않는 경우

  • 의존성을 외부에서 주입받는 경우 : 객체 생성 시 외부에서 의존성을 주입받아야 하는 경우
public class OrderServiceImpl implements OrderService{  
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;  

	@Autowired 
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {  
		this.memberRepository = memberRepository;  
		this.discountPolicy = discountPolicy;  
	}  
}
@Configuration  // 설정 정보 등록
public class AppConfig {  
  
	@Bean  // 스프링 빈으로 등록
	public MemberService memberService(){  
		return new MemberServiceImpl(memoryMemberRepository());  // 의존성 주입
	}  
    
	@Bean  // 스프링 빈으로 등록
	public MemoryMemberRepository memoryMemberRepository(){  
		return new MemoryMemberRepository();  
	}  
  
	@Bean  // 스프링 빈으로 등록
	public DiscountPolicy discountPolicy(){  
		return new FixDiscountPolicy();  
	}  
}

위 코드를 보면 AppConfig 라는 설정정보를 갖고있는 클래스에서 MemberService, MemoryMemberRepository, DiscountPolicy 에 DI를 적용하고 스프링 빈으로 등록했다.

그리고 OrderServiceImpl 클래스에서는 MemberRepositoryDiscountPolicy 에 대한 DI를 적용했는데 이때는 @Autowired 어노테이션을 통해 DI를 적용한 것이다.

DI된 스프링 빈들이 final 키워드를 붙혀 필수값인 경우에 생성자 주입을 통해 의존관계를 주입한다.

이 때 반드시!! DI가 완료된 스프링 빈을 생성자의 파라미터로 DI해야만 한다.

이는 스프링의 의존성 주입 원칙과 관련이 있는데 스프링은 의존성 주입을 통해 객체간의 결합도를 낮추고 유연성을 높이는 것을 지향한다. 이를 위해 스프링은 생성자를 통해 DI를 할때 객체의 생성과 동시에 의존성이 주입되어야 한다는 원칙을 따른다.

또한 생성자는 컴파일시에 호출되기 때문에 생성자를 통해 주입할 의존성은 빈으로 등록되어 있어야 생성자를 통한 DI가 가능한 이유도 있다.

중요! 생성자가 단 하나인경우에는 @Autowired 어노테이션이 생략가능하다.



굳이 OrderServiceImpl 클래스에서 @Autowired태그를 이용한 DI 말고 기존 AppConfig 클래스에서 @Bean 어노테이션을 사용해 DI를 주입할 수도 있다.

@Configuration  // 설정 정보 등록
public class AppConfig {  
  
	@Bean  // 스프링 빈으로 등록
	public MemberService memberService(){  
		return new MemberServiceImpl(memoryMemberRepository());  // 의존성 주입
	}  
  
	@Bean  // 스프링 빈으로 등록
	public MemoryMemberRepository memoryMemberRepository(){  
		return new MemoryMemberRepository();  
	}  
  
	@Bean  // 스프링 빈으로 등록
	public DiscountPolicy discountPolicy(){  
		return new FixDiscountPolicy();  
	}

	@Bean  // 스프링 빈으로 등록
	public OrderService orderService(){  
		return new OrderServiceImpl(memoryMemberRepository(), discountPolicy());  // 의존성 주입
	}  
}

코드 마지막 부분을 보면 OrderServiceOrderServiceImpl 를 DI해주고 , OrderServiceImpl 에 다시 memoryMemberRepository, discountPolicy 를 DI해준걸 볼 수 있다. 앞서 언급한 @Autowired DI와 같은 역할을 하는 코드이다.




📌 수정자(Setter) 주입

자바 빈 규약에 따라 Setter를 통해 필드의 값을 수정할 수 있는 수정자 메서드를 통해 의존관계를 주입할 수 있다.

선택, 변경 가능성이 있는 의존관계에 사용된다.

public class OrderServiceImpl implements OrderService{  
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;  

	@Autowired 
	public void setMemberRepository(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

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

각각의 수정자(Setter) 메서드에 @Autowired 어노테이션을 붙여 의존관계를 주입한다.

수정자 주입은 생성자 주입과는 다르게 의존 관계로 주입되어야 할 객체가 스프링 빈으로 등록되지 않아도 사용이 가능하다.

그러나 @Autowired 어노테이션은 주입 할 대상이 없으면 오류가 발생하는데, 오류가 발생하지 않게 하려면 속성으로 (required = false) 를 지정하면 된다.




📌 필드 주입

필드에 바로 의존성을 주입하는 방식도 있다.

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

접근제어자가 private 인 경우 외부에서 값을 변경하거나 접근하는게 불가능하지만 @Autowired를 사용하면 문제없이 DI가 가능하다.

그러나 외부에서의 변경 및 테스트가 어렵다는 큰 단점이 있다.


❗❗ 왜 테스트가 어려울까 ?

간단한 예시를 통해 알아보자.

UserService

  • 매우 간단한 기능만 가지고 있음
  • 그럼에도 불구하고 세 가지 타입의 의존 오브젝트가 필요
    1. UserDao : DB 처리 ( 필드를 통한 DI를 했다고 가정 )
    2. MailSender : 메일 처리 ( 필드를 통한 DI를 했다고 가정 )
    3. PlatformTransactionManager : 트랜잭션 처리 ( 필드를 통한 DI를 했다고 가정 )

UserServiceTest

  • 본래의 목적 : UserService에 대한 테스트
  • 세 가지 의존관계를 갖고 있으므로 테스트가 진행되는 동안 세 가지 오브젝트가 모두 정상이어야 한다.
  • 실제로는 해당 오브젝트의 뒤에 존재하는 훨씬 더 많은 오브젝트와 환경, 서비스, 서버 등을 함께 테스트하는 것

이런 상황이 있을 때, 우리는 UserService의 간단한 기능만 테스트 하고 싶다. 그러나 DB, 메일, 트랜젝션을 처리하는 메소드들이 존재하기 때문에 이를 제외하고 테스트를 하는 편이 훨씬 효율적이다.

만약 생성자를 통한 DI를 활용해 세가지 기능에 대한 DI를 했다면, 우리는 스프링이 제공하는 mock 라이브러리를 통해 세가지 기능을 제외하거나, 임의의 값을 넣어준다던가, 생성자에 가상 오브젝트를 주입하여 복잡한 로직을 간단하게 처리가 가능하다.

그러나 필드 단위에 DI를 적용해놓은 경우에는, 이미 세가지 기능에 대한 DI가 서비스 단위에 적용되었기 때문에, 앞서 언급한 여러 간단한 처리가 불가능하고, 모든 로직을 다 확인해야만 한다.

때문에 간단한 로직만을 테스트 하고싶어도 포함된 모든 로직을 테스트 해야만 하는 상황이 벌어질수 밖에없다. 그래서 필드에 DI를 하는건 권장하지 않는다.




📌 일반 메서드 주입

일반 메서드를 통해서 주입받을 수도 있다.

생성자처럼 한번에 여러 필드를 주입받을수도 있는데, 일반적으로 사용되지는 않는다. (생성자 주입이나 수정자주입으로 다 해결되기 때문이다.)

더하여, 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 한다. 스프링 빈으로 등록되지 않은 클래스(ex: Member와 같은 Domain ) 는 주입받을 수 없다.

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

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





✅ 생성자 주입 방식을 선택해라!

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




📌 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키워드를 사용했다. 그리고 롬복을 활용하여 코드를 최적화 했는데 다음 코드들을 비교하며 어떤 방식으로 최적화가 되는지 살펴보자.



기본코드

@Component
public class OrderServiceImpl implements OrderService {

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
 
	@Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}



⭐ Autowired 생략 코드

@Component
public class OrderServiceImpl implements OrderService {

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
 
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}



⭐ 롬복 적용 코드

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
}

롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. 이를 활용하면 정말 깔끔한 코드를 만들 수 있다!

댓글남기기