[Spring DI/IoC] 스프링의 의존성 주입 (1) - 의존성 주입 방법

Jihoon Oh·2022년 5월 4일
4

Spring DI/IOC

목록 보기
2/3
post-thumbnail

그냥 주는 대로 먹자. 스프링의 의존성 주입 기능을 사용해서.

이전 게시글에서 DI와 IoC가 어떤 개념인지에 대해 알아보았다. 그렇다면 우리가 알고 싶어하는 본론으로 들어가서, 스프링에서는 의존성 주입을 어떤 방식으로 사용하는지 알아보자.

Spring은 의존성을 자동으로 주입해준다.

@RestController
@RequestMapping("/members")
public class MemberController {
    private final MemberService memberService;
    
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
    
    ...
}

위와 같은 Controller 클래스가 있다고 하자. MemberControllerMemberService를 의존하고 있으며, 어떤 MemberService에 의존할 지는 앞선 게시물에서 언급했던 의존성 주입 방법 중 생성자를 통한 주입 방법을 사용하고 있다. MemberController 내부에서는 MemberService를 직접 생성하고 있지 않다. 그렇다면 결국 Controller가 생성되는 시점에 외부로부터 특정한 MemberService를 주입받아야 한다는 의미가 된다. 그렇다면 MemberController보다 상위 계층의 메서드에 가보면 new MemberService()와 같은 코드를 볼 수 있을까? MemberController보다 상위 계층인 DiApplication 클래스(main 메서드를 포함하고 있는 임의의 클래스)로 가보자.

@SpringBootApplication
public class DiApplication {

	public static void main(String[] args) {
		SpringApplication.run(DiApplication.class, args);
	}

}

어라? 놀랍게도 MemberService의 생성자를 찾을 수 없다. 심지어 MemberController도 생성하고 있지 않다. 이는 MemberController 클래스가 @Component 어노테이션(@RestController 어노테이션이 @Component를 포함)을 포함하고 있어 스프링 빈으로 등록되어 자동으로 생성되기 때문이다. MemberService 클래스도 빈으로 등록되어 있다면 스프링에 의해 자동으로 생성된다.

@Service // @Component 어노테이션을 포함하고 있어 MemberService를 자동으로 빈 등록
public class MemberService {
    ...
}

자 그렇다면 빈으로 등록되어 있기 때문에 MemberServiceMemberController가 자동으로 생성된다는 것은 알 것 같은데, 문제는 의존관계가 포함되어 있는데 이를 어떻게 넣어주냐는 것이다. 이 역시 스프링에서 자동으로 해준다.

스프링 프레임워크는 DI 컨테이너(또는 IoC) 컨테이너를 통해 적절한 의존성을 자동으로 주입해준다. 스프링을 사용하지 않았을 때 우리가 생성자나 setter에 직접 의존 객체를 만들어서 주입해주던 일을 프레임워크가 대신 해주는 것이다. 앞서 DI가 IoC를 구현하는 방식 중 하나라고 했는데, 스프링에서 DI를 해줌으로써 의존관계의 제어권이 스프링 프레임워크로 넘어간 것이라고 볼 수 있다.

스프링이 컨테이너를 통해 자동으로 의존성을 주입 해주는 것은 알겠는데, 주입에도 어떠한 기준이 있어야 하지 않을까?

스프링이 자동으로 의존성 주입을 해주도록 하려면 @Autowired 어노테이션을 사용하면 된다.

@Autowired

@Autowired에 대한 공식 문서를 보면 다음과 같이 설명이 되어 있다.

Marks a constructor, field, setter method, or config method as to be autowired by Spring's dependency injection facilities.

Spring의 의존성 주입 기능에 의해 자동으로 연결되도록 생성자, 필드, setter 메서드 또는 구성 메서드를 표시한다.

한마디로 나는 여기에 스프링이 의존성을 자동으로 주입해 주기를 바란다. 라고 명시하는 어노테이션이라고 볼 수 있다. 물론 편의를 위해 제공된 어노테이션인 만큼, 반드시 이 어노테이션을 통해서만 스프링의 의존성 주입 기능을 사용할 수 있는 것은 아니다. 위의 MemberControllerMemberService를 자동으로 주입받고자 한다면, @Autowired 없이도 자바 코드나 XML 파일을 통해 주입시킬 수 있다.

@Configuration
public class ApplicationConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
    
    @Bean
    public MemberController memberController() {
        return new MemberController(memberService());
    }
}

@Configuration 어노테이션과 @Bean 어노테이션을 활용한 빈 생성 메서드를 통해 MemberControllerMemberService를 주입해줄 수 있다. 하지만 이런 방식으로 의존성을 주입해 주는 것은 너무 불편하다. @Autowired 어노테이션을 사용하면 굉장히 간편하게 의존성을 주입해 줄 수 있다. 물론 이 경우, @Bean 어노테이션이 붙은 생성 메서드가 없기 때문에 @Component 또는 해당 어노테이션을 상속받은 다른 어노테이션(@Controller, @Service, @Repository 또는 커스텀 어노테이션)을 주입시키고자 하는 클래스에 붙여서 스프링 빈으로 등록해주어야 한다.

@RestController
@RequestMapping("/members")
public class MemberController {
    @Autowired
    private MemberService memberService;
    ...
}

이렇게 @Autowired 어노테이션을 붙여주는 것 만으로도 복잡한 설정 파일이나 XML 파일을 작성할 필요 없이 자동으로 의존성을 주입시킬 수 있다.

Spring의 의존성 주입 방법

@Autowired를 사용하면 간편하게 의존성을 주입할 수 있다는 것을 알아보았다. 그런데 @Autowired를 사용하는 위치에 따라 여러 방법으로 의존성을 주입할 수 있다. 이에 대해 알아보자.

필드 주입

가장 간단한 방법이지만 가장 추천되지 않는 방법으로 필드 주입이 있다. 필드 주입 방법은 간단하다. 바로 이전의 MemberController 예제처럼 의존성을 주입할 필드 위에 @Autowired 어노테이션을 붙이면 된다. 이 때 필드는 public일 필요가 없다.

하지만 필드 주입은 이제 더이상 추천되지 않는다. 심지어 IntelliJ에서는 필드 주입을 사용하면 경고를 띄워준다.

왜 필드 주입은 추천되지 않는걸까? 우선 필드 주입에 비해 다른 주입 방법들(특히 생성자 주입)이 훨씬 더 많은 장점을 가지고 있다. (해당 장점들은 해당 주입 방법에서 설명하도록 하겠다.)

하지만 그보다 더 큰 문제는, 필드 주입이 치명적인 단점을 가지고 있다는 것이다. 첫째로, 필드 주입을 하게 되면 외부 접근이 불가능하다. 일반적으로 필드 주입을 한다는 것은 해당 필드를 초기화하는 생성자도, 해당 필드에 값을 넣어주는 setter도 없다는 뜻이 되는데, 보통 필드는 private으로 선언하는 것이 권장된다는 것을 생각해보면 DI 프레임워크나 리플렉션 없이는 필드 값을 주입해 줄 방법이 없다는 의미가 된다. 따라서 의존성을 주입받아야 하는 객체가 프레임워크에 강하게 종속적인 객체가 되어버린다. 이렇게 되면 테스트 등의 이유로 자동이 아니라 수동으로 특정한 의존성을 넣어주어야 할 필요가 생길 때 리플렉션 외에는 처리할 방법이 없다.

둘째로, 순환 참조의 문제(여기서 정확히는 순환 호출 문제)가 생긴다. A가 B를 의존하고, B가 A를 의존한다고 하자.

@Component
public class A
    @Autowired
    private B b;
    
    public void doSomething() {
        b.doSomething();
    }
}

@Component
public class B
    @Autowired
    private A a;
    
    public void doSomething() {
        a.doSomething();
    }
}

이 코드는 빈을 생성하는 시점에는 문제가 되지 않기 때문에(생성자에서 필드를 초기화 하지 않으므로 기본 생성자를 통해 빈을 생성할 때 문제가 발생하지 않는다.) 무사히 컴파일된다. 하지만 한 객체의 doSomething 메서드를 실행한다면, 서로가 서로의 doSomething 메서드를 무한히 호출하여 StackOverflowError가 발생할 것이다. 필드 주입은 컴파일 타임에 이를 잡아낼 수 없기 때문에 문제를 방지할 수 없다. setter 주입 역시 필드 주입과 마찬가지로 이런 문제가 발생할 수 있지만, setter 주입은 최소한 setter를 통해 의존성을 바꿔줄 수 있다. 하지만 필드 주입은 그것조차 불가능하다.

결론적으로, 필드 주입은 테스트 등의 특수한 경우를 제외하고는 쓰지 말자.

setter 주입

setter 주입은 말 그대로 setter를 통해 주입하는 방식이다.

Setter-based DI is accomplished by the container calling setter methods on your beans after invoking a no-argument constructor or a no-argument static factory method to instantiate your bean.

setter 기반 DI는 빈을 인스턴스화하기 위해 인수가 없는 생성자 또는 인수가 없는 정적 팩토리 메서드를 호출한 후 빈에서 setter 메서드를 호출하는 컨테이너에 의해 수행됩니다.

setter 주입을 사용하기 위해서는 클래스에 빈 생성자 또는 빈 정적 팩토리 메서드가 정의되어 있어야 하며, setter 위에 @Autowired 어노테이션을 붙여서 사용한다. 당연한 이야기지만, 의존성을 주입해주려는 필드는 final일 수 없다.

@RestController
@RequestMapping("/members")
public class MemberController {
    private MemberService
    
    @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }
    ...
}

setter 주입 역시 필드 주입과 마찬가지로 순환 참조 및 순환 호출의 문제가 발생할 수 있다. 그런데 왜 setter 주입은 필드 주입과 다르게 아직 남아있을까? 바로 의존성을 수정할 수 있다.라는 장점 때문이다. 그리고 setter 주입을 사용하면 의존성의 선택적 주입이 가능하다. (모든 의존성이 주입되지 않아도 빈이 생성될 수 있기 때문에) 물론 이는 주입되지 않은 의존성에 대한 참조를 호출할 때 NPE가 발생할 수 있기 때문에 단점이 될 수도 있다.

때문에 보통 setter 주입은 런타임에 의존성을 수정해야 할 필요가 있을 때 사용한다.

아, 참고로 메서드 이름이 전통적인 setter 방식(set + 필드 이름)을 따르지 않더라도 사용이 가능하다. 추천되는 방법은 아니지만.

생성자 주입

드디어 스프링에서 공식적으로 추천하는 방법인 생성자 주입이다. 생성자 주입은 생성자를 통해 객체를 생성하는 시점에 모든 의존성을 주입해주는 방식이다.

@RestController
@RequestMapping("/members")
public class MemberController {
    private final MemberService memberService;
    
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
    ...
}

생성자 주입을 사용할 경우, 의존성 주입이 최초 빈 생성 시 1회만 호출됨을 보장할 수 있다. (애초에 생성자는 1회만 호출되니까) 그리고 위 코드를 보면 기존의 주입 방식과는 다르게 필드를 final로 선언할 수 있는 것을 알 수 있다. 의존성 주입이 1회만 이루어지며 final 필드가 가능하기 때문에 의존성의 불변을 유지해줄 수 있다는 것이 생성자 주입의 큰 장점이다.

또한 생성자 주입을 사용할 경우, 특정 의존성이 없는 상태로 코드가 실행되지 않도록 의존성 주입을 강제할 수 있다는 장점도 가지고 있다.

또한 이전에 필드 주입과 setter 주입은 순환 참조가 가능하다는 언급을 했는데, 생성자 주입을 사용하면 컴파일 타임에 순환 참조를 체크해서 막아줄 수 있다. 만약 생성자 주입을 사용하는 빈이 순환 참조를 이루고 있다면 어플리케이션을 실행할 때 빈을 생성하지 못해서 다음과 같은 메시지와 함께 어플리케이션이 자동으로 종료된다.

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

Description:

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

┌─────┐
|  memberController
↑     ↓
|  memberService defined in file [C:\Jihoon\woowacourse\techotalk\di-ioc\build\classes\java\main\com\example\diioc\service\MemberService.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

때문에 만약 순환 참조가 정말로 필요한 상황이거나 의존성이 런타임에 수정될 필요가 있을 경우에는 생성자 주입을 사용할 수가 없다. setter 주입을 사용하거나, 순환 참조 구조를 이루지 않도록 설계하자.

그런데 위 예제를 보면 의문점이 하나 존재할 수 있다. 바로 @Autowired 어노테이션이 없다는 것. @Autowired가 없는데 어떻게 스프링이 의존성을 주입해줄 수 있었을까? 자바 설정 파일이라도 만든 것일까?

스프링이 공식적으로 생성자 주입을 권장하기 때문일까, 스프링 4.3 버전 이후부터는 단일 생성자에 대해서는 @Autowired를 붙이지 않더라도 자동으로 의존성을 주입하도록 설정되어있다.

생성자 주입을 할 때 주의점

If multiple non-required constructors declare the annotation, they will be considered as candidates for autowiring. The constructor with the greatest number of dependencies that can be satisfied by matching beans in the Spring container will be chosen. If none of the candidates can be satisfied, then a primary/default constructor (if present) will be used. Similarly, if a class declares multiple constructors but none of them is annotated with @Autowired, then a primary/default constructor (if present) will be used. If a class only declares a single constructor to begin with, it will always be used, even if not annotated. An annotated constructor does not have to be public.

필요하지 않은 여러 생성자가 @Autowired를 선언하면 자동 연결 후보로 간주되고, 스프링 컨테이너에서 빈을 일치시켜 충족할 수 있는 의존성이 가장 많은 생성자가 선택된다. 후보 중 어느 것도 만족할 수 없으면 기본 생성자(있는 경우)가 사용된다. 마찬가지로 클래스가 여러 생성자를 선언했지만 그 중 아무 것도 @Autowired가 붙지 않은 경우 기본 생성자(있는 경우)가 사용된다. 클래스가 시작할 단일 생성자만 선언하면 어노테이션이 없는 경우에도 항상 사용된다. 어노테이션이 달린 생성자는 public일 필요가 없다.

단일 생성자만 있을 때는 자동으로 해당 생성자에 @Autowired가 붙는다고 했는데, 문제는 생성자가 여러개 있는 빈이 있을 수도 있다는 점이다. 만약 생성자가 여러개 있을 경우, 스프링의 의존성 주입 기능을 사용할 생성자에 @Autowired 어노테이션을 붙여야 한다. @Autowired를 여러 생성자에 붙일 수도 있는데, 이 경우에는 가장 많은 의존성을 주입할 수 있는 생성자를 사용한다. 만약 아무 생성자에도 @Autowired가 붙지 않거나, @Autowired 어노테이션이 붙은 어떤 생성자도 사용이 불가능하면 기본 생성자(있는 경우에만)를 사용한다. 그것조차 없다면? 당연히 빈이 생성이 되지 않고 어플리케이션이 정상적으로 실행되지 않는다.

생성자 주입 vs setter 주입

앞서 생성자 주입과 setter 주입의 사용법과 장단점을 알아보았다. 어떤 경우에 생성자 주입을 사용할지, 어떤 경우에 setter 주입을 사용할 지 어느정도 감이 왔겠지만, 공식 문서의 설명을 통해 다시 한번 되짚어보자.

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Required annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

생성자 기반 및 설정자 기반 DI를 혼합할 수 있으므로 필수 의존성에는 생성자를 사용하고 선택적 의존성에는 setter 또는 구성 메서드를 사용하는 것이 좋다. setter 메서드에서 @Required 어노테이션을 사용하여 속성을 필수 의존성 만들 수 있다. 그러나 프로그래밍 방식의 인수 유효성 검사가 포함된 생성자 주입이 더 좋다.

Spring 팀은 일반적으로 생성자 주입을 추천한다. 생성자 주입을 사용하면 애플리케이션 구성 요소를 변경할 수 없는 객체로 구현할 수 있고 필요한 의존성이 null이 아님을 확인할 수 있기 때문이다. 또한 생성자 주입 구성 요소는 항상 완전히 초기화된 상태로 클라이언트(호출) 코드에 반환된다. 참고로 많은 수의 생성자 인수는 좋지 않은 패턴이며, 이는 클래스에 너무 많은 책임이 있을 수 있으며 적절한 문제 분리를 더 잘 처리하기 위해 리팩토링해야 함을 의미한다.

Setter 주입은 주로 클래스 내에서 합리적인 기본값을 할당할 수 있는 선택적 의존성에만 사용해야 한다. 그렇지 않으면 코드에서 해당 의존성을 사용하는 모든 곳에서 null이 아닌 검사를 수행해야 한다. setter 주입의 한 가지 이점은 setter 메서드가 해당 클래스의 개체를 나중에 재구성하거나 다시 주입할 수 있도록 만든다는 것이다. 따라서 JMX MBeans를 통한 관리는 setter 주입을 위한 매력적인 사용 사례다.

특정 클래스에 가장 적합한 DI 스타일을 사용하라. 때로는 소스가 없는 외부 라이브러리 클래스를 처리할 때 선택이 이루어진다. 예를 들어, 외부 라이브러리의 클래스가 setter 메서드를 노출하지 않는 경우 생성자 주입이 유일하게 사용 가능한 DI 형식일 수 있다.

정리

스프링은 의존성을 자동으로 주입해주기 때문에, 스프링의 의존성 주입 방법을 사용하므로써 의존성 제어권을 프레임워크로 역전시킬 수 있다. 물론, 필요한 경우 개발자가 직접 수동으로 의존성을 주입해 줄 수도 있다.

스프링의 의존성 주입 기능을 사용하기 위해서는 주입하고자 하는 의존 객체가 스프링 빈으로 등록되어있어야 하며, 스프링 설정 클래스를 이용하거나 XML 파일을 이용, 또는 @Autowired 어노테이션을 이용하여 의존성을 주입시켜줄 수 있다.

스프링의 의존성 주입 방법으로는 생성자 주입, setter 주입, 필드 주입이 있으며, 필드 주입은 더이상 추천되지 않는 방법이므로 다른 주입 방법을 사용할 필요가 있다. 기본적으로는 생성자 주입이 추천되나, 생성자 주입을 사용하기 곤란한 경우도 존재하므로 생성자 주입과 setter 주입 중 상황에 맞는 주입 방법을 선택하여 사용하는 것이 추천된다.

다음 게시글에서는 DI/IoC 컨테이너를 알아보며 어떻게 스프링이 자동으로 의존성을 주입해 줄 수 있는지, 타입이나 이름 등이 겹칠 경우 어떤 우선순위로 의존성을 주입해주는 지에 대해 알아보도록 하겠다.

참고자료

Spring.io - Core Technologies
@Autowired
[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
[Spring]@Autowired란 무엇인가?
DI(의존성 주입)가 필요한 이유와 Spring에서 Field Injection보다 Constructor Injection이 권장되는 이유

profile
Backend Developeer

1개의 댓글

comment-user-thumbnail
2022년 5월 4일

좋은글이네요^.^

답글 달기