포스트

스프링 핵심 원리 (10) - 다양한 의존관계 주입 방법과 생성자 주입

스프링 핵심 원리 (10) - 다양한 의존관계 주입 방법과 생성자 주입

1. 개요

앞 글까지는 @Autowired를 생성자에 붙여서 의존관계를 주입하는 방식만 사용해왔다. 그런데 스프링이 의존관계를 주입하는 방법은 생성자뿐만이 아니다. 크게 네 가지 방법이 있고, 각각 동작 시점과 용도가 다르다. 이번 글에서는 네 가지 주입 방법을 비교하고, 그중에서도 왜 생성자 주입이 권장되는지, 그리고 주입 대상이 없어도 동작해야 하는 경우를 어떻게 처리하는지 정리한다.


2. 의존관계를 주입하는 네 가지 방법

2.1 생성자 주입

생성자를 통해 의존관계를 전달받는 방식이다. 지금까지 예제에서 계속 사용해온 방법이 바로 이것이다.

1
2
3
4
5
6
7
8
9
10
11
12
@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를 생략해도 자동 주입이 동작한다는 것이다(스프링 빈에 한해서).

1
2
3
4
5
6
7
8
9
10
11
@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;
    }
}

2.2 수정자 주입(setter 주입)

자바빈 프로퍼티 규약을 따르는 setXxx 메서드를 통해 의존관계를 주입받는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
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는 기본적으로 주입 대상이 없으면 오류를 내기 때문에, 정말 선택적인 의존관계라면 required 옵션을 함께 다뤄줘야 한다(자세한 내용은 3장 참고).

2.3 필드 주입

필드에 애노테이션을 붙여 바로 주입받는 방식이다.

1
2
3
4
5
6
7
8
@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}

코드가 가장 짧아 보이지만, 외부에서 필드 값을 바꿔 넣을 방법이 없어 순수 자바 코드로는 테스트하기 어렵고, 스프링 같은 DI 컨테이너 없이는 아무 역할도 하지 못한다는 치명적인 단점이 있다. 그래서 애플리케이션 실제 코드에서는 사용을 지양하고, 테스트 코드나 @Configuration 설정 클래스처럼 DI 컨테이너를 이미 전제로 하는 특수한 경우에만 제한적으로 사용하는 것이 좋다.

참고: @Bean이 붙은 메서드의 파라미터는 자동으로 의존관계가 주입된다. 컴포넌트 스캔으로 자동 등록된 빈을 수동 등록 설정에서 그대로 가져다 쓸 때 유용하다.

1
2
3
4
@Bean
OrderService orderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    return new OrderServiceImpl(memberRepository, discountPolicy);
}

2.4 일반 메서드 주입

생성자나 setter가 아닌 일반 메서드에도 @Autowired를 붙여 주입받을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@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;
    }
}

한 번에 여러 필드를 묶어서 주입받을 수 있다는 점을 빼면 수정자 주입과 크게 다르지 않고, 실무에서 사용하는 경우는 거의 없다.

참고: 의존관계 자동 주입은 스프링이 관리하는 빈끼리만 동작한다. 스프링 빈이 아닌 일반 클래스에 @Autowired를 붙여도 아무 일도 일어나지 않는다.


3. 주입 대상이 없어도 되는 경우 - 옵션 처리

@Autowiredrequired 옵션의 기본값이 true라서, 매칭되는 빈이 하나도 없으면 예외를 던진다. 하지만 주입 대상이 없어도 정상 동작해야 하는 상황이 있다. 이럴 때 선택할 수 있는 방법은 세 가지다.

방법대상이 없을 때 동작
@Autowired(required = false)메서드 자체가 호출되지 않음
org.springframework.lang.@Nullable파라미터에 null이 전달됨
Optional<T>Optional.empty가 전달됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 주입 대상이 없으면 호출 자체가 안 됨
@Autowired(required = false)
public void setNoBean1(Member member) {
    System.out.println("setNoBean1 = " + member);
}

// 주입 대상이 없으면 null이 들어옴
@Autowired
public void setNoBean2(@Nullable Member member) {
    System.out.println("setNoBean2 = " + member);
}

// 주입 대상이 없으면 Optional.empty가 들어옴
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
    System.out.println("setNoBean3 = " + member);
}

여기서 Member는 스프링 빈으로 등록되어 있지 않다고 가정한 예시다. setNoBean1은 호출조차 되지 않고, setNoBean2setNoBean3을 실행하면 각각 다음처럼 출력된다.

1
2
setNoBean2 = null
setNoBean3 = Optional.empty

@NullableOptional은 수정자 주입뿐 아니라 생성자 주입에서도 특정 파라미터에만 선택적으로 적용할 수 있다.


4. 그래서 어떤 방법을 써야 할까 - 생성자 주입을 기본으로

네 가지 방법 중 어느 것을 골라도 동작은 하지만, 최근 스프링을 포함한 대부분의 DI 프레임워크는 생성자 주입을 기본으로 권장한다. 이유는 크게 세 가지다.

불변성을 지킬 수 있다. 애플리케이션이 동작하는 동안 의존관계가 바뀔 일은 거의 없고, 오히려 바뀌면 안 되는 경우가 대부분이다. 수정자 주입은 setXxxpublic으로 열어둬야 하므로 누군가 실수로, 혹은 의도치 않게 런타임에 의존관계를 바꿔버릴 여지가 남는다. 생성자는 인스턴스를 만들 때 딱 한 번만 호출되므로, 생성자로만 주입하면 이후에는 값이 변경될 통로 자체가 없다.

의존관계 누락을 컴파일 시점에 잡아낸다. 수정자 주입 방식의 클래스를 프레임워크 없이 순수 자바 코드로 테스트한다고 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
    }
}
1
2
3
4
5
@Test
void createOrder() {
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}

@Autowired는 스프링 컨테이너 안에서 동작할 때만 의존관계를 강제할 뿐, 이렇게 순수 자바 코드로 new를 직접 호출하면 아무 제약도 걸지 않는다. 위 테스트는 컴파일도 되고 실행도 되지만, memberRepositorydiscountPolicy가 채워지지 않은 채로 createOrder가 실행되어 NullPointerException이 터진다. 반면 생성자 주입을 쓰면 필요한 값을 넘기지 않은 코드는 아예 컴파일이 되지 않으므로, 문제를 실행 전에 IDE에서 바로 확인할 수 있다.

final 키워드를 쓸 수 있다. 생성자에서만 값을 대입하는 필드는 final로 선언할 수 있고, 덕분에 생성자 안에서 값 대입을 빠뜨리면 컴파일 오류로 즉시 드러난다.

1
2
3
4
5
6
7
8
9
10
11
12
@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 대입을 빠뜨렸다면?
    }
}

이렇게 discountPolicy 대입을 빠뜨리면 자바 컴파일러가 variable discountPolicy might not have been initialized라는 오류를 즉시 발생시킨다. 생성자 이후에 호출되는 수정자 주입, 필드 주입, 일반 메서드 주입으로는 애초에 final 필드를 사용할 수 없으므로, 이런 방어는 생성자 주입에서만 가능하다.


5. 정리

의존관계 주입 방법은 생성자, 수정자, 필드, 일반 메서드 네 가지가 있지만, 실무에서 기본으로 삼을 방법은 생성자 주입이다. 한 번 주입되면 바뀌지 않는 불변성을 보장하고, 필수 의존관계 누락을 런타임이 아닌 컴파일 시점에 잡아주며, final 키워드로 한 번 더 안전장치를 걸 수 있다는 점이 이유다.

기본은 생성자 주입으로 가되, 정말 선택적인 의존관계가 있다면 @Autowired(required = false), @Nullable, Optional<T> 중 하나로 옵션 처리를 해주고, 필요하면 수정자 주입을 보조 수단으로 함께 쓰면 된다. 필드 주입은 테스트 코드나 설정 클래스처럼 특별한 상황이 아니라면 피하는 것이 좋다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.