Spring 핵심 원리 - 의존관계 자동 주입

김태훈·2023년 3월 4일
0

Spring 핵심 원리

목록 보기
14/15

본 게시글은 인프런 '김영한'님의 '스프링 핵심원리 - 기본편' 강의를 듣고 정리한 글입니다.
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

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

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

1. 생성자 주입

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

다음과 같은 것이 생성자 주입이다. 우리가 평소에 해왔던 그대로.. 이다
MemberServiceImpl 클래스 안 생성자에 Autowired로 설정이 되어있어서 Spring 빈에 등록된 MemberRepository를 가져와 MemberServiceImpl 생성자에 주입시키는 방법을 말한다.
불변 의 의존관계에 사용한다. (getter setter를 통한 임의 변경을 막아야 함)
필수 의 의존관계에 사용한다. 보통의 생성자를 사용할 때 항상 주입하는 변수를 "무조건" 채워 넣는다고 생각하는 것이 정신 건강에 이롭다.

이 때, 생성자가 하나만 존재하는 클래스의 경우 , @Component로 스프링 빈에 등록이 되었을 때, 생성자에 @Autowired를 붙이지 않아도 자동으로 생성자 주입이 된다.

변수에 final 을 붙이는 이유
변수에 final을 붙이는 이유는 변수의 수정을 막기 위함이다.

2. 수정자 주입

클래스 내부 필드 변수를 수정할 때는 set과 get 메서드로 수정함을 이미 알고 있다.

따라서, 다음과 같이 코드를 작성해도 수정자(set메서드)로 주입이 가능하다.
당연히 @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;
	}
};

선택 의 의존관계에 사용한다. 스프링 빈에 등록되지 않은 것들을 주입할 수 있다. 예를들어서, memberRepository가 스프링빈에 등록되지 않는다면, 생성자 주입을 통해서는 사실상 필수적으로 스프링빈에 등록이 된 클래스만 주입을 해야하지만, 수정자 주입에 상황에서는 꼭 그렇지 않아도 된다. 이 때에는

@Autowired(required=false)

의 어노테이션을 추가한다. 만약, @Autowired 로 스프링빈에 등록되지 않은 대상을 주입하려면 오류가 발생한다.

자바 빈 프로퍼티
필드의 값을 직접 변경하는 것이 아니라 메서드를 이용하여 변경하는 방식을 말한다.

class Student{
	private String major;   
    public void setMajor(String major){
    	this.major = major
    }
    public void getMajor(){
    	return major;
    }
}

3. 필드 주입

@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;

와 같은 방식처럼 필드에 직접 @Autowired 어노테이션을 다는 방식인데,
외부에서 변경이 불가능해서, 테스트하기 힘들다는 단점이 있다.
이제는 사용하지 않으므로, 그냥 이런 게 있구나 하고 넘어가자.
물론, 진짜 한정된 상황 (이번만 쓰는 테스트)의 경우는 쓸 수도 있다.

4. 일반 메서드 주입

그냥 아무 메서드를 하나 만들어서, @Autowired 어노테이션만 달면 끝이다.
수정자 주입이랑 큰 차이는 없다.

2. 옵션 처리

앞서서 수정자 주입과(set 주입), 생성자 주입의 차이점을 말할 때, 수정자 주입의 경우, 스프링빈에 등록되어있지 않은 것들을 주입할 수 있다고하면서 @Autowired의 required 어노테이션을 언급하고 넘어갔었다. 여기에는 해당 방법을 포함하여 총 세가지 방법이 있다.

다음의 세가지 사항을 코드로 보면 다음과 같다.

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("noBean2 = " + noBean3);
        }
    }
}

다음처럼 새로운 Bean (스프링 Bean에 등록되어 있지 않은 Member 객체를 포함) 하는 클래스 TestBean을 선언하고, 해당 클래스를 AnnotationConfigApplicationContext에 등록한다.
저렇게 세 상황에서는 다음과 같은 결과가 나온다는 것을 알 수 있다.

1. @Autowired(required=false)

required = false가 걸린 noBean1은 아예 출력이 되지 않는다.
이유는 Member객체가 스프링빈에 등록이 되어있지 않은 상태에서 해당 어노테이션이 붙게 되면 해당 메서드가 아예 호출이 되지 않기 때문이다.

2. @Autowired(@Nullable ..)

이번에는 호출이 되지만, null로 등록이 된다.

3. @Autowired(Optional<..> ..)

이번에는 Optional.empty로 등록이 된다.

번외로, 혹시나 하는 마음에 AutoAppConfig 에서 등록한 MemberServiceImpl도 찍어봤다.

@Autowired
public void setYesBean(MemberServiceImpl memberServiceImpl){
   System.out.println("memberService = " + memberServiceImpl);
}

이렇게 되면 당연히, 오류가 뜨게 된다.
당연한 것이 TestBean에 속한 것들만 컴포넌트 스캔의 대상이 되기 때문이겠지?
이제 해당 코드를 required=false 처리를 하고 저장하겠다.

3. 많은 주입 방법중 생성자 주입을 선택하자

이유는 다음과 같다.

  • 어플리케이션을 제작하고 종료할 때 까지 의존관계를 변경할일이 거의 없기 때문이다. 즉, 생성자 주입의 이유인 "불변성" 을 유지해야 하기 때문이다. 수정자 주입을 해야한다면? public으로 set 메서드를 공개해야해서 불변성이 깨지기 쉽다.(버그 터지면 찾기가 힘들다)
    따라서 생성자 주입은 객체 생성시 단 한번만 생성 되므로 불변하게 설계할 수 있다.

  • final 키워드를 넣을 수 있다.
    "생성자" 에서만 값을 설정할 수 있다와 동치의 의미이다.
    final을 쓰면 생성자에서 누락된 파라미터가 있으면 '컴파일 오류' 가 나서 디버깅이 훨씬 쉬워진다!

생성자 주입 이외의 주입 방법은 final 키워드를 사용할 수 없다. 왜냐하면 그 외의 것들은 생성자 주입 이후에 호출되기 때문이다.

4. 롬복과 최신 트렌드

먼저 해야할 설정

  • lombok 관련 라이브러리를 build.gradle에 추가
  • plugin에서 lombok 을 다운로드
  • Compiler 설정에서 "Annotation Process" 에서 Enable annotation processing 켜주기

이렇게 하면 IntelliJ 에서 lombok을 사용할 수 있게 된다.

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

    public static void main(String[] args) {
        HelloLombok hellolombok = new HelloLombok();
        hellolombok.setName("hi lombok");

        String name =hellolombok.getName();
        System.out.println("name = " + name);
    }
}

@RequiredArgsConstructor 같은 경우, 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;
    }
}

다음과 같은 코드를

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

이렇게만 바꿔도, 알아서 생성자를 호출해준다는 의미이다.

5. @Autowired로 조회 빈이 2개이상인 경우의 문제

스프링 빈의 경우의 Type으로 조회를 하게 된다. 따라서 마치,

ac.getBean(~~~.class);

로 탐색하는 것과 비슷하다.
이 때, Type으로 조회하게 되는 경우 같은 타입의 등록된 스프링 빈이 2개 이상인 경우 문제가 생긴다.
예를들어 우리가 했던 RateDiscountPolicy와 FixDiscountPolicy의 종류이다.
두 구현체에 각각 @Component를 붙이면 어떤 결과가 발생하는지 보자.

테스트를 돌리면 이렇게 오류가 발생한다.

내용은 이러하다.

해결책

지금 RateDiscountPolicy 와 FixDiscountPolicy 구현체에 @Component를 붙여서 같은 타입의 빈이 두개 이상 등록된 상황이다.

1. @Autowired 필드 명 매칭

@Autowired는 처음에 '타입'매칭을 시도한다. 하지만 중복되는 타입의 빈이 여러개라면, 필드(파라미터) 이름으로 빈 이름을 추가 매칭한다.
따라서 다음처럼 코드를 수정하면 해결할 수 있다.

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

즉, 다음 순으로 매칭을 우선한다.

  • 타입 매칭
  • 필드 명 매칭

2. @Qaulifier 사용

이는 추가 구분자를 붙여주는 방법으로, 주입 시에 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
예를 들어, RateDiscountPolicy를 가져오길 원한다면,
RateDiscountPolicy에 Qualifier 어노테이션을 붙이고, 별명을 지어준다.

@Component
@Qualifier("mainDiscountPolicy")

그리고 FixDiscountPolicy에는 아무 별명이나 마찬가지로 지어준다. (안붙여도 괜찮다)

그후에, OrderServiceImpl에서 생성자 주입을 할때, discountPolicy 앞에 @Qualifier("mainDiscountPolicy") (RateDiscountPolicy의 별명)을 붙여주면 해결할 수 있다.

만약 해당 Qualifier 의 별명을 찾을 수 없다면?

해당 별명의 이름을 가진 스프링 빈을 추가로 찾는다.
그래도 없으면, NoSuchBeanDefinitionException 이 발생한다.

즉, 정리하면 다음과 같다.

  • @Qaulifier 매칭
  • 빈 이름 매칭
  • 예외

3. @Primary 사용

이는 우선순위를 정하는 방법이다.
마찬가지로, RateDiscountPolicy를 사용하고 싶으면, 해당 구현체에 @Primary 하나만 붙여주면 끝이난다.

Primiary vs Qualifier

좀더 복잡하고 자세한 Qualifier가 우선순위를 가져서 동작한다.

정리

이는 스프링 빈에 등록된 같은 타입의 데이터베이스의 커넥션을 변경할 때 사용한다.

6. 어노테이션 만들기

@Qualifier("~~") 와 같이 적으면 컴파일시 타입체크가 되지 않는다.
이를 직접 어노테이션을 만들어서 해결해보자.
Qualifier 어노테이션에 해당하는 외부 라이브러리를 살펴보면 (Shift 2번으로 검색)

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited

와 같은 것이 있는데 이를 그대로 복붙한다.
그후 annotation 패키지 내에 MainDiscountPolicy의 클래스(어노테이션)을 만들고 이를 붙여넣는다.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {

}

이를 활용하여 아까 RateDiscountPolicy와 이를 주입받은 OrderServiceImpl도 바꾸면된다.

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy)

이는 스프링에서 제공하는 어노테이션의 '상속' 느낌이라고 할 수 있다. 절대 어노테이션의 상속개념은 자바에 존재하지 않는다.

7. 조회한 빈이 모두 필요할 때, (List, Map)

의도적으로 해당 타입의 스프링 빈이 모두 필요할 때가 있다.
예를 들면, 사용자가 할인의 종류를 선택할 수 있는 경우가 그러한 경우이다.

public class AllBeanTest {
    @Test
    void findAllBean(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(DiscountService.class); //ctrl alt v
    }

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

        }
    }
}

여기까지만 하면, 당연히 아무것도 없는게 정상이다.
그래서 AutoAppConfig.class 까지 AnnotationConfigApplicationContext에 추가한다.

public class AllBeanTest {
    @Test
    void findAllBean(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class); //ctrl alt v
    }

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

        }
    }
}


그러면 이렇게 List와 Map에 두가지 스프링 빈이 포함되어있는 결과를 볼 수 있다.

이제 검증 코드를 보자

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class); //ctrl alt v
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
        Assertions.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);
        }
    }
}
  • DiscountService 클래스
    - 해당 클래스는 Map과 List로 모든 DiscountService를 주입받는다. (fix & rate)
    - discount 메서드는 discountCode ( fixDiscountPolicy or rateDiscountPolicy ) 를 인자로 받아서 같은 이름을 가진 policy를 map에서 get으로 꺼내온다.

    - 그 후에, 해당 Policy에 따라 discount를 적용한다.

8. 자동, 수동 빈 등록의 올바른 기준

기본으로 편리한 자동 기능을 사용하는 것이 좋다.
자동으로 해도, OCP(개방폐쇄원칙), DIP(의존역전원칙) 를 모두 지킬 수 있기 때문이다.

그렇다면 언제 수동으로 사용해야 하는가?

어플리케이션은 업무 로직과 기술 지원 로직 둘로 나눌 수 있다.

1. 업무 로직 빈

컨트롤러, 서비스, 리포지토리가 모두 업무 로직으로써 비즈니스 요구사항을 개발할 때 건드려야 하는 부분이다.
업무 로직은 숫자가 매우 많고, 문제가 발생하면 어디서 발생했는지 찾기 쉽기 때문에 자동화 기능을 사용하는 편이 좋다.

2. 기술 지원 빈

기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. DB 연결 혹은 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
업무 로직과 비교해서 수가 매우 적고, 어플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 문제가 어디서 발생했는지 찾기가 어렵기 때문에, 수동으로 직접 등록해서 사용하는 편이 좋다.

따라서 기술 지원 객체는 수동빈으로 등록해서 루트 디렉토리에 설정정보로 바로 존재하게끔 하는 것이 좋다.

3. 하지만, 업무 로직중에서도 수동 등록이 필요할 때가 있다.

바로 '다형성'을 활용할 때이다.
예를 들어서 DiscountPolicy 빈을 모두 조회하고, 해당 등록 빈들의 이름같은 것들을 알고 싶을 때, 한눈에 파악하기 위해서 수동으로 등록하기도 한다.

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

이렇게 해당 설정 Configuration 정보만 봐도, 어떤 빈들이 주입될지를 알 수가 있다. 그래도 자동으로 등록하고 싶다면, 한눈에 파악하기 좋게, 같은 디렉토리에 묶어서 저장하자(원래 했던 방식임)

profile
기록하고, 공유합시다

0개의 댓글