스프링 프레임워크에서는 객체간의 낮은 결합도를 제공하기 위해 DI(Dependency Injection)을 제공한다. 이 기능은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받기도 하고, 런타임에 의존관계가 결정되기 때문에 유연한 구조를 가진다.
의존성 주입의 종류
Field Injection
변수에 @Autowired 어노테이션을 붙이게 되면 스프링 프레임워크에서 해당 객체를 자동으로 스프링 빈으로 등록한다.
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class MissionService {
@Autowired
private SessionService sessionService;
}
하지만 인텔리제이에서는 필드에 @Autowired 어노테이션을 붙이는걸 지양하라고 한다. 왜 그러는 걸까?
스프링 컨테이너와 높은 결합도를 가지게 된다.
@Autowired 어노테이션의 동작 방식은 스프링 컨테이너에 등록된 빈을 찾아 해당 객체에 직접 주입시키는 방식이다. 하지만 이 방식은 스프링 컨테이너 외에서는 실행할 수 없기 때문에 스프링 컨테이너와의 높은 결합도를 가지게 된다는 것이다.
불변 상태로 생성할 수 없다.
마찬가지로 스프링 컨테이너에서 등록된 빈을 찾아 직접 주입해야 하기 때문에 final 키워드를 사용할 수 없다. 따라서 해당 객체는 언제든지 변할 수 있는 mutable한 객체다.
변경 가능한 객체를 생성하는 것 보다 외부에서 변경할 수 있는 여지를 줄이는 것이 더 좋은 설계이기 때문에 @Autowired를 지양하는 것이 좋다.
Setter Injection
불변 상태로 생성할 수 없다.
Setter Injection의 경우 Field Injection과 마찬가지로 public으로 구현하기 때문에, 관계를 주입받는 객체의 변경 가능성이 열려있다. 이런 이유 때문에 Setter Injection 방식은 주입 받는 객체가 변경될 필요성이 있을 때만 사용해야 한다.
변경의 가능성을 열어두면 다른 곳에서 임의로 객체를 변경할 수 있기 때문에 에러가 발생할 위험이 높다. (Setter 메서드의 경우, immutable 속성을 보장하지 못해서 DTO에서 사용을 지양하기도 한다.)
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class MissionService {
private SessionService sessionService;
@Autowired
public MissionService(SessionService sessionService) {
this.sessionService = sessionService;
}
}
Constructor Injection
생성자 주입 방식을 사용하면 위의 문제들을 해결하고 장점을 작용할 수 있는 몇 가지의 기능을 더 제공해준다.
Final 키워드 사용 가능
생성자 주입은 생성자라는 키워드 특성상 필드를 final로 선언하여 immutable한 객체로 만들 수 있다. 또한 스프링 프레임워크에서 단 한번만 주입 받는 것이 보장된다.
Lombok 지원
생성자 주입 방식은 필드, setter과 다르게 lombok이라는 라이브러리에서 지원해준다.
Lombok?
Java의 라이브러리로 반복되는 메소드를 Annotation을 사용해서 자동으로 작성해주는 라이브러리
다.
만약 롬복으로 생성자 주입을 하고 싶다면 아래와 같이 사용하면 된다. final 키워드를 무조건 붙여줘야 하며, 의존성 주입을 위한 생성자는 필요 없다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MissionService {
private final SessionService sessionService;
}
순환 참조 방지
순환 참조 에러는 A객체가 B객체를 참조하고, B객체가 A객체를 서로 참조하고 있을 때 발생한다. 필드 주입 방식으로 A, B 객체 사이의 순한 참조 상황을 만들어 보겠다.
@Service
public class AService {
@Autowired
private BService bService;
public void hello() {
bService.hello();
}
}
@Service
public class BService {
@Autowired
private AService aService;
public void hello() {
aService.hello();
}
}
위 AService와 BService는 서로를 의존하기 때문에 어느 한쪽에서라도 hello 메서드를 실행한다면 순환참조가 발생하여 애플리케이션이 다운된다.
하지만 위 상황과 같이 런타임에서 애플리케이션이 강제적으로 다운이 되야 개발자가 알 수 있는게 좋은 방법일까? 게다가 순환참조 때문인지 다른 문제 때문인지도 판단 하는데 시간도 걸릴 것이다.
그렇기 때문에 생성자 주입 방식을 사용하면 위와 같은 문제를 해결할 수 있다.
@Service
public class AService {
private BService bService;
@Autowired
public AService(BService bService) {
this.bService = bService;
}
public void hello() {
bService.hello();
}
}
@Service
public class BService {
private AService aService;
@Autowired
public BService(AService aService) {
this.aService = aService;
}
public void hello() {
aService.hello();
}
}
같은 코드지만 의존성 주입 방식만 바꿨다. 이렇게 생성자 주입을 사용한다면 컴파일 타임에 아래와 같은 에러 메시지가 출력된다.
'Spring Framework' 카테고리의 다른 글
[Spring] 필터(Filter)와 인터셉터(Interceptor) 역할과 차이 (0) | 2023.06.16 |
---|---|
[Spring] CustomException이 아닌 Exception 적절하게 처리하기 (4) | 2023.05.09 |
[Spring] 스프링 프레임워크란? 개념/특징/동작 원리 (2) | 2023.04.20 |
[Spring Boot, Database] Spring Boot + Redis 제대로 활용하기(2) (1) | 2023.01.18 |
[Spring] 트랜잭션과 @Transactional 정리 (0) | 2023.01.14 |