거인의 코딩일지
[Spring] 다양한 의존성 주입 방법 본문
728x90
Spring 프레임 워크의 핵심 기술 중 하나는 DI(Dependency Injection, 의존성 주입)이다.
Spring 프레임워크와 같은 DI 프레임워크를 이용하면 다양한 의존성 주입을 하는 방법이 있다.
다양한 의존성 주입 방법
1. 생성자 주입 (Constructor Injection)
- 생성자 주입은 생성자를 통해 의존 관계를 주입하는 방법이다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
- 생성자 주입은 생성자의 호출 시점에 1회 호출되는 것이 보장
- 주입받은 객체가 변하지 않거나 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있다.
- 생성자가 1개만 있을 경우에 @Autowired 를 생략해도 주입이 가능하도록 편의성을 제공한다.
- 따라서 아래 코드는 위에 코드와 동일한 코드가 된다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
2. 수정자 주입 (Setter 주입, Setter Injection)
- 수정자 주입은 필드값을 변경하는 Setter를 통해서 의존 관계를 주입하는 방법이다.
- Setter 주입은 생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용 (실제로 변경이 필요한 경우는 드물다.)
- @Autowired 로 주입할 대상이 없는 경우에는 오류가 발생한다.
- 아래 코드에서는 XXX 빈이 존재하지 않을 경우에 오류가 발생한다. 주입할 대상이 없어도 동작하도록 하려면 @Autowired(required = false)를 통해 설정 가능하다
- 스프링 초기에는 수정자 주입이 자주 사용되었는데, 그 이유는 바로 getX, setX 등 프로퍼티를 기반으로 하는 자바 기본 스펙 때문이였다. 하지만 시간이 지나면서 점차 수정자 주입이 아닌 다른 방식이 주목받게 되었다.
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
3. 필드주입 (Field Injection)
- 필드 주입은 필드에 바로 의존관계를 주입하는 방법이다.
- IntelliJ에서 필드 인젝션을 사용하면 Field injection is not recommended이라는 경고 문구가 발생한다.
- 필드 주입을 이용하면 코드가 간결해져서 과거에 상당히 많이 이용되었던 주입 방법
- 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재하는데, 테스트 코드의 중요성이 부각됨에 따라 필드의 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않게 되었다.
- 필드 주입은 반드시 DI 프레임워크가 존재해야 하므로 반드시 사용을 지양해야 한다. 그렇기에 애플리케이션의 실제 코드와 무관한 테스트 코드나 설정을 위해 불가피한 경우에만 이용
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
4. 일반 메소드 주입 (Method Injection)
- 일반 메소드를 통해 의존 관계를 주입하는 방법이다.
- 수정자 주입과 동일하며 마찬가지로 거의 사용할 필요가 없는 주입 방법이다.
- 수정자 주입을 사용하면 한 번에 여러 필드를 주입 받을 수 있도록 메소드를 작성할수도 있다.
생성자 주입을 사용해야 하는 이유
1. 객체의 불변성 확보
- 실제로 개발을 하다 보면 의존 관계의 변경이 필요한 상황은 거의 없다. 하지만 수정자 주입이나 일반 메소드 주입을 이용하면 불필요하게 수정의 가능성을 열어 두기에 유지 보수성을 떨어뜨린다.
- 따라서 생성자 주입을 통해 변경 가능성을 배제하고 불변성을 보장하는 것이 좋다
2. 테스트 코드의 작성
- 테스트가 특정 프레임워크에 의존하는 것은 침투적이므로 좋지 못하다.
- 그러므로 가능한 순수 자바로 테스트를 작성하는 것이 가장 좋은데, 생성자 주입이 아닌 다른 주입으로 작성된 코드는 순수한 자바 코드로 단위 테스트를 작성하는 것이 어렵다.
- 생성자 주입을 사용하면 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 주입하는 객체가 누락된 경우 컴파일 시점에 오류를 발견할 수 있다.
- 또한 우리가 테스트를 위해 만든 Test객체를 생성자로 넣어 편리함을 얻을 수도 있다.
3. final 키워드 작성 및 Lombok과의 결합
- 생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있으며, 컴파일 시점에 누락된 의존성을 확인할 수 있다. 반면에 다른 주입 방법들은 객체의 생성(생성자 호출) 이후에 호출되므로 final 키워드를 사용할 수 없다.
- 또한 final 키워드를 붙이면 Lombok과 결합되어 코드를 간결하게 작성할 수 있다. Spring과 같은 DI 프레임워크는 Lombok과 환상적인 궁합을 보여주는데, 위에서 작성했던 생성자 주입 코드를 Lombok과 결합시키면 다음과 같이 간편하게 작성할 수 있다.
4. 스프링에 비침투적인 코드 작성
- 필드 주입을 사용하려면 @Autowired를 이용해야 하는데, 이것은 스프링이 제공하는 어노테이션이다. 그러므로 @Autowired를 사용하면 다음과 같이 UserService에 스프링 의존성이 침투하게 된다.
import org.springframework.beans.factory.annotation.Autowired;
// 스프링 의존성이 UserService에 import되어 코드로 박혀버림
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
- 우리가 사용하는 프레임워크는 언제 바뀔지도 모를 뿐만 아니라, 사용자와 관련된 책임을 지는 UserService에 스프링 코드가 박혀버리는 것은 바람직하지 않다.
- 프레임워크는 비즈니스 로직을 작성하는 서비스 계층에서 알아야 할 대상이 아니다. 물론 이는 필요한 자바 파일을 임포트해야 하는 정적 언어인 자바의 한계이기도 하다. 그래도 가능하다면 스프링이 없이 코드가 작성되면 더욱 유연한 코드를 확보하게 된다.
- 프레임워크가 자주 바뀌는 것도 아니므로 비록 스프링 코드가 침투하는게 치명적인 문제는 아니긴하다. 하지만 그래도 더 좋은 방법(생성자 주입)이 있는데, 굳이 사용할 필요는 없다.
5. 순환 참조 에러 방지
- 생성자 주입을 사용하면 애플리케이션 구동 시점(객체의 생성 시점)에 순환 참조 에러를 예방할 수 있다.
- 예를 들어 다음과 같이 필드을 사용해 서로 호출하는 코드가 있다고 하자.
@Service
public class UserService {
@Autowired
private MemberService memberService;
@Override
public void register(String name) {
memberService.add(name);
}
}
- UserSerivce가 이미 MemberService에 의존하고 있는데, MemberService 역시 UserService에 의존하는 것이다.
@Service
public class MemberService {
@Autowired
private UserService userService;
public void add(String name){
userService.register(name);
}
}
- 위의 두 메소드는 서로를 계속 호출할 것이고,
메모리에 함수의 CallStack이 계속 쌓여 StackOverflow 에러가 발생하게 된다.
- 만약 이러한 문제를 발견하지 못하고 서버가 운영된다면 어떻게 되겠는가?
해당 메소드의 호출 시에 StackOverflow 에러에 의해 서버가 죽게 될 것이다. 하지만 생성자 주입을 이용하면 이러한 순환 참조 문제를 방지할 수 있다.
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| memberService defined in file [C:\Users\Mang\IdeaProjects\build\classes\java\main\com\mang\example\user\MemberService.class]
↑ ↓
| userService defined in file [C:\Users\Mang\IdeaProjects\build\classes\java\main\com\mang\example\user\UserService.class]
└─────┘
- 애플리케이션 구동 시점(객체의 생성 시점)에 에러가 발생하기 때문이다.
- 그 이유는 Bean에 등록하기 위해 객체를 생성하는 과정에서 다음과 같이 순환 참조가 발생하기 때문이다.
- @Autowired를 이용한 필드 주입에서 이러한 문제가 애플리케이션 구동 시점에 에러가 발생하지 않는 이유는 빈의 생성과 조립(@Autowired) 시점이 분리되어 있기 때문이다.
- 생성자 주입은 객체의 생성과 조립(의존관계 주입)이 동시에 실행되다 보니 위와 같은 에러를 사전에 잡을 수 있다.
- 하지만 @Autowired는 모든 객체의 생성이 완료된 후에 조립(의존관계 주입)이 처리된다. 그러다 보니 위와 같이 호출이 되고 나서야 순환 이슈를 확인할 수 있는 것이다.
[ 생성자 주입 요약 맟 정리 ]
- 객체의 불변성을 확보할 수 있다.
- 테스트 코드의 작성이 용이해진다.
- final 키워드를 사용할 수 있고, Lombok과의 결합을 통해 코드를 간결하게 작성할 수 있다.
- 스프링에 침투적이지 않은 코드를 작성할 수 있다.
- 순환 참조 에러를 애플리케이션 구동(객체의 생성) 시점에 파악하여 방지할 수 있다.
참고문헌
728x90
'코딩 > JAVA' 카테고리의 다른 글
[JPA] 기본키 생성 전략 과 차이 (0) | 2023.10.07 |
---|---|
[Spring boot] Swagger API 연동하기 (0) | 2023.10.06 |
[Annotation] @CrossOrigin 이란? (0) | 2023.09.25 |
[Query_DSL] Query_DSL 이란??? (0) | 2023.09.25 |
[JPA_Query DSL] BooleanBuilder, BooleanExpression 차이 (0) | 2023.09.25 |