[Spring] DI / IOC는 낯설어서

skyepodium·2021년 12월 18일
1

DI, IOC 알기위해 책 읽어봐도 잘 이해 안되고 나만 모르는 것 같고...
예시로 쉽게 알아봅시다.

1. 예시

만약 스파게티를 만든다고 해볼께요

사실 맛을 잘 못느껴서 그런지 면에 어떤 소스를 넣는지에 따라 달라지더라구요

  • 토마토 소스 넣으면 -> 토마토 스파게티
  • 크림 소스 넣으면 -> 크림 스파게티

만약, 우리들의 쉐프가 아래 코드처럼 행동한다면

토마토 소스와 너무 강결합 되어 있기 때문에 토마토 스파게티만 먹게 될거에요.

// Spaghetti.java
public class Spaghetti {
    // 요리에 사용할 소스
    private Source source;

    // 생성자
    public Spaghetti() {
        // 나는 토마토 소스만 가질 수 있기 때문에 토마토 스파게티만 만들 수 있어
        // 토마토 소스와 너무 강하게 결합되어 있어요.
        this.source = new TomatoSource();
    }
    
    public void addSource() {
    	source.add();
    }
}
// Source.interface
public interface Source {
    void add();
}
// 토마토소스는 인터페이스 소스를 구현한 구현체
public class TomatoSource implements Source{

    @Override
    public void add() {
    	System.out.println("토마토 소스를 넣습니다!!!")
    }
}
// Chef.java
public class Chef {
	
    // 우리들의 쉐프는 토마토 스파게티만 만들 수 있습니다.
    public Spaghetti spaghetti(){
        return new Spaghetti();
    }
}

맛있어도 계속 먹으면 질리니까 조금 유연하게 바꿔볼께요.

다음은 스파게티를 만들때 외부에서 이미 만들어진 소스를 받기 때문에 토마토 뿐만 아니라 크림, 올리브 스파게티도 만들 수 있습니다.

// Spaghetti.java
class Spaghetti {
    // 요리에 사용할 소스
    private Source source;

    // 생성자
    public Spaghetti(Source source) {
        // 외부에서 이미 만들어진 소스를 받습니다.
        // 오우!, 토마토 이외에 크림도 받을 수 있고 올리브도 받을 수 있어요!
        this.source = source;
    }
    
    public void addSource() {
    	source.add();
    }    
}
// Chef.java
public class Chef {
	
    // 우리들의 쉐프는 토마토 스파게티만 만들 수 있습니다.
    public Spaghetti spaghetti(){
	// 스파게티를 생성할 때 소스를 넣어줍니다.
        return new Spaghetti(source());
    }
    
    public Source source() {
    	return new TomatoSource();
    }
}

두 번째 코드처럼 이미 만들어진 소스를 외부에서 넣어주는 것을 의존성 주입 이라고 합니다.

2. DI (의존성 주입)

1) 정의

의존성 주입을 한 마디로 정리하면

의존하는 객체를 외부에서 생성해서 넣어주는 것
(소스를 미리 만들어서 스파게티 만들 때 넣어주는 것)

2) 장점

의존성을 주입하게 되면 유연해진다는 장점이 있습니다.

느슨하게 결합되어 있기 때문에 만약, 새로운 소스가 필요하면 외부에서 미리 만들어서 넣어주기만 하면 새로운 스파게티가 만들어집니다.

3) 참고

의존성 주입 방식에는 참고로 3가지가 있습니다.

  • 필드 주입
  • 생성자 방식
  • 메서드 방식(setter)

주로 생성자 방식을 사용합니다. 이 부분은 관련 포스트에서 알아보겠습니다.

3. IOC (제어의 역전)

1) 누가 제어 하나요?

근데 문제가 있습니다. 외부에서 소스를 넣어준다면 아래의 작업이 순서대로 이루어져야합니다.

  1. 누군가 미리 소스를 만듬 - (생성의 제어)
  2. 스파게티를 만들때 1번에서 만든 소스를 넣어주어야함 - (연관관계의 제어)
// 1. 소스를 미리 만들고
Source source = new TomatoSource();

// 2. 스파게티를 만들때 소스를 넣어줍니다.
Spaghetti spaghetti = new Spaghetti(source);

생성과 연관관계가 필요한데, 그래서 이걸 누가 하지요...

스프링에서는 IOC 컨테이너가 이를 수행합니다.

우리가 필요한 1. 생성의 제어와, 2. 연관관계의 제어를 개발자가 하지 않고, 스프링에 위임한다고 해서 제어의 역전 이라고 부릅니다.

2) Bean 등록

스프링 IOC 컨테이너에 객체를 등록하는 것을 Bean 등록이라고 부릅니다.

스프링의 의존성 주입은 Bean에 등록된 객체 사이에서 이루어집니다.

Bean을 등록하는것에는 2가지 방법이 있습니다. 1. 컴포넌트 스캔, 2. 직접 등록

2가지 모두 알아봅시다.

3) 의존성 주입 과정

  1. 스프링이 실행될 때 IOC 컨테이너도 같이 실행
  2. Bean 등록된 클래스의 객체를 생성
  3. 2번에서 객체 생성할때 생성자 호출 (생성자 주입 방식)
  4. 컨테이너 안에서 의존성 주입

4. 컴포넌트 스캔

1) 정의

컴포넌트 스캔이란 @ComponentScan annotation이 있는 폴더 부터 시작해서 하위로 검색하면서 @Component라는 어노테이션이 있는 클래스를 Bean에 등록하는 것을 의미합니다.

스프링에서 사용하는 @Controller, @Service, @Repository, @Configuration 등은 내부에 @Component 어노테이션을 가지고 있습니다.

그리고 메인 메서드의 @SpringBootApplication 어노테이션은 @ComponentScan 을 가지고 있습니다.

따라서, 메인 메서드 기준으로 컴포넌트 스캔이 이루어집니다.

2) 코드

오우!, Spaghetti 생성자에 TomatoSource를 넣는다고 작성하지 않았음에도 토마토 소스가 제대로 들어갔습니다.

// Spaghetti.java
package com.example.diioc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// 빈에 등록합니다.
@Component
class Spaghetti {

    private final Source source;
   
    // 생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어줍니다.
    @Autowired
    public Spaghetti(Source source) {
        this.source = source;
    }

    public void addSource() {
        source.add();
    }
}
// TomatoSource.java
package com.example.diioc;

import org.springframework.stereotype.Component;

// 빈 등록
@Component
public class TomatoSource implements Source{

    @Override
    public void add() {
        System.out.println("토마토 소스를 넣습니다.");
    }
}

테스트 코드로 알아봅시다.

// DiIocApplicationTests.java
package com.example.diioc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

@SpringBootTest
class DiIocApplicationTests {

    private final ApplicationContext applicationContext;
    private final Spaghetti spaghetti;

    @Autowired
    public DiIocApplicationTests(ApplicationContext applicationContext, Spaghetti spaghetti) {
        this.applicationContext = applicationContext;
        this.spaghetti = spaghetti;
    }

    @Test
    void beanTest() {
        // 빈 등록 확인
        applicationContext.getBean(Spaghetti.class).addSource();
    }

    @Test
    void addSourceTest() {
        // 어떤 의존성이 주입되었는지 확인
        spaghetti.addSource();
    }
}

3) 확인

토마토 소스가 잘 나왔습니다.

5. 직접 등록

// Spaghetti.java
package com.example.diioc;

class Spaghetti {

    private final Source source;

    public Spaghetti(Source source) {
        this.source = source;
    }

    public void addSource() {
        source.add();
    }
}
TomatoSource.java
package com.example.diioc;

public class TomatoSource implements Source{

    @Override
    public void add() {
        System.out.println("토마토 소스를 넣습니다.");
    }
}

보통은 XXXConfig 라는 클래스를 생성해서 해당 클래스 내부에서 Bean을 등록해줍니다.

@Configuration 어노테이션에는 @Component 어노테이션이 있기 때문에 컴포넌트 스캔으로 Bean에 등록됩니다.

@Bean 어노테이션이 붙은, Spaghetti, Source는 반환하는 객체가 Bean에 등록됩니다.

컴포넌트 스캔 대신 직접 등록방식을 사용해도 상관은 없는데 한가지 문제점은
return new TomatoSource(); 이렇게 직접 특정 구현체를 반환해야한다는 것입니다. 이를 해결하기 위해 스프링은 팩토리 패턴을 사용하며, 해당 포스트에서 알아보겠습니다.

package com.example.diioc;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Chef {

    @Bean
    public Spaghetti spaghetti(){
        return new Spaghetti(source());
    }

    @Bean
    public Source source() {
        return new TomatoSource();
    }
}

컴포넌트 스캔에서 사용한 테스트 코드를 동일하게 사용합니다.

// DiIocApplicationTests.java
package com.example.diioc;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

@SpringBootTest
class DiIocApplicationTests {

    private final ApplicationContext applicationContext;
    private final Spaghetti spaghetti;

    @Autowired
    public DiIocApplicationTests(ApplicationContext applicationContext, Spaghetti spaghetti) {
        this.applicationContext = applicationContext;
        this.spaghetti = spaghetti;
    }

    @Test
    void beanTest() {
        // 빈 등록 확인
        applicationContext.getBean(Spaghetti.class).addSource();
    }

    @Test
    void addSourceTest() {
        // 어떤 의존성이 주입되었는지 확인
        spaghetti.addSource();
    }
}

3) 확인

6. 참고

인텔리제이를 사용하신다면, 빈에 등록되고, 의존성 주입을 받은 경우 아래와 같이 왼쪽에 아이콘이 표시됩니다.

7. 주입식 정리

1) DI (의존성 주입)

의존관계에 있는 객체를 외부에서 생성해서 넣어주는 것

2) 의존성 주입 3가지 방법

  • 필드 주입
  • 생성자 방식
  • 메서드 방식(setter)

주로 생성자 방식 사용

3) 의존성 주입 장점

유연한 연결로 인한

  • 확장이 쉬움
  • 재사용성이 높아짐
  • 테스트 용이

3) IOC (제어의 역전)

1 생성의 제어와, 2 연관관계의 제어를 스프링 IOC 컨테이너에 위임하는 것

4) Bean 등록 2가지 방법

  • 컴포넌트 스캔
    @ComponentScan가 붙은 클래스 디렉토리에서 하위로 @Component 가 붙은 클래스 Bean에 등록

  • 직접 등록
    @Configuration가 있는 클래스에서 @Bean을 통해 등록

profile
callmeskye

0개의 댓글