Dependency Injection
Dependency Injection, DI 는 의존관계 주입으로 Spring 프레임워크에서 제공하는 IoC 의 형태이다. 클래스 사이의 의존관계를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결해준다.
DI 를 통해 의존성을 주입하면 모듈 간의 결합도가 낮아지고 유연성이 높아지는 장점을 활용할 수 있다.
스프링에선 스프링 컨테이너 ApplicationContext 를 통해 설정 정보를 생성 및 등록하고 객체를 생성자 혹은 setter 를 통해 주입한다.
DI 종류
생성자 주입
@Service
public class Service {
private final Repository repository;
public Service(Repository repository) {
this.repository = repository;
}
}생성자를 통해 의존성을 주입하는 방식이다. 의존 관계를 모두 주입해야 객체 생성이 가능하기 때문에 NPE 를 방지할 수 있다. 의존 관계가 많아질 경우 생성자가 길어져 SRP 원칙을 상기시켜주기도 한다.
추가적인 설정이 필요하지 않고 뜻하지 않게 의존성과 설정값을 빠뜨리는 일이 없으며, 테스트하기에도 용이하기에 가장 권장되는 주입 방식이다.
특징
- 생성자 호출시점에 딱 1번만 호출되는 것이 보장됨
- 불변, 필수 의존관계에 사용
- 생성자가 1개만 있으면
@Autowired생략 가능
Setter 주입
@Service
public class Service {
private Repository repository;
public void setRepository(Repository repository) {
this.repository = repository;
}
}setter 를 통해 의존성을 주입한다. 선택적으로 의존성을 주입할 수 있기 때문에 모든 의존성을 주입 받지 않고도 객체 생성이 가능하다. 때문에 NPE 가 발생할 가능성이 높다.
특징
- 선택, 변경 가능성이 있는 의존관계에서 사용
- Java Bean Property 규약의 수정자 메서드 방식을 사용하는 방법
@Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면@Autowired(required = false)로 지정하면 된다.
필드 주입
@Service
public class Service {
@Autowired
private Repository repository;
}@Autowired 애너테이션을 통해 의존성 주입한다. 의존성 주입이 쉽기 때문에 SRP 원칙을 위배할 가능성이 높다. 사용하기 편리한 만큼 단점도 많은 방식이다.
DI 프레임워크 없이는 작동하기 힘들며, 외부에서 변경이 불가능하기 때문에 테스트하기 힘들다.
주로 애플리케이션과 관계없는 테스트코드나 @Configuration 과 같은 스프링 설정 목적으로 사용한다.
특징
- 외부에서 변경이 불가능하기 때문에 스프링 컨테이너를 사용하지 않고 순수한 자바 코드로 테스트하기 어려움
- 애플리케이션의 실제 코드와 관계 없는 테스트 코드에 사용
- 스프링 설정을 목적으로 하는
@Configuration같은 곳에서 특별한 용도로 사용
일반 메서드 주입
- 일반 메서드를 통해 주입하는 방법
- 특징
- 한 번에 여러 필드를 주입받을 수 있음
- 일반적으로 잘 사용하지 않음
옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있다. 그런데 @Autowired 만 사용하면 required = true 가 기본값이기 때문에 자동 주입 대상이 없으면 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.
@Autowired(required=false)- 자동 주입 대상이 없으면 수정자 메서드 자체가 호출 안됨
@Nullable- 자동 주입 대상이 없으면
null이 입력됨
- 자동 주입 대상이 없으면
Optional<>- 자동 주입 대상이 없으면
Optional.empty가 입력됨
- 자동 주입 대상이 없으면
생성자 주입을 선택해라!
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 그 이유는 다음과 같다.
불변
- 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션이 종료될 때 까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.
- 수정자 주입을 사용하면 setter 를 public 으로 열어두어야 한다.
- 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계가 아니다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
누락
- 수정자 주입을 사용하면 스프링의
@Autowired없이 순수한 자바 코드에서 의존관계를 누락할 가능성이 생긴다. - 생성자 주입을 사용하면 컴파일 오류로 미리 잡아낼 수 있다.
final 키워드
- 생성자 주입을 사용하면
final키워드를 사용할 수 있다. 이를 통해 값이 설정되지 않는 오류를 컴파일 시점에 알아낼 수 있다. - 다른 주입 방식은 모두 생성자 이후에 호출되기 때문에
final키워드를 사용할 수 없다.
정리
- 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
- 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
롬복과 최신 트렌드
막상 개발을 해보면, 대부분이 다 불변이고, 그래서 다음과 같이 생성자에 final 키워드를 사용하게 된다. 그런데 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하고…
필드 주입처럼 좀 편리하게 사용하는 방법은 없을까?
- 롬복 라이브러리가 제공하는
@RequiredArgsConstructor기능을 사용하면final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. - 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class 를 열어보면 다음 코드가 추가되어 있는 것을 확인할 수 있다.
- 최근에는 DI 를 할 때 생성자를 딱 1개 두고,
@Autowired를 생략하는 방법을 주로 사용한다. 여기에 Lombok 라이브러리의@RequiredArgsConstructor함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.
롬복 적용
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
// lombok
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
// lombok
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// lombok
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}- Preferences → plugin → lombok 설치 후 재시작
- Preferences → Annotation Processors 검색 → Enable annotation processing 체크
조회 빈이 2개 이상 - 문제
@Autowired 는 타입으로 빈을 탐색
2개일 경우 문제 발생
@Autowired 필드 명, @Qualifer, @Primary
조회 대상 빈이 2개 이상일 때
@Autowired필드명 매칭@Qualifier→@Qualifier끼리 매칭 → 빈 이름 매칭@Primary사용
어노테이션 직접 만들기
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}- 애노테이션에는 상속이라는 개념이 없음
- 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능