의존관계 주입 방법

gorapaduckoo·2023년 7월 14일
0

스프링 기본편

목록 보기
7/10
post-thumbnail

인프런 김영한님의 스프링 핵심 원리 - 기본편 강의 내용을 바탕으로 작성한 글입니다.


의존관계 주입이란, 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성해 클라이언트에 전달하는 것이다. 이전에 제어의 역전과 함께 알아본 적이 있었다.

그렇다면 클라이언트에게 구현 객체를 어떻게 전달할까? 크게 4가지 방법이 있다.

1. 생성자 주입

말 그대로 생성자를 이용해서 의존관계를 주입하는 방법이다. 생성자를 호출할 때, @Autowired가 붙어있으면 스프링 컨테이너에서는 매개변수와 같은 타입의 빈을 찾아 생성자에 넣어준다.

@Component
public class OrderServiceImpl implements OrderService {
	
	private final DiscountPolicy discountPolicy;
    
    // 생성자가 딱 1개만 존재하는 경우, @Autowired 생략 가능
    @Autowired
    public OrderServiceImpl(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
}

객체를 스프링 컨테이너에 스프링 빈으로 등록하려면, 먼저 객체의 인스턴스를 생성해야 한다. 이 인스턴스를 생성하는 방법은 생성자 뿐이고, 스프링 빈은 싱글톤으로 관리되기에 인스턴스가 1개만 생성된다. 따라서 생성자 주입은 생성자 호출 시점에 딱 1번만 일어난다. 그래서 주로 필수적이고 변하지 않는 의존관계에 사용한다.

  • 필수: 할인 정책을 결정해주지 않으면 주문 서비스를 제공할 수 없으므로 할인 정책 구현체는 반드시 주입해주어야 한다.
  • 불변: 예를 들어, 위의 경우에는 OrderService 빈이 생성되는 순간 할인 정책이 결정된다. 빈 생성 이후에는 할인 정책 구현체를 변경할 수 없다. 따라서 모든 손님이 언제 주문하든 동일한 할인 정책을 적용받는다.


2. 수정자 주입 (setter 주입)

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

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

수정자 주입은 수정자를 통해 의존관계를 주입하는 방법으로, 선택의 여지, 변경 가능성이 있는 의존관계에 사용하는 것이 특징이다. 따라서 애플리케이션 실행 중 주입받는 객체가 변경될 가능성이 있을 때는 수정자 주입을 사용한다.
(하지만 이런 경우는 매우 드물고, 대부분의 의존관계는 애플리케이션 종료 시까지 변하면 안된다.)

수정자는 생성자가 호출되고 난 이후에 스프링 프레임워크에 의해 호출된다. 따라서 외부에서 setter를 호출할 수 있도록 setter를 public으로 열어두어야 한다.

만약 주입 객체를 변경하고 싶으면 아래와 같이 스프링 컨테이너에서 빈을 조회하여 의존관계를 수정하면 된다.


public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        // OrderService 빈을 가져옴
        OrderService orderService = context.getBean(OrderService.class);

        // 새로운 DiscountPolicy 객체 생성
        DiscountPolicy newDiscountPolicy = new RateDiscountPolicy();

        // OrderService의 DiscountPolicy 필드를 변경하는 수정자 호출
        orderService.setDiscountPolicy(newDiscountPolicy);
    }
}


3. 필드 주입

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired private DiscountPolicy discountPolicy;
}

필드 주입은 필드에 바로 객체를 주입하는 방식이다. 코드가 간결하다는 장점이 있지만, 외부에서 주입하는 객체를 변경할 수 없어 테스트가 어렵다는 단점이 있다.
@Autowired는 스프링에서 지원하는 기능이기 때문이다. 이 말은 곧 DI 프레임워크가 있어야 한다는 말이기도 하다. 필드 주입을 사용한 상태로 테스트 코드를 실행하려면, 스프링을 전부 띄워주어야 한다.



4. 일반 메서드 주입

@Component
public class OrderServiceImpl implements OrderService {

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

그 외에 일반 메서드를 통해서도 객체를 주입받을 수 있다. 사실상 수정자 주입과 동일하며, 한 번에 여러 필드를 주입받을 수도 있다.



5. 생성자 주입을 사용하자

지금까지 다양한 의존관계 주입 방법에 대해 알아보았다. 여러 방법이 있었지만, 그 중에서도 가장 권장되는 방법은 바로 생성자 주입이다. 아래와 같은 이유 때문이다.

(1) 객체 불변성 확보

대부분의 의존관계는 애플리케이션 종료 때까지 변하지 않는다.
생성자 주입은 객체를 생성할 때 딱 1번만 실행되고, 이후로는 실행되지 않으므로 객체의 불변을 보장할 수 있다. 하지만 수정자 주입과 일반 메서드 주입은 setter 메서드를 열어두기 때문에 주입받는 객체가 수정될 가능성이 있다.

(2) 테스트 시 의존관계 주입 누락 방지

순수한 자바 코드로 단위 테스트를 작성할 때, 수정자 의존관계를 사용하면 의존관계 주입이 누락될 수 있다.

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

	@Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
    
    public void createOrder(int price, Member member) {
    	price = discountPolicy.calcurateDiscountPrice(price, member);
        ...
    }
}

위와 같은 OrderServiceImpl 클래스가 있다. 수정자 주입을 사용하고 있으며, createOrder()discountPolicy로부터 할인된 금액을 받아오고 있다.

이제 이 클래스의 주문 생성 기능에 대해 테스트 코드를 작성해보자.

class OrderServiceImplTest {

	@Test
    void createOrder() {
    	OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(..
    	...

위와 같은 코드를 작성하면 NullPointerException이 발생한다. 스프링에서는 컨테이너가 알아서 setter를 호출하여 의존관계를 주입해 주었지만, 스프링이 없는 순수한 자바 코드에서는 setter가 호출되지 않기 때문이다. 이렇게 런타임 에러가 발생하면 에러를 잡기 매우 어려워진다.

하지만 수정자 주입 대신 생성자 주입을 사용하면 컴파일 오류가 발생한다. OrderServiceImpl의 생성자에 매개변수가 누락되었기 때문이다. 게다가 IDE가 어떤 값이 누락되었는지 친절하게 알려주기까지 한다.

💡 순수한 자바 코드로 테스트를 작성하는 이유?
특정 프레임워크에 의존하지 않는 테스트를 작성하면 테스트의 독립성을 보장할 수 있다. 또한 프레임워크를 띄울 필요가 없어 테스트 실행 속도가 향상된다. 마지막으로 의존관계로 엮인 여러 컴포넌트를 함께 테스트하지 않아도 되기 때문에, 단위 테스트에 집중할 수 있다.

(3) final 키워드 사용 가능

생성자 주입을 제외한 다른 주입 방법은 필드에 final 키워드를 사용할 수 없다. final 을 초기화하려면 (1) 필드에서 초기화하거나, (2) 생성자를 통해 초기화해야 하기 때문이다. 생성자 주입을 제외한 주입 방식은 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 있다. (필드에서 초기화하면 되지않나? 라고 생각했다면 IoC와 DI를 복습하고 오자.)

이처럼 final 키워드를 사용하면, 생성자 매개변수로 넘어와야 할 값이 넘어오지 않은 경우 컴파일 에러가 발생한다.



6. 생성자 주입을 더 편리하게 사용하는 방법

그래서 생성자 주입을 사용해야 한다는 것까진 알았는데, 너무 귀찮다. final 키워드도 붙여줘야 하고, 생성자에 매개변수도 하나하나 넣어줘야 하고... 그런 사람들을 위해 바로 롬복 라이브러리가 존재한다.

롬복 라이브러리는 다양한 어노테이션을 지원한다. 생성자 주입에 사용할 어노테이션은 @RequiredArgsConstructor 어노테이션으로, 컴파일 시점에 final 키워드가 붙은 클래스 필드를 매개변수로 갖는 생성자를 자동으로 생성해준다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
	
	private final DiscountPolicy discountPolicy;

//    @Autowired
//    public OrderServiceImpl(DiscountPolicy discountPolicy) {
//    	this.discountPolicy = discountPolicy;
//    }
}

위와 같이 어노테이션 하나만 붙이면, 아래에 주석 처리한 생성자가 존재하는 것처럼 동작한다. (코드에는 나타나지 않지만, 생성자를 호출하면 정상적으로 동작한다.) 이처럼 생성자 주입과 롬복을 함께 이용하면, 코드를 간결하게 작성할 수 있다.

💡 롬복의 대표적인 어노테이션

  • @Getter: 클래스 필드에 대해 getter 메서드를 자동으로 생성
  • @Setter: 클래스 필드에 대해 setter 메서드를 자동으로 생성
  • @ToString: 클래스 필드에 대해 toString() 메서드를 자동으로 생성
  • @NoArgsConstructor: 매개변수 없는 기본 생성자를 자동으로 생성
  • @AllArgsConstructor: 모든 필드를 매개변수로 갖는 생성자를 자동으로 생성

0개의 댓글