jhhan의 블로그

스프링 코어(10) - 의존관계 주입 본문

Spring

스프링 코어(10) - 의존관계 주입

jhhan000 2021. 7. 14. 23:35

오랜만에 다시 포스트를 작성해 봅니다.

 

이번 포스트에는 의존관계 주입에 대해 작성해 보려고 합니다.

의존관계 주입은 다른 블로그에서도 많이 설명하고 있는 내용이기 때문에

굳이 제 블로그에 오셔서 볼 필요는 없겠지만,

그래도 중요한 내용이므로 정리를 해놓겠습니다.

 

의존관계 주입은 크게 4가지로 나눌 수 있습니다.

  • 생성자 주입(권장)
  • Setter 주입
  • 필드 주입
  • 일반 메서드 주입

4번째는 처음 들어보는 것 같습니다.

그럼 순서대로 알아보겠습니다.

 

1. 생성자 주입

생성자를 통해서 주입 받는 방법입니다.

  • 현재 Spring에서 가장 많이 사용되고 있음.
  • Spring 정책 상으로도 가장 권고되는 방법
  • 많이 쓰이므로 꼭 알아놓는 것이 좋습니다.

특징

  • 생성자 호출 시점에서 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;
    }

	....
}

제가 이런 식으로 OrderServiceImpl 파일에 생성자 주입을 사용했습니다.

물론 다른 파일에서도 사용되었습니다.  한번 찾아보시면 좋을 것 같네요.

 

private final ~  이라고 선언되었기 때문에

두 변수는 모두 값을 무조건 할당받게 됩니다.

-> 생성자 주입을 통해 값을 할당받습니다. 그렇기 때문에 null 값이 될 걱정을 하지 않아도 됩니다.

 

여기서 짚고 넘어갈 부분이 있습니다.

생성자 주입의 경우 생성자가 1개만 있다면 @Autowired를 생략할 수 있습니다.

즉, 위의 코드는 다음과 같이 변경될 수 있습니다.

@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;
    }

	....
}

만약 여기서 생성자 주입 코드가 한 개 이상 생긴다면 더 이상 @Autowired를 생략할 수 없게 됩니다.

 

2. Setter 주입

두번째는 Setter(수정자)를 이용해서 주입하는 방법입니다.

  • 자바에서 많이 쓰는 setter를 사용하는 방법
  • 선택, 변경 가능성이 있는 경우 사용

보통 setter를 사용하면 값이 변경이 됩니다.

그렇다면 setter 주입도 마찬가지겠지요. setter 주입을 사용한다면 Bean 값의 수정이 일어납니다.
(아마 중간에 값 변경이 있는 경우 사용.  하지만 중간에 값 변경이 거의 일어나지 않는다.)

그리고 선택적으로 사용할 수도 있습니다.

@Autowired(required = false)라고 애노테이션을 붙여주면 선택적으로 사용할 수 있는 코드가 됩니다.

즉, 주입할 대상이 없어도 동작이 가능합니다.

 

OrderServiceImpl 클래스의 코드를 예제로 살펴보겠습니다.

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        System.out.println("Setter: memberRepository = " + memberRepository);
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        System.out.println("Setter: discountPolicy = " + discountPolicy);
        this.discountPolicy = discountPolicy;
    }

Setter 주입을 하는 경우에는 final을 붙일 수 없습니다.

private ~ 이런 방식으로 선언이 되고, setter로 값이 변경이 됩니다.

  • System.out.println()이 사용된 이유는 값이 들어오는지 확인하기 위해 추가했습니다.
  • @Autowired는 꼭 붙여줘야 합니다. 아니면 동작하지 않습니다.

 

Setter 주입은 이렇게 사용하면 됩니다.

 

3. 필드 주입

바로 주입하는 방법입니다.

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

이렇게만 하면 바로 필드 주입이 됩니다.

이 방식이 제일 간단합니다.(코드도 간결해집니다.)

그래서 많은 사람들이 쓰는 방식이기도 했고, 예전에 이 방식을 많이 사용했다고 합니다.

하지만 사용하면 warning이 뜨면서 추천하지 않는다는 경고를 볼 수 있습니다.

 

필드 주입을 사용하는 경우 NullPointerException 예외가 뜰 가능성이 굉장히 높아집니다.

그리고 필드 주입은 DI 프레임워크에서만 동작하기 때문에

순수 자바 코드만으로 테스트 시 에러가 발생할 수 있습니다.

 

그래도 필드 주입을 사용할 수 있는 경우도 있습니다.

바로 테스트 코드 작성할 때입니다.

@SpringBootTest
class CoreApplicationTests {
    
    @Autowired
    OrderService orderService;

    @Test
    void contextLoads() {
    }

}

테스트 코드는 이 코드 내에서만 작동하면 되기 때문에 필드 주입으로 간단하게 해결해도 됩니다.

  • 이 코드에서 필드 주입이 되는 이유
  • @SpringBootTest 애노테이션이 사용되어서 스프링 컨테이너가 테스트에 통합되었기 때문
  • 그래서 필드 주입을 사용해도 됩니다.

이런 식으로 사용하면 됩니다.

그래도 필드 주입은 가능한 사용 안 하시는게 좋습니다.

 

4. 일반 메서드 주입

그냥 아무 메서드를 사용해서 주입을 하는 방법입니다.

private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;

@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
  this.memberRepository = memberRepository;
  this.discountPolicy = discountPolicy;
}
  • init이라는 메서드를 사용해서 주입을 진행한 코드입니다.
  • Setter 주입과 크게 다른 점이 없습니다.
  • 대신 Setter 주입과는 다르게 한번에 여러 개를 주입할 수 있습니다.

사실 이렇게 코드를 짜면 메서드 이름에 따라 어디서 주입이 일어났는지 찾기 어려울 것 같습니다.

그리고 생성자 주입과 Setter 주입으로 대부분 해결되기 때문에 이 방식을 굳이 사용하지 않아도 됩니다.

 

아마 이런 이유로 대부분의 블로그에서 소개하지 않은 것 같습니다.

 

 

 

그리고 의존 관계 자동 주입의 경우 스프링 빈으로 등록이 되어 있어야 작동합니다.

스프링 빈이 아닌 클래스를 사용하면 아무리 주입하려 해도 되지 않습니다.

 

 

그럼 포스트를 마칩니다.

 

 

2021.07.26 추가분량

의존관계 주입은 현재 생성자 주입이 권장되고 있다고 했습니다.

하지만 과거에는 필드 주입과 Setter 주입도 많이 사용되었다고 합니다.

이 부분에 대해 조금 더 자세히 알아보고자 합니다.

  • 대부분의 의존관계 주입은 한번 일어난 후 변경될 일이 거의 없다고 합니다.(...정말 정말 특수한 케이스 제외..)
  • 즉, 불변 상태를 유지해야 한다는 것
  • Setter 주입의 경우 setX 메서드를 사용 → public으로 설정이 됨
  • public으로 설정이 되면 누군가가 변경할 수 있는 코드를 작성할 수 있음.
  • 좋지 않음..
  • 생성자 주입: 객체 생성 시 한 번 호출 -> 그 후 호출 없음 → 불변상태로 만들기 쉬움.

다시 한번 Setter 주입 코드를 살펴보죠.

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

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

그리고 간단한 테스트 코드를 만들어 보겠습니다.

@Test
void createOrder() {
	OrderServiceImpl orderService = new OrderServiceImpl();
	orderService.createOrder(1L, "itemA", 10000);
}

아마 전체 코드를 생성자 주입으로 만들었기 때문에 이 코드는 작동하지 않을 것입니다.

하지만 작동한다는 가정하에 실행한다면 → NullPointerException이 발생합니다.

위에 선언한 memberRepository와 discountPolicy에게 의존관계 주입이 되지 않았기 때문입니다...

하지만 똑같은 코드에서 생성자 주입을 사용했다면, 컴파일 오류가 납니다.

 

다음으로 final을 사용해서 이득을 볼 수 있습니다.

생성자 주입 코드를 살펴보겠습니다.

@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 값을 설정하지 않았기 때문입니다.

 

이런 이유들 때문에 생성자 주입 사용을 권장하는 것입니다.

▶ 가능한 생성자 주입을 사용하도록 하고, 특수한 경우에서는 Setter 주입을 사용하는 것이 가장 바람직할 것 같습니다.

 

 

 

 

 

 

 

출처: 인프런 - 스프링 핵심원리(기본편) by 김영한