[Spring] 의존성 주입과 의존성 주입 방법에 대하여.
0. 들어가며 🏃🏻♂️
Spring을 사용하며 객체간의 의존성을 주입할때 생성자 주입을 권장한다는 이유를 듣고는 이를 별생각없이 사용해왔습니다. 이번 글을 통해 의존성 주입 방법에는 무엇이 있고, 생성자 주입에는 어떤 장점이 있길래 이를 권장하는지 알아보도록 하겠습니다.
1. 의존성 주입이란❓
의존성 주입 방법에 대해 알아보기 전 간단하게 의존한다는 것은 무엇인지 그리고 의존성 주입이 무엇인지에 대해 알아보도록 하겠습니다.
A가 B에 의존한다라는 것은 B가 변할 때 A에 영향이 미친다는 것과 같은 말입니다. 한 예로 A가 B의 특정 메소드를 사용한다고 하면 B의 특정 메소드 내부 기능이 변할 때 A에 영향을 미치게 되겠죠. 혹은 B의 특정 메소드 형식이 바뀌면 A에 영향을 미치게 되는 것이 또다른 예시가 될 수 있습니다.
또한 두 개의 클래스 사이에 의존관계가 있다고 말할때는 항상 방향성이 존재합니다. 예를 들어 A Class ➡️ B Class 같은 식으로 말이죠. 이와 같이 나타내면 A가 B에 의존하고있다 라고 말합니다.
이 때, 구체클래스끼리 의존하게되면 변화에 영향을 많이 받게 되고 이는 객체지향적으로 좋지 않은 설계가 됩니다. 이러한 문제는 인터페이스를 통해 객체간의 결합도를 낮추는 방식을 사용하면 어느정도 해결이 가능합니다. 구체 클래스간의 의존관계를 맺지 않고 인터페이스를 통해 의존관계를 제한해주면 그만큼 변경에 자유로워지는 것이죠.
이런 설계 방식을 사용하기 위해서는 런타임에 어떤 구체클래스가 선택되어야할지 결정해주는 제 3자가 필요한데 Spring에서는 DI 컨테이너라고 불리는 것이 바로 그 역할을 해줍니다. DI, 즉 의존성 주입을 해주는 역할을 하는 객체인 것이죠.
의존성 주입은 아래 세 가지 조건을 충족하는 작업을 말합니다.
- 코드에는 런타임 시점에 의존관계가 드러나있지 않는다. 즉, 인터페이스에만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
- 의존관계는 사용할 객체에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
Spring에서 의존성 주입은 아래 소개할 의존성 주입 방법을 통해서 행할 수 있습니다.
2. 의존성 주입 방법 📚
Spring에서 의존성 주입 방법은 크게 3가지가 있습니다. 이에 대해 알아보도록 하겠습니다.
1) 필드 주입 (Field Injection)
필드 주입은 객체 필드에 바로 의존 관계를 주입하는 방법입니다. @Autowired 어노테이션을 사용해 아래 코드처럼 사용할 수 있죠.
@Controller
public class UserController {
@Autowired // Field Injection
private UserService userService;
}
필드 주입을 사용하면 코드가 깔끔하여 과거에 많이 사용되었던 주입 방법입니다. 하지만 이러한 방식을 사용하면 외부에서 접근이 불가능하다는 단점이 생깁니다. 해당 단점은 테스트 코드를 작성할 때 부각됩니다. 테스트 코드 작성시 DI 프레임워크가 반드시 존재해야해서 점점 사용하지 않게 되었고, 실제로 필드 주입을 사용하게 되면 IntelliJ에서 "Field injection is not recommended" 라는 경고가 발생합니다.
2) 수정자 주입 (Setter Injection)
수정자 주입은 setter 메서드를 통해 의존관계를 주입하는 방법입니다. 아래 코드와 같이 사용하죠. 이 방식을 사용하면 의존관계가 있는 객체를 런타임에도 변경할 수 있습니다. 하지만 실제로 개발환경에서는 의존관계 객체를 변경하는 일이 거의 없고, 오히려 변경가능성이 코드의 안정성을 해칠수 있다는 단점으로 작용하기도 합니다.
@Controller
public class UserController {
private UserService userService;
@Autowired // Setter Injection
public void setUserService(UserService userService) {
this.userService = userService;
}
}
3) ⭐️ 생성자 주입 (Constructor Injection) ⭐️
생성자 주입은 아래와 같이 생성자를 사용하여 의존관계를 주입하는 방법입니다.
@Controller
public class UserController {
private final UserService userService;
@Autowired // Constructor Injection
public UserController(UserService userService) {
this.userService = userService;
}
}
생성자 주입의 경우 스프링에서 권장하고 있는 의존관계 주입방식입니다. 그렇다면 생성자 주입의 장점은 무엇일까요? 장점이 여러개가 있으니 아래에서 좀 더 자세하게 살펴보겠습니다.
3. 생성자 주입을 사용해야하는 이유 😎
생성자 주입에는 어떤 장점이 있길래 스프링에서 권장하는지에 대해 알아보도록 하겠습니다.
1. 객체의 불변성 확보
생성자 주입을 사용하면 객체의 불변성을 확보할 수 있습니다. 실제 개발과정에서 의존관계를 런타임에 변경시켜야하는 경우는 거의 없습니다. 따라서 수정자 주입같이 수정가능성을 열어두는 것이 오히려 위험한 일이 될 수 있죠. 생성자 주입에서는 필드에 final 키워드 또한 추가할 수 있기 때문에 객체의 불변성 확보를 통한 안정성을 확보할 수 있습니다.
2. 테스트 코드 작성의 용이성
테스트 코드를 작성할 때 DI 프레임워크가 항상 필요하다면 무거워질 가능성이 높습니다. 따라서 순수한 자바 코드로만 테스트를 작성하는 것이 좋은데, 이 때 생성자 주입이 아닌 필드, 수정자 주입을 사용한다면 그렇게 하기가 어려워집니다. 이러한 면에서 생성자 주입은 강점을 가지고 있습니다.
3. Lombok 활용
생성자 주입을 활용할 때 만약 클래스의 생성자가 하나라면 @Autowired 어노테이션을 생략할 수 있습니다. 또한 의존하는 객체에 final 키워드를 붙일 수 있는데 이러한 특성을 사용하면 Lombok의 @RequiredArgsConstructor의 도움을 받아 직접 생성자를 직접 쓰지 않아도 됩니다. 이는 코드가 간결해지는 효과를 만들어낼 수 있죠.
4. 순환 참조 방지
개발을 하다보면 의존성이 복잡하게 구성되는 경우가 생깁니다. 이 때 개발자가 실수로 A ➡️ B, B ➡️ A 식의 객체 의존관계를 만들면 순환 참조 문제로 문제가 발생합니다. 이러한 문제가 발생해도 필드 주입과 수정자 주입은 실제 코드를 호출하기 전까지는 구동이 잘 됩니다. 빈의 생성과 조립과정이 분리되어있기 때문이죠. 하지만 생성자 주입을 사용한다면 이를 애플리케이션 로딩 시점에 알아낼 수 있습니다. Bean을 등록하기 위해 객체를 생성하는 과정에서 순환참조가 발생하고, 이 때 문제가 생기기 때문입니다. (스프링부트 2.6부터는 필드 주입의 경우에도 애플리케이션 로딩 시점에 문제가 발생한다고 합니다.)
4. 나가며 💨
이번 글에서는 의존성 주입이 무엇인지 간단히 알아보았고, Spring에서 의존성 주입을 하는 방식 세가지와 그 중 생성자 주입을 사용해야하는 이유에 대해 알아보았습니다. 감사합니다.
레퍼런스
https://reflectoring.io/constructor-injection/
https://mangkyu.tistory.com/125
https://dev-coco.tistory.com/70
https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/
토비의 스프링