[Spring] IoC와 DI - 제어의 역전이란 무엇인가

2021. 5. 7. 16:15·Web Backend/Spring
반응형

스프링과 JPA를 개념 순서대로 정리하는 시리즈입니다. 1부 스프링 컨테이너 (1/5)

스프링의 IoC와 DI

이번에는 스프링의 가장 기본이 되는 개념인 IoC(Inversion of Control, 제어의 역전)와 DI(Dependency Injection, 의존성 주입)에 대해 알아보겠습니다.

스프링을 공부하면 가장 먼저 만나게 되는 개념입니다.

하지만 "제어의 역전"이라는 말이 추상적이라 무엇이 무엇으로부터 뒤집히는 것인지 잘 와닿지 않습니다.

따라서 이번에는 운동 기록 앱을 예제로 new 키워드 하나를 걷어냈을 때 실제로 무엇이 달라지는지 살펴보겠습니다.

예제 코드는 Java 21, Spring Boot 3.5, Hibernate 6 환경에서 작성했습니다.


1. 객체를 직접 생성하면 생기는 문제

운동 세션을 저장하는 서비스를 스프링 없이 작성하면 다음과 같습니다.

public class WorkoutService {

    private final WorkoutSessionRepository repository = new JpaWorkoutSessionRepository();

    public Long record(Member member, LocalDate date, String memo) {
        WorkoutSession session = new WorkoutSession(member, date, memo);
        return repository.save(session);
    }
}

동작에는 문제가 없습니다.

하지만 WorkoutService가 자신이 사용할 객체를 직접 생성하고 있습니다.

이때 세 가지 문제가 생깁니다.

구현을 변경하려면 이 클래스를 수정해야 합니다.

저장소를 인메모리 구현으로 바꾸려면 WorkoutService를 열어서 고쳐야 합니다.

저장 방식이 바뀌었을 뿐인데 비즈니스 로직이 들어 있는 클래스가 함께 수정됩니다.

테스트에서 다른 구현으로 교체할 수 없습니다.

record() 메서드 하나를 테스트하려고 해도 실제 데이터베이스가 함께 붙습니다.

가짜 저장소를 끼워 넣을 방법이 없습니다.

의존성이 계속 전파됩니다.

JpaWorkoutSessionRepository는 EntityManager가 필요합니다.

그리고 EntityManager는 EntityManagerFactory가 필요하고, EntityManagerFactory는 다시 DataSource를 필요로 합니다.

결국 다음과 같은 조립 코드를 직접 작성해야 합니다.

DataSource dataSource = new HikariDataSource(config);
EntityManagerFactory emf = createFactory(dataSource);
EntityManager em = emf.createEntityManager();
WorkoutSessionRepository repository = new JpaWorkoutSessionRepository(em);
WorkoutService service = new WorkoutService(repository);

저장소 하나를 사용하기 위해 그 아래에 딸린 객체들의 생성 순서까지 전부 알아야 합니다.

객체를 사용하는 쪽이 객체를 만드는 방법까지 알고 있는 상태입니다.

이것이 스프링이 뒤집으려고 하는 대상입니다.


2. 제어의 역전이란?

위 코드를 스프링 방식으로 바꾸면 다음과 같습니다.

@Service
public class WorkoutService {

    private final WorkoutSessionRepository repository;

    public WorkoutService(WorkoutSessionRepository repository) {
        this.repository = repository;
    }

    public Long record(Member member, LocalDate date, String memo) {
        WorkoutSession session = new WorkoutSession(member, date, memo);
        return repository.save(session);
    }
}

new가 사라진 것 자체는 중요하지 않습니다.

중요한 것은 WorkoutService가 이제 필요한 것을 선언만 한다는 점입니다.

"나는 WorkoutSessionRepository가 필요하다"고 알릴 뿐이고, 어떤 구현체가 들어올지는 알지 못합니다.

그리고 알 필요도 없습니다.

여기서 뒤집힌 것은 객체를 생성하고 연결하는 통제권입니다.

원래는 객체를 사용하는 쪽이 이 권한을 가지고 있었습니다.

이제는 외부의 스프링 컨테이너가 가지게 됩니다.

내 코드가 프레임워크를 호출하던 관계가 프레임워크가 내 코드를 호출하는 관계로 바뀐 것이라고 생각하면 됩니다.

이러한 원칙을 헐리우드 원칙(Hollywood Principle)이라고 부르기도 합니다.

"우리에게 연락하지 마세요, 우리가 연락하겠습니다(Don't call us, we'll call you)"라는 의미입니다.


3. 의존성 주입(DI)

의존성 주입(Dependency Injection)은 필요한 객체를 외부에서 넣어주는 것을 말합니다.

위 코드에서 생성자의 파라미터로 저장소를 받는 부분이 의존성 주입입니다.

주입 방식에는 생성자 주입, 세터 주입, 필드 주입 세 가지가 있습니다.

스프링에서는 이 중 생성자 주입을 권장합니다.

자세한 이유는 시리즈 3번 글에서 다루겠습니다.

한 가지 눈여겨볼 점은 생성자에 @Autowired가 붙어 있지 않다는 것입니다.

생성자가 하나뿐이면 `@Autowired`를 생략할 수 있습니다.

Spring 4.3부터 적용된 규칙입니다.

생성자가 둘 이상이면 어느 것을 사용할지 알 수 없으므로 명시해야 합니다.


4. IoC와 DI는 같은 말인가?

두 용어는 자주 같은 의미로 사용되지만 정확히는 다릅니다.

IoC는 개념이고, DI는 그 개념을 구현하는 방법 중 하나입니다.

DI 외에도 IoC는 다음과 같이 여러 형태로 나타납니다.

형태 뒤집힌 것
의존성 주입(DI)협력 객체를 누가 생성하고 연결하는가
서블릿 컨테이너doGet()을 누가 호출하는가 (내가 아니라 컨테이너가)
템플릿 메서드 패턴전체 흐름은 부모 클래스가 가지고, 자식은 일부만 구현
이벤트 콜백실행 시점을 내가 아니라 이벤트 루프가 결정

즉 DI는 IoC에 포함되는 개념입니다.

스프링이 IoC 컨테이너이면서 동시에 DI 컨테이너라고 불리는 이유입니다.


5. 스프링 컨테이너의 동작 과정

@Service 하나를 붙였을 뿐이지만 내부에서는 다음과 같은 세 단계가 진행됩니다.

1) 스캔

@ComponentScan이 지정된 패키지를 탐색하면서 @Component 계열 애노테이션이 붙은 클래스를 찾습니다.

@Service, @Repository, @Controller는 모두 @Component를 포함하고 있습니다.

2) 빈 정의 등록

찾은 클래스를 곧바로 생성하지는 않습니다.

대신 "이런 이름의 빈이 있고, 타입은 무엇이며, 어떤 의존이 필요하다"는 정보를 담은 빈 정의(BeanDefinition)를 먼저 등록합니다.

3) 생성과 주입

등록된 빈 정의를 보고 의존 순서를 정렬합니다.

그리고 순서대로 객체를 생성하면서 생성자에 필요한 빈을 찾아 주입합니다.

2단계가 따로 존재한다는 점을 눈여겨볼 만합니다.

설계도를 먼저 모두 모으기 때문에 컨테이너는 전체 의존 관계를 미리 파악할 수 있습니다.

따라서 순환 참조 같은 문제를 애플리케이션이 시작되는 시점에 발견할 수 있습니다.


6. 등록된 빈 확인해보기

컨테이너에 실제로 무엇이 등록되는지 직접 출력해 보겠습니다.

애플리케이션 클래스에 다음과 같은 코드를 추가합니다.

@Bean
ApplicationRunner beanInspector(ApplicationContext ctx) {
    return args -> {
        System.out.println("=== 등록된 빈 ===");
        Arrays.stream(ctx.getBeanDefinitionNames())
              .filter(name -> name.startsWith("workout"))
              .forEach(name -> System.out.printf("%-28s -> %s%n",
                      name, ctx.getBean(name).getClass().getName()));
    };
}

실행하면 다음과 같이 출력됩니다.

=== 등록된 빈 ===
workoutLogApplication        -> com.example.workoutlog.WorkoutLogApplication$$SpringCGLIB$$0
workoutService               -> com.example.workoutlog.service.WorkoutService
workoutSessionRepository     -> jdk.proxy2.$Proxy131

여기서 두 가지를 확인할 수 있습니다.

빈 이름은 클래스 이름의 첫 글자를 소문자로 바꾼 것입니다.

WorkoutService는 workoutService라는 이름으로 등록됩니다.

직접 만들지 않은 클래스가 보입니다.

$Proxy131이나 $$SpringCGLIB$$는 스프링이 실행 시점에 생성한 대리 객체(프록시)입니다.

프록시 이름 뒤의 숫자는 실행할 때마다 달라지므로 그대로 나오지 않아도 정상입니다.

컨테이너가 빈을 그대로 보관하지 않고, 필요에 따라 감싸서 기능을 덧붙인다는 것을 알 수 있습니다.

이 프록시가 이후에 AOP와 트랜잭션의 정체가 되므로 시리즈 4번 글에서 다시 다루겠습니다.


7. 자주 헷갈리는 부분

인터페이스만 만들었는데 구현체는 어디서 왔나요?

WorkoutSessionRepository는 인터페이스인데도 빈으로 등록되어 있습니다.

Spring Data JPA가 인터페이스를 보고 구현체를 실행 시점에 생성해서 등록하기 때문입니다.

위 출력의 $Proxy131이 그것이며, 시리즈 17번 글에서 다루겠습니다.

빈은 요청할 때마다 새로 만들어지나요?

기본 스코프는 싱글톤입니다.

컨테이너당 하나만 생성해서 재사용합니다.

따라서 빈에는 상태를 두면 안 됩니다.

여러 요청이 같은 객체를 공유하기 때문입니다.

스코프에 대해서는 다음 글에서 이어서 살펴보겠습니다.


마무리

이번에는 스프링의 IoC와 DI에 대해 알아봤습니다.

제어의 역전에서 뒤집히는 것은 객체를 생성하고 연결하는 통제권이었습니다.

그 통제권이 객체를 사용하는 쪽에서 스프링 컨테이너로 넘어가면서, 클래스는 필요한 것을 선언만 하면 되도록 바뀌었습니다.

또한 DI가 IoC를 구현하는 방법 중 하나이며, 두 용어가 서로 다른 층위의 개념이라는 점도 함께 살펴봤습니다.

컨테이너는 스캔, 빈 정의 등록, 생성과 주입의 순서로 동작하며, 그 과정에서 빈을 프록시로 감싸기도 한다는 것을 확인했습니다.

다음 글에서는 빈이 등록되는 방법을 자세히 살펴보겠습니다.

@Component와 @Bean, @Configuration이 각각 어떤 역할을 하고 언제 무엇을 사용해야 하는지 정리하겠습니다.

반응형
'Web Backend/Spring' 카테고리의 다른 글
  • [Spring] @Transactional이 동작하지 않는 이유 - 프록시 자기 호출
  • [Spring] AOP - 관점 지향 프로그래밍과 프록시
  • [Spring] 의존성 주입 - 생성자 주입을 권장하는 이유
  • [Spring] 빈 등록과 생명주기 - @Component, @Bean, @Configuration
seunghwaan
seunghwaan
공부한 내용을 정리하는 개발 기록 블로그
    반응형
  • seunghwaan
    SH's Devlog
    seunghwaan
  • 전체
    오늘
    어제
    • 분류 전체보기 (164)
      • Android (62)
        • Basic (17)
        • Kotlin(Java) (14)
        • UI & Animation (1)
        • Compose (2)
        • Coroutines (1)
        • Dependency Injection (6)
        • RxJava (8)
        • BLE (3)
        • TDD (2)
        • JetPack (1)
        • NextStep (4)
        • Error Log (3)
      • Flutter (14)
        • Basic (5)
        • Dart (1)
        • State Management (2)
        • Widgets (4)
        • Error and Tips (2)
      • iOS (8)
        • Basic (0)
        • Swift (8)
      • Web Frontend (16)
        • Basic (0)
        • JavaScript (5)
        • TypeScript (0)
        • React (11)
      • Web Backend (5)
        • Spring (5)
      • CS(Computer Science) (18)
        • Network (4)
        • Database (10)
        • Design Pattern (1)
        • Computer Architecture (3)
        • Operating System (0)
      • Cloud (6)
        • AWS (6)
      • DevOps (25)
        • GIT (4)
        • CI CD (8)
        • Linux (4)
        • Docker (9)
        • Error Log (0)
      • 코딩테스트 (10)
        • DB (6)
        • 알고리즘 (4)
      • Mac Tip (0)
      • Language (0)
        • English (0)
        • Japanese (0)
      • Temporary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Linux
    Computer Science
    error
    Dependency Injection
    CICD
    IOS
    docker
    컴퓨터공학
    Algorithm
    Kotlin
    gradle
    di
    database
    Android
    시작하세요! 도커
    BLE
    JavaScript
    AWS
    Web Backend
    MySQL
    FLUTTER
    cs
    Swift
    Network
    React
    RxJava
    Web Frontend
    Jenkins
    spring
    Dagger
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
seunghwaan
[Spring] IoC와 DI - 제어의 역전이란 무엇인가
상단으로

티스토리툴바