[Spring] DI (Dependency Injection, 의존성 주입)

윤여준·2023년 10월 23일
0

spring

목록 보기
1/1
post-thumbnail

Dependency (의존성)

Dependency Injection이 무엇인지 알아보기 위해 Dependency(의존성)가 무엇인지부터 알아보자.

하나의 클래스를 작성할 때 자신의 클래스에 있는 메서드만을 사용할 수도 있지만, 프로그램의 규모가 커지면 다른 클래스에 있는 메서드를 사용하기도 한다.

만약 아래와 같은 코드가 있다고 해보자.

public class MemberRegisterService {
	private MemberDao memberDao = new MemberDao();
    
    public void regist(RegisterRequest req) {
    	Member member = memberDao.selectByEmail(req.getEmail());
        ...
    }
    ...
}

MemberRegisterService 클래스에서는 MemberDao 클래스의 메서드를 사용하는 것을 확인할 수 있다.

만약 MemberDao 클래스가 변경된다면 MemberRegisterService도 영향을 받을 수 있을 것이다.

이처럼 변경에 의해 영향을 받는 관계를 의존이라고 표현한다. 이 경우에는 MemberRegisterService 클래스가 MemberDao 클래스에 의존한다고 표현한다.

만약 위의 코드처럼 의존 객체를 직접 생성하게 된다면, 변경에 영향을 받기 때문에 유지보수 관점에서 문제가 생길 수 있다.

이 때 발생하는 문제를 해결하기 위해 스프링에서 사용하는 방법이 DI이다.

DI (Dependency Injection, 의존 주입)

공식 문서에서 DI를 정의한 내용을 살펴보자.

Dependency injection (DI) is a process whereby objects define their dependencies (that is, the other objects with which they work) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method.

정의를 살펴보면 DI는 매개변수 또는 객체의 속성을 이용해서 의존성을 정의하는 방식임을 알 수 있다. 여기서 말하는 객체의 속성은 setter 메서드이다.

매개변수와 setter 메서드 모두 클래스 밖에서 값을 넣는 방식이다. 즉, DI는 클래스 밖에서 의존 객체를 만든 뒤에 클래스 안으로 넣어주는 방식임을 알 수 있다.

다시 말해, DI 방식을 사용하게 되면 의존 객체를 직접 생성하지 않고 외부에서 전달 받게 된다.

이렇게 되면 의존 객체가 변경되어도 전달 받는 곳은 코드를 수정할 필요가 없다. 의존 객체를 만드는 곳만 수정하면 된다.

그렇다면 DI는 어디서 진행할까? 어딘가에서 의존 객체를 만들고 클래스에 넣어줘야 할 것이다.

공식 문서를 살펴보면 다음과 같다.

The container then injects those dependencies when it creates the bean.

바로 스프링이 DI를 수행한다. 정확히 말하자면 스프링의 컨테이너가 DI를 수행한다. 스프링은 DI를 지원하는 일종의 조립기(assembler)이다. 필요한 객체를 생성하고 생성한 객체의 의존을 주입한다.

이러한 DI 과정을 잘 살펴보면 제어의 역전(Inversion of Control)이 발생하는 것을 알 수 있다.

This process is fundamentally the inverse (hence the name, Inversion of Control) of the bean itself controlling the instantiation or location of its dependencies on its own by using direct construction of classes or the Service Locator pattern.

직접 의존 객체를 생성하는 클래스와 비교했을 때, 의존 객체를 주입 받는 클래스는 의존 객체를 제어할 수 없다. 왜냐하면 스프링 컨테이너가 의존 객체 생성과 주입을 해주기 때문이다. 즉, 의존 객체에 대한 제어가 스프링 컨테이너로 넘어갔다고 말할 수 있다. 이러한 상황을 IoC(Inversion of Control, 제어의 역전)라고 한다.

DI를 사용하면 코드가 더 깔끔해지며, 코드가 더욱 효과적으로 분리된다. 또한 테스트가 수월해진다.

DI는 크게 두 가지 방식이 있다. 생성자 기반 DI와 setter 기반 DI이다.

생성자 기반 DI

Constructor-based DI is accomplished by the container invoking a constructor with a number of arguments, each representing a dependency.

공식 문서에 의하면, 생성자 기반 DI는 의존성을 나타내는 여러 인수를 사용하여 생성자를 호출하는 컨테이너에 의해 수행되는 DI 방식이다.

다음 코드는 생성자 기반 DI 방식을 사용하는 예시이다.

public class SimpleMovieLister {

	// the SimpleMovieLister has a dependency on a MovieFinder
	private final MovieFinder movieFinder;

	// a constructor so that the Spring container can inject a MovieFinder
	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// business logic that actually uses the injected MovieFinder is omitted...
}
// 설정 클래스
@Configuration
public class AppCtx {
	@Bean
    public MovieFinder movieFinder() {
    	return new MovieFinder();
    }
    
    @Bean
    public SimpleMovieLister simpleMovieLister() {
    	return new SimpleMovieLister(movieFinder());
    }
}

클래스에서 직접 의존 객체를 생성하지 않고 생성자를 통해 외부에서 의존 객체를 주입 받는 것을 확인할 수 있다.

위의 코드 중 AppCtx 클래스는 스프링 설정 클래스이다.
@Configuration 어노테이션을 붙이면 스프링 설정 클래스로 지정할 수 있다.
@Bean 어노테이션을 붙이면 해당 메서드가 생성한 객체를 스프링 빈이라고 설정한다.

setter 기반 DI

Setter-based DI is accomplished by the container calling setter methods on your beans after invoking a no-argument constructor or a no-argument static factory method to instantiate your bean.

setter 기반 DI는 컨테이너에서 객체를 생성한 후 setter 메서드를 이용해서 의존 객체를 주입하는 방식임을 알 수 있다.

다음 코드는 setter 기반 DI 방식을 사용하는 예시이다.

public class SimpleMovieLister {

	// the SimpleMovieLister has a dependency on the MovieFinder
	private MovieFinder movieFinder;

	// a setter method so that the Spring container can inject a MovieFinder
	public void setMovieFinder(MovieFinder movieFinder) {
		this.movieFinder = movieFinder;
	}

	// business logic that actually uses the injected MovieFinder is omitted...
}
@Configuration
public class AppCtx {
	@Bean
    public MovieFinder movieFinder() {
    	return new MovieFinder();
    }
    
    @Bean
    public SimpleMovieLister simpleMovieLister() {
    	SimpleMovieLister simpleML = new SimpleMovieLister();
        simpleML.setMovieFinder(movieFinder());
    	return simpleML;
    }
}

클래스에서 직접 의존 객체를 생성하지 않고 setter 메서드를 통해 외부에서 의존 객체를 주입 받는 것을 확인할 수 있다.

생성자 기반 DI Vs setter 기반 DI

생성자 기반 DI와 setter 기반 DI의 장단점을 비교해보자. 각 방식의 장점이 곧 다른 방식의 단점이다.

  • 생성자 기반 DI
    • 장점
      • 빈 객체를 생성하는 시점에 모든 의존 객체가 주입됨
    • 단점
      • 파라미터 개수가 많을 경우, 각 인자가 어떤 의존 객체를 설정하는지 알아내려면 생성자의 코드를 확인해야함
  • setter 기반 DI
    • 장점
      • 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있음
    • 단점
      • 필요한 의존 객체를 전달하지 않아도 빈 객체가 생성되기 때문에, 사용 시점에 NullPointerException이 발생할 수 있음

@Autowired

@Autowired 어노테이션을 사용하면 의존 객체를 자동 주입할 수 있다. 자세한 내용은 다른 게시물에서 서술하겠다.

주입 대상 객체를 모두 빈 객체로 설정해야 할까?

굳이 그럴 필요는 없다.

객체를 스프링 빈으로 등록할 때와 등록하지 않을 때의 차이는 스프링 컨테이너가 객체를 관리하는지 여부이다. 여기서 말하는 관리는 스프링 컨테이너가 제공하는 자동 주입, 라이프사이클 관리 등의 기능을 말한다. 이러한 기능은 스프링 빈으로 등록한 객체에만 적용된다.

최근에는 의존 대상 주입을 프로젝트 전반에 사용하는 추세이기 때문에 의존 주입 대상을 스프링 빈으로 등록하는 것이 일반적이다.

참고 자료

profile
Junior Backend Engineer

0개의 댓글