반응형
# 의존관계 자동 주입
## 롬북과 최신 트랜드
- 개발을 해보면 대부분 불변, 그래서 final 키워드 사용하게 된다. 그런데 생성자도 만들어야하고 주입받는 값을 대입하는 코드도 만들어야 하는 귀찮음이 존재한다. 필드 주입처럼 편리하게 사용하는 방법은 없을까 라는 고민에서 시작.
- 생성자가 1개만 있을 경우 아래와 같이 @Autowired 생략 가능.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// 테스트 용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
롬북 라이브러리 적용. (build.gradle 에 라이브러리 및 환경 추가)
plugins {
id 'org.springframework.boot' version '2.6.3'
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'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
}
tasks.named('test') {
useJUnitPlatform()
}
- 1. File Settings(맥은 Preferences) > plugin lombok 검색 설치 실행 (재시작)
- 2. File Settings > Annotation Processors 검색 Enable annotation processing 체크 (재시작)
- 3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
package hello.core;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class HelloLombok {
private String name;
private int age;
public static void main (String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("abc");
//String name = helloLombok.getName();
//System.out.println("name = " + name);
System.out.println("helloLombok = " + helloLombok);
}
}
- @RequiredArgsConstructor : final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. (코드가 간결해진다.)
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// 테스트 용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
- 최근에는 생성자를 1개만 두고, @Autowired 를 생략하는 방법을 주로 사용한다. Lombok 라이브러리의 @RequiredArgsConstructor 함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.
## 조회 빈이 2개 이상인 경우.
- @Autowired 는 타입(Type)으로 조회한다.
@Autowired
private final MemberRepository memberRepository;
@Autowired
private final DiscountPolicy discountPolicy;
- 타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작. (실제로는 더 많은 기능을 제공한다.)
ac.getBean(DiscountPolicy.class)
## @Autowired 필드명, @Qualifier, @Primary
- 조회 대상 빈이 2개 이상인 경우 해결 방법 : @Autowired 필드명, @Qualifier, @Primary 등의 방법 사용.
1. @Autowired 필드 명 매칭
- @Autowired 는 타입 매칭을 시도, 이때 여러 빈이 있는 경우 필드명, 파라미터 이름으로 빈 이름을 추가 매칭한다.
- 기존코드 가 아래와 같을 때.
@Autowired
private DiscountPolicy discountPolicy
- 필드명을 빈 이름으로 변경.
@Autowired
private DiscountPolicy rateDiscountPolicy
- 필드 명이 rateDiscountPolicy 이므로 정상 주입.
- 필드명 매칭은 먼저 타입 매칭을 시도, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능.
- @Autowired 매칭 정리
1. 타입 매칭
2. 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭
2. @Qualifier @Qualifier끼리 매칭 빈 이름 매칭
- 추가 구분자를 붙여주는 방법. (주입 시 추가적인 방법을 제공할 뿐, 빈 이름을 변경하는게 아님)
- 빈 등록 시 @Qualifier 를 붙여서 사용.
생정자 자동 주입 예시.
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; // 할인율 10%
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000; // 1,000원 할인
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
수정자 자동 주입 예시.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// 테스트 용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
- @Qualifier 로 주입 시 @Qualifier("mainDiscountPolicy") 를 못찾게 될 경우 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
- @Qualifier 는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다.
- @Qualifier 정리
1. @Qualifier끼리 매칭
2. 빈 이름 매칭
3. NoSuchBeanDefinitionException 예외 발생
3 . @Primary 사용 (자주 사용!!)
- @Primary 는 우선순위를 정하는 방법으로, @Autowired 시 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; // 할인율 10%
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
- @Qualifier 의 단점 : 주입 받을 때 모든 코드에 @Qualifier 를 붙여주어야 한다는 점. (@Primary 를 사용하면 @Qualifier 를 붙일 필요가 없다.)
- 우선순위 : 자세한것(좁은선택범위) 이 우선순위가 높다. (스프링은 자동보다 수동, 넒은 범위의 선택권 보다 좁은 범위의 선택권이 우선 순위가 높다. @Qualifier 가 우선권이 높다.)
## 애노테이션 직접 만들기
- @Qualifier("mainDiscountPolicy") 와 같이 적게되면 컴파일시 타입 체크가 되지 않는 문제가 발생하는데, 애노테이션을 직접 만들어서 해결할 수 있다.
- 생성한 애노테이션에 Ctrl + B (인텔리제이, 윈도우 기준) 입력 시, 해당 애노테이션을 사용하는 곳 추적 가능하다.
package hello.core.annotation;
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)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
- 생성한 애노테이션 추가.
package hello.core.discount;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; // 할인율 10%
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
package hello.core.order;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// 테스트 용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
- 애노테이션에는 상속이라는 개념이 없다.
- 여러 애노테이션을 모아 사용하는 기능은 스프링에서 지원해주는 기능이다.
- @Qulifier 뿐만 아니라 다른 애노테이션들도 조합해서 사용 가능하다.
- @Autowired 도 재정의 할 수 있지만, 뚜렷한 목적 없이 무분별하게 재정의 하는것은 유지보수에 혼란을 줄 수 있다.
## 조회한 빈이 모두 필요할 때 List, Map
- 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우 사용.
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.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
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);
}
}
}
- DIscountService는 Map으로 모든 DiscountPolicy를 주입 받음. 이때 fixDiscountPolicy, rateDiscountPolicy 주입.
- discount() 멧드는discountCode로 fixDiscountPolicy(or rateDiscountPolicy)가 오는경우 map에서 fixDiscountPolicy(or rateDiscountPolicy) 빈을 찾아 실행한다.
## 자동과 수동의 올바른 실무 운영 기준.
편리한 자동 기능을 기본으로 사용.
- 스프링이 나오고 시간이 갈수록 자동을 선호하는 추세.
- 스프링은 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.
- 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건만 맞으면 자동으로 등록되도록 설계함.
- 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
수동 빈 등록은 언제 하면 좋을지.
- 애플리케이션은 크게 업무 로직과 기술지원 로직으로 나뉨.
- 업무로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등. 보통 비즈니스 요구사항을 개발할 때 추가 or 변경된다.
- 기술지원 빈 : 기술적 문제나 공통 관심사(AOP)를 처리할 때 사용. 데이터베이스 연결, 공통 로그 처리처럼 업무를 지원하기 위한 하부 기술 또는 공통 기술. (수동 빈 등록하여 설정 정보에 바로 나타나도록 하는게 유지보수 에 좋음)
- 업무 로직은 숫자가 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 등 어느정도 유사한 패턴이 존재. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. (문제 발생 시 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉬움)
- 기술 지원 로직은 업무 로직과 비교 시 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 광범위하게 영향을 미친다. 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
- 비즈니스 로직 중 다형성을 적극 활용할 때에는 수동 빈 등록하는 것이 좋다.
- 수동, 자동 등록의 핵심은 빈의 이름 및 어떤 빈들이 주입될지 한 눈에 파악 가능하도록 하는 것.
- 정리.
편리한 자동 기능을 기본으로 사용.
직접 등록하는 기술 지원 객체는 수동 등록.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민.
반응형
'인프런 강의 학습 > 스프링 핵심 원리(기본편)' 카테고리의 다른 글
재학습_10일차 빈 스코프 (0) | 2022.02.18 |
---|---|
재학습_9일차 빈 생명주기 콜백 (0) | 2022.02.17 |
재학습_7일차 의존관계 자동 주입_1 (0) | 2022.02.14 |
재학습_6일차 컴포넌트 스캔 (0) | 2022.02.13 |
재학습_5일차 싱글톤 컨테이너 (0) | 2022.02.12 |