의존관계 자동 주입

2024. 10. 23. 15:32·Study/Spring

강의 링크

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

다양한 의존관계 주입 방법

  1. 생성자 주입
  2. 수정자 주입(setter 주입)
  3. 필드 주입
  4. 일반 메서드 주입

생성자 주입

생성자 자동 생성 단축키 : Alt+Insert (Windows/Linux)

  • 생성자를 통해 의존관계를 주입받는 방법.
    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
    • “불변, 필수” 의존관계에 사용된다. (값 세팅을 고정할 수 있음)
@Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  • 만약 여러개의 생성자가 존재한다면, @Autowired를 지정해주어야한다.
    • @Component가 지정되어있고 생성자가 하나라면, 하나뿐인 생성자에 @Autowired가 자동으로 지정된다.
  • 좋은 개발 방법은 “제약”에 있다.
    • 누구나 어디서든 수정할 수 있다면, 그 코드가 어디서 수정되었는지 찾기 쉽지않다.
      • 공연 중간에 배우를 바꾸지 않도록 제약을 설정한다고 생각하자.
  private final MemberRepository memberRepository ;
  private final DiscountPolicy discountPolicy;
  • 변하지 않고 필수 사용하도록 final로 선언 후 의존관계를 주입한다.





수정자 주입

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법.
    • “선택, 변경” 가능성이 있는 의존관계에 사용.
    • 자바빈 프로퍼티 규약의 수정자 메서드를 사용하는 방식.
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
  • setter에 @Autowired를 사용하면 의존관계를 주입할 수 있다. (@Autowired가 없으면 의존관계주입을 사용하는데 사용되지 않는 것으로 스프링 부트에서 인식함.)
  • 스프링 컨테이너의 두 단계의 라이프 사이클
    1. 스프링 빈 등록 (전부 등록) → 등록 시 생성자 주입은 자동적으로 일어남.
    2. Untitled
2. @Autowired가 있는 경우 의존관계를 주입한다. → 수정자는 이 단계에서 주입이 일어남.

    ![Untitled 1](https://github.com/Jedo0224/Jedo0224.github.io/assets/90050514/a803cb7e-56d7-4f96-8bc0-5db2a5dfeb3a)


- 따라서 순서는 OrderServiceImpl → (setMemberRepository → setDiscountPolicy) 순으로 주입.





필드 주입

    @Autowired private  MemberRepository memberRepository ;
    @Autowired private  DiscountPolicy discountPolicy;
  • 간결하다. 하지만.. → Field injection is not recommended
    • 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 존재한다.
      • 의존성 주입을 숨김: 필드 주입은 클래스의 필드에 바로 의존성을 주입하기 때문에 의존성이 어디서 주입되는지 명확하게 드러나지 않는다.
      • 의존성 주입을 변경할 수 없음: 필드 주입은 생성자 주입과 달리 의존성을 변경할 수 있는 방법을 제공하지 않는다
      • 테스트를 위해 어쩔 수 없이 생성자 또는 setter를 추가해줘야하는 번거로움
      • → 따라서 테스트 및 테스트 확장이 어려움
      • 스프링 컨테이너가 아닌 순수한 자바로 테스트할 수 있는 방법이 없다..*
      • 순수한 자바로 테스트하는 경우?
      • 단위 테스트(unit test)를 의미한다. → 프레임워크나 컨테이너의 도움 없이 테스트 코드만으로 클래스의 기능을 검증한다.
      • 사용 가능한 경우는? (하지만 가급적 쓰지 말자.)
      1. @SpringBootTest를 사용한 테스트를 진행할 때 (스프링 컨테이너에서 테스트할 때)
      2. @Configuation을 사용하는 경우(AppConfig 같이 의존성 주입시 최상위에 위에 위치하는 경우.. )





옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

 

언제?

선택적 의존성: 어플리케이션이 특정 기능을 제공하는 빈을 필요로 하지만, 그 기능이 항상 활성화되어 있지 않을 경우.

  • 어떤 기능이 특정 프로필이나 설정에 따라 사용되거나 사용되지 않는 경우.
  • 다양한 유형의 결제 시스템을 지원하도록 설계되었을 때, 특정 국가나 시장에 따라 결제 시스템을 선택적으로 활성화.

환경별 설정: 개발 환경과 실제 운영 환경 사이에서 사용되는 빈이 다를 수 있다.

  • 로컬 개발 환경에서는 특정 서비스의 가벼운 버전을 사용하고, 운영 환경에서는 전체 기능을 갖춘 서비스를 사용하는 경우.



자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안된다.
  • org.springframework.lang.@Nullable : 자동 주입 대상이 없으면 null이 입력된다.
  • Optional<> : 대상이 없으면 Optional.empty가 입력됨.
//호출 안됨
@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);
}
  • 결과 :setNoBean2 = null
    setNoBean3 = Optional.empty
  • setNoBean1은 호출이 아예 안됨.





생성자 주입을 선택하라!

  • 불변
    • 대부분 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변!)
    • 수정자 주입은 public으로 열어 놓기 때문에 변경이 가능하다.
    • 누군가 실수로 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 위험
    • 생성자는 객체 생성 시 단 1번만 호출!
  • 누락
    • 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우 해당 객체가 누락되어도(지금 실재로 존재하지 않아 가짜 객체를 사용하더라도) 해당 기능 및 동작을 테스트해볼 수 있다.
class OrderServiceImplTest {

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

        @Test
    void createOrder2() {
        MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository();
        memoryMemberRepository.save(new Member(1L, "name", Grade.VIP));

        OrderServiceImpl orderService  = new OrderServiceImpl(memoryMemberRepository, new FixDiscountPolicy());
        orderService.createOrder(1L,"itemA", 10000);
    }

}
  • 내가 단위 테스트를 진행하려는데 생성자 주입을 사용하지 않는다면.
    • 어떤 의존성을 주입해야하는지 모를 수 있다
    • 내가 새롭게 만든 더미클래스를 주입할 경우 → setter를 사용하는 건 위험 할 수 있다.
  • 생성자 주입을 사용한다면.
  • Untitled 2
개발자는 쉽게 의존 관계를 파악할 수 있으며, 라이브러리를 이용해  mock 객체를 만들며 쉽게 테스트가 가능하다.

```java
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository ;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {

    }
```

의존관계가 누락되었을 경우 → 개발자는 컴파일 시점에서 바로 이를 확인 할 수 있게 된다.





롬복 사용

@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 : arg가 요구되는 생성자를 자동으로 생성하고 의존성을 자동으로 주입함.





조회 빈이 2개 이상일 경우 해결 방법

  • @Autowireed 필드 명 매칭
  • @Qualifier → @Quilifier끼리 매칭 → 빈 이름 매칭
  • @Primary 사용



@Autowired 필드 명 매칭

Autowired는 Type으로 매칭을 시도한다 → 여래 개라면 → 파라미터 이름으로 빈 이름을 추가로 매칭한다.

  • MemberRepository memberRepository Or DiscountPolicy rateDiscountPolicy 로 구별
    • DiscountPolicy rateDiscountPolicy : rateDiscountPolicy로 구별해서 의존성을 준다.
       private final MemberRepository memberRepository ;
    private final DiscountPolicy discountPolicy;

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



@Qualifier를 사용

  • 주입시 @Qualifier를 붙여주고 등록한 이름을 적어준다.
@Component
@Qualifier("mainDiscountPoilcy")
public class RateDiscountPolicy implements DiscountPolicy {
  • 생성자에 해당하는 의존성을 @Qualifier로 주입한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,@Qualifier("mainDiscountPoilcy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}



@Primary 사용

메인데이터 베이스가 있고 보조 데이터 베이스가 있다고 가정한다면 → 메인데이터 베이스에 Primary를 부여하고 나머지에는 @Qualifier를 사용하도록 팀에서 룰을 지정할 수 있음.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

스프링은 제한이 있을 수록 권한을 더 준다. → 따라서 Primary보다 Qualifier가 더 우선순위가 있는데 이는 사용자가 수동으로 이름을 설정해 주입하는 것이기 때문에 더 제한이 있다고 판단하기 떄문이다.





어노테이션 직접 만들기

  • Qualifier에 이름을 등록할 경우, 문자열이기 때문에 컴파일 시 Qualifier 이름을 잘못 적었더라도 알 수가 없다.
  • 따라서 어노테이션을 직접 만들어서 사용할 수 있다.
    • 어노테이션 생성
    • @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @Qualifier("mainDiscountPolicy") public @interface MainDiscountPolicy { }
    • 어노테이션 사용
      @Autowired
      public OrderServiceImpl(MemberRepository memberRepository,@MainDiscountPolicy DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
      }
    • @Component @MainDiscountPolicy public class RateDiscountPolicy implements DiscountPolicy {





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

의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 어떤 경우일까?

  • 사용자가 할인의 종류를 선택할 수 있다(rate, fix) → 할인 전략 패턴을 간단하게 구현할 수 있다.
 @Test
    void findAllBean() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class,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);
        }

          public int discount(Member member, int price , String discountCode){

           DiscountPolicy discountPolicy = policyMap.get(discountCode);
           return discountPolicy.discount(member,price);
        }
    }

Untitled 3

  • 모든 빈을 조회 할 수 있으며, 이제 필요한 bean을 사용할 수 있음
    • 예시 (rate Or fix)
    • @Test void findAllBean() { AnnotationConfigApplicationContext 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"); // 멤버에 값 넣기 Assertions.assertInstanceOf(DiscountService.class, discountService); Assertions.assertEquals(1000,discountPrice); int rateDiscountPrice = discountService.discount(member, 20000 , "rateDiscountPolicy"); Assertions.assertEquals(2000,rateDiscountPrice); }
    • “로직 분석”*
    1. DiscountService은 discountPolicy를 주입받는다 (이때 rate, fixDiscountPolicty를 주입을 Map으로 주입받음)
    2. 이후 Arg에 따라 해당 가격에 rate 또는 fix를 부여한다.

스프링으로 모든 빈을 조회하고 다형성을 바탕으로 전략적으로 로직을 적용할 수 있다.





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

편리한 자동 기능을 기본으로 사용하는 것을 추천함.

  • 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다.
  • 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

@Controller , @Service , @Repository 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원하고 있음.

그러면 수동 빈은 언제 사용할까?

  1. 애플리케이션을 업무로직 / 기술 지원 로직으로 나눈다면 → 기술 지원 로직일 경우 수동 빈 등록을 사용.
    • 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포 지토리등이 모두 업무 로직.
    • 기술 지원 빈*:
    • 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술을 뜻함.
      • 프로젝트에서 라이브러리를 사용해서 로직에 적용하거나, 따로 CORS를 적용시켜주어야 했을 경우 수동 빈으로 처리했다.
      • 기술 지원 로직은 적용 이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.
  2. 업무 로직 빈 :



  1. 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보며 해당 로직과 관련된 클래스들은 패키지로 묶어 유지보수하기 편하게 만들고 싶을 때 사용한다.이 코드만 확인했을 경우 DiscountPolicy에 어떤 빈이 등록되어 있는지 확인하기 쉽지않다.
    • 하지만 이처럼 수동으로 등록하여 클래스에 정리해 놓았을 경우. 개발자는 쉽게 빈 등록 정보를 알 수 있다. → 좋은 추상화가 가능함.
  2. @Configuration public class DiscountPolicyConfig { @Bean public DiscountPolicy rateDiscountPolicy() { return new RateDiscountPolicy(); } @Bean public DiscountPolicy fixDiscountPolicy() { return new FixDiscountPolicy(); } }
  3. 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); }

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체, 비즈니스 로직 전략은 수동 빈으로 등록해 제대로 확인 할 수 있도록 !! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.

저작자표시 비영리 (새창열림)
'Study/Spring' 카테고리의 다른 글
  • 빈 스코프
  • 빈 생명주기 콜백
  • 컴포넌트 스캔
  • 싱글톤 컨테이너
Jedo0224
Jedo0224
쟤도 하면 나도 할 수 있다.
    글쓰기
    관리
  • Jedo0224
    JEDO의 개발일지
    Jedo0224
  • 전체
    오늘
    어제
    • Total (48)
      • Develop (0)
      • Study (48)
        • Spring (18)
        • Algorithm (15)
        • Coding-test (13)
        • Java (2)
        • K8s (0)
        • MSA (0)
  • 공지사항

  • 인기 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
Jedo0224
의존관계 자동 주입
상단으로

티스토리툴바