- 코딩 공부/Spring

[스프링 핵심 원리 기본편] 섹션7 - 의존관계 자동 주입

방개입니다 2022. 7. 4. 20:21

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

본 게시글의 모든 내용의 저작권은 위의 강의에 있습니다. 


 

 

 

1. 다양한 의존관계 주입 방법


 

  • 생성자 주입 
  • 수정자 주입 
  • 필드 주입
  • 일반 메서드 주입 

 

 

생성자 주입 

생성자를 통해 의존 관계를 주입 받는 방법이다. 

 

특징 - 생성자 호출시점에 딱 1번만 호출되며 불변, 필수 의존관계에 사용 

 

@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;
}
}

생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. - 스프링 빈에 한해서만! 

private final을 넣어줌으로써 1번 호출보장하며 싱글톤 또한 보장한다 

 

 

수정자 주입 

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;
}
}

 

기존 final 을 제외하고 set 빈이름으로 메소드를 만들어주면 @autowired가 달린 setter 메소드를 자동으로 호출해준다. 

수정자 주입방식은 선택, 변경 가능성이 있는 의존관계에 사용 

 

필드 주입 

이름 그대로 필드에 주입하는데 필드에 @Autowired를 집어넣음으로써 의존성을 주입하는 방식 

 

이러면 외부에서 변경이 불가능하고 테스트가 어려워지고 난해한 상황이 발생한다. 

@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}

 

일반 메소드 주입 

 

수정자 주입과 비슷하며, 한번에 여러 필드를 주입 받을 수 있다. 

일반적으로 잘 사용하지 않음 

 

@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}

생성자 주입으로 해결되기 때문에, 거의 메소드에 @autowired를 사용할 일이 없다.

 

 

2. 옵션처리


 

 

@autowired는 연결이 없는 경우나 연결할 Bean이 Null인 경우 아래와 같이 처리할 수 있다. 

spring에서 @autowired 옵션으로 required=false로 호출을 막을수도 있고, 

 

@Nullable을 통해서 익셉션을 막거나, 자바 8이상에서 Optional을 통해서도 처리 가능 

 

//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}

 

 

 

 

 

3. 생성자 주입을 선택해라 


 

다양한 관계 주입방법중에서도 우리는 생성자 주입을 공격적으로 사용해야한다. 

 

 

불변성, 누락시 컴파일 에러, final 키워드를 통한 생성자 설정여부를 컴파일 시점에 확인 가능 

 

@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
}
//...
}

위의 코드를 보면은 discountPolicy에 값을 설정해야하는데, 이 부분이 누락어서 컴파일시에 오류를 발생시킨다. 

 

컴파일 오류는 세상에서 가장 빠르고, 좋은 오류라고 하기에 생성자주입을 쓰자! 

 

생성자 주입의 선택 이유는 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이다. 가끔 옵션이 필요한 경우네만 수정자 주입을 사용하고 그 외에 방법은 지양하자. 

 

 

4. 롬북과 최신 트렌드 


 

개발을 하자보면 컴포넌트 대부분이 불편성을 가지므로 생성자 주입을 사용한다. 

 

하지만 생성자 주립은 private final 필드를 만들어 생성자를 통해 주입 받도록 설정하는데 귀찮다. 

 

롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final 이 붙은 필드를 모아서 생성자를 자동으로 만들어 줌 

 

롬복 사용 전 (간단하게 비교해보자) 

@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;
    }
}

 

사용 후 

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

 

5. 조회 빈이 2개 이상 - 문제 


@Autowired는 타입으로 Bean을 조회하기 때문에 2개 이상의 빈이 등록되는 경우, 

NoUniqueBeanDefintionException 이 발생 

 

ex ) discountPolicy - fixDiscountPolicy, rateDiscountPolicy 

 

 

해결법: 

@Autowired, @Qualitifer, @Primary 

 

 

1) @Autowired 사용 

@Autowired
private DiscountPolicy discountPolicy
@Autowired
private DiscountPolicy rateDiscountPolicy

의존관계 주입을 필드명을 빈이름으로 생성되며 필드명이 rateDiscountPolicy이므로 정상주입 된다. 

정리: 타입 매칭하고 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭 

 

2) @Qualifier 사용 

빈등록시 @Qualifier를 붙여 준다. 빈 객체에 별칭을 부여하는것과 같음 

 

@Qualifier("명칭")을 줘서 등록된 빈의 별칭을 찾아서 매칭해줌 

 

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

 @Qualifeir끼리 매칭하는것이 좋고 서로 찾는 용도로만 사용하는게 명확하고 좋기때문에 아예 사용을 안하거나, 모든 bean에 설정하여 사용하는 방식으로 통일된 작업을 하는것이 낫다. 

 

 

 

3) @Primary 

 

@Primary는 최우선순위를 정해주는 어노테이션이다. 

@Autowired시에 여러빈이 매칭되면 @primary가 우선권을 가진다. 

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

 

 

@Qulifier 보다는 사용성이 좋고 만약 @Qualifier 와 @primary가 동시에 맵핑되었을때 스프링의 특성을 인해 수동 설정이 많은 @Qualifier가 우선적으로 맵핑된다. 

 

 

7. 어노테이션 직접 만들기 


 

@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 {
}

 

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

 

//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

 

어노테이션은 상속이라는 개념이 없다. 

 

이렇게 여러 어노테이션이 모여서 사용하는 기능은 스프링이 지원해주는 기능이다. 

 

8. 조회한 빈이 모두 필요할때 List, Map 


 

해당타입의 스프링 빈이 다 필요한 경우가 있다. 

 

할인서비스를 제공하는데, 클라이언트가 할인의 종류(rate,fix)를 선택하는 경우 

 

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 {
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);
}
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);
}
}
}

해당 테스트에서 DisountService라는 정적 DI클래스 정의 한 뒤 테스트 진행 

 

ac.getBean을 통해 2개 이상의 조회된 빈을 DiscountService 생성자를 통해 Map, List 사용 

 

이렇게 2개 이상의 빈을 applicationContext에 getBean을 통해 받아 오는 빈 객체들을 map, List를 통해 받는게 가능 

 

 

마무리 


 

스프링은 수동보다 자동의존관계 주입을 더 사용한다. @Component @ Autowired를 적극 활용하여 OCP, DIP등 SOLID원칙을 잘 지키자. 

 

다형성을 적극활용 할 때 

위와 같이 다형성을 가지는 Bean을 관리 할 때에는 아래와 같이 수동등록을 따로 관리해주는게 쉽고 변경하기 좋다 

코드가 다른사람에게는 한 눈에 안들어오기 때문 

 

@Configuration
public class DiscountPolicyConfig {
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}