7.1 다양한 의존 관계 주입(Dependency Injection) 방법
Def) 의존 관계 주입(DI): 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다.
- @Autowired: 의존 관계 주입할 때, 사용하는 애너테이션이며 IoC컨테이너에 존재하는 빈을 찾아서 주입하는 역할을 한다.
- 객체의 의존성을 가지는 부분에 애너테이션을 사용해서 의존성을 주입할 수 있다.
의존 관계 주입
- 생성자 주입: 생성자를 통해 의존 관계를 주입한다. 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
- 불변, 필수 의존관계에서 사용된다.
- 생성자가 1개만 있으면 @Autowired를 생략해도 자동으로 주입된다. (스프링 빈에만 해당)
- 수정자 주입(setter): setter를 통해서 의존 관계를 주입하는 방법이다.
- 선택이나 변경 가능성이 있는 의존 관계에서 사용한다.
- setter의 매개변수가 스프링 빈에 등록되지 않은 경우에도 사용이 가능하다. 선택적으로 의존 관계 주입
- 선택적으로 하려면 @Autowired(required = false) 라고 주면 된다. 즉, 주입한 대상이 없어도 동작하게 하려면 @Autowired(required = false)
- @Autowired(required = true)는 주입 대상이 없으면 기본적으로 오류가 발생한다. true가 기본값이다.
- 앞에 set을 붙이는 것이 자바 프로퍼티 규약
- 필드 주입: 필드에 의존 관계를 주입한다. 필드 주입은 사용하지 않는다.
- 코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 스프링 설정을 목적으로 하는 @Configuration같은 곳 또는 애플리케이션 실제 코드와 관계없는 테스트 코드에만 특별한 용도로 필드 주입을 사용한다.
- @SpringBootTest처럼 스프링 컨테이너를테스트에 통합한 경우에만 가능하다. (순수한 자바 테스트 코드에는 @Autowired가 동작하지 않는다.)
- 필드 주입은 setter를 만들어야한다. 그럴바에는 setter에 주입하는 것이 낫다.
- 일반 메서드 주입: 일반 메서드를 통해 주입 받을 수 있다.
- 한 번에 여러 필드를 주입 받을 수 있다. 일반적으로 잘 사용하지는 않는다.
- 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야만 동작한다.
- 보통 생성자 주입, 수정자 주입 안에서 해결하므로 보통 일반 메서드 주입은 사용하지 않는다.
Ex 1) 생성자 주입 - 빈 등록하면서 의존 관계 주입도 함께 일어난다.
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
public MemberRepository getMemberRepository(){
return memberRepository;
}
@Autowired
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);
}
}
- OrderServiceImpl에 @Component가 있으면 빈에 등록하기 위해 스프링이 생성자를 호출한다.
- 그런데, 생성자에 @Autowired가 있으면? 스프링 컨테이너에서 스프링 빈(MemberRepository, DiscountPolicy)을 꺼낸다.
-> MemberRepository, DiscountPolicy를 꺼내서 주입해준다.
Note) 실행 결과
Ex) OrderServiceImpl의 생성자에 Autowired를 붙이기 전
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
System.out.println("memberRepository = " + memberRepository);
System.out.println("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;
}
}
Note) 실행 결과
Ex) Autowired 후 (OrderServiceImpl의 생성자에 Autowired를 붙인 경우)
<hide/>
package hello.core.singletone;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class ConfigurationSingletonTest {
@Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
// 모두 같은 인스턴스를 참조하고 있다.
System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
}
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
Note) basicScan 실행 결과
- 붙이기 전이나 후나 결과가 똑같다
- 생성자가 딱 하나기 때문에 자동으로 주입되기 때문이다.
Ex 2) 수정자(setter) 주입
- OrderServiceImpl클래스를 다음과 같이 수정한다.
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository; // = new MemoryMemberRepository();
private DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
@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;
}
// 생성자 주입
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;
}
}
<hide/>
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
Note) 실행 결과 - basic
1) OrderServiceImpl을 스프링 컨테이너에 등록한다.
2) 그 다음 스프링 컨테이너는 스프링 빈을 모두 등록하고 나서 연관 관계를 자동으로 주입한다. (생성자 먼저하고 그 다음에 수정자 setter)
- Autowired가 없으면 아래처럼 출력이 안된다.
Ex 3) 필드 주입
- OrderServiceImpl은 Autowired가 있기 때문에 생성자가 필요없다.
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
@Autowired private MemberRepository memberRepository; // = new MemoryMemberRepository();
@Autowired private DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
// @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;
// }
// public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
// System.out.println("1. OrderServiceImpl.OrderServiceImpl");
// 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;
}
}
<hide/>
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
System.out.println("call AppConfig.orderService");
// return new OrderServiceImpl(memberRepository(), discountPolicy());
return null;
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
<hide/>
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
Note) basicScan 실행 결과 - 성공
- basicScan에 내용을 추가해서 실행한다.
<hide/>
package hello.core.scan;
import hello.core.AutoAppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoAppConfigTest {
@Test
void basicScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
OrderServiceImpl bean = ac.getBean(OrderServiceImpl.class);
MemberRepository memberRepository = bean.getMemberRepository();
System.out.println("memberRepository = " + memberRepository);
}
}
Note) basicScan 실행 결과 - 성공
- private이더라도 의존 관계를 필드에 직접 넣을 수 있다.
- 그런데, 필드 삽입은 권장되지 않는다는 메시지가 뜬다.
-> 코드는 간단하지만 외부에서 변경이 불가능하다는 단점이 있기 때문이다.
Ex 4) 일반 메서드 주입 - 한 번에 여러 필드를 주입받을 수 있다.
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository; // = new MemoryMemberRepository();
private DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Autowired
public void init(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;
}
}
- 위와 같이 init 메서드를 추가하고 애너테이션을 붙인다.
- 자동으로 의존 관계가 주입된다.
Ex) 테스트 오류 수정하기
- ConfigurationTest()에서 ac.getBean("orderService", ..) => 이 부분에서 getBean()할 때 문제가 생긴다.
-> NullBean
-> 설정 정보인 AppConfig클래스로 가서 확인해본다.
- AppConfig에서 orderService()의 반환부에 return null 이었던 부분을 다시 원상 복구한다.
<hide/>
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){ // 누군가 오더서비스를 조회하면?
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
Note) 실행 결과 - 모든 테스트 완료
7.2 옵션 처리
옵션 처리
- 주입할 스프링 빈이 없어도 동작해야할 때가 있다.
- 그런데, @Autowired만 사용하면 'required'의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
Ex) 자동 주입 대상을 옵션 처리하는 방법
<hide/>
package hello.core.autowired;
import hello.core.member.Member;
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 org.springframework.lang.Nullable;
import java.util.Optional;
public class AutowiredTest {
@Test
void AutowiredOption(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean{
@Autowired(required = false)
public void setNoBean1(Member noBean1){
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2){
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3){
System.out.println("noBean3 = " + noBean3);
}
}
}
Note) 실행 결과
- 1번은 Member가 스프링 빈으로 등록되지 않았기 때문에 (required = false)를 해주지 않으면 예외가 발생한다.
-> 의존 관계가 없으면 수정자 메서드 자체가 호출되지 않는다.
- @Nullable은 자동 주입할 대상이 없는 경우, null이 입력된다.
-> 2번은 의존 관계가 없더라도 호출은 되지만 null
- Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
- @Nullable과 Optional운 스프링 전반에 걸쳐 지원된다. 생성자 자동 주입에서 특정 필드에만 사용해도 된다.
7.3 생성자 주입을 선택하라
생성자 주입을 선택하는 이유 (1) - 불변
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존 관계를 변경할 일이 없다. 오히려 대부분의 의존 관계는 변경하면 안된다.
- 수정자 주입을 사용하면, setXX 메서드를 public으로 열어둬야한다.
- 누군가 실수로 변경할 수 있고 변경하면 안 되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
- 생성자 주입은 객체를 생성할 때 한 번만 호출되므로 이후에 노출되는 일이 없다. 따라서, 변하지 않도록 설계할 수 있다.
생성자 주입을 선택하는 이유 (2) - 누락
- 생성자 주입을 사용하면 데이터를 누락했을 때, 컴파일 오류가 난다. 그리고 IDE에서 바로 어떤 값을 필수로 주입해야하는지 알 수 있다.
생성자 주입을 선택하는 이유 (3) - final 키워드
- 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 컴파일 시점에 오류 수정 가능하다
- 다른 주입 방법 세 가지는 final 사용 불가능하다. 생성자 이후에 호출되기 때문이다.
Note)
- 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
- 기본으로 생성자 주입을 사용하고 필수값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
- 따라서 항상 생성자 주입을 사용하고 옵션이 필요한 경우는 수정자 주입을 선택하면 된다. 필드 주입은 사용하지 않는게 좋다.
Ex)
- OrderServiceImpl 코드에 생성자를 지우고 setter를 추가한다.
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository; // = new MemoryMemberRepository();
private DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
@Autowired
public void setMemberRepository(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
// public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
// this.memberRepository = memberRepository;
// this.discountPolicy = discountPolicy;
// }
@Autowired
public void init(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;
}
}
- OrderServiceImpl테스트를 만든다. 순수한 자바 코드로 만든다.
<hide/>
package hello.core.order;
import org.junit.jupiter.api.Test;
class OrderServiceImplTest {
@Test
void createOrder(){
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
}
Note) 실행 결과
- NullPointerException이 난다.
- 아무리 createOrder()만 테스트하더라도 OrderServiceImp의 두 멤버변수의 값을 세팅해줘야한다.
-> 따라서, 가짜 멤버리포지토리라도 만들어서 넣어줘야한다.
- 다시 OrderServiceImp의 setter를 지우고 생성자를 살린다.
<hide/>
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.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository; // = new MemoryMemberRepository();
private DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Autowired
public void init(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;
}
}
- 그리고 한 명을 만들어준다.
<hide/>
package hello.core.order;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Test;
class OrderServiceImplTest {
@Test
void createOrder(){
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
memberRepository.save(new Member(1L, "name", Grade.VIP));
OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
orderService.createOrder(1L, "itemA", 10000);
}
}
Note) 실행 결과 - 성공
7.4 롬복(lombok)과 최신 트렌드
롬복과 최신 트렌드
- @getter, @setter, @toString
- @RequiredArgsConstructor: final이 붙은 필드에 대한 생성자를 만들어준다.
- 기능은 모두 사용하면서 코드를 깔끔하게 만들 수 있다.
Ex) @RequiredArgsConstructor
- build.gradle 파일을 다음과 같이 수정한다.
<hide/>
plugins {
id 'org.springframework.boot' version '2.7.2'
id 'io.spring.dependency-management' version '1.0.12.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'
// implementation 'org.projectlombok:lombok:1.18.22'
// implementation 'org.springframework.boot:spring-boot-starter-test'
// implementation 'junit:junit:4.13.1'
// testImplementation 'org.springframework.boot:spring-boot-starter-test'
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'
}
}
tasks.named('test') {
useJUnitPlatform()
}
- 다음과 같이 애너테이션 처리 활성화를 체크해줘야 롬복을 쓸 수 있다.
<hide/>
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
// RequiredArgsConstructor가 붙었으니 제거 가능
/* 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;
}
}
7.5 조회된 빈이 2개 이상인 경우 - 문제
- @ Autowired는 타입으로 조회한다.
- 그래서 마치, ac.getBean(DiscountPolicy.class)와 같이 동작한다.
- 스프링 빈 조회에서 본 것처럼 선택된 빈이 2개 이상일 때 문제가 발생한다.
Ex) DiscountPolicy의 하위 타입인 FixDiscountPolicy, RaterDiscountPolicy 둘다 스프링 빈으로 선언
<hide/>
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;
}
return 0;
}
}
- FixDiscountPolicy, RateDiscountPolicy 클래스에 각각 @Component를 붙여서 스프링 빈으로 등록한다.
Note) 테스트 실행 결과
- basicScan 테스트 실패한다. (NoUniqueBeanDefinitionException)
-> 빈이 유니크하지 않아서 오류가 발생
- 이 때, 하위 타입으로 지정할 수도 있지만 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.
- 스프링 빈을 수동 등록해서 문제를 해결해도 되지만 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.
7.6 여러 개의 빈이 선택될 때 해결방법 - @Autowired 필드명, @Qualifier, @Primary
@Autowired 필드명 매칭
- @Autowired는 먼저 타입 매칭을 시도한다. 타입이 하나면 바로 주입한다.
- 그런데, 타입 매칭의 결과가 2개 이상이면 필드 이름이나 파라미터 이름으로 빈 이름을 추가 매칭한다.
Ex) Autowired의 특별한 기능
- 타입이 여러 개가 똑같으면
- OrderServiceImpl의 생성자 부분에 내용을 수정한다.
- Autowired가 필드인 경우와 Autowired가 파라미터인 경우이다.
<hide/>
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
// Autowired가 필드인 경우
@Autowired
private DiscountPolicy rateDiscountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) { // Autowired가 파라미터 일 때
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
@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;
}
}
Note) basicScan 실행 결과 - 성공
@Qualifier 사용
- 추가 구분자를 붙여주는 방법이다.
- 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
- @Qualifier를 주입할 때, @Qualifier("mainDiscountPolicy")를 못 찾으면 어떻게 될까?
- 그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 그래도 없으면 NoSuchBeanException발생한다.
- 하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 게 명확하고 좋다.
Ex) qualifier
- RateDiscountPolicy에 @Qualifier를 붙인다.
<hide/>
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;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
return 0;
}
}
- 클래스 앞에 @Qualifier 붙이는 경우
<hide/>
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("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return discountFixAmount;
}
return 0;
}
}
- @Qualifier는 필드 인젝션에도 붙일 수 있고 매개변수에도 붙일 수 있다.
<hide/>
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.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; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
// Autowired가 필드인 경우
@Autowired
private DiscountPolicy rateDiscountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy")DiscountPolicy discountPolicy) { // Autowired가 파라미터 일 때
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;
}
}
Note) 실행 결과
@Primary
- 우선순위를 지정하는 방법이다.
- @Autowired 시에 여러 번 매칭되면 '@Primary'가 우선권을 가진다.
Ex) @Primary
- RateDiscountPolicy클래스에 애너테이션을 추가한다.
<hide/>
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;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
return 0;
}
}
Note) 실행 결과 - 성공
@Primary와 @Qualifier 활용
- @Qualifier의 단점은 주입 받을 때 모든 코드에 @Qualifier를 붙여야한다는 단점이 있다.
- 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 할 때, 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정없이 편리하게 조회하는 것이 좋다.
- 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지 가능하다.
- 이 때, 메인 데이터베이스 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것을 상관없다.
- @Primary는 기본값처럼 동작하고 @Qualifier는 매우 상세하게 동작한다.
- 스프링은 항상 자세한 것이 우선순위가 높다.
- 따라서, @Qualifier가 우선권이 높다.
7.7 애너테이션 직접 만들기
Ex) 애너테이션 만들기
- @MainDiscountPolicy 애너테이션을 만든다. 아래과 같이 애너테이션을 모두 붙이면 MainDiscountPolicy를 쓸 때, 스프링 컨테이너 안에서 @Target 부터 5가지 기능이 모두 동작한다.
<hide/>
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 {
}
- 아래 클래스에 애너테이션 추가
<hide/>
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;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
return 0;
}
}
- OrderServiceImpl에도 애너테이션을 추가한다.
<hide/>
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 lombok.RequiredArgsConstructor;
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; // = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy; // = new RateDiscountPolicy();
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) { // Autowired가 파라미터 일 때
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;
}
}
Note) 실행 결과 - 아직도 테스트 3개가 오류 난다.
====================================오류====================================
7.8 조회한 빈이 모두 필요할 때 List, Map으로 한 번에 조회하는 방법
- 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 있다.
- 예를 들어, 클라이언트가 할인 종류(rate, fix)를 선택 가능한 경우가 있다고 할 때, 스프링을 사용해서 전략 패턴을 간단히 구현할 수 있다.
Ex)
<hide/>
package hello.core.autowired;
import hello.core.discount.DiscountPolicy;
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;
public class AllBeanTest {
@Test
void findAllBean(){
ApplicationContext ac = new AnnotationConfigApplicationContext(DiscountService.class);
}
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
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);
}
}
}
- AllBeanTest 클래스를 만든다.
Note) 실행 결과
- 아무 값도 나오지 않는다.
- DiscountService만 스프링 빈으로 등록한 상태이기 때문이다.
- 다음과 같이 매개변수에 AutoAppConfig를 등록하고 DiscountService도 등록한다
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
- DiscouintService의 생성자 앞에 @Autowired도 추가한다.
Note) 실행 결과
Ex)
- DiscountService는 map으로 모든 DiscountPolicy를 주입 받는다.
- 그리고 map에 fixDiscountPolicy와 rateDiscountPolicy 주입이 된다.
<hide/>
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 rageDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rageDiscountPrice).isEqualTo(1000);
}
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);
return discountPolicy.discount(member, price);
}
}
}
Note) 실행 결과 - 성공
7.9 자동, 수동의 올바른 실무 운영 기준
자동, 수동의 올바른 실무 운영 기준
- 편리한 자동 빈 등록 기능을 기본으로 사용한다. 자동 빈 등록을 사용해도 DIP, OCP를 지킬 수 있다.
- 수동 빈 등록은 언제 사용하는 게 좋을까?
- 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.
- 기술 지원 빈: 기술적 문제나 공통 관심사(AOP)를 처리할 때, 주로 사용된다. 데이터베이스 연결이나 공통 로그 처리처럼 업무로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
- 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱 설정 정보에 바로 나타나게 하는 것이 유지보수 하기에 좋다.
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해본다.
'Spring Framework > [인프런] Spring 핵심 원리' 카테고리의 다른 글
Chapter 09. 빈 스코프(Bean Scope) (0) | 2022.08.16 |
---|---|
Chapter 08. 빈 생명주기(Bean Life Cycle) 콜백 (0) | 2022.08.16 |
Chapter 06. 컴포넌트 스캔(@ComponentScan) (0) | 2022.08.14 |
Chapter 05. 싱글톤 컨테이너 (0) | 2022.08.14 |
Chapter 04. 스프링 컨테이너와 스프링 빈 (0) | 2022.08.12 |