티스토리 뷰

IT/Spring

Spring의 DI 방식 완전 정복 - 생성자 / 필드 / 세터 주입, 그리고 언제 어떻게 써야 하는가?

Stv 2025. 5. 13. 19:53

목차


    1. DI란 무엇인가?

    "DI(Dependency Injection, 의존성 주입)"은 객체가 의존하는 객체를 직접 생성하지 않고, 외부에서 주입 받는 설계 방식이다.

     

    • DI는 DIP(의존 역전 원칙)을 실현하는 핵심 수단이며
    • Spring의 핵심 원리 중 하나인 IoC(제어의 역전)를 구체적으로 구현하는 방법이기도 하다.

    2. Spring에서 DI는 어떻게 동작하는가?

    Spring은 ApplicationContext가 Bean을 생성하고,
    의존성 있는 객체를 자동으로 찾아서 주입한다.
    Bean 등록 방식에 따라 주입 방식이 다르게 동작할 수 있다.
    • 컴포넌트 스캔 기반 (@Component, @Service, @Repository)
    • Java Config 기반 (@Bean 메서드)
    • @Autowired나 여러 Bean이 있을 때
      • 기본적으로 Spring은 ApplicationContext 초기화 시 Bean을 생성하고, @Autowired, 생성자 주입 등을 통해 타입 기반으로 의존성을 자동 주입한다.
      • 주입 대상 Bean이 1개뿐이면 그대로 주입되며, 2개 이상일 경우네느 @Primary, @Qualifier 이름 매칭 등을 통해 충돌을 해결한다.
      • @Primary : 우선순위를 지정해 기본 후보로 등록
      • @Qualifier("beanName") : 명확히 특정 Bean을 선택
      • @Autowired(required = false) -> Optional 의존성 처리
      • 모든 Bean들은 컨테이너에 의해 생성되고 관리되며, 주입 시점에는 이 컨텍스트 내에서 주입 대상이 결정된다.

    3. DI의 3가지 주요 방식

    ✅ 1) 생성자 주입 (Constructor Injection)

    @Component
    public class OrderService {
    
        private final OrderRepository orderRepository;
    
        @Autowired // 생략 가능 (Spring 4.3+)
        public OrderService(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
    }

    📌특징

    • 불변성 보장 (final 필드 가능)
    • 주입 대상이 반드시 필요한 경우에 적합
    • 테스트 코드에서 명시적 주입이 쉬움
    • 순환 참조 문제가 발생하면 명확히 드러남

    ✅ 가장 권장되는 방식

     

    ✅ 2) 필드 주입 (field Injection)

    @Component
    public class OrderService {
    
        @Autowired
        private OrderRepository orderRepository;
    }

    📌특징

    • 코드 간결하고 직관적
    • 테스트/유닛테스트 불가능 (필드 주입은 리플렉션 없이는 변경 불가)
    • 의존성이 보이지 않음 -> 클래스 간 관계가 명확하지 않음
    • 스프링 컨테이너가 없으면 객체 자체가 무용지물

    🚫 실무에선 지양

     

    ✅ 3) 세터 주입 (Setter Injection)

    @Component
    public class OrderService {
    
        private OrderRepository orderRepository;
    
        @Autowired
        public void setOrderRepository(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
    }

    📌특징

    • 선택적 의존성 주입 가능
    • 라이브러리처럼 초기 상태 설정이 필요한 객체에 적합
    • 테스트 용이성은 있으나, 주입 순서 보장 어려움

    ⚠️ 설정용, 보조적인 주입에 사용 권장

     

    📊 4. 3가지 주입 방식 비교 요약

    구분 생성자 주입 필드 주입 세터 주입
    선언 위치 생성자 필드 Setter 메서드
    불변성 보장 ✅ (final 사용 가능)
    테스트 용이성
    스프링 외 사용
    선택적 주입
    의존성 명확성 중간
    실무 권장도 ⭐ ⭐ ⭐ ⭐ ⭐ ⭐

     

    5. 언제 어떤 방식을 선택해야 할까?

    상황 추천 방식 이유
    필수 의존성 생성자 주입 불변성 보장, 테스트 쉬움
    선택 의존성 세터 주입 필요할 때만 설정
    설정/초기화 목적 세터 주입 DSL이나 설정용
    실습/테스트 간편 필드 주입 간단히 확인 가능 (단, 실무 지양)

     

    6. 실무 팁 : Lombok과 생성자 주입

    @RequiredArgsConstructor
    @Service
    public class OrderService {
        private final OrderRepository orderRepository;
    }
    • @RequiredArgsConstructor -> final 필드 기반 생성자 자동 생성
    • @Autowired 없어도 자동 주입 (Spring 4.3+)
    • 가장 현업에서 많이 사용하는 패턴

    ⚠️7. 순환 참조와 DI

    예: 서로가 서로를 주입하면?

    @Component
    public class A {
        public A(B b) { ... }
    }
    
    @Component
    public class B {
        public B(A a) { ... }
    }
    • 생성자 주입에서는 BeanCurrentlyInCreationException
    • 해결 : 필드 주입/세터 주입 or 설계 변경 필요

    ✅마무리 요약

    • DI는 객체 간결합을 낮추고, 테스트와 확장성을 확보하는 핵심 설계 기법
    • 생성자 주입은 필수 의존성, 세터 주입은 선택적 의존성, 필드 주입은 실습용
    • Spring은 DI 기반 설계를 통해 DIP, OCP 원칙을 자연스럽게 실현
    • 실무에서는 Lombok + 생성자 주입 조합이 가장 많이 쓰임

    🔜 다음 포스팅에서는 Spring AOP 깊이 파기 - 핵심 로직과 횡단 관심사의 완전 분리, 그리고 실무 적용 예제라는 주제로 내용을 다뤄보도록 하겠다.