DI 파헤치기

GreatCloud·2025년 12월 17일

백엔드

목록 보기
6/14

Spring의 대표적인 DI 방식에는
생성자 주입(Constructor Injection) , 필드 주입(Field Injection), 세터 주입(Setter Injection)이 있다.
그러나 이런 것이 있고 사용하긴 하지만 정확하게 이해하고 있지는 않은 것 같기도 하고 호기심이 있기에 한번 파헤쳐 보려고 한다.
세가지의 DI방식을 JVM에서 실제로 어떻게 실행되는지 이해 하고,
그래서 대체 이걸 왜 쓰는데?에 대한 명확한 이유를 한번 알아보자

의존성 주입의 3가지 방식

일단 3가지 의존성 주입에 대해 명확히 알고가자

1. 필드 주입(Field Injection)

@Service
public class UserService {
    
    @Autowired
    private EmailService emailService;  // ← 필드에 직접 주입
    
    public void createUser(User user) {
        emailService.sendEmail(user.getEmail());
    }
}

Spring이 하는일:

  1. UserService 객체 생성 (new UserService)
  2. Reflection으로 emailService필드에 값 할당
   Field field = UserService.class.getDeclaredField("emailService");
   field.setAccessible(true);  // private 필드 접근 가능하게
   field.set(userServiceInstance, emailServiceInstance);

2. 세터 주입(Setter Injection)

@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이 하는일:

  1. UserService 객체 생성 (new UserService)
  2. setEmailService() 메서드 호출
userServiceInstance.setEmailService(emailServiceInstance);

3. 생성자 주입(Constructor Injection)

@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이 하는 일:

  1. 생성자 호출과 동시에 의존성 주입
   new UserService(emailServiceInstance);

참고
다른 생성자 없이@ReqiredArgsConstructor 사용하는 이유이며

Lombok의 @RequiredArgConstructor는 생성자를 만들어 주는 것이기 때문에 생성자 주입에 사용된다.
final, @NonNull = 반드시 초기화 되어야 하는 변수라는 의미 = required
final, @NonNull 없음 = NoRequired


그래서 왜 씀?(방식의 차이점)

1. 불변성

필드 주입/세터 주입

@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 키워드 = 한 번 할당하면 절대 변경 불가


2. 순환 참조 방지

필드 주입

@Service
public class UserService {
    @Autowired
    private OrderService orderService;
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;  // ← 순환 참조!
}

문제 발생!

  • 애플리케이션 시작됨(에러 감지가 안됨)
  • 순환 참조로 인한 런타임에 StackOverflowError 발생 가능

생성자 주입

@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

문제를 바로 발견할 수 있다.

3. 테스트 용이성

필드 주입

@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없이 순수 자바로 테스트가 가능하다


4. 필수 의존성 표현(코드 직관성)

필드 주입

@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;
    }
}

Lombok @ReqioredArgsConstructor

생성자 주입 코드를 더욱 간단하게!

기본 생성자 주입 작성

@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가 하는 일:

  • final 필드만 파라미터로 받는 생성자 자동 생성
  • @NonNull 필드도 포함
  • 생성자가 1개면 @Autowired 자동 적용

번외: 물론 예외도 있겠지?

예외 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이 아니고 불변성 보장 필요 없음

profile
모르는 것에 부끄러워 말고 아는 것에 자만하지 말 것

0개의 댓글