Spring DI 세 가지 방식: 생성자·세터·필드 주입 완전 비교

생성자 주입, 세터 주입, 필드 주입의 동작 원리와 장단점을 코드로 비교하고, 생성자 주입이 권장되는 이유를 순환 참조 감지·불변성·테스트 관점에서 심층 분석합니다.

· 8 min read · PALDYN Team

지난 글에서는 컴포넌트 스캔이 클래스패스를 순회하며 빈을 자동 등록하는 과정을 살펴봤습니다. 이번에는 IoC 컨테이너가 빈을 생성한 뒤 의존 관계를 어떤 방식으로 연결하는지, 즉 의존성 주입(Dependency Injection)의 세 가지 전략을 집중적으로 파고듭니다.

Spring은 세 가지 방법으로 의존성을 주입할 수 있습니다. 생성자 주입(Constructor Injection), 세터 주입(Setter Injection), 그리고 **필드 주입(Field Injection)**입니다. 셋 모두 @Autowired(또는 생략 가능한 경우)를 활용하지만 동작 시점·불변성 보장·테스트 용이성에서 뚜렷한 차이를 보입니다.

생성자 주입 — 가장 권장되는 방식

생성자 주입은 빈이 생성되는 시점에 의존 객체를 한 번에 받아 final 필드에 저장합니다. Spring 4.3부터는 생성자가 하나뿐이면 @Autowired를 생략할 수 있고, Lombok의 @RequiredArgsConstructor를 사용하면 보일러플레이트를 거의 없앨 수 있습니다.

@Service
@RequiredArgsConstructor          // Lombok: final 필드 생성자 자동 생성
public class OrderService {

    private final ItemRepository itemRepository;   // 불변
    private final StockService   stockService;

    public void placeOrder(Long itemId, int qty) {
        Item item = itemRepository.findById(itemId)
                .orElseThrow(ItemNotFoundException::new);
        stockService.decrease(item, qty);
    }
}

final 선언 덕분에 한 번 주입된 의존 객체는 교체될 수 없습니다. 또한 컨테이너는 ApplicationContext 초기화 단계에서 모든 생성자 파라미터를 충족시킬 수 없으면 즉시 BeanCurrentlyInCreationException을 던집니다. 순환 참조나 누락된 빈을 운영 투입 전에 발견할 수 있는 이유가 바로 이것입니다.

의존성 주입 세 가지 방식 비교

단위 테스트도 매우 간단합니다. 스프링 컨테이너 없이 new로 직접 생성할 수 있기 때문입니다.

// 스프링 컨텍스트 불필요 — 순수 JUnit 테스트
class OrderServiceTest {

    ItemRepository mockRepo  = Mockito.mock(ItemRepository.class);
    StockService   mockStock = Mockito.mock(StockService.class);

    OrderService sut = new OrderService(mockRepo, mockStock);

    @Test
    void placeOrder_decreasesStock() {
        given(mockRepo.findById(1L)).willReturn(Optional.of(new Item(1L, "책", 10)));
        sut.placeOrder(1L, 3);
        then(mockStock).should().decrease(any(), eq(3));
    }
}

세터 주입 — 선택적 의존에 적합

세터 주입은 @Autowired가 붙은 setter 메서드를 통해 의존 객체를 주입합니다. 빈이 먼저 기본 생성자로 만들어진 다음, setter가 호출되므로 의존 객체 없이도 빈 생성 자체는 가능합니다. 이 특성 덕분에 선택적(optional) 의존성에 어울립니다.

@Service
public class NotificationService {

    private EmailSender emailSender;

    @Autowired(required = false)   // 빈이 없어도 무시
    public void setEmailSender(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void notify(String msg) {
        if (emailSender != null) {
            emailSender.send(msg);
        }
    }
}

required = false로 설정하면 EmailSender 빈이 등록되어 있지 않아도 예외 없이 기동됩니다. 단, final을 사용할 수 없어 불변성을 보장하지 못하며, 순환 참조가 런타임까지 드러나지 않을 수 있습니다.

필드 주입 — 간편하지만 지양

필드 주입은 리플렉션으로 private 필드에 직접 값을 밀어 넣습니다. 코드가 가장 짧지만 단점이 많습니다.

@Service
public class PaymentService {

    @Autowired                  // 리플렉션으로 private 필드에 주입
    private PaymentRepository paymentRepository;

    @Autowired
    private AuditService auditService;
}
  • 테스트 어려움: new PaymentService()로 생성하면 필드가 null이어서 Mockito @InjectMocks 같은 리플렉션 도구 없이 테스트할 수 없습니다.
  • final 불가: 주입 후 언제든 교체 가능한 상태가 됩니다.
  • IDE 경고: IntelliJ가 “Field injection is not recommended”를 경고합니다.
  • 숨겨진 의존성: 생성자를 보는 것만으로 의존 관계 파악이 불가능합니다.

순환 참조와 생성자 주입의 방어

생성자 주입의 가장 큰 장점 중 하나는 순환 참조의 조기 감지입니다.

생성자 주입과 순환 참조 감지

ServiceAServiceBServiceA 구조가 있을 때, 생성자 주입이라면 컨테이너가 ServiceA를 만들기 위해 ServiceB를 요청하고, ServiceB를 만들기 위해 다시 ServiceA를 요청하면서 스택이 한없이 깊어집니다. Spring은 이 상황을 감지해 애플리케이션 기동 단계에서 BeanCurrentlyInCreationException을 던집니다.

반면 세터·필드 주입은 빈 생성 후 주입이 이루어지므로 두 빈 모두 일단 만들어진 뒤 서로를 가리키게 됩니다. 순환 참조가 숨어있는 채로 운영 환경에 배포될 위험이 있습니다.

[생성자 주입 + 순환 참조 — 기동 시 즉시 실패]
***************************
APPLICATION FAILED TO START
***************************
The dependencies of some of the beans in the application
context form a cycle:
orderService → stockService → orderService

Spring Boot 2.6부터는 기본값으로 순환 참조를 금지하며(spring.main.allow-circular-references=false), 이를 허용하더라도 생성자 주입은 여전히 시작 시점에 감지합니다.

세 가지 방식 선택 가이드

상황추천 방식
필수 의존성 (대부분)생성자 주입
선택적 의존성 (플러그인, 옵셔널 기능)세터 주입
레거시 코드, 빠른 프로토타입필드 주입 (단, 점진적 교체 권장)

실무에서는 @RequiredArgsConstructor를 표준으로 사용하고, 선택적 의존에만 @Autowired(required=false) setter를 추가하는 패턴이 가장 많습니다.

정리

  • 생성자 주입: final 필드, 불변성, 조기 오류 감지, 테스트 용이 → 항상 우선 고려
  • 세터 주입: 선택적 의존성 표현, required=false 활용
  • 필드 주입: 코드가 짧지만 테스트·불변성·가시성 모두 손해 → 신규 코드에서 지양

다음 글에서는 같은 타입의 빈이 여러 개일 때 어떻게 하나를 선택하는지, @Qualifier@Primary 전략을 다룹니다.


지난 글: 컴포넌트 스캔: Spring이 빈을 자동 탐색하는 원리

다음 글: @Autowired와 @Qualifier: 빈 선택 전략 완전 정리


읽어주셔서 감사합니다. 😊