[Spring] 의존성 주입(DI) 이란?

gogori6565·2024년 7월 11일
0

Spring

목록 보기
1/3
post-thumbnail

🚀 들어가기에 앞서,

의존성 주입(DI)을 코드를 건드리지 않는 마법~🪄 이라고 생각하지 말자.
위에서 길게 말했지만 중요한 건, 의존성 분리의 핵심 장점이란 어디를 고쳐야 할지 명확해지고, 코드 수정이 최소화된다는 것이다.
개발에는 유지보수가 중요하고, 그 유지보수를 위해 마법을 부리는 게 아니라 보다 편하게 유지보수를 할 수 있게 해주는 것임을 명심하도록.

의존성 주입 (DI)

의존성 주입(Dependency Injection, DI) 이란?
: 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 게 아니라, 외부에서 주입 받아 사용하는 방법
(즉, Java를 배우면서 알던 new 연산자를 이용해 객체를 생성하는 것이 아니라고 보면 된다.)

  • 한 클래스가 다른 클래스에 의존한다는 것은 해당 클래스가 다른 클래스의 인스턴스나 메서드를 사용한다는 것을 의미한다.
  • 의존성은 클래스 간의 결합도를 나타내는 중요한 요소이다.

강한 결합
객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다. A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.

느슨한 결합
객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.

의존성 예시 코드

@Service
public class ExampleService {

    private ExampleRepository exampleRepository;

    public ExampleService(ExampleRepository exampleRepository) {
        this.exampleRepository = exampleRepository;
    }
}
public class ExampleRepository {
      // DI Test
}

위 코드에서 ExampleService 클래스가 만들어지기 위해서는 ExampleRepository 클래스를 필요로 한다.
이를 ExampleService 클래스는 ExampleRepository 클래스의 의존성을 가진다라고 한다.

위 코드같이 설계하면 코드의 재활용성이 떨어지고, 위 예제에서 ExampleRepository가 수정될 경우, ExampleService도 함께 수정해야 하는 문제가 발생한다.
즉, 결합도가 높아지게 되는 것이다.

추가로 위의 코드에서, ExampleRepository 클래스를 직접 new로 객체 주입을 하는 게 아니라 생성자로 주입을 받고 있다.
이 역시 객체의 의존성을 주입하는 방법 중 하나로 제어의 역전(Inversion of Control)이라고도 하나, 여전히 의존성을 직접 주입하여 직접 관리해야 하므로, 결합도가 남아있다.

제어의 역전(Inversion of Control, IoC)이란?
: 프로그램의 제어 흐름을 직접 제어하지 않고 외부에서 제어하는 것.
객체의 생성과 관리, 그리고 객체 간의 의존성 주입을 프레임워크나 컨테이너가 담당하도록 하는 방식이다.

  • IoC와 DI의 차이?
    : IoC는 보다 광범위한 설계 원칙으로, 제어 흐름의 역전을 통해 소프트웨어의 유연성을 높이는 것을 목표로 한다. DI는 IoC를 구현하는 실제 기술로, 객체 간의 의존성 관리를 보다 쉽게 하기 위해 사용한다.

💻 의존성 주입 - 코드로 설명

public class Computer {
	MouseA mouseA = new MouseA();
    
    public void click() {
		mouseA.click();
    }
}

위 코드는 컴퓨터가 특정 마우스에 직접 접근해 두 객체 사이의 결합도가 크다.
만약 마우스A에서 마우스B로 변경하는 경우에는, 컴퓨터 클래스 코드도 싹 다 수정해야 한다.

이는 객체지향의 원칙 중 하나인 DIP를 위배하는 것이다.

그러므로 결합도를 낮추기 위해 Computer 클래스는 추상화된 Mouse 인터페이스에 접근해야한다.

public class Computer {
	Mouse mouse = new MouseA();
    
    public void click() {
		mouseA.click();
    }
}

인터페이스를 추가해 설계를 다시하여 메서드는 특정 마우스에게 의존하지 않게 되었지만 Mouse 구현체를 생성하는 코드에서 여전히 결합이 발생한다.

즉, 우리가 결합도를 낮추기 위해 해야할 일은

  • 객체를 직접 생성하지 말 것
  • 객체를 직접 주입하지 말 것

이 두가지를 지키면 객체지향 원칙 중 하나인 OCP를 지키며 기능을 확장할 수 있다.

그럼 팩토리 패턴(Factory Pattern)을 사용해 객체를 주입하는 권한을 팩토리 객체에 넘겨보자.

🔹개선 1. 팩토리 패턴 사용

팩토리 패턴(Factory Pattern) 이란?
객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하게 만드는 패턴이다.

public class Computer {
	Mouse mouse;
    
    public Computer(MouseFactory mouseFactory){
   		mouse = mouseFactory.getMouse("A");
    }
    
    public void click(){
    	mouse.click();
    }
}

public class MouseFactory{
	public Mouse getMouse(String mouse){
    	if(mouse.equals("A")) return new MouseA();
        else if(mouse.equals("B")) return new MouseB();
        else return new MouseC();
    }
}

Computer가 MouseFactory에게 어떤 객체를 사용할 것인지 명시만 해주면 MouseFactory가 Mouse 구현체를 대신 생성(new)하여 주입(Injection)한다. 그리고 Computer는 특정한 Mouse 구현체와 직접 연관되는 것이 없어진다.

🤔 A를 B로 바꾸려면 내가 코드 변경해줘야 하는데, 뭐가 다른 건데??

여전히 A를 B로 직접 변경해야 한다는 점이 있지만, 실제로는 이런 식으로 하드 코딩 하지 않음

  • 환경 변수나 설정 파일(config) 에서 값을 받아오도록 만들면 코드 수정 없이 변경할 수 있음
//application.properties 파일
mouse.type = A

🔹개선 2. 생성자 주입(DI) - (향상된 개선)

Computer는 여전히 MouseFactory 를 직접 생성해야 한다.
완전히 의존성을 제거하기 위해, 생성자를 통한 주입(Dependency Injection, DI) 을 적용하자.

public class Computer {
    private Mouse mouse;

    public Computer(Mouse mouse) { // 외부에서 주입
        this.mouse = mouse;
    }

    public void click() {
        mouse.click();
    }
}

이렇게 하면 Computer는 Mouse가 무엇인지 전혀 신경 쓰지 않아도 된다.

🤔 여기서 '외부'란 무엇인가?
Computer 객체를 생성하는 곳을 의미한다 (like Main 클래스)

  • 즉, Computer는 Mouse의 타입에 대해 신경 쓰지 않고 단지 Mouse 객체만 외부에서 받으면 된다.
  • Main이나 다른 외부 클래스에서 Mouse 객체를 쉽게 바꿀 수 있다는 뜻!

Spring Framework 의 역할 (DI)

자, 그럼 이제 Spring 프레임워크가 무엇을 하는 것인지 알아보자.

Spring 프레임워크는 스프링 컨테이너에 객체(Bean)를 미리 생성해놓는다. 이것을 Bean이라고 부른다. 생성된 Bean은 다른 객체에 주입할 수 있다.
이것이 의존관계 주입이다.

public class Computer {

    @Autowired // 의존관계 주입
    @Qualifier("mouseA") // 명시
    Mouse mouse;
  
    public void click(){
        mouse.click();
    }
}


@Configuration
public class SpringConfig{

    @Bean
    Mouse mouseA(){
        return new MouseA(); // MouseA Bean 생성
    }

    @Bean
    Mouse mouseB(){
        return new MouseB(); // MouseB Bean 생성
    }

    @Bean
    Mouse mouseC(){
        return new MouseC(); // MouseC Bean 생성
    }
}

@Configuration으로 선언된 설정클래스에 명시된 Bean 정보는 BeanDefinition 객체 변환되고 Spring은 BeanDefinition을 참조하여 Bean을 생성하고 스프링컨테이너에 등록한다.
Bean 생성이 완료 후, Spring은 @Autowired가 선언된 변수에 맞는 타입을 가진 Bean을 스프링 컨테이너에서 찾아 주입한다. @Qualifier로 명시된 Bean 이름이 있으면 찾아서 주입한다.

  • 빈을 수동으로 정의하고 싶다면 @Configuration@Bean을 사용하면 된다.
  • 그리고 만약 동일한 타입의 빈이 여러 개가 등록되어 있다면, 빈을 명시적으로 지정해야 한다. 이때, @Qualifier를 사용한다.

➕ DIP, OCP란?

  • DIP(Dependency Inversion Principle)
    : 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

  • OCP(Open/Close Principle)
    : 자신의 확장에는 열려있어야 하고, 주변의 변화에는 닫혀있어야 한다.


Spring Boot에서 의존성 주입의 작동 원리

@Autowired
private ArticleRepository articleRepository;

위와 같은 코드가 있다고 해보자. @Autowired를 통해 articleRepository를 주입해주는 것이다.
근데 spring이 어떻게 내가 원하는 articleRepository를 찾아서 주입한다는 것일까?🤔

👉
Spring Boot에서 의존성 주입은 IoC 컨테이너(Inversion of Control Container)가 주도하며, 이는 설정과 주석(annotation)을 기반으로 개발자가 원하는 알맞은 객체를 자동으로 찾아서 주입한다.

📖 작동 원리

1. IoC 컨테이너
: IoC 컨테이너는 애플리케이션의 컴포넌트 객체를 생성, 구성, 관리한다. IoC는 제어의 역전을 의미하며, 객체의 생성과 의존성 관리를 개발자가 아닌 컨테이너가 담당하는 것이다.

2. 컴포넌트 스캔과 빈 생성

  • 컴포넌트 스캔
    : Spring Boot는 애플리케이션이 시작될 때, 특정 패키지 및 하위 패키지에서 @Component, @Service, @Repository, @Controller와 같은 주석이 달린 클래스를 스캔하여 IoC 컨테이너에 빈(Bean)으로 등록한다.

  • 빈 생성 : 스캔된 클래스는 자동으로 빈으로 등록되고, IoC 컨테이너가 관리하는 객체가 된다.

3. 빈 주입
: 빈 주입은 @Autowired, @Inject, @Resource 주석을 통해 이루어진다. 컨테이너는 의존하는 클래스에 필요한 빈을 자동으로 주입한다.

  • @Autowired: 주로 필드나 생성자, 메서드에서 의존성을 자동으로 주입하는 데 사용
  • @Inject: 자바 표준 주입 주석으로, Spring의 @Autowired와 비슷하게 동작
  • @Resource: 자바의 JSR-250 표준 주석으로, 특정 이름의 빈을 주입할 때 사용

+) Spring Framework에서 @Autowired를 사용해 의존성을 주입할 때, 타입 기반으로 의존성을 검색한다.
@Autowired는 클래스 필드나 생성자, 메서드의 타입(클래스 또는 인터페이스)을 기준으로 IoC 컨테이너에 등록된 빈을 찾아 주입한다.

4. 빈 검색과 주입 전략

  • 타입 기반 검색 : 주로 빈의 타입에 따라 자동 주입을 한다. (@Autowired가 많이 사용)
  • 이름 기반 검색 : 빈의 이름을 명시해 주입한다. (@Qualifier@Resource를 사용)
  • 프라이머리 빈 : 여러 빈이 동일한 타입을 가질 때, @Primary 주석을 사용하여 기본으로 주입할 빈을 지정할 수 있다.
  • 빈 이름 충돌 방지: 빈 이름이 충돌할 때, @Qualifier를 사용하여 명시적으로 지정할 수 있다.
  • 빈 수동 정의 : 만약 빈을 수동으로 정의하고 싶다면 @Configuration@Bean을 사용하면 된다.

🧷 [Spring] 여러 빈이 동일한 타입을 가질 때, 의존성 주입을 하는 방법


➕ 의존성 주입이 필요한 이유

위 내용을 요약하면 '의존성 주입(DI) = 객체 간의 결합도를 낮추는 것' 이라고 할 수 있겠다.

그럼 객체 간의 결합도를 낮추는 것은 왜 중요할까?

1. 유지보수성 향상
: 결합도가 낮으면 코드 변경이 더 쉽고, 한 객체의 수정이 다른 부분에 미치는 영향을 최소화할 수 있다.
위에서도 말했듯, 객체가 서로 강한 결합을 하고 있으면 하나의 객체의 수정 때문에 관련된 다른 객체도 함께 수정해야 하는 상황이 발생한다.
이는 유지보수를 어렵게 하는 일이다.

2. 재사용성 증가
: 특정 객체가 다른 객체를 강하게 의존하지 않으면 다양한 상황에서 독립적으로 사용할 수가 있다.
재사용 가능한 코드를 작성하면 개발 속도를 높이고 중복을 줄일 수 있다.

3. 시스템의 유연성과 확장성 향상
: 새로운 기능을 추가하거나 기존 기능을 확장할 때, 기존 코드를 최소한으로 수정하면서 필요한 변경을 적용할 수 있다.
이는 시스템을 더 쉽게 확장하고, 새로운 요구사항에 빠르게 대응할 수 있게 한다.

4. 코드 가독성 향상
: 객체 간의 관계가 단순해 코드의 구조를 명확하게 쉽게 파악할 수 있다.

5. 테스트 용이
: 내부에서 주입하지 않고 외부에서 주입을 하면 본인이 원하는 주입을 외부에서 만들어 넣은 후에 테스트를 할 수 있다.

🚀 글의 초입에도 언급했듯이,
의존성 주입(DI)을 코드를 건드리지 않는 마법~🪄 이라고 생각하지 말자.
위에서 길게 말했지만 중요한 건, 의존성 분리의 핵심 장점이란 어디를 고쳐야 할지 명확해지고, 코드 수정이 최소화된다는 것이다.
개발에는 유지보수가 중요하고, 그 유지보수를 위해 마법을 부리는 게 아니라 보다 편하게 유지보수를 할 수 있게 해주는 것임을 명심하도록.


참고 사이트
https://devlog-wjdrbs96.tistory.com/165 - 의존성 주입
https://lordofkangs.tistory.com/633 - 의존성 주입을 하는 이유

profile
p(´∇`)q

0개의 댓글