[Spring] 의존관계 자동 주입

kdkdhoho·2022년 2월 27일
0

Spring

목록 보기
11/26

이 글은 인프런 - 스프링 핵심 원리 기본편을 보고 공부한 것을 정리한 글입니다.

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

생성자 주입

이름 그대로 생성자로써 의존관계를 주입하는 것

특징으로 생성자 호출 시점에 딱 한 번만 호출되는 것이 보장
따라서 불변, 필수 의존관계에 사용

참고로 생성자가 딱 1개만 있을 경우, @Autowired 생략 가능
물론 스프링 빈에만 해당

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

수정자 주입(setter 주입)

필드의 값을 바꾸기 위해 setter라 불리는 수정자 메서드를 통해 의존관계를 주입하는 방법

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

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired(required = false)
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired(required = false)
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

@Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생. 하지만 @Autowired(required = false) 해주면 대상이 없어도 동작


참고로, 스프링은 크게 두 가지 라이프사이클로 나뉜다.

  1. 스프링 컨테이너에 스프링 빈 등록
  2. 의존관계 주입

여기서 의존관계 주입 시, @Autowired를 통해 의존관계 주입

생성자 주입과 수정자 주입의 차이

생성자 => 객체를 생성 시 생성자 호출. 때문에 빈 등록 시 자동주입. 1번 단계에 가까움
수정자 => 생성자 이후에 호출되어 의존관계 주입. 2번 단계에 가까움

결국, 순서를 따지자면 생성자 -> 수정자 순서

그리고 setter 쓰면 생성자 굳이 필요 x

생성자는 필수성, 불변성, 수정자는 선택성, 변경성

요즘은 생성자를 주로 사용하지만, 가끔 선택 및 변경 가능성이 있을 경우 setter도 사용

필드 주입

이름 그대로 필드에 @Autowired를 붙여서 바로 주입하는 방법

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

코드가 간결하다는 장점이 있음

하지만 외부에서 변경이 불가능하여 테스트 하기 힘들다는 치명적 단점이 있다. 따라서 사용하지 말자!

다만, 어플리케이션 실제 코드와 상관없는 테스트 코드인 경우나, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용하자

일반 메서드 주입

일반 메서드를 통해 주입하는 방법

특징으로, 한번에 여러 필드를 주입받을 수 있지만 일반적으로 잘 사용하지 않는다.

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

2. 옵션 처리

의존관계를 주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 이때, @Autowired만 명시하면 default 값으로 require = true로 설정되어 오류가 발생한다.

이 문제를 해결하기 위해 아래 세 가지 방법이 있다.

  1. @Autowired(required = false): 자동 주입 대상 없으면 호출되지 않는다.
  2. org.springframework.lang.@Nullable: 자동 주입 대상 없으면 null이 입력된다.
  3. Optional<>: 자동 주입 대상 없으면 Optional.empty가 입력된다.
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); // 호출 x
        }

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

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3); // noBean3 = Optinal.empty
        }
    }
}

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

의존관계 주입 방법 네 가지 중 가장 좋은 방법은 생성자 주입이다.

그 이유로 첫 번째, 불변성을 지킬 수 있다.
웬만한 어플리케이션의 의존관계는 초기에 한 번 설정되면 종료시점까지 변경되면 안된다.
따라서 setter나 다른 메서드로 public하게 열어두어 변경 가능성을 애초에 배제시킬 수 있다.

두 번째, 누락을 막을 수 있다.
생성자를 사용하면 필드에 final을 사용할 수 있다.
final을 사용하면 초기화가 되지 않는 것을 막을 수 있고, 값이 한 번 들어오면 더 이상 변경할 수 없게 된다.

생성자 주입 방식을 택하는 이유에 여러가지가 있지만, 프레임워크에 의존하지 않고 순수 자바 언어의 특징을 잘 살리는 방법이다.
따라서, 기본으로 생성자 주입 방법을 사용하고, 필수 값이 아닌 경우엔 수정자 주입 방식을 옵션으로 설정하여 생성자 주입, 수정자 주입을 동시에 사용할 수 있다.

4. 롬복과 최신 트렌드

이 생성자 주입을 더 간단하게 해줄 수 있는 라이브러리가 있다.
바로 Lombok 라이브러리이다.

우선 이 롬복 라이브러리를 사용하기 위해 해줘야 할 설정이 있다.

  1. gradle.build에 아래 코드들을 추가한다.
configurations {
  compileOnly {
  	extendsFrom annotationProcessor
  }
}
dependencies {
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  testCompileOnly 'org.projectlombok:lombok'
  testAnnotationProcessor 'org.projectlombok:lombok'
}
  1. Settings -> Plugin -> Lombok 설치
  2. Settings -> Annotaion processors -> Enable annotation processing 체크

간단한 롬복 사용 예시는 아래 코드와 같다.

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

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

        System.out.println("age: " + helloLombok.getAge() + " name: " + helloLombok.getName());
        System.out.println("helloLombok = " + helloLombok);
    }
}
// age: 11 name: hongdo
// helloLombok = HelloLombok(name=hongdo, age=11)

위와 같이 어노테이션으로 간단히 getter, setter, toString과 같은 메서드를 알아서 만들어 사용할 수 있게 해준다.

이 롬복을 이용하여 생성자 의존관계 주입을 간단히 해보자.

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

@RequiredArgsConstructor를 명시하면, 인자가 필요한 모든 필드에 대한 생성자를 알아서 만들어준다.

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

만약 이름이 다르고 타입은 똑같은 두 개의 스프링 빈이 동시에 의존관계에 주입된다고 가정해보자.
이러한 상황을 만들고 테스트하면 NoUniqueBeanDefinitionException이 터진다.

이를 해결하기 위한, 3가지 방법이 있다.

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

@Autowired 필드명 매칭

@Autowired는 타입 매칭을 우선적으로 시도하고, 이때 여러 빈이 있을 시, 필드 이름, 파라미터 이름으로 매칭한다.

이 점을 이용해서

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

필드 명을 적어주는 방법이 있다.

@Qualifier 사용

@Quilifier추가 구분자를 붙여주는 방법이다.
의존관계 주입 시 메타 정보를 제공하는 식이다.

@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를 붙여주고 등록한 이름을 적어준다.

만약 @Qualifier("mainDiscountPolicy")를 못찾으면 어떻게 될까? 그러면 mainDiscountPolicy라는 스프링 빈을 찾는다.
그럼에도 찾지 못할 경우, NoSuchBeanDefinitionException 예외가 발생한다.

추가로, 직접 스프링 빈 등록시에도 @Qualifier("mainDiscountPolicy")를 동일하게 적용할 수 있다.

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {}

@Primary 사용

이름 그대로 우선순위를 정해주는 방법이다.

의존관계로 등록하려는 구현체에 @Primary를 달아주자.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

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

어떤 것을 사용할까?

1, 2, 3번 방법 중 대부분의 경우 사용하는 구현체에 @Primary를 달아주고, 가끔가다 사용하는 테스트용? 구현체는 직접 @Qualifier를 통해 주입해주자.

@Primary보다 @Qualifier의 우선순위가 더 높다.
@Qualifier가 매우 상세하게 동작하기 때문 -> 스프링에서는 자동보다는 수동, 넓은 범위보단 좁은 범위의 선택권이 더 높은 우선순위를 가진다.

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

@Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일 시 타입 체크가 안된다.

아래와 같이 어노테이션을 직접 만들어 해결할 수 있다.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented // 여기까지는 모두 @Qualifier에 있는 어노테이션 가져온 것
@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 {}

이와 같이 어노테이션을 직접 만들어 이용하면 오타 발생 시 컴파일 오류가 발생해 쉽게 파악할 수 있다.

7. 조회와 빈이 모두 필요할 때, List, Map

의도적으로 해당 타입의 빈이 모두 필요한 경우가 있다.

만약 클라이언트가 할인 서비스의 종류를 선택할 수 있다고 가정해보자.
이때, 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.

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;

       @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 = this.policyMap.get(discountCode);

            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);

            return discountPolicy.discount(member, price);
        }
    }
}
policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@6bea52d4, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@11981797}
policies = [hello.core.discount.FixDiscountPolicy@6bea52d4, hello.core.discount.RateDiscountPolicy@11981797]
discountCode = fixDiscountPolicy
discountPolicy = hello.core.discount.FixDiscountPolicy@6bea52d4
discountCode = rateDiscountPolicy
discountPolicy = hello.core.discount.RateDiscountPolicy@11981797

이와 같이 Map 혹은 List에 원하는 타입의 구현체를 모두 보관하고, 유동적으로 구현체를 선택할 수 있다.

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

웬만하면 자동 기능을 기본으로 사용하자.

그럼 어느 경우에 수동 빈 등록을 하면 좋을까?

애플리케이션은 크게 업무 로직기술 지원 로직으로 나눌 수 있다.

  • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 지원 로직.
    보통 비즈니스 요구사항을 개발할 때 추가 & 변경된다.
    숫자도 많고, 한번 개발하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있다. 이러한 경우엔 자동 기능을 적극 사용하자.

  • 기술 지원 빈: 기술적인 문제나 AOP를 처리할 때 주로 사용.
    DB연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
    수가 적고, 보통 애플리케이션 전반에 걸쳐 영향을 미친다. 문제가 생겼을 때 어디가 문제인지 명확하게 파악하기 힘들다.
    그래서 이러한 기술 지원 로직은 가급적 수동 빈 등록으로 명확하게 들어내는 것이 중요하다.

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

그리고, 비즈니스 로직 중 다형성을 적극 활용할 때이다.
위 List, Map 코드를 봤을 때, 개발자가 아닌 사람이 코드를 봤을 때 Map<String, DiscountPolicy>만 있을 때 DiscountPolicy에 어떤 구현체들이 있는지 금방 파악하기 어렵다.
때문에 이럴 때는 따로 DiscountPolicyConfig.java 같이 별도의 설정 정보를 만들어 한눈에 보고 파악하며 관리할 수 있도록 하는 것이 좋다.

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

혹은 특정 패키지에 같이 두는 것이 좋다.

profile
newBlog == https://kdkdhoho.github.io

0개의 댓글