Kotlin

DI (Dependency Injection) - 의존성 주입

빈코더 2024. 12. 20. 01:00
728x90

DI (Dependency Injection) - 의존성 주입

DI란?

객체가 필요로 하는 의존성을 스프링 컨테이너가 주입하는 설계 패턴입니다.
DI는 IoC의 구체적인 구현 방식 중 하나로, 객체 간의 의존성을 스프링 컨테이너가 자동으로 설정해줍니다.

DI의 핵심

  • 객체 간의 의존성을 스프링 컨테이너(ApplicationContext)가 관리.
  • 객체를 필요로 하는 클래스는 의존성을 직접 생성하지 않고 외부에서 주입받음
  • 주입 방법: 생성자, 필드, Setter(메서드)를 통해 이루어짐

DI의 장점

  1. 결합도 감소
    • 클래스 간의 강한 의존성을 없애고 유연성을 높임
    • 객체가 내부의 의존성을 생성하지 않으므로, 다른 객체로 쉽게 교체 가능
  2. 테스트 용이성
    • DI를 통해 실제 객체 대신 Mock 객체를 주입하여 유닛 테스트를 쉽게 작성 가능
  3. 재사용성 증가
    • 객체가 외부 의존성에 대해 알 필요가 없으므로 코드 재사용성이 향상

DI의 주요 방법

스프링에서는 3가지 DI 방법을 제공합니다.

1. 생성자 주입(Constructor Injection) - 권장 방식

  • 객체 생성 시점에 의존성을 주입받는 방식
  • 의존성을 final로 선언할 수 있어, 불변성을 보장하고 안정적임
  @Component
  class MyService(val repository: MyRepository)

  @Component
  class MyRepository
  • 장점
    • 필수 의존성을 명확히 알 수 있음
    • 객체가 생성될 때 모든 의존성을 주입받아야 하므로 의존성 누락 방지

2. 필드 주입(Field Injection)

  • 클래스 내부의 필드에 의존성을 직접 주입하는 방식
  • @Autowired를 사용하여 주입
@Component
class MyService {
    @Autowired lateinit var repository: MyRepository
}
  • 장점
    • 구현이 간단하고 코드가 짧음
  • 단점
    • 테스트 시 Mock 객체를 주입하기 어려움
    • 의존성이 숨겨져 코드 가독성이 떨어질 수 있음

3. Setter 주입(Setter Injection)

  • Setter 메서드를 통해 의존성을 주입받는 방식
  @Component
  class MyService {
      private lateinit var repository: MyRepository

      @Autowired
      fun setRepository(repository: MyRepository) {
          this.repository = repository
      }
  }
  • 장점
    • 선택적인 의존성에 적합
    • 주입된 의존성을 나중에 변경 가능
  • 단점
    • 의존성이 명시적이지 않아 필수 의존성과 선택적 의존성 구분하기 어려움

DI 예제

1. 의존성이 있는 클래스

  @Component
  class MyRepository {
      fun findData(): String {
          return "Hello from Repository"
      }
  }

2. DI를 사용하는 서비스 클래스

  @Service
  class MyService(private val repository: MyRepository) {
      fun getData(): String {
          return repository.findData()
      }
  }

3. DI를 사용하는 컨트롤러

  @RestController
  @RequestMapping("/api")
  class MyController(private val service: MyService) {
      @GetMapping("/data")
      fun getData(): String {
          return service.getData()
      }
  }
  • 동작 원리
    1. 스프링 컨테이너(ApplicationContext)가 @Component, @Service, @RestController 등을 스캔해 Bean을 등록.
    2. MyService가 MyRepository를 필요로 하면, 컨테이너가 MyRepository를 생성하여 MyService에 주입.
    3. MyController가 MyService를 필요로 하면, 컨테이너가 주입.

DI의 동작 원리

  1. ApplicationContext가 Bean 스캔 및 등록
    • @ComponentScan에 의해 모든 Bean을 스캔하여 컨테이너에 등록
    • 등록된 Bean들은 스프링 컨테이너가 관리
  2. Bean 생성 및 주입
    • 컨테이너가 의존성을 확인하고 필요한 Bean을 생성 후 주입
  3. 의존성 주입 완료 후 애플리케이션 실행
    • 모든 의존성이 주입된 상태로 애플리케이션이 시작됨

DI의 한계 및 보완

  1. Bean 순환 참조 문제
    • 서로 의존하는 Bean 간에 순환 참조 문제가 발생할 수 있음
    • 해결책: 순환 참조를 제거하거나 @Lazy로 지연 주입
  2. Bean 스코프 관리
    • 기본적으로 Bean은 싱글톤으로 관리되며, 이를 명시적으로 변경해야 하는 경우가 있음
    • 해결책: @Scope로 Bean 스코프를 설정

요약

  • DI란 객체가 필요로 하는 의존성을 외부에서 주입받아 결합도를 줄이고 유연성을 높이는 설계 방식
  • 스프링에서 DI 구현 방법은 생성자 주입, 필드 주입, Setter 주입 3가지 방식이 있다.
  • 장점: 결합도 감소, 테스트 용이성 증가, 재사용성 향상
  • 동작 원리: 스프링 컨테이너가 Bean을 생성 및 관리하며, 필요한 객체를 자동으로 주입한다.
728x90