autowired vs constructor injection

2023. 2. 25. 20:30카테고리 없음

의존 관계 자동 주입

 

의존 관계 주입(DI)에는 크게 4가지 방법이 있다.

 

  • 생성자 주입
  • 수정자 주입(setter주입)
  • 필드 주입
  • 일반 메서드 주입 

스프링 컨테이너가 하는 일

1. 스프링 빈을 등록

2. 스프링 빈에 등록된 것들에 di 의존 관계 주입을 한다.

 

생성자 주입

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

- 생성자 호출 시점에 딱 1번만 호출 되는것이 보장된다.

필수 , 불변 의존 관계에 사용

-> 값을 바꿀수 없게 생성자에 다가 값을 넣는다.

final 함수를 사용해 무조건 값이 있어야 한다 . -> 만약 누군가 생성자 메서드를 지우게 되면 컴파일 에러 발생 시킨다.

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

생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다

생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.

 

 

 

수정자 주입(setter주입)

-setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

-선택,변경 가능성이 있는 의존 관계에 사용

(setxxxx이런 식으로 이름을 보통 짓는다.

생성자는 스프링이 빈을 등록할때 어쩔 수 없이 생성자를 통해서 생성이 되기에 자동 주입이 되지만 set은

주입을 시켜줘야 한다.생성자 주입은 필수 값인데, 수정자는 선택적으로 의존 관계 주입을 할수 있다)

주입이 필요한 객체가 주입이 되지 않아도 얼마든지 객체를 생성할 수 있다는 것이 문제다.

이런 방식으로 만들게 될시 값이 변경이  가능하다)-> 공동 작업을 할때 누군가 setXXX 메서드를 건드려서 해당 필드가 주입 되지 않아도 컴파일 에러가 발생되지 않는다. 큰 불상사가 생길수있다.

-자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

 

@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;
 }
}
@Bean
 public OrderService orderService(){
 		OrderServiceImpl orderServiceImpl = new OrderServiceImpl();
        orderServiceImpl.setMemberRepository(memberRepository());
        orderServiceImpl.setDiscountPolicy(discountPolicy());
        return OrderServiceImpl;

    }

 

필드 주입

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

-특징 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.

-DI 프레임워크가 없으면 아무것도 할 수 없다.

-사용하지 말자! 애플리케이션의 실제 코드와 관계 없는 테스트 코드 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

 

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

: 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다

 

 

생성자 주입을 이용한 순환참조 방지

개발하다보면 여러 서비스들 간에 의존관계가 생기게 되는 경우가 있다. 이 예제에서는 CourseService 에서 StudentService 에 의존하고, StudentService 가 CourseService 에 의존하는 경우를 볼 것이다.

Field Injection 의 경우

public interface CourseService {
    void courseMethod();
}
@Service
public class CourseServiceImpl implements CourseService {

    @Autowired
    private StudentService studentService;

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

    @Autowired
    private CourseService courseService;

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

이 상황은 StudentServiceImple 의 studentMethod() 는 CourseServiceImpl 의 courseMethod() 를 호출하고, CourseServiceImpl 의 courseMethod() 는 StudentServiceImple 의 studentMethod() 를 호출하고 있는 상황이다. 서로서로 주거니 받거니 호출을 반복하면서 끊임없이 호출하다가 결국 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]
…
…
…

이게 순환참조의 문제인데, 실제 코드가 호출이 되기 전까지는 아무것도 알지 못한다. 스프링 애플리케이션 구동도 너무나 잘된다. 

이것의 핵심은 객체 생성 시점에서 순환참조가 일어나는 것과 , 객체 생성후 비즈니스 로직상에서 순환 참조가 일어나는 것은 완전히 다른 것이다.

필드 주입이나, 수정자 주입은 객체 생성시점에는 순환참조가 일어나는지 아닌지 발견할 수 있는 방법이 없다.

Constructor based Injection 의 경우

@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 를 뱉기 때문에, 오류를 뱉을 수 밖에 없는 로직을 품고 애플리케이션이 구동되는 것이다.

마지막으로, 생성자 주입을 사용하면 단위테스트 작성하기가 좋아진다.