새소식

Welcome to the tech blog of Junior Backend Developer Seulbin Kang!

Study

(CS Study) Spring의 DI는 정말 필수적인가? 우리는 왜 Spring을 사용해야할까?

  • -

👀 들어가기 전에

최근 "DI 프레임워크는 필요 없다"는 글을 보고, 과연 Spring의 DI는 정말 필수적인가에 대해 고민하게 되었다.

https://dannyryman.github.io/tech-blog/2024/12/18/you-do-not-need-a-di-framework.html

 

Why You Don’t Need a DI Framework: A Case for Manual Dependency Injection

Dependency Injection (DI) frameworks are so widely adopted that many developers assume they are the only option for managing dependencies in an application. However, this prevailing wisdom often goes unchallenged, leading to unnecessary complexity, opaque

dannyryman.github.io

 

(Reddit 논쟁)

https://www.reddit.com/r/Kotlin/comments/1hisoo6/why_you_dont_need_a_di_framework_a_case_for/?tl=ko

 

단순히 편해서 쓰는 것인지, 아니면 구조적으로 반드시 필요한 것인지에 대해
Spring의 Bean, DI, IoC 개념을 다시 정리해보며 이해해보려 합니다.


👀 본론

1. DI 없이 개발하면 어떻게 될까?

DI가 없다면 아래와 같이 코드가 작성됩니다.

public class OrderService {
    private OrderRepository orderRepository = new OrderRepository();
}

 

OrderService가 어떤 구현체를 사용할지를 직접 결정하게 만들게 됩니다.

반면 DI가 있다면, 아래와 같이 변경될 수 있습니다.

public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

 

위와 아래의 차이를 조금은 파악하실 수 있으셨을까요?

DI를 통해 어떤 구현체를 사용할지에 대한 책임을 Spring Framework로 넘기게 되는 것입니다.


예를 들어 OrderRepository의 구현체로 DbOrderRepository를 쓰다가, DbPlusOrderRepository 이 두가지가 있다고 가정해보겠습니다.

코드는 아래와 같습니다.

@Component
public class DbOrderRepository implements OrderRepository {
    public void findOrder() {
        System.out.println("memory");
    }
    
    public void getMyOrder(){
        System.out.println("myOrderNo1.");
    }
}

//이후 변경
@Component
public class DbPlusOrderRepository implements OrderRepository {
    public void findOrder() {
        System.out.println("db");
    }
    
    public void getMyOrder(){
        System.out.println("myOrderPlus");
    }
}

 

이때, DI가 없다면 OrderService에서는 DbOrderRepository를 썼는데,

이후 DbPlusOrderRepository를 주입받도록 서비스 코드 자체를 바꿔주어야하는 것입니다.

public class OrderService {
    private OrderRepository orderRepository = new DbOrderRepositroy();
}

//이후 변경
public class OrderService{
    private OrderRepository orderRepository = new DbPlusOrderRepository();
}

 

하지만, DI가 있다면 OrderService에서는 OrderRepository만 주입받아주면 되는 것입니다.

(변경 이후에도 서비스 코드의 변경이 없습니다.)

@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

//이후에도 코드 재사용 가능

 

위처럼 DI를 통해 주입받게 된다면, Spring 내부에서는 컨테이너에서 OrderRepository로 등록된 객체를 찾게 되는 것입니다.

Spring은 @Component, @Bean 등을 통해 객체를 Bean으로 등록하고, ApplicationContext라는 IoC 컨테이너에서 이를 관리합니다.

이후 객체 생성 시점에 필요한 타입을 기준으로 Bean을 찾아 생성자에 주입하게 되는 것입니다.


2. Bean 이란 무엇인가

Bean은 단순한 객체가 아니라, "Spring IoC 컨테이너의 관리 대상이 되는 객체"입니다.

별거 아니여보이는 이 차이는 매우 중요합니다.

일반 객체는 개발자가 직접 생성하고 라이프사이클을 관리해야 하지만, Bean은 생성, 의존성 주입, 초기화, 소멸까지 모든 과정을 Spring이 담당하게 됩니다. 즉, Bean으로 등록된 순간부터 해당 객체는 Spring이 관리하는 객체가 되는 것입니다.

@Component
public class OrderService{
	//이 클래스의 인스턴스를 스프링의 Bean이 관리
}

 

위 코드와 같이 Component 어노테이션만 붙이면, 스프링이 자동으로 클래스의 인스턴스를 만들어서 Bean으로 등록하게 되는 것입니다.


3. IoC 컨테이너의 역할

IoC란 무었일까요? 이전 포스트에서도 다뤘지만 가볍게 정리하고 넘어가겠습니다.

IoC (Inversion Of Control) 즉, 제어의 역전이란, 원래 개발자가 직접하던 객체 생성과 관리를 컨테이너가 대신해준다는 뜻입니다.

객체 생성과 의존성 관리 책임을 애플리케이션 코드에서 프레임워크로 이동시키는 것입니다.

 

그리고 이 컨테이너의 실체가 이전 포스트에서 봤던 ApplicationContext입니다.

 

해당 컨테이너는 아래와 같은 일을 주로 합니다.

  1. 설정 읽기 : @Component, @Bean 스캔
  2. 객체 생성 : 인스턴스화시키기
  3. 의존성 주입 : 필요한 객체를 연결
  4. 초기화 : 설정 완료 후 초기화 작업
  5. 소멸 : 어플리케이션 종료 시 정리

그리고 이 전체의 과정이 바로 Bean의 생명주기가 되는 것입니다.


4. Bean 생명주기 전체 흐름

그렇다면 이 Bean의 생명주기의 전체적인 흐름을 파악해본다면 아래와 같습니다.

  1. Bean 정의 읽기 : @Component, @Bean등으로 등록된 정보를 스캔
  2. 인스턴스 생성 : 기본 생성자로 객체를 메모리에 생성
  3. 의존성 주입 (DI) : @Autowired, 생성자 주입으로 필요한 객체 연결
  4. Aware 콜백(framework 내부) : BeanNameAware, ApplicationContextAware 등 호출
  5. 초기화 콜백 : @PostConstruct -> afterPropertiesSet -> init-method
  6. Bean사용 : 어플리케이션에서 실제로 사용된느 단계
  7. 소멸 콜백 : @PreDestroy -> destroy -> destroy-method

 


4-1 . Bean 등록 방법

그럼 Bean은 어떻게 등록되어있는가로 잠시 들어가보겠습니다.

결국 DI해주고, 해당 객체를 찾을때는 IoC 컨테이너에 등록되어있는 Bean을 찾아서 해결하게 됩니다.

그럼 그 Bean은 어떻게 등록될까요? 방법에는 아시는 것과 같이 두가지가 있습니다.

 

방법 1 : 컴포넌트 스캔 (@Component)

@Service, @Controlller, @Repository etc

 

방법 2 : 직접 등록 (@Bean)

@Configuration안에 메소드 만들고 @Bean을 붙이면 해당 메소드의 반환 값!! 이 Bean으로 등록됩니다.

 

이렇게 등록하는 방법은 결국 외부 라이브러리 코드에 직접 @Component 붙일 수 없는 문제를 해결해주고는 합니다.

아래 가벼운 예시를 살펴보겠습니다.

public class OrderService {
    private ObjectMapper objectMapper = new ObjectMapper();
}

 

위 코드가 어때 보이시나요?

Object Mapper를 Spring이 관리하지 않기 때문에 DI, 설정 공유, 재사용이 불가능합니다!

ObjectMapper는 

com.fasterxml.jackson.databind.ObjectMapper
 
Jackson 라이브러리에 있는 클래스입니다.
 
그렇기에 아래와 같이 Bean을 직접 등록해 IoC 컨테이너에서 관리할 수 있도록 만들어주는 것입니다.
@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        
        //커스텀 사용자 설정~~
        
        return mapper;
    }
}

@Service
public class OrderService {

    private final ObjectMapper objectMapper;

    public OrderService(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
}

5. 다시 돌아와서 : DI를 제공하고 여러 편의 기능을 제공하는 Spring Framework에 대한 생각.

결론적으로 DI는 객체 생성과 의존성 관리 책임을 분리하여 코드의 변경에 유연하게 대응할 수 있도록 만들어주는 구조적 도구라고 생각합니다. 물론 수동 DI로도 동일한 구조를 만들 수 있지만, 객체 수가 많아질수록 관리 비용이 급격히 증가하게 됩니다.

이에 대한 예시로 아래 그림을 통해 확인해보겠습니다

의존성 주입 구조

 

위와 같은 거대한 시스템에서 의존성 주입을 직접한다면, 아래와 같은 코드를 적게 되는 상황이 발생합니다.

직접 의존성 주입 (1)
직접 의존성 주입 (2)

 

직접 의존성 주입 (3)


이러한 점에서 Spring의 DI는 복잡한 애플리케이션 구조를 안정적으로 유지하기 위한 필수적인 기반이라고 생각합니다.


👀 마무리하며

이번에 정리하면서 느낀 점은, DI가 반드시 필요한가에 대한 질문의 답은 상황에 따라 달라질 수 있다는 것이었습니다.
객체 수가 적고 구조가 단순한 경우에는 직접 생성하거나 수동 DI로도 충분히 관리할 수 있습니다.
오히려 이런 상황에서는 DI 프레임워크가 불필요한 추상화처럼 느껴질 수도 있습니다.

하지만 서비스가 점점 커지고, 객체 간 의존 관계가 복잡해지는 순간부터는 객체 생성과 연결의 책임을 코드에서 분리하지 않으면 변경에 취약한 구조가 된다는 것을 느꼈습니다.

특히 구현체 변경, 공통 설정 적용, 테스트 환경 구성과 같은 상황에서 DI를 통해 얻는 이점은 구조적인 안정성에 가깝다고 생각합니다.
결국 DI는 “무조건 써야 하는 기술”이라기보다는, 복잡도가 증가할 때 그 가치를 발휘하는 설계 도구에 가깝다고 느꼈습니다.
이번 글을 통해 막연하게 사용해왔던 DI를 조금 더 명확한 기준을 가지고 선택할 수 있는 기술로 이해하게 된 것 같습니다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.