의존성 주입 DI(Dependency Injection) 개념

홍나연·2024년 10월 16일

Backend

목록 보기
6/8
post-thumbnail

DI(Dependency Injection)란

DI는 소프트웨어 개발에서 의존성 주입이라는 개념을 말한다.
여기서 '의존성'이란 한 객체가 다른 객체를 필요로 하는 것을 의미한다.
ex) 자동차가 엔진이 필요하듯이 어떤 프로그램도 특정 기능을 위해 다른 코드나 객체를 필요로 한다.


1. 의존관계 역전 원칙

프로그램의 큰 틀(추상화)에 의존하고, 작은 세부사항(구체화)에 의존하지 말라는 의미

이 원칙을 따르면 다음과 같은 장점을 얻을 수 있다.

  1. 서로 다른 부분(모듈)이 독립적으로 작동할 수 있기 때문에 변경이 쉽다.
  2. 프로그램을 수정하거나 유지보수하는 것이 쉽다.

ex) 만약 자동차의 엔진을 교체해야 할 때, 차체에 대한 구조가 바뀌지 않으면 쉽게 교체할 수 있는 것과 같다.

1-1. 객체 생성 의존성 제거

전통적인 방법으로, 어떤 객체가 다른 객체를 직접 생성하는 경우가 존재한다.

ex) UserService라는 객체가 UserRepository라는 객체를 직접 만드는 경우

class UserService {
    private UserRepository userRepository;

    public UserService() {
        this.userRepository = new UserRepository(); // 의존성 직접 생성
    }
}

⇒ 이렇게 되면 UserRepository를 바꿀 때마다 UserService도 수정해야 해서 불편함이 존재한다. 그래서 DI를 사용하면 UserService가 필요한 UserRepository를 외부에서 주입받도록 할 수 있다.

class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) { // 의존성 주입
        this.userRepository = userRepository;
    }
}

⇒ 이렇게 하면 UserServiceUserRepository의 여러 종류에 얽매이지 않아서 훨씬 유연해진다.

1-2. 타입 의존성 제거

타입 의존성은 특정 클래스에 의존하는 것을 의미한다.

만약 UserService가 특정 UserRepository 클래스에 의존하면, 그 클래스를 바꿀 때마다 UserService도 바꿔야 한다.

interface UserRepository {
    void save(User user);
}

class MySQLUserRepository implements UserRepository {
    public void save(User user) {
        // MySQL에 저장하는 로직
    }
}

class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository; // 외부에서 주입받음
    }
}

⇒ 이렇게 하면 UserService는 특정 클래스에 의존하지 않게 되어 필요할 때 쉽게 변경할 수 있다.


2. 의존성 주입 방법

DI는 여러 방법으로 구현할 수 있다.

  1. 생성자 주입 : 생성자를 통해 의존성을 받는 방법
  2. setter 주입 : setter 메서드를 통해 의존성을 주입하는 방법
  3. 인터페이스 주입 : 의존성을 주입하기 위한 인터페이스를 사용하는 방법

가장 많이 쓰이는 방법은 생성자 주입 ! → 테스트하기 쉽고 코드가 깔끔하기 때문


3. Spring Framework에서의 DI

Spring Framework는 DI를 쉽게 사용할 수 있게 도와주는 도구를 제공하고 있다.

Spring Container 구조 | 객체를 생성하고 서로 연결하는 역할

Spring Container 안에는 여러 가지 객체(필요한 도구들)가 들어 있다. 이 객체들은 서로 도움을 주고받으며, 프로그램이 잘 작동하도록 한다.

+-----------------------+
|      Spring           |  <- Spring 상자
|      Container        |
+-----------------------+
          |
          |
          |  +------------------+
          |  |       객체들       |  <- 우리가 필요한 도구들
          |  +------------------+
          |            |
          |            |
          |  +------------------+
          |  |     의존성 관리     |  <- 도구들이 서로 도와주는 관계
          |  +------------------+
          |
          |  +------------------+
          |  |      생명 주기     |  <- 도구가 언제 만들어지고 사라지는지 관리
          |  +------------------+
          |
+-----------------------+
|      빈 팩토리          |  <- 도구들을 만들고 관리하는 일
+-----------------------+
  1. Spring Container : 프로그램의 모든 도구(객체)를 관리하는 큰 상자
  2. 객체 : 프로그램에서 필요한 다양한 도구들 (ex 사용자 정보를 저장하는 도구)
  3. 의존성 관리 : 각 도구가 서로 어떻게 연결되어 있는지를 관리한다. 어떤 도구가 다른 도구를 필요로 할 때, Spring Container가 그 관계를 만들어준다.
  4. 생명 주기 : 도구가 언제 만들어지고, 언제 사라지는지를 관리한다. (ex 사용자가 필요할 때 도구를 만들고, 더 이상 필요하지 않을 때 없애는 과정 관리)
  5. 빈 팩토리 : 도구를 실제로 만드는 작업을 하는 곳. Spring Container가 도구들을 만들고 관리하는데 빈 팩토리가 그 일을 도와준다.

4. Spring에서 DI 구현하는 방법

4-1. XML 방식

[ Java 파일 ]

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="userService" class="com.example.UserService"> <!-- 전체 패키지명 -->
        <constructor-arg ref="userRepository"/> <!-- 생성자 주입 -->
    </bean>

    <bean id="userRepository" class="com.example.MySQLUserRepository"/>
</beans>
  • UserServiceUserRepository의 객체를 정의하고 있다.
  • userServiceUserService 클래스를 나타내고, userRepositoryUserRepository의 구현체인 MySQLUserRepository를 참조한다.
  • 이 방식은 명시적으로 의존성을 정의하여 Spring Container가 객체를 생성하고 관리할 수 있도록 한다.

4-2. 어노테이션 방식

[ Java 파일 ]

@Component
public class UserService {
    @Autowired // 의존성 주입
    private UserRepository userRepository;
}
  • @Component : 해당 클래스가 Spring Container에 의해 관리되는 "구성 요소"라는 것을 알려준다.
    즉, Spring이 이 클래스를 자동으로 인식하고 객체를 생성한다.
  • @Autowired : 어노테이션은 “자동으로 주입”이라는 의미로, 여기서는 UserService가 사용할 UserRepository라는 도구(객체)를 Spring이 자동으로 찾아서 넣어준다는 의미이다.
    즉, UserServiceUserRepository를 필요로 할 때, Spring이 자동으로 userRepository 변수에 UserRepository의 인스턴스를 할당해준다.

[ XML 파일 ]

<context:component-scan base-package="com.example"></context:component-scan>
  • 이 부분은 Spring에게 어떤 패키지에서 구성 요소를 찾아야 하는지 알려주는 설정이다.
    즉, base-package="com.example"은 Spring이 com.example 경로 아래의 모든 클래스에서 @Component가 붙은 것을 찾아 자동으로 객체를 생성하겠다는 의미이다.

4-3. Java Config 방식

@Configuration 어노테이션으로 설정 클래스를 만들고, 메서드에 @Bean 어노테이션을 붙여 객체를 생성한다.

[ Java 파일 ]

@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService(userRepository()); // UserRepository의 생성자 주입
    }

    @Bean
    public UserRepository userRepository() {
        return new MySQLUserRepository(); // MySQLUserRepository의 인스턴스 생성
    }
}
  • @Configuration : 어노테이션은 해당 클래스가 Spring의 설정 클래스를 나타내며, Bean을 정의하는 메서드가 포함될 것임을 명시한다.
  • @Bean: 해당 메서드가 반환하는 객체를 Spring Container에 Bean으로 등록하겠다는 의미이다. userService() 메서드는 UserService의 인스턴스를 생성하고, userRepository() 메서드는 UserRepository의 구현체인 MySQLUserRepository의 인스턴스를 생성하여 반환한다.

요약

DI는 프로그램의 유연성을 높이고, 코드의 품질을 개선하는 중요한 개념이다.

Spring Framework는 DI를 쉽게 구현할 수 있도록 다양한 방법을 제공하고 있으며, 상황에 맞는 방법을 선택하는 것이 중요하다 !

0개의 댓글