본문 바로가기
Application/Spring

[Spring] Spring DI 개념과 방법

by 윤루트 2022. 8. 22.

Spring DI란?

  • 외부에서 두 객체 간의 관계를 결정해주는 것으로
  • 클래스 레벨에서는 의존 관계가 고정되지 않게 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.
  • 객체를 직접 생성하는 것이 아니라, 외부(IoC/DI 컨테이너)에서 생성한 후 주입시켜주는 방식이다.
  • 애플리케이션 실행 시점에 필요한 객체(빈)을 생성해야 하며, 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 한다.
    • 이때, 주입시켜주는 역할을 위해 IoC/DI 컨테이너가 필요하다.
  • 의존성 주입은 제어의 역전(IoC)라고 불리기도 한다.
    • 어떠한 객체를 사용할지에 대한 책임은 프레임워크에게 넘어갔고, 사용자는 수동적으로 주입받는 객체를 사용하기 때문이다.
  • 한 객체가 어떤 객체에 의존할 것인지는 별도의 관심사이다. Spring은 의존성 주입을 도와주는 IoC/DI 컨테이너로써, 강하게 결합된 클래스들을 분리하고, 애플리케이션 실행 시점에 객체간의 관계를 결정해 줌으로써 결합도를 낮추고 유연성을 확보해 준다.
  • 이러한 방법은 상속보다 훨씬 유연하다.
    • 상속은 제약이 많고 확장성이 떨어진다.
  • 단, 한 객체가 다른 객체를 주입받으려면 반드시 DI 컨테이너에 의해 관리되어야 한다.
  • 정리하자면, DI는 
    • 두 객체 간의 관계라는 관심사의 분리
    • 두 객체 간의 결합도를 낮춤
    • 객체의 유연성을 높임
    • 테스트 작성을 용이하게 함

다양한 의존성 주입 방법

요약

  • 생성자 주입, 수정자(세터)주입, 필드 주입이 있다.
  • 다음과 같은 이유 때문에 생성자 주입을 사용한다.
    • 객체의 불변성을 확보할 수 있다.
    • 테스트 코드의 작성이 용이해진다.
    • final 키워드를 사용할 수 있고 Lombok과의 결합을 통해 코드를 간결하게 작성할 수 있다.
    • 순환 참조 에러를 애플리케이션 구동(객체의 생성) 시점에 파악하여 방지할 수 있다.

생성자 주입

@Service
public class UserService {

    private UserRepository userRepository;
    private MemberService memberservice;

    @Autowired
    public UserService(UserRepository userRepository, MemberService memberService) {
        this.userRepository = userRepository;
        this.memberService = memberService;
    }
}
  • 생성자를 통해 의존 관계를 주입하는 방법이다.
  • 생성자의 호출 시점에 1회 호출되는 것이 보장된다.
  • 주입받은 객체가 변하지 않거나 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용한다.
  • 생성자가 한 개 있을 경우 @Autowired 생략 가능하다.

수정자 주입 (Setter 주입)

@Service
public class UserService {

    private UserRepository userRepository;
    private MemberService memberservice;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

  @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }
}
  • 필드 값을 변경하는 Setter를 통해 의존 관계를 주입하는 방법이다.
  • 주입받는 객체가 변경될 가능성이 있는 경우 사용하지만, 실제로 변경이 필요한 경우는 드물다.
  • 주입할 대상이 없어도 동작하도록 하려면
    • @Autowired(required=false)를 통해 설정한다.

필드 주입

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberservice;
}
  • 필드에 바로 의존 관계를 주입하는 방법이다.
  • 코드가 간결해져서 과거에 많이 사용된 방법이다.
  • 외부에서 접근이 불가능하다는 단점이 있다.
    • 테스트 코드의 중요성에 따라 필드 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않게 되었다.

생성자 주입을 사용해야 하는 이유

  1. 객체의 불변성 확보
    • 의존 관계의 변경이 필요한 상황은 거의 없다.
    • 세터 주입은 불필요하게 수정의 가능성을 열어두어 유지보수성을 떨어뜨린다.
    • 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋다.
  2. 테스트 코드의 작성 
public class UserServiceTest {
	
	@Test
	public void addTest() {
		UserService userSerivce = new UserService();
		userSerivce.register("Yunkeun");
	}
}
  • 생성자 주입이 아닌 다른 주입으로 작성된 코드는 순수한 자바 코드로 단위 테스트 작성하는 것이 어렵다.
public class UserServiceTest {
	
	@Test
	public void addTest() {
		UserService userSerivce = new UserService();
		userSerivce.register("Yunkeun");
	}
}
  • register 메서드를 순수 자바 테스트 코드로 작성하면 위와 같이 작성한다.
  • 위의 테스트 코드는 Spring 위에서 동작하지 않으므로 의존 관계 주입이 되지 않을 것이다.
  • 그러므로 UserService의 userRepository가 null이 되어 add 호출 시 NPE (NullPointException)가 발생할 것이다.
  • 만약 이를 해결하기 위해 Setter를 사용하면 변경 가능성을 열어두게 되는 단점을 갖게 된다.
  • 반대로, 테스트 코드에서 @Autowired를 사용하기 위해 스프링을 사용하면
    • 단위 테스트가 아니면서
    • 컴포넌트들을 등록하고 초기화하는 시간 때문에, 테스트 비용이 증가하게 된다.
  • 생성자 주입을 사용하면 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 주입하는 객체가 누락된 경우 컴파일 시점에 오류를 발견할 수 있다.

3. final 키워드 작성 및 Lombok과의 결합

  • UserRepository와 MemberService와 같은 빈들은 Spring 컨테이너가 관리하는 싱글톤 객체이기 때문에 변하지 않는다.
    • 따라서, 해당 빈들이 생성자를 통해 주입되는 시점에 불변성을 보장하도록 final 키워드를 붙여주는 것이 좋다.
  • 생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있으며 컴파일 시점에 누락된 의존성을 확인할 수 있다.
    • 반면, 다른 주입 방법들은 객체의 생성 (생성자 호출) 이후에 호출되므로 final 키워드를 사용할 수 없다.
  • final 키워드를 붙이면 Lombok과 결합되어 간결한 코드를 작성할 수 있다.
    • final 변수를 위한 생성자를 대신 생성해 주는 @RequiredArgsConstructor가 있다.
@Service
@RequiredArgsConstructor
public class UserService {

	private final UserRepository userRepository;
	private final MemberService memberService;

	public void register(String name) {
		userRepository.add(name);
	}
}

4. 순환 참조 에러 방지

  • 생성자 주입을 사용하면 애플리케이션 구동 시점(객체의 생성 시점)에 순환 참조 에러를 예방할 수 있다.
  • 예를 들어, 다음과 같이 UserService와 MemberService가 서로 의존하는 코드가 있다고 하자.
@Service
public class UserService {

	@Autowired
	private MemberService memberService;

	public void register(String name) {
		memberService.add(name);	
	}
}

@Service
public class MemberService {

	@Autowired
	private UserService userService;

	public void add(String name) {
		userService.register(name);
	}
}
  • 두 메소드는 서로를 계속 호출하여 메모리에 함수의 CallStack이 계속 쌓여 StackOverflow 에러가 발생할 것이다.
  • 만약, 생성자 주입 사용 시 객체의 생성 시점인 애플리케이션 구동 시점에 에러가 발생하기 때문에 순환 참조 문제를 방지할 수 있다.

 

Ref

https://mangkyu.tistory.com/150

 

[Spring] 의존성 주입(Dependency Injection, DI)이란? 및 Spring이 의존성 주입을 지원하는 이유

1. 의존성 주입(Dependency Injection)의 개념과 필요성 [ 의존성 주입(Dependency Injection) 이란? ] Spring 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데, 그 중 하나가 의존성 주입(Depen..

mangkyu.tistory.com

https://mangkyu.tistory.com/125

 

[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)

Spring 프레임워크의 핵심 기술 중 하나가 바로 DI(Dependency Injection, 의존성 주입)이다. Spring 프레임워크와 같은 DI 프레임워크를 이용하면 다양한 의존성 주입을 이용하는 방법이 ..

mangkyu.tistory.com

https://velog.io/@gillog/Spring-DIDependency-Injection

 

[Spring] DI, IoC 정리

DI(Dependency Injection)란 스프링이 다른 프레임워크와 차별화되어 제공하는 의존 관계 주입 기능으로,객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다.DI(의존성 주입)

velog.io

 

댓글