생성자 주입(Constructor Injection), 필드 주입(Field Injection), 수정자 주입(Setter Injection)은 모두 의존성 관계 주입이라고 합니다. 각각의 의존성 관계 주입 방법에 대해서 알아보기 전에 의존성과 의존성 관계 주입(Dependency Injection, DI)이 무엇인지에 대해서 먼저 알아보고 시작하겠습니다.
의존성과 의존성 관계 주입(Dependency Injection, DI)
'의존성 관계 주입'은 Spring 프레임워크의 3가지 핵심 프로그래밍 모델 중에 하나로 Spring에서만 사용되는 용어가 아니라 객체지향 프로그래밍 어디서나 통용되는 개념입니다.
의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 하며, A클래스가 B클래스 또는 인터페이스를 사용하고 있는 경우에 우리는 A클래스가 B클래스 또는 인터페이스에 의존성이 있다고 표현합니다.
***
A가 B를 의존한다는 것이 의미하는 것은 의존 대상 B가 변하면, 그것이 A에 영향을 미친다는 것입니다.
=> 즉, B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미친다는 것인데요. 아래 예시를 통해 조금 더 자세하게 살펴보겠습니다.
Class Minicar {
private DoubleMotor doubleMoter;
public Minicar() {
doubleMotor = new DoubleMotor();
}
}
예시를 보면 Minicar 클래스는 DoubleMoter 클래스를 사용합니다. 때문에 Minicar 클래스는 DoubleMoter 클래스에 의존성이 있다고 볼 수 있습니다.
Class DoubleMotor {
public void booster() {
....
}
public void powerBooster() {
....
}
}
이처럼 Minicar 클래스가 DoubleMotor를 의존하고 있는 상황에서 DoubleMotor에 booster 메서드가 있고, Minicar 클래스에서는 DoubleMotor의 booster 메서드를 사용하고 있다고 했을 때, DoubleMotor 클래스에서 booster 메서드를 제거하고 powerBooster 메서드를 새로 추가한다고 한다면 booster 메서드를 사용하고 있던 Minicar 클래스는 새로 변경된 powerBooster 메서드를 사용하도록 변경할 수밖에 없습니다.
Class Minicar {
private TripleMotor tripleMotor;
public Minicar() {
tripleMotor = new TripleMotor();
}
}
또한 만약 Minicar 클래스가 DoubleMotor를 TripleMotor로 바꾼다고 해도 이처럼 Minicar 클래스의 변경이 불가피하게 됩니다.
의존성 관계 주입(Dependency Injection, DI)
이렇게 의존성이 강할 때 나타나는 문제점을 해결하기 위해서 의존성 주입(Dependency Injection)을 사용하게 되는데요.
두 객체 간의 관계(의존성)를 맺어주는 것을 의존성 주입이라고 하며, 앞에서 이야기한 것처럼 생성자 주입, 필드 주입, 수정자 주입 등의 방법이 있습니다.
객체를 주입받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해 넘겨받는다는 것이며, 이렇게 하면 결합도를 낮출 수 있고, 런타임 시에 의존 관계가 결정되기 때문에 유연한 구조를 가지는데 이를 느슨한 결합이라고 합니다.
(위에서 본 예시가 반대인 강한 결합으로 내부에서 다른 객체를 생성하는 것입니다. 클래스 내부에서 객체를 생성하고 그 객체가 변경될 시 클래스가 전체적으로 수정되어야 할 수 있다는 문제가 있습니다.)
Class Minicar {
private Motor motor;
public Minicar() {
motor = new TripleMotor();
}
}
interface Motor {
public void booster();
}
class TripleMotor implements Motor {
public void booster() {
....
}
}
(의존관계를 인터페이스로 추상화한 형태로, 이렇게 되었을 때 더 다양한 의존 관계를 맺을 수 있게 됩니다.)
***
의존성 관계 주입에 대한 결론을 먼저 이야기하면 Spring Framwork에서는 @Autowired를 사용한 필드 주입 방식을 사용하면 "Field injection is not recommended"라는 메시지와 함께 생성자 주입 방식(Construct Injection)을 이용한 의존 관계 주입을 권장하는데요. 이어지는 내용을 통해 각각의 방식을 살펴보고, 그 이유에 대해서 살펴보겠습니다.
필드 주입 방식(Field Injection)
@Controller
public class FieldInjectionController {
@Autowired
private FieldInjectionService fieldInjectionService;
}
필드에서 바로 @Autowired 어노테이션을 통해 의존성을 주입하는 방식으로, 사용법이 매우 간단하다는 장점이 있습니다.
하지만 주입된 객체를 Immutable 한 상태(불변)를 만들 수 없다는 단점이 존재하며, @Autowired 어노테이션을 통해 주입하는 방식, 즉 생성자를 통해서도, setter 주입을 통해서도 주입받는 방식이 아닌데, 그렇기 때문에 Spring이 아니면 해당 필드에 Injection 할 수 있는 방법이 없습니다. 즉, Spring DI 컨테이너 밖에서 작동할 수 없는 코드가 됩니다.
수정자 주입 방식 (Setter Injection)
@Controller
public class SetterInjectionController {
private SetterInjectionService setterInjectionService;
@Autowired
public void setSetterInjectionService(SetterInjectionService setterInjectionService) {
this.setterInjectionService = setterInjectionService;
}
}
Setter를 기반으로 한 의존성 주입(DI)은 다음과 같이 사용합니다. (함수 이름이 꼭 setXX일 필요는 없지만 일관성을 위해 setXX 형식으로 사용합니다.)
수정자 주입 방식은 이어서 나올 생성자 주입 방식과는 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용하게 됩니다.
하지만 실제 개발에서는 의존 관계 주입의 변경이 필요한 상황이 거의 없으며, 수정자 주입을 사용하게 되면 불필요하게 객체의 수정 가능성을 열어두게 되는데, 이는 OOP(객체 지향 프로그래밍)의 5가지 개발 원칙 중에 OCP(Open-Closed Principal, 개방-폐쇄 원칙)를 위반하게 됩니다.
때문에 수정자 주입이 아닌 생성자 주입을 통해 변경의 가능정을 배제하고, 불변성을 보장하는 것이 좋습니다.
***
OCP(Open-Closed Principle, 개방-폐쇄 원칙)
소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다는 프로그래밍 원칙입니다.
객체 지향 프로그래밍 언어(Java, C++ 등)에서는 고정되기는 해도 제한되지 않은, 가능한 동작의 묶음을 표현하는 추상화가 가능하며, 모듈은 추상화를 조작할 수 있습니다.
이런 모듈은 고정된 추상화에 의존하기 때문에 수정에 대해 닫혀있을 수 있고, 반대로 추상화의 새 파생 클래스를 만드는 것을 통해 확장이 가능합니다. 따라서 추상화는 개방-폐쇄 원칙의 핵심 요소로 볼 수 있습니다.
생성자 주입 방식(Constructor Injection)
@Controller
public class ConstructorInjectionController {
private ConstructorInjectionService constructorInjectionService;
// 생성자가 1개인 경우 @Autowired를 생략해도 주입 가능하도록 지원
@Autowired
public ConstructorInjectionController(ConstructorInjectionService constructorInjectionService) {
this.constructorInjectionService = constructorInjectionService;
}
}
(생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능하도록 Spring 프레임워크에서 지원됩니다.)
생성자 주입 방식을 사용하게 되면 객체의 불변성(Immutable)을 확보할 수 있기 때문에 주입받을 필드를 final으로 선언 가능합니다.
또한 컴파일 시점에 누락된 의존성을 확인할 수 있다는 장점과 객체 생성 시점에 필수적으로 빈 객체 초기화를 수행해야 하기 때문에 NullPointerException을 방지할 수 있다는 장점이 있습니다.
@Controller
@RequiredArgsConstructor
public class ConstructorInjectionController {
private final ConstructorInjectionService constructorInjectionService;
}
또한 final 키워드를 붙임으로써 Lombok과 결합되어 코드를 간결하게 작성할 수 있게 됩니다.
(Lombok에는 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor 어노테이션이 존재합니다.)
생성자 주입을 이용한 순환 참조 방지
@Service
public class UserService {
@Autowired
private MemberService memberService;
public void userMethod() {
memberService.memberMethod();
}
}
@Service
public class MemberService {
@Autowired
private UserService userService;
public void memberMethod() {
userService.userMethod();
}
}
다음 예시는 '필드 주입 방식을 통한 의존성 주입'입니다. UserService와 MemeberService는 서로의 메서드를 호출하고 있는데요.
UserService와 MemberService가 서로를 참조하는 상황입니다.
(수정자 주입 방식도 같은 결과가 나옵니다.)
이러한 코드의 경우 어느 한쪽이 호출되었을 때, new A(new B(new A(new B(new A...))) 같은 상황이 반복되기 때문에 반복이 지속되다가 결국 StakOverflowError가 발생합니다.
이때 문제는 어느 한쪽이 호출되기 전에 즉, 프로그램을 처음 실행할 때는 오류를 찾지 못하고 정상적으로 실행이 된다는 것인데요.
이어서 생성자 주입 방식을 통한 의존성 주입을 보겠습니다.
@Service
public class UserService {
private final MemberService memberService;
public UserService(MemberService memberService) {
this.memberService = memberService;
}
public void userMethod() {
memberService.memberMethod();
}
}
@Service
public class MemberService {
private final UserService userService;
public MemberService(UserService userService) {
this.userService = userService;
}
public void memberMethod() {
userService.userMethod();
}
}
다음 예시는 '생성자 주입 방식을 통한 의존성 주입'입니다. 위와 같이 서로가 서로의 메서드를 호출하고 있는 상황인데요.
생성자 주입으로 코드가 짜여있는 상황에서 프로그램을 실행시키면 프로그램 실행에 실패하게 됩니다.
***
이 두 경우의 순환 참조의 오류를 발견하는 시점의 차이는 빈 생성 시기의 차이 때문에 발생합니다.
수정자 주입(Setter Injection)과 필드 주입(Field Injection) 방식은 빈을 먼저 생성한 후에 어노테이션이 붙은 필드에 해당하는 빈을 찾아서 주입하는 방식입니다. 즉, 빈을 먼저 생성한 후에 필드에 대해서 주입하기 때문에 빈 객체를 생성한 시점에는 순환 참조의 발생 여부를 알 수가 없습니다. (생성 -> 주입)
반면 생성자 주입(Constructor Injection) 같은 경우에는 생성자로 객체를 생성하는 시점에 필요한 빈을 주입합니다.
다시 말하면 빈 객체를 생성하는 시점에 생성자의 파라미터 빈 객체를 찾아서 먼저 주입한 뒤에 주입받은 빈 객체를 이용하여 생성하게 됩니다. (주입 -> 생성)
때문에 런타임 시점이 아니라 애플리케이션 구동 시점에서 순환 참조 오류를 발견할 수 있게 됩니다.
< 참고 자료 >
'Programming > Spring Boot' 카테고리의 다른 글
@Value 어노테이션 null이 나오는 문제 해결 방법 (5) | 2022.05.07 |
---|---|
JPA @Modifying clearAutomatically 속성 발생할 수 있는 문제 (0) | 2022.04.20 |
Spring Batch 개념과 구조, 동작 방식에 대한 정리 (0) | 2022.03.15 |
Redis 동시성 처리를 위한 Transaction 사용 (MULTI, EXEC, DISCARD, WATCH) (2) | 2022.03.05 |
Spring Boot 타임리프 Thymeleaf layout 적용하는 방법 (4) | 2022.02.28 |