의존관계 주입(Dependency Injection)은 크게 4가지의 방법이 존재한다. 생성자 주입, 수정자 주입, 필드 주입, 일반 메서드 주입이다.
이전까지 주로 사용했던 방식이 생성자 주입이며, 실제 개발에서도 이 방식을 주로 채택하게 된다. 생성자 주입은 객체 생성 시점에 단 한 번만 호출되기 때문에, 주로 불변하고 필수적인 의존 관계를 처리하는 데 사용된다.
스프링의 자동 의존관계 주입인 @Autowired는 생성자에 들어갈 요소가 스프링 빈이라면, 생성자가 하나만 존재할 경우 @Autowired를 생략해도 스프링이 자동으로 의존 관계를 주입해준다. 이는 코드를 간결하게 유지하면서도 의존성 관리의 편의성을 제공한다.
Setter 주입은 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입하는 방식이다. 주로 변경 가능성이 있는 의존관계에 사용된다.
그러나, 불변이어야 할 상황에서 Setter 주입을 선택하면, 주입 자체는 성공하지만 Setter 메서드가 남아있어 외부에서 객체를 수정할 수 있는 위험이 생긴다. 이는 여러 개발자가 함께 작업할 때 실수를 유발할 가능성을 높인다.
따라서, 불변성이 기본 원칙이며, 아주 가끔 객체의 변경이 필요할 때만 Setter 주입 방식을 선택하도록 한다.
필드에 바로 주입하는 방법이지만 외부에서 변경이 불가능하다 이는 무슨 말일까.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
위의 코드는 필드 주입 방식을 적용한 클래스이다. 이 클래스의 인스턴스를 생성할 때, new OrderServiceImpl()과 같이 작성할 수 있다. 만약 생성자 주입 방식이었다면, 생성자에 필요한 의존성을 new OrderServiceImpl(memberRepository, orderRepository)와 같이 전달해야 한다.
하지만 필드 주입 방식에서는 생성자를 통해 의존성을 주입할 수 없기 때문에, 스프링 컨테이너를 통해 빈을 가져오지 않는 한 필드에 값을 할당할 방법이 없다. 순수 자바 코드만으로는 필드에 구현체를 넣을 수 없으며, 이를 위해 Setter 메서드를 작성하거나 추가 작업이 필요하다.
결과적으로, 필드 주입 방식은 스프링 프레임워크에 강하게 종속된다. 이러한 종속성 때문에 순수한 단위 테스트 작성이 불가능해지고, 의존성 주입 패턴의 원칙에도 어긋난다. 코드가 간단해 보이긴 하지만, 이러한 이유로 필드 주입 방식은 지양해야 할 주입 패턴이다. 실제로 IntelliJ와 같은 개발 환경에서는 필드 주입 사용 시 경고로 애노테이션에 노란 밑줄을 표시하며, 사용을 권장하지 않음을 나타낸다.
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;
} }
이 방식또한 스프링 아래에서는 잘 동작한다. 하지만 순수한 자바 코드에서 생성자가 없기에 이 또한 스프링에서는 자동주입, 순수한 자바 코드에서는 객체를 만든 후. init을 따로 실행시켜야 한다. 위의 init에 해당하는 메서드를 자동으로 생성시 처리하는 것이 생성자 개념이기에 일반 메서드 주입 방식도 채택하지 않는다.
만약 @Autowired를 사용했는데 스프링 컨테이너 내에 해당 빈이 존재하지 않으면 오류가 발생한다. 이를 해결하기 위해 required 옵션을 조절하거나, 자동 주입 대상을 옵션으로 처리하는 몇 가지 방법이 존재한다:
@Autowired(required=false)
@Nullable
null이 입력된다.Optional<>
Optional.empty가 입력된다.이러한 옵션들을 활용하면 자동 주입 대상이 필수적이지 않은 경우에 유연하게 처리할 수 있다.
// 사용 코드
//호출 안됨
@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);
}
불변성을 가장 잘 유지할 수 있는 방식은 생성자 주입이며, 이는 일반적으로 권장되는 방법이다. 대부분의 의존관계 주입은 애플리케이션이 시작될 때 한 번 설정되며, 애플리케이션 종료 시점까지 변경될 일이 없다. 만약 의존관계를 수정해야 한다면, 주입 과정을 변경한 뒤 애플리케이션을 재시작하면 된다.
수정자 주입(setter 주입)은 수정 가능성을 열어두기 때문에 불변성을 해친다. 수정자 주입을 사용하려면 setter를 퍼블릭으로 열어두어야 하며, 이는 외부에서 setter를 통해 의존성을 변경할 가능성을 남겨놓게 된다. 이러한 이유로, 생성자 주입이 불변성을 유지하며 의존관계 관리를 안전하게 할 수 있는 가장 적합한 방식이다.
테스트 코드에서는 반드시 프레임워크에 의존적인 코드만 작성하는 것이 아니다. 생성자 주입 방식은 순수한 자바 코드에서도 잘 동작하는 방식이다.
예를 들어, setter 주입 방식을 선택하면 순수 자바 코드에서는 객체를 생성한 후 setter를 호출해야 하는 두 가지 과정이 필요하다. 이 과정에서 setter 주입에 대한 이해가 부족한 개발자는 다음과 같은 문제를 경험할 수 있다.
반면, 생성자 주입 방식은 프레임워크 없이도 객체 생성 시 필수 의존성을 생성자 매개변수로 전달해야 하므로, 컴파일 타임에서 오류를 잡을 수 있다. 생성자에 필요한 의존성을 전달하지 않으면 즉시 컴파일 오류가 발생하므로, 런타임에 null 문제로 예외가 발생하는 것을 사전에 방지할 수 있다.
컴파일 오류는 환영받을 수 있는 오류이다. 런타임 환경에서 예외가 발생하는 것보다, 코드 작성 시점에서 문제를 발견할 수 있는 컴파일 오류가 훨씬 더 안전하고 효율적이다.
생성자 주입을 사용하면 필드에 final 키워드 사용이 가능하다. final 키워드는 객체 생성 시점에 해당 필드가 null이 아니어야 하며, 반드시 초기화되어야 한다. 만약 final 필드를 초기화하지 않는 방식으로 객체를 생성하려 하면, 컴파일 오류가 발생한다.
반면, setter 주입이나 다른 방식에서는 필드를 final로 선언할 수 없다. 이유는, setter 호출은 객체 생성 이후에 이루어지는 작업이기 때문이다. 따라서 final 필드는 수정할 수 없고, 이로 인해 setter 주입 방식에서는 필드의 불변성을 보장할 수 없다.
필드 주입은 개념만 알고 절대 사용하지 않도록 한다.
Lombok은 반복적인 생성자, getter, setter 코드를 간편하게 작성하도록 도와주는 라이브러리로, 생산성과 가독성을 크게 향상시킨다. @RequiredArgsConstructor는 final 필드나 @NonNull 필드를 대상으로 생성자를 자동으로 생성하며, 스프링의 @Component 클래스에서 생성자가 하나일 경우 @Autowired를 생략할 수 있게 해준다. 또한, @Getter와 @Setter를 통해 필드의 getter와 setter 메서드를 자동으로 생성해주며, 코드에 필수 로직만 남겨 가독성을 높인다. Lombok을 사용하면 기본적인 메서드 작성을 위한 시간을 절약하고, 유지보수를 쉽게 할 수 있어 코드의 간결화와 생산성을 동시에 만족시킬 수 있다.
이전 포스팅에서 빈 등록 객체가 두 개 이상일 경우에 대해 작성했었는데, @Qualifier를 사용시에 이름을 지정할 수 있다. 하지만 만약 이름을 실수로 잘못 적었을 때 컴파일 시 타입 체크가 되지 않는다.
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
위의 코드는 @Qualify에 붙은 애노테이션을 모두 복붙해오고 마지막에 @Qualifier("mainDiscountPolicy")만 추가한 것으로, @Qualifier("mainDiscountPolicy") 대신에 @MainDiscountPolicy를 사용하면 될 것이다.
@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 여 기서도 @Qualifier 가 우선권이 높다.
스프링은 점점 더 자동화를 추구하고 있다. 우리가 먼저 수동 빈 등록을 공부한 후 자동 빈 등록을 학습한 이유도 여기에 있다. 수동 방식을 경험한 후 자동으로 전환했을 때, 자동 빈 등록 방식의 효율성을 더 잘 느낄 수 있기 때문이다.
대부분의 경우, 자동 빈 등록 방식을 사용하게 된다. 그러나 수동 빈 등록의 장점도 분명하다. 자동 빈 등록을 사용할 경우, 수정이 필요할 때 애노테이션이 붙은 클래스를 일일이 찾아가야 한다. 반면, 수동 빈 등록은 AppConfig와 같은 하나의 파일에서 모든 빈 설정을 관리할 수 있으므로, 수정 작업이 간편하고 명확하다.
웹을 지원하는 컨트롤러, 서비스, 리포지토리 등은 비즈니스 로직에 해당하며, 이는 주로 비즈니스 요구사항에 따라 추가되거나 변경된다. 이러한 비즈니스 로직은 개수가 많아질 수 있지만, 문제가 발생하더라도 위치를 특정하기 쉽기 때문에, 굳이 수동으로 중앙 파일(AppConfig 등)에서 관리할 필요가 없다. 이러한 특성 때문에 비즈니스 로직은 대부분 자동 빈 등록 방식으로 관리한다.
기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용되며, 예를 들어 데이터베이스 연결이나 공통 로그 처리 등의 업무 로직을 지원하는 하부 기술이나 공통 기술들이 있다. 이러한 기술들은 보통 애플리케이션 전반에 걸쳐 광범위하게 영향을 미치기 때문에, 기술 지원 로직들은 가급적 수동 빈을 사용하여 명확하게 드러내는 것이 좋다.
또한 비즈니스 로직 중에서 다형성을 적극적으로 활용하는 경우, 수동 빈으로 관리하여 가독성을 높일 수 있다. 이러한 부분은 선택 사항으로, 명확한 해답은 없으며, 프로그래머가 상황에 맞게 적절히 사용해야 한다.