![[스프링 핵심 원리 - 기본편] 7. 의존관계 자동 주입](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJaul%2FbtsCR9F4pkI%2F5vhs7pBCctcX98pBoihPl0%2Fimg.png)
- 📌 다양한 의존관계 주입 방법
- ✅ 생성자 주입
- ✅ 수정자 주입 (Setter 주입)
- ✅ 필드 주입
- 📌 옵션 처리
- ✅ 자동 주입 대상 옵션 처리 방법
- 📌 생성자 주입을 선택해라.
- ✅ 생성자 주입을 권장하는 이유
- 📌 롬복
- ✅ 생성자 주입에 대한 개발자의 고민
- ✅ 롬복 적용하기
- ✅ 롬복 적용 방법
- 📌 조회 빈이 2개 이상일 때의 문제점
- ✅ 조회 빈이 2개 이상일 때
- ✅ 해결 방법 : @Autowired 필드 명 매칭
- ✅ 해결 방법 : @Qualifier 사용
- ✅ 해결 방법 : @Primary 사용
- ✅ 우선 순위
- 📌 어노테이션 직접 만들기
- ✅ 어노테이션 직접 만들기
- 📌 조회한 빈이 모두 필요할 때 : List, Map
- ✅ 조회한 빈이 모두 필요할 때
- 📌 실무 운영
- ✅ 기본적으로는 자동 기능을 사용하자
- ✅ 수동 빈 등록을 사용해야 하는 경우
- ✅ 비즈니스 로직 중 다형성을 적극 활용할 때
- ✅ 정리
인프런 김영한 강사님의 스프링 핵심 원리 - 기본편을 수강하고 정리한 글입니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢
www.inflearn.com
📌 다양한 의존관계 주입 방법
✅ 생성자 주입
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자를 통해 의존관계를 주입 받는 방식
생성자 호출 시점에서 딱 1번만 호출되는 것이 보장된다.
불변, 필수 의존관계에서 사용된다.
만약 생성자가 1개만 존재한다면, @Autowired를 생략해도 의존 관계가 자동 주입 된다.
스프링 빈을 등록하는 단계에 의존관계 주입이 발생한다.
일찍 실행되는 만큼, 다른 주입 방법들 중 우선 순위가 가장 높다.
✅ 수정자 주입 (Setter 주입)
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
필드의 값을 변경하는 수정자 (Setter) 메서드를 통해 의존관계를 주입하는 방식이다.
선택, 변경 가능성이 있는 의존관계에서 주로 사용한다.
✅ 필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
필드에 바로 의존관계를 주입하는 방식으로, 코드가 간결하다.
대신 외부에서 변경이 불가능하기 때문에 테스트하기 어렵고, DI 프레임워크가 없다면 아무것도 할 수 없다.
따라서, 비추천하는 의존관계 주입 방식이다.
스프링 설정을 목적으로 하는 @Configuration 같은 곳에서 특별하게 사용하긴 한다.
📌 옵션 처리
✅ 자동 주입 대상 옵션 처리 방법
기본적으로 @Autowired는 주입할 대상이 없으면 에러를 발생시킨다.
주입할 대상이 없더라도 동작을 시키고 싶다면, @Autowired(required = false) 의 방식이 있긴 하다.
이 방식은 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안된다.
org.springframework.lang.@Nullable : 자동 준비할 대상이 없으면 null을 입력한다.
Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 자동 입력된다.
📌 생성자 주입을 선택해라.
✅ 생성자 주입을 권장하는 이유
최근에는 스프링을 포함한 대부분의 DI 프레임워크가 생성자 주입을 권장한다. 그 이유는 아래와 같다.
불변
- 대부분의 의존관계 주입은 한번 해주면 애플리케이션 종료까지 변경할 일이 거의 없고, 불변하다.
- 수정자 주입을 사용하려면, Setter를 public으로 열어두어야 하는데, 분명 좋지 않은 방식이다.
- 생성자 주입은 객체 생성 시에 딱 한 번만 호출되므로, 불변 설계가 가능하다.
누락
- 수정자 주입일 경우, 주입 데이터를 누락했을 때 실행이 되는 반면, 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생한다.
final 키워드
- 생성자 주입을 사용하면 불변하기 때문에 필드에 final 키워드를 사용할 수 있다.
- 이는 생성자에서 혹시 값이 설정되지 않을 때 오류를 컴파일 시점에서 발생시켜 개발의 편의성이 높다.
- 컴파일 오류는 세상에서 가장 빠르고 좋은 오류이다.
- 생성자 주입 이외의 방식은 모두 생성자 이후에 호출되기 때문에, 생성자 주입 방식만 final 키워드를 사용할 수 있다.
📌 롬복
✅ 생성자 주입에 대한 개발자의 고민
막상 개발을 하다 보면, 대부분은 불변이고 그에 따라 생성자에 final을 사용하게 된다.
그런데, 생성자도 만들어야 하고 주입 받은 값을 대입하는 코드도 매번 동일하게 반복 작성해야 하니 귀찮아졌다.
이러한 귀찮음을 해결해주는 것이 바로 롬복 라이브러리이다.
✅ 롬복 적용하기
롬복 적용 전 (Before)
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
롬복 적용 후 (After)
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@RequiredArgsConstructor 이외에도 @Getter, @Setter, @NoArgsConstructor 등, 다양한 간편한 어노테이션들이 롬복에 존재한다.
✅ 롬복 적용 방법
build.gradle 파일에 라이브러리 관련 의존성 코드를 추가해야 한다.
plugins {
id 'org.springframework.boot' version '2.3.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.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') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
📌 조회 빈이 2개 이상일 때의 문제점
✅ 조회 빈이 2개 이상일 때
기본적으로 @Autowired는 타입으로 조회하는 방식이기에, 조회 결과 스프링 빈이 2개 이상일 수 있다. (ac.getBean(타입)의 방식과 같다.)
이 때, NoUniqueBeanDefinitionException이 발생한다.
물론 이 문제를 스프링 빈을 수동으로 등록하여 해결할 수도 있지만, 귀찮고 무엇보다 의존관계 자동 주입으로 해결할 수 있는 방법이 있다.
✅ 해결 방법 : @Autowired 필드 명 매칭
사실 @Autowired는 기본적으로 타입 매칭을 우선 시도하고, 이 때 여러 결과가 있으면 차선책으로 필드 이름 (파라미터 이름)으로 빈 이름을 추가 매칭한다.
1. 타입 매칭 (부모 자식 모두 끌고온다.)
2. 타입 매칭 결과가 2개 이상일 경우, 필드 명 / 파라미터 명으로 빈 이름 매칭을 실행한다.
✅ 해결 방법 : @Qualifier 사용
@Qualifier는 추가로 구분자를 붙여주는 방식이다. 빈 이름을 변경하지는 않는다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{}
주입 시에 @Qualifier를 붙이고 등록한 이름을 적으면 된다.
// 생성자 자동 주입 예시
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy ) {
this.memberRepository = memberRepository;
this.discountPolicy = discouontPolicy;
}
1. @Qualifier 끼리 매칭
2. 빈 이름 매칭
3. NoSuchBeanDefinitionException 발생
✅ 해결 방법 : @Primary 사용
우선 순위를 정해주는 방식이다.
@Autowired의 결과가 여러 개일 때, @Primary 어노테이션이 달려있는 것이 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
✅ 우선 순위
@Primary는 기본값 처럼 동작하고, @Qualifier는 매우 상세하게 동작한다.
스프링에서는 자동보다는 수동이, 넓은 범위 선택권 보다는 좁은 범위 선택권이 우선 순위가 높다.
우선 순위 :
@Qualifier > @Primary
📌 어노테이션 직접 만들기
✅ 어노테이션 직접 만들기
@Qualifier("mainDiscountPolicy")
위 방식 처럼 문자를 작성하면, 컴파일 시 타입 체크가 안된다.
문자는 컴파일 때 원래 타입 체크가 안되기 때문인데, 이를 해결하기 위해 어노테이션을 직접 만들 수 있다.
package hello.core.annotataion;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
참고로 어노테이션에는 상속 개념이 없다.
스프링에서 여러 어노테이션을 모아서 사용하도록 기능을 제공하는 것 뿐이다.
📌 조회한 빈이 모두 필요할 때 : List, Map
✅ 조회한 빈이 모두 필요할 때
의도에 따라, 특정 타입의 스프링 빈이 모두 필요한 경우가 있을 수도 있다.
이 때는 List나 Map과 같은 Collections를 사용한다.
package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
}
static class DiscountService {
// Map의 Key 값으로 스프링 빈의 이름을 넣고, Value 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 넣는다.
private final Map<String, DiscountPolicy> policyMap;
// DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담는다.
private final List<DiscountPolicy> policies;
// Map으로 모든 DiscountPolicy를 주입받는다.
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
📌 실무 운영
✅ 기본적으로는 자동 기능을 사용하자
자동 기능은 편리하다. 기본적으로는 자동 기능을 활용하자.
참고로, SpringBoot 또한 컴포넌트 스캔을 기본으로 사용한다.
자동 등록 방식이여도 DIP, OCP를 지킬 수 있다. 문제 없으니 걱정 말고 사용하자!
✅ 수동 빈 등록을 사용해야 하는 경우
기본적으로 업무 로직 스프링 빈은 숫자도 많고, 어느 정도 유사한 패턴이다. 따라서 자동 등록을 적극 사용한다.
그러나 기술 지원 로직 스프링 빈의 경우, 수가 적고 애플리케이션 전반에 광범위하게 영향을 미치고, 어디가 문제고 적용이 잘 되는지 안되는지 파악하기 어렵다.
따라서 이러한 기술 지원 로직 스프링 빈의 경우, 가급적 수동 빈 등록을 사용하여 명확하게 들어내는 것이 좋을 수 있다.
✅ 비즈니스 로직 중 다형성을 적극 활용할 때
여러 명이서 같이 개발하는 실무에서는 수동 빈으로 등록하는 것이 다른 사람이 파악하기에 편할 수 있다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
위 처럼 별도의 설정 정보를 만들고 수동으로 등록한다면, 한 눈에 빈의 이름과 어떤 빈들이 주입되는지 파악하기 편하다.
자동 빈 등록을 사용하고 싶다면 파악하기 편하도록 DiscountPolicy 인터페이스의 구현 빈들만 따로 모아서 특정 패키지에 두는 것이 편하다.
✅ 정리
기본적으로는 편리한 자동 기능을 사용하자.
직접 등록하는 기술 지원 객체의 경우, 수동 등록을 하자.
다형성을 적극 활용하는 비즈니스 로직의 경우, 수동 등록을 고민해보자.
'Backend > Spring' 카테고리의 다른 글
[스프링 핵심 원리 - 기본편] 9. 빈 스코프 (0) | 2024.01.02 |
---|---|
[스프링 핵심 원리 - 기본편] 8. 빈 생명주기 콜백 (0) | 2024.01.02 |
[스프링 핵심 원리 - 기본편] 6. 컴포넌트 스캔 (0) | 2024.01.02 |
[스프링 핵심 원리 - 기본편] 5. 싱글톤 컨테이너 (1) | 2024.01.01 |
[스프링 핵심 원리 - 기본편] 4. 스프링 컨테이너와 스프링 (0) | 2024.01.01 |
개발자가 되고 싶어요.