스프링 프로젝트를 진행 중 필드 주입이라는 편리한 의존관계 주입을 사용하다가 불편한 점이 발생하여 생성자 주입을 사용하는 이유를 알아보았습니다.
의존관계 주입의 종류
기본적으로 스프링에서는 아래와 같은 여러 종류의 의존관계 주입 방법을 지원합니다.
- 생성자 주입
- 수정자 주입
- 필드 주입
- 일반 메서드 주입
여기서 필자가 자주 사용하던 방식은 필드 주입 입니다.
필드 주입
@Component
public class UserService {
@Autowired
private UserDao userDao;
public void join() {
userDao.insert();
}
}
위와 같은 형태가 필드 주입 입니다.
필드 주입의 장점
필드 주입은 아주 간결합니다. 단순히 필드 선언부에 @Autowired라는 어노테이션만 달아주면 끝이기에 굉장히 편리하죠. 저도 이 편리성에 빠져 대부분 필드 주입을 사용하여 의존성을 주입하였습니다.
하지만 이렇게 필드 주입 을 사용하다보니 어느 순간 필드 주입의 단점을 알게되었습니다.
필드 주입의 단점
프로젝트를 진행하다 보면 테스트 코드를 작성하는 경우가 굉장히 빈번하게 발생합니다. 이때 필드 주입의 단점이 명확히 들어납니다. 아래의 코드를 통해 어떤 단점이 있는지 확인해보겠습니다.
class UserServiceTest {
@Test
void createOrder() {
// 테스트를 위한 커스텀한 UserDao 객체를 생성
UserDao myCustomUserDao = new UserDao();
myCustomUserDao.save(new User("홍길동", "남자", "경기도 의왕시 거주"));
// myCustomUserDao 객체를 UserService 의 userDao로 사용하기 위해서 주입하려하나 생성자 주입이 없기 때문에 주입할 방법이 없음!
UserService userService = new UserService();
}
}
위 코드를 보면 테스트 코드에서 myCustomUserDao 라는 테스트를 위한 임시 UserDao를 생성하는데 이 생성된 Dao를 UserService에 넣어줄 방법이 없습니다.
이게 필자가 겪은 필드 주입의 큰 단점입니다. 그렇다면 생성자 주입으로 위 단점이 해결될지 확인해보겠습니다.
생성자 주입
생성자 주입은 말 그대로 생성자를 통해 의존성을 주입하는 것 입니다. 예시는 아래와 같습니다.
@Component
public class UserService {
private UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void join() {
userDao.insert();
}
}
위와 같은 형태의 생성자 주입을 사용하면 테스트 코드에서 커스텀 객체를 주입할 수 없던 문제를 해결할 수 있습니다.
생성자가 단 하나일 경우 @Autowired 어노테이션 생략 가능
생성자 주입을 위해서는 생성자 위에 @Autowired 어노테이션을 명시해야합니다.
하지만 예외가 있습니다. 바로 생성자가 단 하나만 있을 경우입니다. 아래와 같이 class에 단 하나의 생성자만 존재할 경우 @Autowired 어노테이션의 생략이 가능합니다.
@Component
public class UserService {
private UserDao userDao;
// 단 하나의 생성자만 존재해야 @Autowired 생략 가능
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void join() {
userDao.insert();
}
}
생성자 주입을 이용한 필드 주입의 단점 해결
class UserServiceTest {
@Test
void createOrder() {
// 테스트를 위한 커스텀한 UserDao 객체를 생성
UserDao myCustomUserDao = new UserDao();
myCustomUserDao.save(new User("홍길동", "남자", "경기도 의왕시 거주"));
// myCustomUserDao 객체를 UserService 의 userDao로 사용하기 위해서 주입하려하나 생성자 주입이 없기 때문에 주입할 방법이 없음!
UserService userService = new UserService(myCustomUserDao);
}
}
위 코드와 같이 생성자 주입을 사용하면 간단하게 해결할 수 있습니다. 그런데 이렇게되면 편리하게 @Autowired 어노테이션 하나로 의존성 주입이 가능하던 필드 주입보다 생성자 코드를 추가하는 번거로움이 발생하였습니다.
또 만약에 아래 예시와 같이 생성자에 인자가 많다면 생성자가 너무 길어져서 코드를 작성하는데 큰 불편을 느끼게 될 겁니다.
@Component
public class UserService {
private UserDao userDao;
private A a = a;
private B b = b;
private C c = c;
private D d = d;
...
@Autowired
public UserService(UserDao userDao, A a, B b, C c, D d, ...) {
this.userDao = userDao;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
...
}
public void join() {
userDao.insert();
}
}
그렇다면 이 생성자 주입을 편리하게 사용하는 방법이 없을까요?
생성자 주입을 편리하게 사용하는 방법
이러한 불편함을 느낀 개발자가 한두명이 아닐겁니다. 그렇기 때문에 아래와 같은 편리한 방법으로 생성자 주입을 할 수 있는 기능이 생겨났습니다.
@Component
@RequiredArgsConstructor
public class UserService {
private final UserDao userDao;
private final A a = a;
private final B b = b;
private final C c = c;
private final D d = d;
...
public void join() {
userDao.insert();
}
}
위 코드와 같이 주입받을 변수를 final 로 선언하여 불변하도록 만들고 class 상단에 @RequiredArgsConstructor 어노테이션을 추가하는 것 입니다. 그렇게 되면 별도로 생성자 코드를 작성하지 않아도 의존성 주입이 가능해집니다.
생성자 코드가 없는데 어떻게 주입될까?
@RequiredArgsConstructor 어노테이션은 룸북이 지원하는 컴파일 타임 어노테이션으로 소스가 컴파일되는 시점에 저희가 수동으로 작성했던 생성자 코드를 자동으로 생성하여 끼워넣어 줍니다. 실제로 컴파일 완료 후 Class 파일을 열어보면 분명히 존재하지 않던 생성자 코드가 추가되어있는걸 확인할 수 있습니다.
그럼 final 은 왜 사용할까?
대부분의 스프링 빈끼리의 의존성 주입은 주입이 완료된 후에 빈이 바뀌는 경우가 거의 존재하지 않습니다. 또한 주입이 완료된 후 빈을 바꾸는 행위 자체를 권장하지 않습니다. 의도치않게 빈이 바뀌게 되면서 개발자가 의도하지 않던 동작이 이뤄질 수 있기 때문이죠. 그렇기 때문에 final 키워드는 생성자 주입 사용 시 필수로 붙이는게 좋습니다.
그럼 필드 주입 방식도 final 키워드를 사용하면 되나?
안타깝게도 생성자 주입 을 제외한 모든 주입 방식은 객체 생성 후 의존성이 주입되는 방식입니다 객체 생성이 완료된 후라는 시점에는 이미 객체의 필드 들의 값이 지정되었다는 소리고 그렇게되면 final 필드로 지정하였을 경우 의존성 주입 시점에는 필드의 값을 바꿀 수 없게됩니다. 그렇기 때문에 생성자 주입을 제외한 의존성 주입 방식들은 final 필드에 의존성 주입이 불가합니다
결론 : @RequiredArgsConstructor 를 통한 생성자 주입을 사용하자
결론적으로 @RequiredArgsConstructor를 이용한 생성자 주입 방식은 코드도 간결하면서 final 을 사용할 수 있는 등 여러 장점을 가집니다. 그렇기 때문에 특별한 상황이 아니라면 대부분 @RequiredArgsConstructor를 이용한 생성자 주입 방식 을 애용하는게 좋겠습니다