SPRING의 ioc(Inversion of Control) 와 DI

이아름·2025년 9월 14일

IOC(Inversion Of Control)

객체의 생성과 생명주기를 프레임워크(Spring IoC Container)가 담당하게 한다는 개념이다.

IOC를 사용하지 않을 경우 개발자가 new 키워드로 객체를 만들고 제어해야 했는데

// IOC 도입하지 않은 경우
public class TestService {
	private TestRepository repository = new TestRepository();
}

Spring에서는 IOC개념을 도입하여 객체 생성/주입/생명주기를 프레임워크가 관리한다.


//IOC 도입한 경우
@Component
public class TestRepository{
	// DB 접근 코드
}

@Component
public class TestService{
	private final TestRepository repository;
	
	// Spring의 생성자 주입
	@Autowired
	public TestService(TestRepository repository){
		this.repository = repository;
	}
}

이를 통해 코드의 결합도를 낮추고, 유지 보수성을 높인다.

DI(Dependency Injection)

객체 간의 의존성을 외부에서 주입하는 방식이다.

객체가 필요로하는 다른 객체를 외부(주로 컨테이너)로부터 주입받아 사용하는 개념이다.

DI는 IOC를 구현하는 하나의 방법이다.

DI의 주입 방식

DI 주입 방식에는 생성자 주입, Setter 주입, 필드 주입 3가지 방식이 있다.

생성자 주입

Spring에서 권장하는 방식

생성자의 호출 시점에 의존성을 주입한다.

@Component
public class OrderService {
    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

→ 생성자 호출 시점에 1회 호출되므로 주입받은 객체가 변하지 않고, 항상 객체가 주입된다.

Setter 주입

Setter 주입은 필드 값을 변경하는 Setter를 통해 의존관계를 주입하는 방법이다.

주입받는 객체가 런타임 시점에 변경되어야 할 때 사용한다.

@Component
public class OrderService {
    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}
  • Setter 주입으로 런타임 시점에 주입받는 객체가 변경되는 예시

// 부모
public interface MessageSender {
    void send(String msg);
}

// 자식 1
@Component
public class EmailSender implements MessageSender {
    @Override
    public void send(String msg) {
        System.out.println("[EMAIL] " + msg);
    }
}

// 자식 2
@Component
public class SmsSender implements MessageSender {
    @Override
    public void send(String msg) {
        System.out.println("[SMS] " + msg);
    }
}

EmailSender 객체와 SmsSender 객체가 MessageSender를 상속받는 상황일 때

@Component
public class NotificationService {
    private MessageSender messageSender;

		// Setter 주입
    @Autowired
    public void setMessageSender(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void notify(String msg) {
        messageSender.send(msg);
    }
}

Service에서 MessageSender로 Setter 주입을 선언하고

@Configuration
public class AppConfig {
    @Bean
    public NotificationService notificationService() {
        NotificationService service = new NotificationService();

        // 운영 환경: EmailSender 주입
        service.setMessageSender(new EmailSender());

        // 상황에 따라 의존성 교체 가능
        // service.setMessageSender(new SmsSender());

        return service;
    }
}

런타임 환경에서 세터 주입으로 주입하는 객체를 변경할 수 있다.

또한 Spring이 주입하는 것이 아니라 Setter를 통해 직접 객체를 주입하므로

테스트 환경에서도 실행 가능하다.

// 테스트용 Sender 클래스
@Component
public class FakeSender implements MessageSender {
    @Override
    public void send(String msg) {
        System.out.println("[FAKE] " + msg);
    }
}

// 테스트 코드
public class NotificationServiceTest {
    @Test
    public void testNotify() {
        NotificationService service = new NotificationService();
        service.setMessageSender(new FakeSender()); // 테스트 전용 객체 주입

        service.notify("테스트 메시지"); 
        // 출력: [FAKE] 테스트 메시지
    }
}

필드 주입

필드에 바로 의존 관계를 주입하는 방법이다.

@Component
public class OrderService {
    @Autowired
    private PaymentService paymentService;
}

필드 주입은 코드가 간단하지만 외부에서 접근이 불가능하기 때문에 필드의 객체를 수정할 수 없다.

반드시 DI 프레임워크(Spring Container)가 존재해야 하기에 사용을 지양하도록 권고한다.

필드 주입은 Spring이 직접 필드에 객체를 넣어주는 방식이기 때문에

외부에서 new를 하거나 테스트 코드에서 직접 주입하는 것이 불가능 하다.

public class ReportServiceTest {
    @Test
    public void test() {
        OrderService service = new OrderService ();
        service.generate(); // NullPointerException 발생!
    }
}

이런 식으로 테스트 환경에서는 Spring 컨테이너가 없으므로 의존성 주입이 되지 않기 때문에 NullPointerException이 발생한다.

때문에 필드 주입은 사용이 권장되지 않는다.

왜 생성자 주입을 권고하는가?

// 생성자 주입 코드
@Component
public class TestService{
	private final TestRepository repository;
	
	// Spring의 생성자 주입
	@Autowired
	public TestService(TestRepository repository){
		this.repository = repository;
	}
}
  • 필수 의존성이 null이 될 가능성이 없다.
    • 객체가 생성될 때 반드시 주입하기 때문에 이후 컴파일/런타임 시점에 오류가 발생하지 않는다.
  • final 키워드를 사용하기 때문에 한번 주입하면 변경되지 않는다.
  • DI 프레임워크(Spring 컨테이너) 없이 순수 자바로 테스트가 가능하다.

생성자 주입과 Setter 주입 테스트 코드 비교

  • 생성자 주입은 테스트 시 간결한 코드로 사용할 수 있다.

    • 단순하게 new 키워드 만으로 테스트가 가능하다.
    • 어떤 객체의 의존성이 필요한지 생성자만으로 확인 가능하다 (명확하다.)
  • 생성자 주입의 단위 테스트

// 생성자 코드
@Component
public class TestService{
	private final TestRepository repository;
	
	// Spring의 생성자 주입
	@Autowired
	public TestService(TestRepository repository){
		this.repository = repository;
	}
}

// 단위 테스트
public class ServiceTest {
    @Test
    public void test() {
        TestService service = new TestService(new TestRepository ());
        
        // 테스트 로
    }
}
  • Setter 주입일 경우 테스트 코드
// 테스트용 Sender 클래스
@Component
public class FakeSender implements MessageSender {
    @Override
    public void send(String msg) {
        System.out.println("[FAKE] " + msg);
    }
}

// 테스트 코드
public class NotificationServiceTest {
    @Test
    public void testNotify() {
        NotificationService service = new NotificationService();
        service.setMessageSender(new FakeSender()); // 테스트 전용 객체 주입

        service.notify("테스트 메시지"); 
        // 출력: [FAKE] 테스트 메시지
    }
}
  • 필드 주입일 경우 테스트 코드 실행 불가 → NullPointException

또한 생성자 주입은 순환 참조가 발생할 경우 스프링 컨텍스트 초기화 시점에 확인 할 수 있다.

@Component
public class AService {
    private final BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }
}

@Component
public class BService {
    private final AService aService;

    public BService(AService aService) {
        this.aService = aService;
    }
}

이렇게 A 서비스와 B 서비스가 서로를 참조하게 될 경우
순환 참조가 발생하고 StackOverFlowError 가 발생한다.

필드 주입 시 빈 생성 후 주입 시점에서 순환참조를 발생시킨다.

  • 순환참조 에레 메시지
Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  AService defined in file [C:\Users\Mang\IdeaProjects\build\classes\java\main\com\mang\example\user\MemberService.class]
↑     ↓
|  BServicedefined in file [C:\Users\Mang\IdeaProjects\build\classes\java\main\com\mang\example\user\UserService.class]
└─────┘

생성자 주입: 실행 타임라인

  1. Spring 컨테이너 시작 → 빈 스캔 → AService 빈 생성 시도
  2. AService 생성자에서 BService 필요
  3. Spring → BService 빈 생성 시도
  4. BService 생성자에서 AService 필요
  5. 다시 AService를 생성하려고 함 → 무한 루프 감지
  6. BeanCurrentlyInCreationException 발생 (컨텍스트 초기화 실패, 앱 실행 불가)

필드 주입: 실행 타임라인

  • Spring 컨테이너 시작 → 빈 스캔
  • AService 인스턴스 생성 (아직 bService 미주입)
  • BService 인스턴스 생성 (아직 aService 미주입)
  • 이제 @Autowired 주입 단계 실행
    • AServiceBService 넣으려 함
    • BService에 다시 AService 넣으려 함
    • 서로 주입하려다 순환 참조 무한 루프 발생
  • BeanCurrentlyInCreationException 또는 StackOverflowError 발생
profile
반갑습니다

0개의 댓글