다양한 의존성 주입 방법

5tr1ker·2023년 5월 5일
1

Spring

목록 보기
3/9
post-thumbnail

생성자 주입 ( Constructor Injection )

생성자 주입은 생성자를 이용해서 의존 관계를 주입하는 방법압니다.

private UserRepository userRepository;
private MemberService memberService;

    @Autowired
public UserService(UserRepository userRepository, MemberService memberService) {
	this.userRepository = userRepository;
	this.memberService = memberService;
}

생성자 주입은 생성자가 1회만 호출 되는 것을 보장합니다. 그렇기 때문에 주입받은 객체는 변하지 않으며, 객체의 주입이 필요한 시점에 사용할 수 있습니다. 또한 Spring 프레임워크에서는 생성자 주입을 적극 지원하고 있기 때문에 위의 코드처럼 생성자가 1개만 있을 경우 @Autowired를 생략하더라도 주입이 가능하게 편의성을 제공합니다.

따라서 위의 코드는 하단의 코드와 같은 코드가 됩니다.

private UserRepository userRepository;
private MemberService memberService;

public UserService(UserRepository userRepository, MemberService memberService) {
	this.userRepository = userRepository;
	this.memberService = memberService;
}

하지만 생성자 파라미터가 늘어나면 코드의 가독성을 떨어뜨릴 수 있는데 이는 Lombok 의 @RequiredArgsConstructor 와 final 키워드를 활용하여 해결할 수 있습니다.

수정자 주입 ( Setter Injection )

수정자 주입은 필드 값을 수정하는 Setter 를 사용해서 의존 관계를 주입하는 방법입니다. Setter 주입은 생성자 주입과 다르게 객체의 변경 가능성이 있을 경우에 사용합니다.

private UserRepository userRepository;
private MemberService memberService;

@Autowired
public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
}

스프링 초기에는 getX , setX 등 프로퍼티를 기반으로 하는 자바 기본 스펙 때문에 수정자 주입을 많이 사용했었는데, 시간이 지나고 난 뒤 수정자 주입이 아닌 다른 방식을 사용합니다.

수정자 주입은 필요한 의존 객체만 주입할 수 있어 많은 의존성을 세밀한 제어를 할 수 있으며 메모리를 최적화할 수 있습니다. 하지만 의존성이 누락될 수 있으며 메서드가 외부로 노출될 수 있습니다. 또한 객체의 생성과 주입이 떨어져 있기 때문에 코드의 가독성이 떨어질 수 있습니다.

필드 주입 ( Field Injection )

필드 주입은 필드에 바로 의존 관계를 주입하는 방법입니다.

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;
}

필드 주입은 코드가 간결해지는 장점이 있습니다. 하지만 클래스 내부의 의존성을 숨기므로 코드의 가독성이 떨어질 수 있으며, 의존성 오류를 컴파일 시점에서 발견할 수 없고 런타임 시점에서 오류를 발견합니다.

생성자 주입을 사용해야 하는 이유

객체의 불변성 확보

실제로 개발을 하다보면 의존 관계를 변경해야 하는 경우가 거의 없습니다. 하지만 수정자 주입을 이용해서 불필요하게 수정의 가능성을 열어두면 유지보수성을 떨어뜨릴 수 있습니다. 따라서 생성자 주입을 사용해서 변경 가능성을 배제해 불변성을 보장하는 것이 좋습니다.

예외 발생 가능성 제거

생성자 주입은 컴파일 시점에 객체를 생성하여 의존성을 주입하기 때문에 누락된 의존성에 대한 오류를 컴파일 시점에 발견할 수 있습니다.

테스트 작성에 용이

생성자 주입을 사용하면 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 의존성 없는 객체를 쉽고 빠르게 테스트할 수 있습니다.
만약 @Autowired를 사용한다면 필요한 의존성을 등록하고 초기화 하기 때문에 테스트의 비용이 증가하게 됩니다.

Lombok 과의 결합

생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있습니다. 반면 다른 방법들은 객체의 생성 ( 생성자 호출 ) 이후에 호출되므로 final 키워드를 사용할 수 없습니다.

또한 final 키워드를 붙이면 Lombok과 결합하여 코드를 간결하게 작성할 수 있습니다. Lombok은 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor를 이용하여 코드를 간결하게 작성할 수 있습니다.

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final MemberService memberService;

    public void register(String name) {
        userRepository.add(name);
    }

}

이러한 코드는 Spring에서 생성자 주입을 사용할 시 생성자가 1개라면 @Autowired를 생략해도 주입해 주기 때문에 코드를 간결하게 작성할 수 있습니다.

비침투적인 코드 작성

import org.springframework.beans.factory.annotation.Autowired;
// 스프링 의존성이 UserService에 import되어 코드로 박혀버림

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;

}

사용자의 비즈니스 로직을 처리하는 Service 계층에서 프레임워크 코드가 침투하는 것은 큰 문제는 아니지만, 생성자 주입을 사용해서 스프링 코드가 없는 유연한 코드를 작성하는 것이 더 좋습니다.

프레임워크는 비즈니스 로직을 작성하는 서비스 계층에서 알아야 할 대상이 아닙니다.

순환 참조 에러 방지

생성자 주입을 사용하면 컴파일 시점( 객체 생성 시점 )에 순환 참조 에러를 예방할 수 있습니다.

그 이유는 Bean을 등록하기 위해 객체를 생성하는 도중 다음과 같은 순환 참조가 발생하기 때문입니다.

new UserService(new MemberService(new UserService(new MemberService()...)))

@Autowired 는 빈의 생성과 조립 시점이 분리되어 있기 때문에 컴파일 시점에 발견되지 않습니다. 반면 생성자 주입은 빈의 생성과 조립이 동시에 이루어 지다보니 위의 에러를 사전에 잡을 수 있습니다.

참고로 스프링부트 2.6부터는 순환 참조가 기본적으로 허용하지 않으므로 해당 내용은 스프링부트 2.6 이하의 버전에서 오류가 발생합니다.

참고

참고 블로그 1 : https://mangkyu.tistory.com/125
참고 블로그 2 : https://alisyabob.tistory.com/327

profile
https://github.com/5tr1ker

0개의 댓글