01. 다양한 의존관계 주입 방법
의존 관계 주입에는 4가지 방식이 존재
- 생성자 주입
- Setter 주입
- 필드 주입
- 일반 메서드 주입
01-1. 생성자 주입
- 생성자를 통해 의존 관계를 주입
- 지금까지 진행한 방식이 생성자 주입
- 특징
- 불변, 필수 의존관계에 사용
- 생성자 호출시점에 딱 1번만 호출 보장
- 생성자 주입이 발생되는 시점은 스프링이 빈을 등록하면서 의존 관계 주입도 같이 발생한다. 즉 스프링도 new OrderServiceImpl(memberRepository, discountPolicy)를 호출해서 의존 관계 주입을 실행 한다
@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;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
// memberId를 기반으로 하여 회원 정보를 찾는다
Member member = memberRepository.findById(memberId);
// 회원 정보를 기반으로 할인율을 받아온다
int discountPrice = discountPolicy.discount(member, itemPrice);
// 회원 정보를 그대로 반환 해준다, DB는 사용하지 않음
return new Order(memberId, itemName, itemPrice, discountPrice);
}
//싱글톤 컨테이너 테스트용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
- 좋은 개발의 습관은 한계점, 제약이 존재하는 구조를 만드는 것
- 생성자를 통해 딱 1번만 초기화가 되고 후에는 변경이 불가능하다(불변)
- 가급적이면 setter와 같이 값을 변경하는 행위는 막아야 한다
- 생성자가 딱 1개만 있으면 @Autowired를 생략 할 수 있다
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자가 하나인 경우에는 @Autowired 생략이 가능, 2개인 경우는 안됨
// @Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
01-2. Setter 주입
- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계 주입
- 특징
- 선택, 변경 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법
package hello.core.order;
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
System.out.println("memberRepository = " + memberRepository);
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
System.out.println("discountPolicy = " + discountPolicy);
this.discountPolicy = discountPolicy;
}
...
}
- setter 주입시에는 필드에 final이 붙으면 안됨
- @Autowired 키워드가 없는 경우에는 주입이 안됨
- @Autowired의 기본 동작은 주입 대상이 없을 시 오류가 발생함
- 주입 대상이 없어도 동작하게 하려면 @Autowired(required = false) 지정
- 자바 빈 프로퍼티
- 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, getter, setter를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 이것이 자바빈 프로퍼티 규약을 말한다.
01-3. 필드 주입
- 이름 그대로 필드에 바로 주입하는 방법
- 특징
- 코드가 간결해서 많은 개발자들을 유혹 하지만 외부에서 변경이 불가능하다
- 테스트가 힘들어진다는 단점이 존재
- DI 프레임워크가 없으면 아무것도 할 수 없음
- 웬만하면 사용하지 않는다 다음과 같은 장소가 아니면
- 애플리케이션의 실제 코드와 관계 없는 테스트 코드
- 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별하게 사용
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
01-4. 일반 메서드 주입
- 일반 메서드를 통해서 주입 받을 수 있음
- 특징
- 한번에 여러 필드 주입 가능
- 메서드 위에 @Autowired 있다고 생각 하면 됨
- 사용할 일이 거의 없음
02. 옵션 처리
- 주입할 스프링 빈이 없어도 동작해야 하는 경우 사용
- 만약 @Autowired 만 사용하면 required 옵션의 기본값이 true 로 되어 있기에 주입 대상이 없으면 오류가 발생함
- 자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다
- @Autowired(required=false) : 자동 주입 대상이 없을 시 수정자 메서드 자체가 호출이 안됨
- org.springframework.lang.@Nullable : 자동 주입 대상 없을 시 null 입력 됨
- Optional<> : 자동 주입 대상 없을 시 Optional.empty 가 입력 됨
package hello.core.autowired;
public class AutowiredTest {
@Test
void test() throws Exception {
//given
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
//when
//then
}
static class TestBean {
/**
* @Autowired(required = false) 사용
* @param noBean1
*/
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
/**
* @Nullable 사용
* @param noBean2
*/
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
03. 생성자 주입을 선택해라!
- 과거에는 setter 주입, 필드 주입을 많이 사용함
- 하지만 현재 생성자 주입을 강조하는 상황
03-1. 생성자 주입을 권장하는 이유?
- 불변
- 대부분의 의존관계 주입은 한번 발생하면 애플리케이션 종료 시점까지 변경될 일이 없다. 오히여 종료 시점까지 변경되면 안된다
- 수정자 주입을 사용하면, setter를 public으로 열어야 함
- 누군가 실수로 변경할 수 있음
- 생성자 주입은 객체 생성 시 딱 1번만 호출, 변경이 불가능함
- 누락
- 프레임워크 없이 순수 자바 코드를 단위 테스트 하는 경우
04. 롬복과 최신 트랜드
- 생성자 주입을 통해 의존 관계 형성은 좋다
- 실제 개발에서는 이렇게 생성자를 일일이 만드는것도 일이다
- 롬복을 사용해서 이러한 부분을 어노테이션 기반으로 처리 한다
04-1. build.gradle Lombok 추가
// Lombok 추가
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
// Lombok 라이브러리 추가
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
04-2. settings
- Lombok 플러그인 설치
- annotation processor 옵션 활성화
04-3. @RequiredArgsConstructor
// AS-IS : Lombok 어노테이션 사용 전
@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;
}
}
// TO-BE : Lombok 어노테이션 사용 후
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
private final BoardService boardService; // 이 줄만 추가가 되면 된다
...
}
- “final”이 붙거나 “@NotNull”이 붙은 필드의 생성자 자동 생성
- 기존에 만든 생성자가 사라지는 효과를 가져옴
- 롬복이 자바의 어노테이션 프로세서라는 기능을 사용하여 컴파일 시점에 생성자 코드를 자동으로 생성함
05. 조회 빈이 2개 이상 - 문제
- 조회할 빈이 2개 이상인 경우 문제 해결
05-1. @Autowired
@Autowired
private DiscoutPolicy discountPolicy
- @Autowired의 경우 Type(타입)으로 빈을 조회
- 타입으로 조회 하기에 다음 코드와 유사하게 동작
// ApplicationContext ac = new Annotation...
ac.getBean(DiscoutPolicy.class)
- 스프링 빈 조회에서 타입으로 조회하는 경우 조회할 빈이 2개가 될 수 있는 문제점이 있다고 말했음
- DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘다 스프링 빈으로 선언해보자
05-2. OrderServiceImpl 수정
@Component
//@RequiredArgsConstructor
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;
}
}
- @RequiredArgsConstructor 어노테이션 주석 처리
- 이전 생성자 주입 방식으로 변경
05-3. RateDiscountPolicy, FixDiscountPolicy 수정
@Component
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;
@Component
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
@Component 어노테이션이 붙으면 ComponentScan 과정에서 해당 어노테이션을 기반으로 스프링이 Bean(빈) 등록을 진행 한다
- RateDiscountPolicy, FixDiscountPolicy 클래스에 @Component 어노테이션을 붙힌다
- 2개의 클래스의 부모 타입은 DiscountPolicy인 상태
- 현재 @Autowired를 통해 DiscountPolicy를 가져오는 상태였다
05-4. 전체 테스트 실행
- 테스트 실행 시 2개의 Bean 발견되어 Exception(예외)가 발생함
06. @Autowired 필드 명, @Qualifier, @Primary
- Bean이 2개인 경우 해결 방법 기재
06-1. 조회 대상 빈이 2개 이상인 경우
- @Autowired 필드 명 매칭
- @Quilifier → @Quilifier끼리 매칭 → 빈 이름 매칭
- @Primary 사용
06-2. @Autowired 필드 명 매칭
- @Autowired는 타입 매칭 시도, 이때 여러 빈이 존재하면 필드 이름(파라미터 이름)으로 빈 이름을 추가 매칭함
기존 코드
@Autowired
private DiscountPolicy discountPolicy
필드 명을 빈 이름으로 변경
@Autowired
private DiscountPolicy rateDiscountPolicy
- Bean이 2개 이상인 경우 필드명을 통해 bean 주입이 가능함
- AS-IS discountPolicy
- TO-BE rateDiscountPolicy
생성자 주입 사용
@Component
//@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy **rateDiscountPolicy**) {
this.memberRepository = memberRepository;
this.discountPolicy = **rateDiscountPolicy**;
}
}
- 생성자의 매개변수에 rateDiscountPolicy 필드를 지정
필드 주입 사용
@Component
//@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
@Autowired
private DiscountPolicy rateDiscountPolicy;
}
- 필드 주입 시 필드의 이름을 하위 타입 rateDiscountPolicy로 지정
@Autowired 정리
- 타입 매칭
- 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭
06-3. @Quilifier 사용
- @Quilifier는 추가 구분자를 붙이는 방법
- 의존 관계 주입 시 추가적인 방법을 제공하는 것
- 빈 이름을 변경하는 것이 아니다
빈 등록시 @Qualifier를 붙힌다
// RateDiscountPolicy
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
...
}
- mainDiscountPolicy라는 이름으로 @Qualifier를 생성
// FixDiscountPolicy
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
...
}
- fixDiscountPolicy라는 이름으로 @Qualifier를 생성
OrderServiceImpl 수정
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// @Qualifier 사용 : 필드명의 이름을 변경하지 않고도 의존 관꼐 주입이 가능하다
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
- @Qualifier는 @Qualifier를 찾을 때만 사용하는 것이 좋다
@Qualifier 정리
- @Qualifier 끼리 매칭 수행
- 만약 못 찾으면 빈 이름 매칭
- 만약 못 찾으면 Exception 발생
06-4. @Primary 사용
- @Primary는 우선순위를 정하는 방법
- @Autowired 시에 여러 빈 매칭되면 @Primary가 우선권을 가짐
RateDiscountPolicy가 우선권을 갖도록 수정
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { }
@Component
public class FixDiscountPolicy implements DiscountPolicy { }
- 여러개 Bean 타입이 조회될 경우 @Primary를 가진 클래스가 우선권을 가짐
- @Primary보다 @Qulifier가 우선순위가 높다
07. 어노테이션 직접 만들기
- 어노테이션 직접 만들기
// 이름이 잘못 되어도 문자열이기에 컴파일시 체크가 불가능
@Qualifier("mmmainDiscountPolicy")
@Qualifier(”mainDiscountPolicy”) 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다. 다음과 같은 어노테이션을 만들어서 문제를 해결 해보자
07-1. 어노테이션 생성
// 기존 Qualifier 인터페이스 확인
/**
* This annotation may be used on a field or parameter as a qualifier for
* candidate beans when autowiring. It may also be used to annotate other
* custom annotations that can then in turn be used as qualifiers.
*
* @author Mark Fisher
* @author Juergen Hoeller
* @since 2.5
* @see Autowired
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default "";
}
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
/**
* Create Custom Annotations
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
- MainDiscountPolicy 어노테이션 생성
RateDiscountPolicy 수정
package hello.core.discount;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
private final int discountPercent = 10;
/* 상품 가격 * 할인율% / 100 */
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
- @MainDIscountPolicy 어노테이션을 붙힌다
OrderServiceImpl 수정
@Component
//@RequiredArgsConstructor
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;
}
}
- 생성자의 매개변수쪽에 @MainDiscountPolicy 지정
- 이전에 @Qualifier와 동일하게 동작
- 웬만한 부분은 Custom 어노테이션을 무분별할게 사용하지 말자
08. 조회한 빈이 모두 필요할 때 List, Map
- 의도적으로 정말 해당 타입의 빈이 다 필요한 경우 존재한다
- 이 때 어떻게 해당 빈을 모두 가져오는지 확인 해보자
- 참고 : 전략 패턴
08-1. AllBeanTest 생성 및 분석
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.DisplayName;
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
@DisplayName("모든 Bean(빈)이 필요한 경우 가져오는 방법 List, Map")
void findAllBean() throws Exception {
// 복기 -> bean 등록
// ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class) 이런식으로 컨테이너에 등록 한다
// --> AutoAppConfig 안에는 DiscountPolicy, RateDiscountPolicy, FixDiscountPolicy Bean 담겨 있기에 첫 번째 인수로 등록
// --> AnnotationConfigApplicationContext(Class<?>... componentClasses)
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class); // IOC Container ApplicationContext에 사용할 Bean 등록
System.out.println("==================================");
System.out.println("--> ac = " + ac.toString());
System.out.println("==================================");
// 모든 bean 이름을 출력한다
// ㄴ DiscountPolicy, RateDiscountPolicy, FixDiscountPolicy, DiscountService
for (String beanName : ac.getBeanDefinitionNames()) {
System.out.println("Checking all bean name = " + beanName);
}
DiscountService discountService = ac.getBean(DiscountService.class); // return 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);
// rateDiscountPolicy
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");// 할인 금액 반환 메서드 호출
assertThat(discountPrice).isEqualTo(2000);
}
/**
* Map, List에 DiscountPolicy와 관련 된 모든 Bean(빈)을 주입 받기 위한 테스트 클래스 생성
*/
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); // fixDiscountPolicy
return discountPolicy.discount(member, price); // override 된 fixDiscountPolicy.discounty 메서드 호출
}
}
}
- DiscountService는 모든 DiscountPolicy를 주입 받는다, 이 때 fixDiscountPolicy, rateDiscountPolicy가 주입됨
- discount 메서드는 discountCode로 ‘fixDiscountPolicy’가 넘어오면 Map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 ‘rateDiscountPolicy’가 넘어오면 ‘rateDiscountPolicy’ 스프링 빈을 찾아서 실행한다
09. 자동 수동의 올바른 실무 운영 기준
09-1. 편리한 자동 기능을 기본으로 사용
ComponentScan, 자동 의존 관계 주입(필드 주입, 생성자 주입, setter 주입)
- 어떤 경우에는 컴포넌트 스캔을 사용하고, 어떤 경우에 수동으로 Bean을 등록해야 하는가?
- 결론부터 말하면 스프링이 나오고 시간이 갈 수록 자동으로 선호하는 추세로 가고 있다
- 스프링은 @Component뿐만 아니라 @Controller, @Service, @Repository, 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원
- 최근 스프링 부트는 컴포넌트 스캔을 기본으로 함
- 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계
- 즉, 일일이 객체를 생성하고 설정파일에 Bean을 등록 하는게 번거로운 일이다
- 결정적으로 자동으로 빈 등록을 해도 OCP, DIP를 지킬 수 있다
09-2. 수동 빈 등록은 언제하면 좋을까?
- 애플리케이션은 크게 업무 로직과 기술 지원 로직으로 구분 가능
- 업무 로직(비즈니스 로직)
- 웹을 지원하는 컨트롤러
- 핵심 비즈니스 로직이 있는 서비스
- 데이터 계층의 로직을 처리하는 리포지토리
- 기술 지원 빈
- 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용
- 데이터베이스 연결
- 공통 로그 처리
- 업무 로직(비즈니스 로직)
- 비즈니스 로직은 자동 빈 등록 사용, 기술 지원 빈은 수동 빈 사용을 권장한다
- 비즈니스 로직 중에서 다형성을 적용 할때는 수동 빈 등록 사용을 권장
- 위에서 작성한 DiscountSerivce 관련 테스트 코드 있었다
- 다음과 같이 별도의 설정 정보로 만드는 것이 좋음
@Configuration public class DiscountPolicyConfig { @Bean public DiscountPolicy rateDiscountPolicy() { return new RateDiscountPolicy(); } @Bean public DiscountPolicy fixDiscountPoslicy() { return new FixDiscountPolicy(); } }
'Spring MVC > Spring MVC - 핵심 원리 기본 1탄' 카테고리의 다른 글
[Spring MVC - 핵심 원리 기본] 빈 스코프 (0) | 2023.04.25 |
---|---|
[Spring MVC - 핵심 원리 기본] 빈 생명주기 콜백 (0) | 2023.04.25 |
[Spring MVC - 핵심 원리 기본]컴포넌트 스캔과 의존관계 자동 주입 (2) | 2023.04.25 |
[Spring MVC - 핵심 원리 기본] 싱글톤 컨테이너 (1) | 2023.04.25 |
[Spring MVC - 핵심 원리 기본] 스프링 핵심 원리 이해2 - 객체 지향 원리 적용 (0) | 2023.04.25 |