월루를 꿈꾸는 대학생

[핵심원리 - 기본] 의존관계 자동 주입 본문

Programing/Spring Boot

[핵심원리 - 기본] 의존관계 자동 주입

하즈시 2023. 7. 18. 21:07
728x90

다양한 의존 관계 주입 방법

  1. 생성자 주입
  2. 수정자 주입 (setter 주입)
  3. 필드 주입
  4. 일반 메서드 주입
생성자 주입
  • 생성자를 통해서 의존관계를 주입받는 방법
  • 지금까지 썼던 코드들

    생성자 호출 시점에 딱 1번만 호출되는 것이 보장
    한번만 세팅하고 그 후 수정 못하도록 하는 것이 가능 -> 불변 , 필수 의존 관계에서 사용
    협업시 아무나 못 건들게 만들도록... 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;
          this.discountPolicy = discountPolicy;
      }
    }

생성자가 딱 한 개만 있는 경우 !! @Autowired가 없어도 알아서 지정해줌


수정자 주입 (setter)
  • 보면 setter를 통해서 의존관계 / 객체를 받아서 주입하는 거 확인
  • @Component가 있는 클래스가 빈에 등록되면서 안에 있는 @Autowired가 적힌 메서드가 자동으로 컨테이너로 들어감 그래서 수정자로 넣어서 가능

선택 , 변경 가능성이 있는 의존관계에서 사용
자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

스프링 컨테이너에 빈을 등록한 후에 의존관계를 등록하는 방식이 또 따로 하나가 있음 = Autowired

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

필드 주입
  • 필드 변수에 바로 주입하는 방식

코드가 간결
외부에서 값 변경이 불가능해 테스트가 힘들다 치명적인 단점
DI프레임 워크가 없으면 아무것도 못함
쓰지 말자

@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}
  • 테스트할 때 구현체를 바꿀 방법이 없음 그래서 제대로 테스트하기가 힘들다
  • 순수하게 테스트 돌리는 @Autowired가 동작하지 않아서 테스트 불가 .. 널에러 터짐

일반 메서드 주입
  • 일반 메서드를 통해서 주입 받기 가능

    한번에 여러 필드를 받을 수 있다
    잘 사용 안 함 ;;

@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만 사용하면 required 기본이 true니까 자동주입을 시키는데 이 때 대상이 없다?? 그럼 또 오류가 나는거다

자동 주입 대상 옵션 처리 방법

  • Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다

public class AutoWiredTest {  
    @Test  
    void AutowiredOption(){  
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);  
    }  

    static class TestBean{  
        //호출 안됨  -> 자동 주입할 대상이 없으니까 !! member는 빈으로 등록 안 됨 
        @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);  
        }  
    }  
}

member 는 스프링 빈이 아니니까 저 위에 코드에서 다 null 같이 빈 객체를 참조하게 된다


생성자 주입을 선택해라!

불변

  • 실행시 배역까지 다 정해져서 변경할 일이 없음
  • 대부분의 의존관계 주입은 한 번 일어나면 종료까지 변경할 일이 없다 -> 오히려 불변해야한다!
  • 수정자 주입을 하려면 set메서드를 public으로 열어야 하니까 이거 누군가 사용할 수도 있고 변경할 수도 있어서 좋지 않음

누락

  • 프레임워크 없이 오로지 순수한 자바 코드로 단위테스트 할 수 있음

OrderServiceImpl 관련 테스트를 할 때 신경 쓸게 많아짐

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

위와 같이 수정자 주입으로 코드를 변경하고
테스트를 진행하면

@Test
void createOrder() {
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}

저기서 객체 만들 때 OrderServiceImpl보면 초기화가 안 되어 있으니까 Null에러가 뜸 ->누락
테스트 할 때 의존관계도 잘 안 보임 ;;

다시 생성자 주입으로 바꾸고 테스트하면 컴파일 에러가 뜸
파라미터가 없다고 -> 가장 좋은 에러 컴파일 에러

class OrderServiceImplTest {  
    @Test  
    void createOrder(){  
        MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository();  

        memoryMemberRepository.save(new Member(1L,"name", Grade.VIP)); 

        OrderServiceImpl orderService = new OrderServiceImpl(memoryMemberRepository, new FixDiscountPolicy());  

        orderService.createOrder(1L, "itemA",10000);  
    }  

}

다음과 같이 생성자 주입은 테스트 할 때 값을 바꿔서 알아서 자기 하고 싶은거 넣어서 테스트 가능!!
파라미터로 테스트 하고 싶은 가짜 객체 넣어주면 되니까

final 키워드

  • 생성자 주입할 때 생성자에 필드 변수에 넣어줄 수 있음
  • 생성자에 값이 설정되지 않는 오류를 컴파일 시점에서 막아줌!!!
  • final은 필수값이니까 설정 안 하면 에러로 알려주고 이는 또 값의 변경을 막아줌

생성자 주입 선택 이유 : 프레임워크 의존하지 않고 순수 자바의 특징을 살리는 방법
그냥 웬만하면 생성자 주입을 선택하자


롬복과 최신 트랜드

막상 개발하면 대부분 불변이라 final 키워드 쓴다
근데 보면 생성자 만들고 주입받은 값대입 코드 등등 공수가 많이 든다

  • 생성자가 하나니까 @Autowired 생략

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

해당 세팅 유효화 시켜두기

@Getter  
@Setter  
public class HelloLombok {  
    private String name;  
    private int age;  

    public static void main(String[] args) {  
        HelloLombok helloLombok = new HelloLombok();  
        helloLombok.setName("he");  
        String name = helloLombok.getName();  
        System.out.println("name : "+name);  
    }  
}

보면 다음과 같이 쉽게 getter setter 자동으로 만들어줘서 편리하다

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

보면 주석처리해도 자동으로 lombok이 메서드 만들어준 거 확인 가능 @RequiredArgsConstructor
final이 붙은 필드를 모아서 생성자를 자동으로 만들어줌

Autowired보다 깔끔하게 작성 되며 생성자 주입 , final로 불변 등 장점만 가득한 코드 작성이 가능하다

생성자를 1개 두고 @Autowired 생략하는 방식을 사용한다
@RequiredArgsConstructor 사용하면 깔끔하게 사용 가능


조회 빈이 2개 이상 - 문제

@Autowired는 타입으로 조회를 해서 의존 관계를 주입한다
타입이기 때문에 같은 혹은 하위타입이 2개 이상 있는 경우 뭐를 주입해야할지 모르니까 에러가 발생

FixDiscountPolicy에도 @Component를 붙여서 조회 할 때 2개가 나오도록 처리

해당 에러가 뜸
NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy

하위 타입을 따로 지정하는 것도 방법이지만 DIP위배와 유연성이 떨어지기에 자동으로 의존 관계 주입 방법을 밑에서 알아보자


Autowired 필드명 , @Quilifier , @Primary

조회 대상 빈이 2개 이상일 때 해결 방법

  1. @Autowired 필드명 매칭
  2. @Quilifier -> @Quailifer 끼리 매칭 -> 빈 이름 매칭
  3. @Primary 사용

@Autowired 필드 명 매칭
  • @Autowired가 타입 매칭을 시도한 다음 여러 빈이 있을 때 파라미터 이름으로 빈 이름을 추가 매칭한다 - 필터링
    // 기존 코드
    @Autowired
    private DiscountPolicy discountPolicy
    

// 변경 후
@Autowired
private DiscountPolicy rateDiscountPolicy


타입이 같은게 여러개 있으면 그 중에 해당 파라미터 이름과 같은 객체를 가져온다 

> 1. 타입매칭 
> 2. 빈이 2개 이상인 경우 파라미터명으로 빈 이름 매칭 


---


##### @Qualifier사용 
- 추가 구분자를 붙여주는 방식 
- 빈 이름 변경이 아니라 구분할 수 있는 추가적인 방법을 제공할 뿐 

```java 
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

생성자 주입 예

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

보면 파라미터에 @Qualifier 넣어서 어떤 빈을 넣을지 추가적인 매칭을 하는 것으로 오류가 안나게 막음

  1. @Quailifier 끼리 매칭
  2. 빈 이름 매칭
  3. NoSuchBeanDefinitionException 예외 발생


@Primary 사용

  • 우선순위를 정하는 방식
  • @Autowired에서 여러 빈이 매칭이 되면 @Primary가 붙은 빈이 우선순위를 가진다
  • 자주 사용하지만 한계가 있음
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

위에 있는 코드보다 깔끔함

활용법

  1. 자주 사용하는 메인 데이터베이스의 커넥션 활용하는 빈
  2. 가끔 사용하는 서브 데이터베이스의 커넥션 활용하는 빈

메인의 경우 자주 사용하니까 @Primary 써서 다른 별도 코드 추가없이 편하게 쓰고 서브 쓸 때는 @Quailifier 지정해서 명시적으로 획득하도록 코드 작성하면 깔끔하게 사용이 가능하다

@Priamary : 기본값 처럼 동작
@Qualifier : 상세하게 동작 / 옵션이 추가 된 느낌
우선순위는 같은 거라면 @Qualifier 가 더 높다


애노테이션 직접 만들기

  • @Qualifier("mainDiscountPolicy") 이렇게 써도 되지만 문자열은 컴파일시에 타입 체크가 안 됨
    @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,  
          ElementType.TYPE, ElementType.ANNOTATION_TYPE})  
    @Retention(RetentionPolicy.RUNTIME)  
    @Documented  
    @Qualifier("mainDiscountPolicy")  
    public @interface MainDiscountPolicy{  
    

}

위에처럼 애노테이션 만들어서 코드에 추가시키면 Qualifier에 컴파일 에러가 안 되는 단점을 없앨 수가 있다 

![](https://i.imgur.com/Pwi9GQv.png)
직접 만드니까 컴파일 할 때 문자열 틀리면 에러 알려줌 
그냥 Qualifier였으면 문자열 머 잘못쳐도 모르는데 굳굳 

다만 이게 생성자 주입할 때 롬복으로 하는 경우는 알아서 지정이 안 되니까 다시 생성자를 그대로 써야함 
![](https://i.imgur.com/t0QvL5X.png)
저거 주석 부분 살려서 다시 애노테이션 붙여줘야 에러 안 뜸 

애노테이션은 상속이란느 개념없이 그냥 조합해서 사용하는 느낌 
무분별하게 쓰면 유지보수만 빡세진다 ...;;


---

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

할인 서비스를 제공하는데 클라이언트가 할인의 종류를 rate, fix 선택할 수 있다고 가정하는 경우 같은 타입의 빈 2개를 모드 조회할 필요가 있음 
**전략 패턴을 활용**
```java
public class AllBeanTest {  
    @Test  
    void findAllBean(){  
        // DiscountService.class만 사용하면 빈이 없음 그래서 AutoAppConfig.class도 스캔  
        // AutoAppConfig은 컴포넌트 스캔을 사용 각 정책들을 빈 컨테이너에 등록  
        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);  
            return discountPolicy.discount(member,price);  
        }  
    }  
}

코드 내용 정리

  • map에는 fix 랑 rate가 들어가서 넣어짐 @Autowired겠지 생성자 하나니까 생략가능

  • dicount메서드는 빈에 들어간 빈에 들어간 객체이름을 인수로 사용해서 맵에서 해당 객체를 찾은 다음 그 객체의 discount 할인 메서드를 실행해서 사용한다

  • map에서는 빈에 들어간 객체이름(앞에 소문자) / 객체의 참조값 을 담아서 사용한다

스프링 컨테이너는 생성자에서 클래스 정보를 받고 클래스 정보를 넘기면 해당 클래스가 자동으로 빈에 등록되어 사용 가능하다

new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
// new로 스프링 컨테이너를 생성
// 클래스 2개를 파라미터로 넘겨서 해당 클래스를 자동으로 빈에 등록 

자동, 수동의 올바른 실무 운영 기준

편리한 자동기능을 기본으로 사용하자

점점 자동화 추세로 진화중
@Component만 넘으면 되는 일을 @Configuration 설정 정보 가서 @Bean만들고 객체 생성하고 주입대상 넣어주는 게 매우매우 귀찮다

자동 빈 등록을 해도 OCP , DIP 지킬 수 있다!!

  1. 업무 로직 빈
    컨트롤러, 서비스 , 리포지토리 등등 비즈니스 요구사항 개발할 때 추가 혹은 변경

  2. 기술 지원 빈
    기술적인 문제, 공통 관심사 AOP처리할 때 주로 사용
    데이터베이스 연결이나 공통로그 같이 업무로직 지원하기 위한 하부 기술 혹은 공통 기술

수동 빈 등록은 언제 사용하는가??

  • 기술 지원 로직에서 사용
  • 보통 광범위하게 영향을 미치기에 어디서 무엇이 잘못되었는지 파악이 힘들다 그래서 가급적 수동 빈 등록
    을 사용해서 명확히 하는 것이 좋다

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나
타나게 하는 것이 유지보수 하기 좋다

의존관계를 파악할 때 자동 빈 등록은 코드를 다 까봐야 어떤 게 들어갔는지 확인이 가능한데 수동 빈 등록은 코드만 보고 어떤 빈이 들어갈지 파악이 가능하다
이런 경우 수동 빈 등록 혹은 자동으로 특정 패키지에 같이 묶어 두는 것이 좋다

//수동빈 등록 예시 
@Configuration
public class DiscountPolicyConfig {
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

이런 설정 파일을 보면 한눈에 어떤 코드가 들어가는지 파악이 가능하다

일단 자동기능을 디폴트로 사용하자!!
직접 등록하는 기술 지우너 객체는 수동 등록으로 !
다형성을 적극 쓰는 비즈니스 로직은 수동 등록을 고민

출처
https://inf.run/wFfL

728x90