[Spring] 의존성 주입(DI)과 생성자 주입 정리
망나니 개발자님의 블로그를 보고 제 생각을 첨가하여 정리한 글입니다.
Why
- 코드스쿼드 리뷰어
kyu
의 말씀으로 내가 그동안 “왜”를 너무 등한시했던 경향이 있었다는 것을 깨달았다.- 그 무엇하나 허투로 넘어가지 말자.
- 도저히 모르겠는 것들은 기록으로 남겨두더라도 절대 그냥 넘어가지는 말자.
- 의존성 주입 안에 내가 가장 궁금했던 생성자 주입이 있으니 단계적으로 정리해 보려 한다.
DI (Dependency Injection)
- Sprin 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데 그 중 하나가 DI다.
- DI를 의존성 주입이라 함
- Inversion of Control이라고도 함
- DI란 외부에서 두 객체간의 관계를 결정해주는 디자인 패턴
- 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고
- 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해줌
- DI를 의존성 주입이라 함
- 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 게 아닌 주입 받아 사용하는 방법을 말함
- 의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 함
- 아래와 같이 Store 객체가 Pencil 객체를 사용하고 있는경우 Store 객체가 Pencil 객체에 의존성이 있다 말함
public class Store {
private Pencil pencil;
}
- 두 객체간의 관계를 맺어주는 것을 의존성 주입이라 함
- 생성자 주입, 필드 주입, 수정자 주입 등 다양한 주입 방법이 있음
- Spring 4부터 생성자 주입을 강력 권장
의존성 주입 방법
생성자 주입
- 생성자 주입 (Constructor Injection)
- 생성자를 통해 의존 관계를 주입하는 방법
@Service
public class UserSerice {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
- 생성자 주입은 생성자의 호출 시점에 1회 호출이 보장됨
- 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우
- 이를 강제하기 위해 사용할 수 있음
- 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우
- Spring 프레임워크에서는 생성자 주입을 적극 지원하고 있음
- 그렇기 때문에 아래처럼 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능
- 내가 미션하면서 이해했던 거로는 생성자가 아니라 필드가 하나일 때 @AutoWired를 쓰지 않는 거였는데
- 사실은 생성자가 1개일 때는 생략해도 주입이 가능했던 거였고, 필드가 아니라 생성자 얘기였다.
- 그렇기 때문에 아래처럼 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능
@Service
public class UserSerice {
private UserRepository userRepository;
private MemberService memberService;
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
수성자 주입 (Setter 주입)
- 수성자 주입 (Setter Injection)
- 필드 값을 변경하는 Setter를 통해서 의존 관계를 주입하는 방법
- Setter 주입은 생성자 주입과는 다르게 주입받는 객체가 변경될 가능성이 있는 경우 사용
- 하지만, 실제로 변경이 필요한 경우는 극히 드묾
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberSerive;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
- @Autowired로 주입할 대상이 없는 경우에는 오류 발생
- 주입할 대상이 없어도 동작하도록 하려면
@Autowired(requied = false)
를 통해 설정 가능
- 주입할 대상이 없어도 동작하도록 하려면
- 스프링 초기에는 수정자 주입이 자주 사용됨
- 그 이유는 게터, 세터 등의 프로퍼티를 기반으로 하는 자바의 기본 스펙 때문
- 시간이 지나면서 점차 수정자 주입이 아닌 다른 방식이 주목받게 됨
- 이는 그동안 내가 수십번 들어왔던 게터와 세터를 쓰지 말라는 것과 일치한다.
- 이번 미션에서는 실력이 부족하지만 어쩔 수 없이 게터와 세터로 수정자 주입을 이용했지만
- 어느 정도 익숙해지면 최대한 쓰지 않도록 노력해보자!
필드 주입
- 필드 주입 (Field Injection)
- 필드에 의존 관계를 주입하는 방법
- 인텔리제이에서 Field Injection을 사용하면
Field injection is not recommended
이라는 경고 문구 발생- 이것이 여기에 첨부한 사진에 나와 있는 것처럼 내가 의존성 주입에 대해서 공부하게 된 계기다.
- 결국 이것이 궁금해서 여기까지 왔다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
- 필드 주입을 이용하면 코드가 간결해져서 과거에 상당히 이용되었던 주입 방법
- 하지만 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재
- 테스트 코드의 중요성이 부각됨에 따라 필드의 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않게 됨
- 또한 필드 주입은 반드시 DI 프레임워크가 존재해야 하므로, 사용을 지양해야 함
- 하지만 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재
- 애플리케이션의 실제 코드와 무관한 테스트 코드나 설정을 위해
- 불가피한 경우에만 이용
일반 메서드 주입
- 일반 메서드를 통해 의존 관계를 주입하는 방법
- 수정자 주입과 동일하며 마찬가지로 거의 사용할 필요가 없는 주입 방법
- 수정자 주입을 사용하면 한 번에 여러 필드를 주입받을 수 있도록 메서드를 작성할 수도 있음
- 수정자 주입과 동일하며 마찬가지로 거의 사용할 필요가 없는 주입 방법
Why 생성자 주입을 사용해야 하는가
- 최근, Spring을 포함한 DI 프레임워크의 대부분이 생성자 주입을 권장
객체의 불변성 확보
- 개발을 하다 보면 의존 관계의 변경이 필요한 상황은 거의 없음
- 하지만 수정자 주입이나 일반 메서드 주입을 이용하면 불필요하게 수정의 가능성을 열어두어
- 유지보수성을 떨어뜨림
- 하지만 수정자 주입이나 일반 메서드 주입을 이용하면 불필요하게 수정의 가능성을 열어두어
- 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋음
테스트 코드의 작성
- 테스트가 특정 프레임워크에 의존하는 것은 침투적이므로 좋지 않음
- 그러므로 순수 자바로 테스트를 작성하는 것이 가장 좋음
- 생성자 주입이 아닌 다른 주입으로 작성된 코드는 순수한 자바 코드로 단위 테스트를 작성하는 것이 어려움
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
public void register(String name) {
userRepository.add(name);
}
}
- 예를 들어 위 코드에 대해 순수 자바 테스트 코드를 작성하면 아래와 같이 작성할 수 있음
public class UserServiceTest {
@Test
public void addTest() {
UserService.userService = new UserService();
userService.register("Sully");
}
}
- 이 테스트 코드는 Spring 위에서 동작하지 않음
- 따라서 의존 관계 주입이 되지 않을 것이고
- userRepository가 null이 되어 add 호출 시 널 포인트 익셉션이 발생
- 이를 해결하기 위해 Setter를 사용하면 변경 가능성을 열어두게 되는 단점을 갖게 됨
- 반대로 테스트 코드에서 @Autowired를 사용하기 위해 스프링을 사용하면
- 단위 테스트가 아닐 뿐만 아니라
- 컨포넌트들을 등록하고 초기화하는 시간 때문에 비용이 증가하게 됨
- 대안으로 Reflection을 사용하면 깨지기 쉬운 테스트가 됨
- 이것도 몇 주 전 미션인 사다리 게임에서 내가 직접 맞닥뜨린 문제였다.
- 객체지향적으로 설계를 해본 적이 없어서 public 메서드 안에 private 메서드가 주렁주렁 달린 형태였는데
- 이 private 메서드를 테스트하기 위해 Reflection을 사용했었다.. 다신 사용하지 말아야지
- 단위 테스트가 아닐 뿐만 아니라
- 반면에 생성자 주입을 사용하면
- 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있고
- 주입하는 객체가 누락된 경우 컴파일 시점에 오류를 발견할 수 있음
- 심지어 테스를 위해 만든 Test 객체를 생성자에 넣어 편리함을 얻을 수도 있음
final 키워드 작성 및 Lombok과의 결합
- 생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있으며
- 컴파일 시점에 누락됨 의존성을 확인할 수 있음
- 반면에 다른 주입 방법들은 객체의 생성(생성자 호출) 이후에 호출되므로
- final 키워드를 사용할 수 있음
- 또한 final 키워드를 붙이면 Lombok과 결합되어 코드를 간결하게 작성할 수 있음
- Lombok에는 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor가 있음
- 이번 미션에서는 호눅스가 Lombok을 사용하지 말라고 하셨기 때문에 나중에 이 내용에 대해서 읽어보자
- Lombok에는 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor가 있음
- Spring과 같은 DI 프레임워크는 Lombok과의 환상적인 궁합을 보여줌
- 위에서 작성했던 생성자 주입 코드를 Lombok과 결합시키면 아래와 같이 간편하게 작성할 수 있음
@Service
@RequiredArgsConstructor
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
public void register(String name) {
userRepository.add(name);
}
}
- 이러한 코드가 가능한 이유는
- 생성자가 1개인 경우 @Autowired를 생략할 수 있도록 하고
- 해당 생성자를 Lombok으로 구현했기 때문
- 이 부분도 Lombok을 사용할 수있을 때 여기에서 읽어 보자!
스프링에 비침투적인 코드 작성
- 필드 주입을 용하려면 @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에 스프링 코드가 박혀버리는 것은 바람직하지 않음
- 프레임워크는 비즈니스 로직을 작성하는 서비스 계층에서 알아야 할 대상이 아님
- 물론 이는 필요한 자바 파일을 import 해야 하는 정적 언어인 자바의 한계이기도 함
- 그래도 가능하다면 스프링을 사용하지 말고 코드를 작성해보자.
- 그럼 더 유연한 코드가 확보되게 된다고 하니까 👀
- 프레임워크가 자주 바뀌는 것도 아니므로 비록 스프링 코드가 침투하는 게 치명적인 문제가 아니긴 함
- 그래도 생성자 주입 같은 더 좋은 방법이 있으니까 굳이 사용하지 말자!
순환 참조 에러 방지
- 생성자 주입을 사용하면
- 애플리케이션 구동 시점(객체의 생성 시점)에 순환 참조 에러를 예방할 수 있음
- 예를 들어 아래와 같이 필드를 사용하여 서로 호출하는 코드를 보자
@Service
public class UserService {
@Autowired
private MemberService memberService;
@Override
public void register(String name) {
userRepository.add(name);
}
}
@Service
public class MemberService {
@Autowired
private UserService memberService;
@Override
public void add(String name) {
userRepository.register(name);
}
}
- UserService가 이미 MemberService에 의존하고 있고
- MemberService 역시 UserService에 의존하고 있음
- 이 두 메서드는 서로를 계속 호출할 것이고
- 메모리에 함수의 CallStack이 계속 쌓여 StackOverFlow 애러가 발생하게 됨
- 만약 이러한 문제를 발견하지 못하고 서버가 운영된다면
- 해당 메서드 호출 시에 StackOverFlow 애러에 의해 서버가 죽게 됨
- 하지만 생성자 주입을 이용하면 순환 참조 문제를 방지할 수 있음
- 애플리케이션 구동 시점(객체의 생성 시점)에 에러가 발생하는 것인데,
- Bean에 등록하기 위해 객체를 생성하는 과정에서 아래와 같은 순환 참조 문제가 발생하는 것
new UserService(new MemberService(new UserService(new MemberService()...)))
- @Autowired를 이용한 필드 주입에서 위와 같은 문제가 애플리케이션 구동 시점에 발생하지 않는 이유는
- Bean의 생성과 조립(@Autowired) 시점이 분리되어 있기 때문
- 생성자 주입은 객체의 생성과 조립(의존관계 주입)이 동시에 실행되다 보니
- 위와 같은 에러를 사전에 잡을 수 있음
- 하지만 @Autowired는 모든 객체 생성이 완료된 이후에 조립(의존관계 주입)이 처리됨
- 즉, 위와 같은 호출이 되고 나서 순환 이슈를 확인할 수 있는 것
- 참고로 SpringBoot 2.6 버전부터 순환 참조가 기본적으로 허용되지 않도록 변경됨
- 필드 주입을 받아도 순환 참조가 발생한다면 애플리케이션 로딩 시점에 에러가 발생하므로
- 위와 같은 에러는 SpringBoot 2.6 이전의 버전을 사용하는 경우에만 발생할 것임
- 필드 주입을 받아도 순환 참조가 발생한다면 애플리케이션 로딩 시점에 에러가 발생하므로
생성자 주입 요약 및 정리
- 객체의 불변성을 확보할 수 있음
- 테스트 코드의 작성이 용이해짐
- final 키워드를 사용할 수 있고
- Lombok과의 결합을 통해 코드를 간결하게 작성할 수 있음
- 스프링에 침투적이기 않은 코드를 작성할 수 있음
- 순환 참조 에러를 애플리케이션 구동(객체의 생성) 시점에 파악하여 방지할 수 있음
kyu
의 단 한줄의 질문으로 여기까지 공부할 수 있었다.- 이 내용 때문에 생성자 주입에 대해서 완벽하게는 아니어도 어느정도는 이해할 수 있었던 것 같다.
- 생성자 주입이라는 키워드와 그 의미를 계속 상기시키면서 내 프로젝트에 적용할 수 있도록 노력해보자 💪🏻
Reference
- https://mangkyu.tistory.com/150
- https://mangkyu.tistory.com/125
- https://devlog-wjdrbs96.tistory.com/165
Leave a comment