[SpringBoot] IoC와 DI

김선형·2025년 9월 8일

Java

목록 보기
14/27

IoC (Inversion of Control, 제어의 역전)

객체의 생성, 생명주기 관리, 의존성 주입 등을 개발자가 직접하지 않고, 컨테이너가 대신 관리해주는 원리이다. 전통적 방식인 개발자 직접 제어와 반대되는 개념이다.
코드의 결합도를 낮추고, 유연한 아키텍처와 테스트가 가능한 구조로 만들 수 있다.

✏️ 전통적 방식 vs IoC

전통적 방식

StockService stockService = new StockService(new StockRepository());

IoC 적용

@Autowired
StockService stockService;

DI (Dependency Injection, 의존성 주입)

객체가 의존하는 다른 객체를 직접 생성하지 않고, 외부 (컨테이너)에서 주입받는 방식이다.
객체 사이의 결합도를 낮추고, 테스트나 변경이 용이해진다.

DI 주입 방식

Stock 정보를 조회하는 서비스 StockService가 데이터 저장소 StockRepository에 의존한다고 가정하자.

생성자 주입 (권장)

@Service
public class StockService {
    private final StockRepository stockRepository;

    public StockService(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    public String getStockInfo(String ticker) {
        return stockRepository.getStockInfo(ticker);
    }
}

장점

  • 불변 객체를 설계할 수 있다.
  • 테스트가 용이하다.
  • 필수 의존성을 보장한다.

단점

  • 코드가 다소 길어질 수 있다.

✏️ 생성자 주입에서 private final 선언의 필요성
final로 선언하면 해당 필드는 초기화 이후 값이 변하지 않는 불변 객체가 된다. 생성자를 통해 반드시 1번만 값이 할당되고 이후에 변경 불가하여, 안정성과 신뢰성이 확보된다. Spring이 주입할 때도 반드시 생성자를 통해 값을 넣기 때문에, 필수 의존성이 반드시 주입되었음을 컴파일 타임에 확인할 수 있다.

필드 주입

@Service
public class StockService {
    @Autowired
    private StockRepository stockRepository;

    public String getStockInfo(String ticker) {
        return stockRepository.getStockInfo(ticker);
    }
}

장점

  • 코드가 간결하다.
  • 간단한 실습에 적합하다.

단점

  • final로 선언할 수 없다.
  • 테스트가 불편하다.
  • 권장되지 않는다.

Setter 주입

@Service
public class StockService {
    private StockRepository stockRepository;

    @Autowired
    public void setStockRepository(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    public String getStockInfo(String ticker) {
        return stockRepository.getStockInfo(ticker);
    }
}

장점

  • 선택적 의존성 주입에 적합하다.

단점

  • Setter가 오남용될 수 있다.
  • 불변성이 약하다.

✏️ Setter 주입에서도 @Autowired가 필요한 이유
Setter 주입 방식에서는 객체 생성 후, Setter 메서드를 통해 Spring이 의존 객체를 주입한다. 일반 Java 메서드는 단순 메서드인지 DI 용도 메서드인지 구분이 안 되므로, 해당 메서드가 DI 용도임을 Spring에게 명확히 알려주기 위해서 @Autowired를 표시한다.

✏️ 생성자 주입이 필드 주입보다 권장되는 이유
불변성 보장: final 키워드를 사용할 수 있어 객체 생성 후 의존성 변경이 불가능하여, 안정성이 증가한다.
필수 의존성 보장: 필수 의존성을 생성자 파라미터로 강제하여 누락 시 컴파일/런타임 에러가 발생해, 오류를 조기에 발견할 수 있다.
테스트 용이성: 단위 테스트 시 생성자를 통해 mock/fake 객체를 직접 주입할 수 있어 테스트 코드 작성이 편리하다.
순환 참조 조기 감지: 순환 참조 발생 시 어플리케이션 구동 시점에 즉시 에러가 발생해 버그 확인을 빠르게 할 수 있다.
의존성 명확성 및 가독성: 생성자 시그니처만 봐도 어떤 의존성이 필요한지 한눈에 파악할 수 있어 코드 가독성이 좋고 유지보수가 용이하다.

✏️ 순환 참조 오류
생성자 주입은 객체를 생성하면서 필수로 모든 의존성을 동시에 주입해야 하므로, 서로 상대방을 필요로 하면 무한루프에 빠져 객체를 만들 수 없는 상태가 된다. Spring은 이러한 구조를 바로 감지해 시작 즉시 에러를 발생시킨다. 의존 구조를 리팩토링하거나 중간 계층 (인터페이스, 이벤트, etc.)으로 분리해서 해결한다.

profile
선형의 비선형적 기록 🐜

0개의 댓글