mojo's Blog
의존관계 자동 주입 본문
다양한 의존관계 주입 방법
의존관계 주입은 크게 4가지 방법이 존재한다.
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
① 생성자 주입
이름 그대로 생성자를 통해서 의존 관계를 주입받는 방법이다.
특징은 다음과 같다.
- 생성자 호출시점에 딱 1번만 호출되는 것이 보장
- "불변, 필수" 의존관계에 사용
@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를 생략해도 자동 주입이 된다. (스프링 빈에만 해당)
② 수정자 주입
setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
특징은 다음과 같다.
- "선택, 변경" 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법
@Component
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository){
System.out.println("memberRepository = " + memberRepository);
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy){
System.out.println("discountPolicy = " + discountPolicy);
this.discountPolicy = discountPolicy;
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
System.out.println("1. OrderServiceImpl.OrderServiceImpl");
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
★ @Autowired 애너테이션이 없으면 자동 주입이 되지 않는다.
★ 이때 setter 실행과 생성자가 만들어지는 실행 순서는 첫번째로 생성자가 만들어지며 두번째로 setter가 실행된다.
선택적이라는 의미는 다음과 같이 의존관계 주입을 할 것인지 안할 것인지를 정할 수 있다.
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository){
System.out.println("memberRepository = " + memberRepository);
this.memberRepository = memberRepository;
}
자바빈 프로퍼티란?
자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이다.
class Data {
private int age;
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
③ 필드 주입
이름 그대로 필드에 바로 주입하는 방법이다.
특징은 다음과 같다.
- 코드가 간결해서 많은 개발자들을 유혹하지만, 외부에서는 변경이 불가능해서 테스트가 어렵다는 단점이 있다.
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 애플리케이션의 실제 코드와 관계 없는 테스트 코드로 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.
@Component
public class OrderServiceImpl implements OrderService{
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
...
}
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
OrderServiceImpl bean = ac.getBean(OrderServiceImpl.class);
MemberRepository memberRepository = bean.getMemberRepository();
System.out.println("memberRepository = " + memberRepository);
}
}
정상적으로 실행되는 것을 알 수 있다.
그러나 필드 주입은 외부에서 변경이 불가능하기 때문에 테스트하기 어려운 점이 있다.
다음 코드를 보도록 한다.
@Test
void fieldInjectionTest(){
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
테스트 코드를 통해 OrderServiceImpl 의 객체를 생성하였지만, memberRepository, discountPolicy 에 의존관계를 주입할 수 있는 방법이 없으므로 어떠한 작업을 하려고 하면 NullPointerException 이 뜨게 된다.
결국 테스트 코드 상에서 memberRepository, discountPolicy 에 의존관계를 주입하기 위해서 setter 를 따로 만들어야 하는 번거로움이 존재한다.
따라서 이렇게 필드에 주입하는 것 보다 setter 를 통해 의존관계 주입을 하는 것이 좋다.
④ 일반 메서드 주입
일반 메서드를 통해서 주입 받을 수 있다.
특징은 다음과 같다.
- 한번에 여러 필드를 주입 받을 수 있다.
- 그러나 일반적으로 잘 사용하지 않는다.
@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;
}
...
}
★ 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
★ 스프링 빈이 아닌 "Member" 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.
옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired 만 사용하면 'required' 옵션의 기본값이 'true' 로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.
- @Autowired(required=false) : 자동 주입 대상이 없으면 수정자 메서드 자체가 호출이 안됨
- org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
public class AutowiredTest {
@Test
void AutowiredOption(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean{
@Autowired(required = false)
public void setNoBean1(Member noBean1){
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2){
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3){
System.out.println("noBean3 = " + noBean3);
}
}
}
Member 는 스프링 빈이 아니다!
setNoBean1() 은 @Autowired(reuired=false) 이므로 호출 자체가 안되는 것을 위 결과를 통해 확인할 수 있다.
@Nullable, Optional 은 스프링 전반에 걸쳐서 지원한다. (ex : 생성자 자동 주입에서 특정 필드에만 사용)
※ 불변
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다. (불변해야 함)
수정자 주입을 사용하면 setXxx 메서드를 public 으로 열어둬야 한다.
누군가 실수로 변경할 수 있으며 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다.
※ 누락
프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우, 다음과 같은 수정자 의존관계인 경우에 대한 코드를 보도록 한다.
@Component
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
public void setMemberRepository(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
public void setDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
...
}
class OrderServiceImplTest {
@Test
void createOrder(){
OrderServiceImpl orderService = new OrderServiceImpl();
Order order = orderService.createOrder(1L, "itemA", 10000);
}
}
setter 를 통한 의존관계 주입이 누락되었기 때문에 NullPointerException이 발생한 것을 볼 수 있다.
※ final 키워드
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
그래서 생성자에서 혹시라도 값이 설정되지 않은 오류를 컴파일 시점에서 막아준다.
아래 코드를 확인해본다.
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemoryMemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
}
...
}
필수 필드인 discountPolicy 에 값을 설정해야 하는데 이 부분이 누락되었다.
이 경우 자바는 컴파일 시점에 다음 오류를 발생하게 된다.
java : variable discountPolicy might not have been initialized
참고로 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다!
오직 생성자 주입 방식만이 final 키워드를 사용할 수 있다.
롬복과 최신 트랜드
우선 롬복 사용을 위한 세팅을 진행하도록 한다.
build.gradle 에서 다음과 같이 추가를 해주도록 한다.
//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//lombok 설정 추가 끝
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
...
}
그리고 Setting 에 들어가서 Annotation Processors 을 입력해서 아래와 같이 Enable annotation processing 을 클릭한 후 OK 를 눌러줘야 한다.
Settings 에서 Plugins 으로 이동한 후에 Lombok 을 검색후 설치해줘야 한다.
@Getter
@Setter
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("hello");
String name = helloLombok.getName();
System.out.println("name = " + name);
}
}
이제 @Getter, @Setter 애너테이션만으로 getter, setter 를 만들지 않아도 되는 것을 알 수 있다.
실무에서 많이 쓰인다고 한다.
@Getter
@Setter
@ToString
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("hello");
System.out.println("helloLombok = " + helloLombok);
}
}
@ToString 애너테이션으로 toString() 메서드도 자동으로 제공해주는 것을 알 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
...
}
@RequiredArgsConstructor 애너테이션을 통해 생성자로 의존관계를 주입하는 과정을 도와주는 역할을 한다.
즉, 애너테이션만으로 생성자를 만들 필요가 없어진 것이다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
최종결과 코드와 이전 코드는 완전히 동일하다.
롬복이 자바의 애너테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다.
실제로 class를 열어보면 위와 같은 코드가 추가되어 있는 것을 알 수 있다.
조회 빈이 2개 이상인 문제
FixDiscountPolicy 와 더불어 RateDiscountPolicy 에 @Component 애너테이션을 달아준다.
그리고 테스트를 실행하면 다음과 같은 결과가 나타난다.
@Component
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
public int discount(Member member, int price){
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
else{
return 0;
}
}
}
@Component
public class FixDiscountPolicy implements DiscountPolicy{
private int discountFixAmount = 1000; // 1000원 할인
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP)
return discountFixAmount;
else
return 0;
}
}
오류메시지에서 하나의 빈을 기대했지만 "fixDiscountPolicy", "rateDiscountPolicy" 2개가 발견되었다고 알려준다.
이때 하위 타입으로 지정할 수 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.
그리고 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다.
스프링 빈을 수동 등록해서 문제를 해결해도 되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.
'Spring' 카테고리의 다른 글
빈 생명주기 콜백 (0) | 2022.03.14 |
---|---|
애너테이션 및 List, Map (0) | 2022.03.10 |
컴포넌트 스캔 (0) | 2022.03.03 |
싱글톤 컨테이너 (0) | 2022.03.02 |
스프링 컨테이너와 스프링 빈 (0) | 2022.01.25 |