본문 바로가기
Backend/Spring | SpringBoot

[SpringBoot] 자동 의존관계 주입 방법 4가지

by 2245 2023. 6. 24.

자동 의존관계 주입에는 4가지 방법이 있습니다.

 

  1. 생성자 주입
  2. 수정자 주입 (Setter 주입)
  3. 필드 주입
  4. 일반 메서드 주입

 

생성자 주입

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

이름 그대로 생성자를 통해서 의존관계를 주입하는 방법입니다.

 

특징

  • 생성자 호출 시점에 딱 1번만 호출이 됩니다.
  • 불변, 필수 의존관계에 사용이 됩니다.
  • 생성자가 1개만 있을 경우, @Autowired 를 생략해도 됩니다.

 

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

 

 

수정자 주입 (Setter 주입)

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

Setter라 불리는 필드의 값을 변경하는 자바빈 프로퍼티 규약의 수정자 메서드를 통해 의존관계를 주입하는 방법입니다.

 

특징

  • 선택, 변경이 있는 의존 관계에 사용합니다
참고
자바빈 프로퍼티
자바에서는 과거부터 필드의 값을 직접 변경하지 않고 setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었습니다. 이것이 자바빈 프로퍼티 규약입니다.

자바빈 프로퍼티 규약 예시
class Data {
	private int age;

	public void setAge(int age) {
		this.age = age;
	}

	public int getAge() {
		return age;
	}
}​

 

 

필드 주입

@Component
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}

이름 그대로 필드에 바로 주입하는 방법입니다.

 

특징

  • 코드가 간결해 많은 개발자들을 유혹합니다.
  • 하지만, 외부에서 변경이 불가능해 테스트가 하기 힘들다는 치명적인 단점이 있습니다.
    예를 들어, OrderServiceImpl을 테스트를 할 때 memberRepository를 다른 더미 데이터를 넘기는 레포지토리로 바꾸고 싶을 때 바꿀 수 있는 방법이 없습니다.
  • DI 프레임워크가 없으면 아무것도 할 수 없습니다.
    즉, 스프링 기반에서 돌리는게 아니면 순수한 자바 코드로 테스트가 불가능합니다. 무조건 스프링 컨테이너를 띄우고 스프링 컨테이너에서 빈을 가져와야 합니다.

 

스프링 컨테이너가 없는 순수 자바 코드로 테스트

@Test
void fieldInjectionTest() {
    OrderServiceImpl orderService = new OrderServiceImpl();     // Error! -> setter을 만들어 주입시켜줘야 한다.
}

 

결론은 필드 주입은 사용하지 말자!

하지만 사용해도 되는 몇가지 경우가 있습니다.

 

스프링 컨테이너에서 돌아가는 테스트 코드를 작성할 경우

@SpringBootTest
class CoreApplicationTests {
	
	@Autowired
	OrderService orderService;
	
	@Test
	void contextLoads() {
	}

}

 

스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용하는 경우

@Configuration
public class AutoAppConfig {
    
    @Autowired MemberRepository memberRepository;
    @Autowired DiscountPolicy discountPolicy;
    
    @Bean
    OrderService orderService() {
        return new OrderServiceImpl(memberRepository, discountPolicy);
    }
}

 

참고
순수한 자바 테스트 코드에서는 당연히 @Autowired가 동작하지 않습니다. @SpringBootTest 처럼 컨테이너를 테스트에 통합한 경우에만 가능합니다.
참고
다음 코드와 같이 @Bean 에서 파라미터의 의존관계는 @Autowired를 생략해도 자동으로 주입됩니다.
필드 주입은 수동 등록 시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결할 수 있습니다.
@Bean
OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy
discountPolicy) {
	new OrderServiceImpl(memberRepository, discountPolicy);
}

 

일반 메서드 주입

@Component
public class OrderServiceImpl implements OrderService {

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

일반 메서드를 통해서 주입받는 방법입니다.

 

특징

  • 한 번에 여러 필드를 주입받을 수 있습니다.
  • 일반적으로 잘 사용하지 않습니다.

 

참고
어쩌면 당연한 이야기이지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작합니다.
OrderServiceImpl이 스프링 빈이 등록이 되어 있기 때문에, @Autowired가 동작합니다. 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 동작도 하지 않습니다.

 

 

스프링 빈이 없어도 주입해야 할 경우가 있다면?  → 옵션 처리

옵션 처리

@Autowired는 required의 옵션이 true 로 되어 있어 자동 주입 대상이 없으면 오류가 발생합니다. 이때 해결 방법은 다음과 같이 3가지 방법이 있습니다.

 

  1. @Autowired(required=false) : 자동 주입할 대상이 없다면 해당 메서드 자체가 호출이 되지 않는다.
  2. org.springframework.lang.@Nullable : 자동 주입할 대상이 없다면 null 이 입력된다.
  3. 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
public void setNoBean3(Optional<Member> member) {
    System.out.println("setNoBean3 = " + member);
}

※ Member는 스프링 빈이 아닙니다.

 

출력 결과

setNoBean2 = null
setNoBean3 = Optional.empty

 

참고
@Nullable, Optional은 스프링 전반에 걸쳐 지원이 됩니다.
생성자 자동 주입에서 특정 필드에만 사용해도 됩니다.

 

 

생성자 주입을 선택해라!

과거에는 수정자 주입과 필드 주입을 많이 사용했습니다. 하지만 최근엔 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권합니다. 그 이유는 다음과 같습니다.

 

불변

대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 의존 관계를 변경할 일이 없습니다.

오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됩니다. (불변해야 한다.)

 

수정자 주입을 사용한다면, setXxx메서드를 public으로 열어두어야 합니다. 

이는 누군가가 실수로 변경할 수도 있는데 이와 같이 변경하면 필드의 메서드를 public 으로 열어두는 것은 좋은 설계 방법이 아닙니다.

 

생성자 주입은 객체를 생성할 때 딱 1번만 호출이 되므로 이후에 호출되는 일이 없습니다. 따라서 불변하게 설계할 수 있습니다.

 

누락

프레임워크없이 순수한 자바 코드를 단위테스트하는 경우는 굉장히 많습니다. 

예를 들어, OrderServiceImpl만 테스트하고 싶을 경우 필드인 MemberRepository와 DiscountPolicy는 가짜 객체를 주입하여 사용합니다.

 

다음과 같이 수정자 주입 방법을 사용하여 테스트를 수행해보겠습니다.

@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;
    }
	
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

테스트 코드

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

 

실행결과는 OrderServiceImpl에 MemberRepository와 DiscountPolicy 주입이 누락되었기 때문에 NPE(Null Point Exception)이 발생합니다.

@Autowired 가 프레임워크 안에서 동작할 때는 의존관계 누락 시 오류가 발생하지만 지금은 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행하기 때문에 실행 전까지 오류를 발생시키지 않습니다.

 

반면, 생성자 주입을 사용한다면 현재와 같이 주입 데이터를 누락했을 때 바로 컴파일 오류가 발생합니다. 

또한 IDE에서 어떤 값을 필수로 주입해야 하는지 파악하기 쉽습니다.

 

final 키워드

생성자 주입을 사용하면 필드에 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;
        //this.discountPolicy = discountPolicy;
    }
}

위의 코드는 필수 필드인 discountpolicy 에 값을 설정해야 하는데, 이 부분이 누락되어 있습니다.

자바는 컴파일 시점에 다음과 같은 오류를 발생시킵니다.

java: variable discountPolicy might not have been initialized

컴파일 오류는 세상에서 가장 빠르고 좋은 오류입니다.

 

참고
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출이 되므로 필드에 final 키워드를 사용할 수 없습니다.
오직 생성자 주입만 final 키워드를 사용할 수 있습니다.

 

순환 참조 방지

개발을 하다 보면 여러 서비스들 간에 의존관계가 생기게 되는 경우가 있습니다.

CourseService → StudentService → CourseService 처럼 순환 구조로 의존하는 경우의 예시를 보겠습니다.

 

필드 주입

@Service
public class CourseServiceImpl implements CourseService {

    @Autowired
    private StudentService studentService;
    
    @Override
    public void courseMethod() {
    	studentService.studentMethod();
    }
}
@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private CourseService courseService;
    
    @Override
    public void studentMethod() {
    	courseService.courseMethod();
    }
}

 

CourseServiceiMpl의 courseMethod()는 StudentServiceImple의 StudentMethod()을 호출하고, StudentMethod()는 courseMethod()를 호출하고 있는 상황입니다. 

이처럼 서로 주거니 받거니 호출을 반복하다보면 결국 StackOverflowError을 발생시키고 죽습니다.

 

2019-08-28 00:14:56.042 ERROR 46104 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause


java.lang.StackOverflowError: null
    at com.yaboong.alterbridge.tmp.CourseServiceImpl.courseMethod(CourseServiceImpl.java:26) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.StudentServiceImpl.studentMethod(StudentServiceImpl.java:25) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.CourseServiceImpl.courseMethod(CourseServiceImpl.java:26) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.StudentServiceImpl.studentMethod(StudentServiceImpl.java:25) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.CourseServiceImpl.courseMethod(CourseServiceImpl.java:26) ~[classes/:na]
…
…
…

 

이것이 순환 참조의 문제인데 필드 주입의 경우, 애플리케이션 구동이 잘될 뿐더러 실제 코드가 호출되기 전까지 알지 못합니다.

 

 

반면 생성자 주입의 경우

@Service
public class CourseServiceImpl implements CourseService {

    private final StudentService studentService;

    @Autowired
    public CourseServiceImpl(StudentService studentService) {
        this.studentService = studentService;
    }

    @Override
    public void courseMethod() {
        studentService.studentMethod();
    }
}
@Service
public class StudentServiceImpl implements StudentService {

    private final CourseService courseService;

    @Autowired
    public StudentServiceImpl(CourseService courseService) {
        this.courseService = courseService;
    }

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}

위와 같은 경우엔 애플리케이션 구동이 되지 않고 아래와 같은 로그가 찍히며 실패하게 됩니다.

 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  courseServiceImpl defined in file [/Users/yaboong/.../CourseServiceImpl.class]
↑     ↓
|  studentServiceImpl defined in file [/Users/yaboong/.../StudentServiceImpl.class]
new CourseServiceImpl(new StudentServiceImpl(new CourseServiceImpl(new ...)))

 

이와 같이 수정자 주입을 사용할 경우, 애플리케이션은 잘 구동이 되다가 순환 참조를 호출하고 있는 시점에 StackOverFlowError를 내기 때문에 쉽게 찾을 수가 없습니다.

반면에 생성자 주입은 컨테이너가 빈을 생성하는 시점에 객체 생성에 사이클 관계가 형성되기 때문에 실행 시에 바로 오류를 찾을 수 있는 장점이 있습니다.

 

(출처: https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/)

 

 

결론

  • 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고 순수한 자바 언어의 특징을 잘 살리는 방법이기도 합니다.
  • 기본으로는 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하여 사용할 수도 있습니다.
  • 항상 생성자 주입을 선택하는 것이 좋습니다! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택하여 사용합니다. 필드 주입은 사용하지 않는게 좋습니다.

 

 


출처 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com