나는 주로 Spring에서 Dependency를 Injection 받을 때 @RequiredArgsConstructor
를 통한 방법을 주로 사용했다.
이 때 정확히 어떤 원리를 통해서 DI가 되는 것인지에 대해서는 잘 알아보지 않고 사용을 했었는데, 이번에 @RequiredArgsConstructor
와 Spring의 DI 방식에 대해서 함께 알아 가보고자 한다.
자세한 내용들을 살펴보기 전에 @RequiredArgsConstructor
어노테이션에 대해서 먼저 알아보면 좋을 것 같다.
@RequiredArgsConstructor 어노테이션은
final
이 붙거나@Noutnull
이 붙은 필드의 생성자를 자동으로 생성해주는 Lombok 어노테이션이다.
아래 코드에 대해서 살펴보면 도움이 될 것이다.
아래는 내가 강아지 산책 플랫폼을 개발하면서 사용한 일부 코드이다.
@Service
@RequiredArgsConstructor
public class WalkService {
private final WalkRepository walkRepository;
private final MemberJpaRepository memberJpaRepository;
private final MatchingRepository matchingRepository;
private final PaymentRepository paymentRepository;
// 아래 코드는 생략
}
위 코드를 컴파일하면 아래와 같은 코드가 된다.
@Transactional(
readOnly = true
)
@Service
public class MemberService {
private static final Logger log = LoggerFactory.getLogger(MemberService.class);
private final MemberJpaRepository memberJpaRepository;
private final RefreshTokenJpaRepository refreshTokenJpaRepository;
private final PaymentRepository paymentRepository;
private final WalkRepository walkRepository;
private final DogJpaRepository dogJpaRepository;
private final ApplicationRepository applicationRepository;
private final NotificationJpaRepository notificationJpaRepository;
private final ReviewRepository reviewRepository;
public MemberService(final MemberJpaRepository memberJpaRepository, final RefreshTokenJpaRepository refreshTokenJpaRepository, final PaymentRepository paymentRepository, final WalkRepository walkRepository, final DogJpaRepository dogJpaRepository, final ApplicationRepository applicationRepository, final NotificationJpaRepository notificationJpaRepository, final ReviewRepository reviewRepository) {
this.memberJpaRepository = memberJpaRepository;
this.refreshTokenJpaRepository = refreshTokenJpaRepository;
this.paymentRepository = paymentRepository;
this.walkRepository = walkRepository;
this.dogJpaRepository = dogJpaRepository;
this.applicationRepository = applicationRepository;
this.notificationJpaRepository = notificationJpaRepository;
this.reviewRepository = reviewRepository;
}
// 아래 코드는 생략
}
이 코드들을 통해서 MemberService class의 final로 선언된 변수들에 대하여 @RequiredArgsConstructor 어노테이션이 생성자를 만들어주는 모습을 확인할 수 있다.
그렇다면 여기서 눈썰미가 좋은 사람이면 눈치 챌 수 있는 부분이 있다.
해당 생성자로 Spring이 어떻게 DI를 해주는걸까?
Spring은 @Autowired
가 되어있는 생성자에 대하여 DI를 해준다.
그러나 위 생성자는 @Autowired가 되어있지 않다.
이 비밀은 바로 Spring에 있다.
Spring 프레임워크는 3.4ver 부터 클래스에 1개뿐인 생성자에 대하여 @Autowired가 없을 경우, 해당 생성자에 대하여 자동으로 @Autowired로 인식하여 처리한다.
즉 생성자가 1개라면 생성자에 @Autowired를 생략해도 된다는 뜻이다.
@RequiredArgsConstructor 어노테이션은 이러한 Spring의 특징을 더 효율적, 효과적으로 이용할 수 있는 어노테이션이라 할 수 있다.
만약 @RequiredArgsConstructor 어노테이션이 없었다면 생성자를 직접 작성하고, 필드 변경시 매번 생성자도 변경을 해줘야 하는 리소스가 소모 되었을 것이다.
여기까지 Spring과 @RequiredArgsConstructor 어노테이션의 장점에 대해서 찬양을 하는 시간을 가졌다.
좋다.
그러나 더 깊게 생각을 해서, 왜 우리는 생성자 주입을 통한 DI를 사용해야 할까?
다들 알다시피 DI를 하는 방법은 @Autowired를 사용한 필드 주입이나 Setter를 이용한 주입 방법도 있다.
그런데 왜 생성자 주입을 다들 권장할까?
가장 첫번째 장점으로는 우리가 위에서 이야기 한 부분이 있다.
생성자 주입을 사용하게 된다면 필드에 final 키워드를 사용할 수 있기에 객체의 불변성을 확보할 수 있고, 컴파일 시점에 누락된 의존성을 확인할 수 있다.
→ 다른 주입 방법들은 객체의 생성 이후에 호출되기에 final 키워드를 사용할 수 없다.
또한 Lombok의 어노테이션을 사용하여 코드를 더욱 쉽고, 간결하게 만들 수 있는 장점도 있다.
만약 Setter를 통한 수정자 주입을 사용하게 된다면, 객체 생성 이후에 해당 의존성이 변경될 수 있는 가능성이 열려있게 된다.
만약 의존성이 변경되지 않더라도, 이러한 변경 가능성은 유지보수성을 떨어트린다.
만약 @Autowired를 통한 필드 주입을 사용한다고 한다고 해보자.
@Service
public class WalkService {
@Autowired
private WalkRepository walkRepository;
@Autowired
private MemberJpaRepository memberJpaRepository;
@Autowired
private MatchingRepository matchingRepository;
@Autowired
private PaymentRepository paymentRepository;
// 아래 코드는 생략
}
그렇다면 위와 같은 코드가 될 것이다.
public class WalkServiceTest {
@Test
public void test() {
WalkService walkService = new WalkService();
// 아래 코드는 생략
}
}
위의 코드를 테스트할 때는 Spring 위에서 동작하지 않는다.
이 말은 즉 @Autowired를 통해 DI를 받을 수 없다는 말이 된다.
그렇기에 의존 객체들을 사용하고자 할 때 NPE가 발생할 것이다.
그러나 만약 생성자 주입을 사용한다면 컴파일 시점에 객체를 주입 받아 테스트 코드를 작성할 수 있고, 주입하는 객체가 누락된 경우에 컴파일 시점에서 오류를 발견할 수도 있게 된다.
[Spring] 스프링(Spring)과 롬복(Lombok)의 결합, @RequiredArgsConstructor를 사용한 생성자 주입