Autowired, 생성자 주입, RequiredArgsConstroctor

Posted by surin01 on January 16, 2022 · 7 mins read

Spring에서 의존성 주입을 하는 방법들

  1. 필드 주입
  2. setter 주입
  3. 생성자 주입
  4. Lookup method 주입

간략하게 4가지 방법이 있다.

근데 이중에서 4번은.. 대부분의 포스팅에서 잘 다루지 않는 모양이다. 잘 안쓰는거부터, 권장하는 방법까지 알아보자

Lookup method 주입

이 주입 타입은 짧은 생명주기때문에 다른 3가지 주입 방식보다 덜 사용된다.

기본적으로, 스프링의 모든 빈들은 싱글톤으로 생성된다. 이것은 컨테이너에서 한번 생성되고, 같은 객체가 필요로 하는 닫른 곳에 주입된다는 말이다. 반면 가끔 다른 전략이 필요할 때가 있다. 예를 들어서, 각 메소드 호출은 각기 다른 객체에서 수행되야 할 때가 있다.

이럴떄 lookup method 주입을 사용하여야 한다.

이 방식의 주입은 타 3가지의 주입 방식과 완전히 다른 방식의 주입이기 때문에, 장단점을 따질수가 없다.

setter 주입

이 방식은 setter 메소드를 사용하는 방식으로 의존성 주입이 이루어진다.

우선 class 파일을 작성하기 전에, 빈 설정을 xml 설정에 서 시도해보자

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="service1" class="example.Service1"/>
    <bean id="service2" class="example.Service2"/>

    <bean id="dependentService" class="example.DependentService">
        <property name="service1" ref="service1"/>
        <property name="service2" ref="service2"/>
    </bean>

</beans>

2개의 서비스를 빈으로 등록하고, dependentService에서 service1과 service2를 프로퍼티로 등록한다.

꽤 직관적인 방식이고, 이제 빈으로 등록한 서비스를 다른 서비스에 주입한 후 사용하는 예제를 살펴보도록 하자.


class DependentService {
    private Service1 service1;
    private Service2 service2;

    public void setService1(Service1 service1) {
        this.service1 = service1;
    }

    public void setService2(Service2 service2) {
        this.service2 = service2;
    }

    public void doSmth() {
        service1.doSmth();
        service2.doSmth();
    }

}

이렇게 된다면, DependentService에서 사용하는 service1과 service2에 대한 의존성 주입은 완료되게 된다.

또한 이와 같은 작업은 @Autowired 라는 어노테이션에 의해서 같은 효과를 볼 수 있게 되는데,

class DependentService {
    private Service1 service1;
    private Service2 service2;

    @Autowired
    public void setService1(Service1 service1) {
        this.service1 = service1;
    }

    @Autowired
    public void setService2(Service2 service2) {
        this.service2 = service2;
    }

    public void doSmth() {
        service1.doSmth();
        service2.doSmth();
    }

}

장점

종속성 해결 또는 개체 재구성의 유연성으로 언제든지 수행할 수 있고, 이러한 유연성은 생성자 주입의 순환 종속성 문제를 해결할 수 있다.

단점

종속성이 헌재 설정되어 있지 않을 수 있기 때문에 Null 체크가 필수다 또한 종속성을 재정의할 가능성이 생기므로 생성자 주입보다 잠재적으로 오류가 발생할 수 있고 덜 안전하게 된다.

필드 주입

@Service
class DependentService {
    @Autowired
    private Service1 service1;
    @Autowired
    private Service2 service2;

    public void doSmth() {
        service1.doSmth();
        service2.doSmth();
    }
}

이 방식에서의 주입은 실제로 새로운 방식의 주입이 아니기 때문에 어노테이션 기반의 접근만 가능하다. 스프링은 이 방식의 주입을 사용하기 위해 리플렉션을 이용한다.

장점

사용하기 쉽고 생성자나 설정자가 필요하지 않다. 또 생성자 및 설정자 접근 방식과 쉽게 결합 가능하다는 장점이 있다.

단점

개체 인스턴스화에 대한 제어가 적다. 테스트를 위한 클래스의 객체를 인스턴스화 하려면 작성중인 테스트에 따라 구성된 스프링 컨테이너 또는 모의 라이브러리가 필요하게 된다. 또 종속성이 수십게에 이를때에야 설계에 문제가 있음을 알아차릴 수 있고, 불변성이 없다는 것이 단점이다.

생성자 주입

이 방식은 의존성 주입에서 권장되는 방법이다. 종속 클래스에는 모든 종속성이 설정되는 생성자가 있고, XML, java 또는 어노테이션 기반 구성에 따라 스프링 컨테이너에서 제고오딘다.

@Service
public class DependentService {

    private final Service1 service1;
    private final Service2 service2;

    public DependentService(Service1 service1, Service2 service2) {
        this.service1 = service1;
        this.service2 = service2;
    }
    
    public void doSmth() {
        service1.doSmth();
        service2.doSmth();
    }
}

위의 예제에서는 2개의 독립적 서비스와 1개의 종속 서비스가 있고, 생성자를 통해서 의존성을 주입하게 된다.

장점

생성된 객체는 변경할 수 없으며 완전히 초기화된 상태로 클라이언트에 반환된다. 이 접근 방식을 사용하면 종속성이 증거하는 문제를 즉시 확인할 수 있고, 종속성이 많을수록 생성자가 커진다.

단점

차후에 개체의 종속성을 변경할 가능성이 없고, 순환 종속성을 가질 가능성이 더 높아진다.

더 깔끔한 방법의 생성자 주입

대부분의 경우, Lombok라이브러리를 통해서 많은 일들을 처리하게 된다.

@AllArgsConstructor를 통한 주입

@AllArgsConstructor
@Service
public class DependentService {

    private Service1 service1;
    private Service2 service2;
    
    public void doSmth() {
        service1.doSmth();
        service2.doSmth();
    }
}

위의 예제에서 생성자를 @AllArgsConstructor가 만들게 된다. 결과적으로 컴파일을 하게 되면 모든 종속성을 가진 서비스들에 대한 생성자가 있는 생성자가 추가되어 있다.

@RequiredArgsConstructor 통한 주입

@RequiredArgsConstructor
@Service
public class DependentService {

    private final Service1 service1;
    private final Service2 service2;
    
    public void doSmth() {
        service1.doSmth();
        service2.doSmth();
    }
}

모든 종속성을 가진 필드들을 생성자 파라메터에 추가하는 AllArgsConstructor와 다르게, 이 방식의 어노테이션은 final키워드가 붙은 필드들을 생성자 파라메터로 선언하여 생성자를 만들게 된다.

모든 객체들이 싱글톤으로 생성되고 관리되며 필드에 주입되는 객체들은 한번만 초기화되기 때문에 걸들면 안되며 조금 더 secure한 코딩 스타일을 위해서는 필드들을 final로 선언하고, @RequiredArgsConstructor로 생성자 주입 방식으로 만드는 것이 나아보인다.

결론

이전에서도 의존성 역전(DI)에 대한 글을 Nest.js를 사용할 때 작성한 적이 있었다.