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

Turtle·2024년 6월 14일
0
post-thumbnail

🙄다양한 의존관계 주입 방법

✔️생성자 주입

  • 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
  • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
    • 생성자가 딱 1개만 있다면 @Autowired를 생략해도 자동 주입이 된다. 이는 스프링 빈에만 해당한다.
  • 불변, 필수 의존관계에 사용
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

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

✔️수정자 주입(setter 주입)

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
  • 특징
    • 선택, 변경 가능성이 있는 의존관계에 사용
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
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 프레임워크가 없으면 아무것도 할 수 없다.
    • 사용하지 말기
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;

✔️일반 메서드 주입

  • 일반 메서드를 통해서 주입받을 수 있다.
  • 특징
    • 한 번에 여러 필드 주입 가능
    • 일반적으로 잘 사용X

🙄옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
@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);

    }

    @Configuration
    static class TestBean {

        @Autowired(required = false)
        public void setNoBean1(Member member) {
            System.out.println("member = " + member);
        }

        @Autowired
        public void setNoBean2(@Nullable Member member) {
            System.out.println("member = " + member);
        }

        @Autowired(required = false)
        public void setNoBean3(Optional<Member> member) {
            System.out.println("member = " + member);
        }
    }
}

🙄생성자 주입을 선택하라

  • ✔️불변

    • 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된다.
    • 수정자 주입을 사용하게 되면 setxxx를 public으로 열어두어야 한다.
    • public으로 열어두면 누군가 실수로 변경할 수 있고 변경하면 안되는 메서드를 열어두는 것은 좋은 설계가 아니다.
    • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
  • ✔️final 키워드

    • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
    • 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

🙄롬복과 최신 트렌드

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

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("test");
        helloLombok.setAge(20);

        System.out.println("helloLombok = " + helloLombok);
    }
}

Lombok을 사용하여 Getter/Setter는 물론 toString()까지 어노테이션 하나로 직접 적용을 할 수 있다.
뿐만 아니라 @RequiredArgsConstructor 를 사용하면 final 키워드가 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
롬복이 자바의 어노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 .class를 열어보면 확인이 가능하다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;


    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member findMember = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(findMember, itemPrice);
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    // MemberRepository 확인 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

🙄조회 빈이 2개 이상인 문제가 발생

@Component
public class FixDiscountPolicy implements DiscountPolicy
@Component
public class RateDiscountPolicy implements DiscountPolicy
  • ✔️문제 원인

    • DiscountPolicy의 구현 클래스인 RateDiscountPolicyFixDiscountPolicy가 모두 스프링 빈으로 등록되어 조회되는 빈이 2개인 문제가 발생
  • ✔️문제 해결

    • @Autowired 필드명
    • @Qualifier
    • @Primary

🙄@Autowired 필드명, @Qualifier, @Primary

  • ✔️@Autowired 필드명
    • 필드명을 빈 이름으로 변경
    • 타입 매칭
    • 타입 매칭 결과가 2개 이상일 때 필드명으로 빈 이름을 매칭
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy fixDiscountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = fixDiscountPolicy;
}
  • ✔️@Qualifier
    • 주입시에 @Qualifier를 붙여주고 등록한 이름을 붙여준다.
    • @Qualifier끼리 매칭
    • 빈 이름 매칭
    • NoSuchBeanDefinition 에러 발생
@Component
@Qualifier("mainDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
  • ✔️@Primary
    • 우선순위를 정하는 방법
    • @Primary를 사용하면 @Qualifier를 붙일 필요가 없다.
@Component
@Primary
public class FixDiscountPolicy implements DiscountPolicy
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

🙄어노테이션 직접 만들기

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class FixDiscountPolicy implements DiscountPolicy
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
	this.discountPolicy = discountPolicy;
}

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

  • ✔️필요한 이유
    • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 있다.
    • 할인 서비스 제공시 클라이언트가 할인 종류를 선택할 수 있다고 가정할 때 스프링을 사용하면 전략 패턴을 간단하게 구현할 수 있다.
public class AllBeanTest {

    @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 discountPrice1 = discountService.discount(member, 10000, "fixDiscountPolicy");
        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
        Assertions.assertThat(discountPrice1).isEqualTo(1000);
        
        int discountPrice2 = discountService.discount(member, 10000, "rateDiscountPolicy");
        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
        Assertions.assertThat(discountPrice2).isEqualTo(1000);
    }

    @Configuration
    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policyList;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
            this.policyMap = policyMap;
            this.policyList = policyList;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policyList = " + policyList);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}
  • ✔️로직 분석
    • DiscountService는 Map으로 모든 DiscountPolicy의 스프링 빈들을 찾아서 넣어준다.
    • 이 때, rateDiscountPolicy와 fixDiscountPolicy가 주입된다.
    • discount() 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 맵에서 fixDiscountPolicy 스프링 빈을 찾아서 실행하고 "rateDiscountPolicy"가 넘어오면 맵에서 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.

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

  • ✔️편리한 자동 기능을 기본으로 사용하자.

    • @Component 뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞추어 일반적인 애플리케이션을 자동으로 스캔할 수 있도록 지원
    • 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다.
  • ✔️수동 빈 등록의 사용처

    • 업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 업무 로직
    • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다.
    • 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유리하다.

0개의 댓글