Spring의 대표적인 DI 방식에는
생성자 주입(Constructor Injection) , 필드 주입(Field Injection), 세터 주입(Setter Injection)이 있다.
그러나 이런 것이 있고 사용하긴 하지만 정확하게 이해하고 있지는 않은 것 같기도 하고 호기심이 있기에 한번 파헤쳐 보려고 한다.
세가지의 DI방식을 JVM에서 실제로 어떻게 실행되는지 이해 하고,
그래서 대체 이걸 왜 쓰는데?에 대한 명확한 이유를 한번 알아보자
일단 3가지 의존성 주입에 대해 명확히 알고가자
@Service
public class UserService {
@Autowired
private EmailService emailService; // ← 필드에 직접 주입
public void createUser(User user) {
emailService.sendEmail(user.getEmail());
}
}
Spring이 하는일:
UserService 객체 생성 (new UserService)emailService필드에 값 할당 Field field = UserService.class.getDeclaredField("emailService");
field.setAccessible(true); // private 필드 접근 가능하게
field.set(userServiceInstance, emailServiceInstance);
@Service
public class UserService {
private EmailService emailService;
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
public void createUser(User user) {
emailService.sendEmail(user.getEmail());
}
}
Spring이 하는일:
UserService 객체 생성 (new UserService)setEmailService() 메서드 호출userServiceInstance.setEmailService(emailServiceInstance);
@Service
public class UserService {
private final EmailService emailService; // ← final 가능!
@Autowired // 생성자가 1개면 생략 가능
public UserService(EmailService emailService) {
this.emailService = emailService;
}
public void createUser(User user) {
emailService.sendEmail(user.getEmail());
}
}
Spring이 하는 일:
new UserService(emailServiceInstance);
참고
다른 생성자 없이@ReqiredArgsConstructor만 사용하는 이유이며
Lombok의 @RequiredArgConstructor는 생성자를 만들어 주는 것이기 때문에 생성자 주입에 사용된다.
final, @NonNull = 반드시 초기화 되어야 하는 변수라는 의미 = required
final, @NonNull 없음 = NoRequired
필드 주입/세터 주입
@Service
public class UserService {
@Autowired
private EmailService emailService; // ← 변경 가능
public void someMethod() {
this.emailService = null; // ✅ 가능 (위험!)
}
}
생성자 주입
@Service
public class UserService {
private final EmailService emailService; // ← final = 불변
public UserService(EmailService emailService) {
this.emailService = emailService;
}
public void someMethod() {
this.emailService = null; // ❌ 컴파일 에러!
}
}
final 키워드 = 한 번 할당하면 절대 변경 불가
필드 주입
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
@Service
public class OrderService {
@Autowired
private UserService userService; // ← 순환 참조!
}
문제 발생!
생성자 주입
@Service
public class UserService {
private final OrderService orderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
}
@Service
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) { // ← 순환 참조!
this.userService = userService;
}
}
얘는 괜찮음
The dependencies of some of the beans in theapplication context form a cycle: userService -> orderService -> userService
문제를 바로 발견할 수 있다.
필드 주입
@Service
public class UserService {
@Autowired
private EmailService emailService;
public void createUser(User user) {
emailService.sendEmail(user.getEmail());
}
}
// 테스트
public class UserServiceTest {
@Test
void testCreateUser() {
UserService userService = new UserService();
// emailService가 null 어떻게 주입하지?;;
// Reflection으로 주입해야 하나? (복잡함)
userService.createUser(user); // NullPointerException!
}
}
생성자 주입
@Service
public class UserService {
private final EmailService emailService;
public UserService(EmailService emailService) {
this.emailService = emailService;
}
public void createUser(User user) {
emailService.sendEmail(user.getEmail());
}
}
// 테스트
public class UserServiceTest {
@Test
void testCreateUser() {
EmailService mockEmailService = mock(EmailService.class);
UserService userService = new UserService(mockEmailService); // mock 사용으로 간단!
userService.createUser(user);
verify(mockEmailService).sendEmail(user.getEmail());
}
}
Spring없이 순수 자바로 테스트가 가능하다
필드 주입
@Service
public class UserService {
@Autowired
private EmailService emailService; // ← 필수인지 선택인지 불명확
@Autowired(required = false)
private SmsService smsService; // ← 선택적
}
명확하게 어떤게 필수인지 알기가 어렵다.
생성자 주입
@Service
public class UserService {
private final EmailService emailService; // ← final = 필수
private final SmsService smsService; // ← final = 필수
public UserService(EmailService emailService, SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
}
생성자 파라미터 또는 선언된 변수를 보면 필수 의존성이 명확하게 보인다.
또한 선택적 의존성 명확하게 파악가능
@Service
public class UserService {
private final EmailService emailService; // 필수
private SmsService smsService; // 선택적 (final 없음)
public UserService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
}
기본 생성자 주입 작성
@Service
public class UserService {
private final EmailService emailService;
private final SmsService smsService;
private final PaymentService paymentService;
public UserService(EmailService emailService,
SmsService smsService,
PaymentService paymentService) {
this.emailService = emailService;
this.smsService = smsService;
this.paymentService = paymentService;
}
}
문제점: 의존성의 갯수 만큼 생성자 코드가 배로 늘어난다.
Lombok @ReqioredArgsConstructor 사용
@Service
@RequiredArgsConstructor // ← final 필드의 생성자 자동 생성
public class UserService {
private final EmailService emailService;
private final SmsService smsService;
private final PaymentService paymentService;
// 생성자 코드 자동 생성됨!
}
@RequiredArgsConstructor가 하는 일:
예외 1: 테스트 코드
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService; // ← 테스트에서는 OK
@MockBean
private EmailService emailService; // ← 테스트에서는 OK
@Test
void testCreateUser() {
// ...
}
}
테스트에는 불변성이 그렇게 중요하지 않을 수도 있다
예외 2: @ConfigurationProperties
@ConfigurationProperties(prefix = "app")
public class AppProperties {
@Autowired
private String name; // ← 설정값 주입은 필드 주입 OK
@Autowired
private int timeout;
}
설정값은 Bean이 아니고 불변성 보장 필요 없음