[Spring] 기본편 #7 - 의존관계 자동 주입

strongmhk·2023년 5월 29일
0

Spring

목록 보기
7/25
post-thumbnail

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

이번 포스트에서는 컴포넌트 스캔에서 잠깐 언급했던 의존관계를 자동 주입해주는 @Autowired에 대해서 파헤쳐볼 것이다


✅생성자 주입


  • 이름 그대로 생성자를 통해 의존관계를 주입받는 방법이다
  • 이전 포스팅부터 쭉 진행해왔던 방법이 생성자 주입이다
  • 생성자 호출시점에 딱 1번만 호출된다
  • 불변, 필수 의존관계에 사용한다
    • 불변 : setter와 getter를 만들지 않아 생성 후에는 수정할 수 없게 만든다
    • 필수 : 스프링 컨테이너에 스프링 빈으로 등록하려면 생성자를 통해 객체로 생성해야 하기 때문에 생성자 호출 시 의존관계가 필수로 주입돼야한다(final 필드로 선언돼있기 때문에 반드시 초기화해줘야한다)
  • 생성자가 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라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입하는 방법이다
  • 선택, 변경 가능성이 있는 의존관계에 사용
    • 선택 : Autowired에서 required 설정값을 생략하면 @Autowired(requierd = true)로 설정돼 의존관계를 반드시 주입해줘야한다. 의존관계 주입을 선택적으로 할때(주입할 대상이 없을 때)는 @Autowired(requierd = false)로 바꿔주면 된다
    • 변경 : setter 메소드를 이용하면 구현체를 변경할 수 있다



💻예제

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

📝참고(자바빈 프로퍼티 규약)

자바에서는 과거부터 필드의 값을 직접 변경하지 않고, set필드이름, get필드이름 메소드를 통해서 필드 값을 읽거나 수정하는 규칙을 사용해왔다.
이때 필드이름의 맨 앞글자는 대문자로 해준다

자바빈 프로퍼티 예시


class Data {
	private int age;
	public void setAge(int age) {
		this.age = age;
	}
	public int getAge() {
		return age;
	}
}	



✅필드 주입

  • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적 단점이 있다
  • DI 프레임워크가 없으면 아무것도 할 수 없다
  • 사용할 필요가 없다
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드
    • 애플리케이션 설정 정보인 @Configuration 같은 곳에서만 특별한 용도로 사용



💻예제

@Component
public class OrderServiceImpl implements OrderService {

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

}

📝참고

  • 순수한 자바 테스트 코드에는 @Autowired가 동작하지 않는다 스프링 컨테이너에 등록된 빈에만 사용할 수 있다, @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다
  • 다음 코드와 같이 @Bean에서 파라미터에 의존관계는 자동 주입된다. 수동 등록시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결할 수 있다
@Bean
OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy discountPolicy) {
	new OrderServiceImpl(memberRepository, discountPolicy)
}



✅일반 메소드 주입

  • 일반 메소드를 통해서 주입 받을 수 있다
  • 한 번에 여러 필드를 주입 받을 수 있다
  • 보통 잘 사용하지 않는다



💻예제

@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<> : 자동 주입할 대상이 없으면 Optionl.empty가 입력된다



💻예제

package Hello.core.autowired;

import Hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;

import java.util.Optional;

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);
        }
        
        //null 호출
        @Autowired
        public void setNoBean2(@Nullable Member noBean2){
            System.out.println("noBean2 = " + noBean2);
        }
		
        //Optional.empty 호출
        @Autowired
        public void setNoBean3(Optional<Member> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }

    }
}

  • Member는 스프링 빈이 아니다
  • setNoBean1()은 @Autowired(required=false)이므로 메소드 호출 자체가 안된다
  • @Nullable, Optional은 스프링 전반에 걸쳐 지원되기 때문에 생성자 자동 주입에서 특정 필드에만 사용해도 된다






📌생성자 주입을 선택해야하는 이유




✅이유1 : 불변

  • 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 변경할 일이 없으며, 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다
  • 수정자 주입을 사용하면, setter 메소드를 public으로 열어두어야 한다(누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메소드를 열어두는 것은 좋은 설계 방법이 아니다)
  • 생성자 주입은 객체를 생성할 때 딱 1번 호출되므로 불변하게 설계할 수 있다



✅이유2 : 누락

🎈수정자 주입 이용

다음과 같이 수정자 주입을 사용하면 컴파일러가 잡아주지 못하는데, NPE(Null Pointer Exception)이 발생하는 것을 알 수 있다



🎈생성자 주입 이용

그러나 생성자 주입을 이용하면 생성자 위에 1 related problem이라고 오류가 뜨는 것을 볼 수 있고, test코드에서 생성자에 파라미터 주입을 누락했음을 컴파일러가 잡아준다(제일 좋은 오류는 실행 이전에 컴파일러가 미리 잡아주는 컴파일 오류이다!)



🎈생성자의 final 키워드

  • 다음과 같이 필드에 final 키워드를 사용했기 때문에, 생성자에서 discountPolicy 필드를 초기화해주지 않으면, 아래와 같이 java: variable discountPolicy might not have been initialized라고 컴파일 오류를 잡아준다
  • 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없고, 오직 생성자 주입 방식만 fianl 키워드를 사용할 수 있다




✅정리

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






📌롬복과 최신 트렌드



✅롬복이란?

  • 실무에서는 대부분의 필드가 다 불변이어서 final 키워드를 사용하게된다
  • 그러려면 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 하는 복잡함이 있다
  • 이 복잡함을 해결하기 위해 lombok이라는 라이브러리가 존재한다






✅롬복 라이브러리 적용방법


    1. build.gradle에 라이브러리 및 환경 추가
plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.10'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'Hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

//lombok 설정 추가 시작
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝



repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	//lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	//lombok 라이브러리 추가 끝

	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}
tasks.named('test') {
	useJUnitPlatform()
}

    1. Preferences(윈도우 File -> Settings) -> plugin -> lombok 검색 설치 실행 (재시작)

    1. Preferences(윈도우 File -> Settings) -> Annotation Processors 검색 -> Enable annotation processing 체크(재시작)







✅예제

  • 다음과 같이 @RequiredArgsConstructor 애노테이션을 이용하면, 생성자 코드를 직접 작성하지 않아도 생성자가 작동한다






  • 그리고 다음과 같이 @Getter@Setter 애노테이션을 통해 코드를 직접 사용하지 않아도 getter와 setter 메소드를 이용할 수 있고, toString 메소드 또한 이용할 수 있다






📌조회 빈이 2개 이상일 경우(@Qualify, @Primary)


✅문제 상황


  • @Autowired는 의존관계 주입 대상을 조회할 때 타입(인터페이스)으로 조회한다
  • 그래서 ac.getBean(DiscountPolicy.class)와 같이 동작한다(실제로는 더 많은 기능을 제공)
  • 이전의 스프링 빈 조회 포스트에서 말했듯이 타입으로 조회하면 선택된 빈(구현체)이 2개 이상일 때 문제가 발생한다
  • 타입(인터페이스) : DiscountPolicy(할인 정책)
  • 하위타입(구현체) : FixDiscountPolicy, RateDiscountPolicy






위와 같이 DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy를 모두 스프링 빈으로 등록하고, 하위타입이 여러 개 일 때, 의존관계 주입이 잘 일어날 지 확인하기 위해 테스트 코드에 ac.getBean(OrderServiceImpl.class)을 넣어 실행해보았다


NoUniqueBeanDefinitionException 오류가 발생한다. 친절하게도 하나의 빈을 기대했는데, fixDiscountPolicy, rateDiscountPolicy가 발견되었다고 알려준다.





✅해결 방법


🎈@Autowired 필드 명 매칭

  • 다음과 같이 주입하고자하는 하위타입의 이름을 필드명으로 쓴다(하위타입이 스프링 빈에 등록될 때는 맨 앞이 소문자로 등록됨을 주의하자)




다음과 같이 잘 실행됨을 알 수 있다
과정을 설명하자면

  • @Autowired타입(할인 정책)으로 매칭을 시도한다(할인 정책의 구체 클래스를 찾는다)
  • 타입 매칭의 결과가 2개 이상이면 필드 명, 파라미터 명으로 빈 이름을 매칭한다(구체 클래스가 2개 이상일 때, 여기서는 fix와 rate 할인 정책)






🎈@Qualifier

  • 빈 등록시 @Qualifier("설정할 이름")를 붙여준다


  • 주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다




📝참고

@Qulifier로 주입할 때 @Qualifier("mainDiscountPolicy")를 못찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier@Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다.
그리고 @ComponentScan이 아닌 @Bean으로 수동으로 빈 등록시에 아래와 같이 @Qulifier를 사용할 수 있다

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


정리

  • @Qualifier끼리 매칭
  • 빈 이름 매칭
  • 위의 과정 모두 매칭 실패하면 NoSuchBeanDefinitionException 예외 발생






🎈@Primary

  • 우선순위를 정하는 방법(@Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다)
  • 다음과 같이 우선권을 주고싶은 구현체에 @Primary를 붙이면 된다






🗒️정리

  • @Primary : 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈에 적용(@Qualifier 지정 없이 편리하게 조회)
  • @Qualifier : 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈에 적용 (명시적으로 획득)
  • @Primary@Qualifier 중 누가 우선권을 가져갈까? : @Primary는 기본값처럼 동작하고, @Qualifier는 매우 상세하게 동작한다. 스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선순위가 높으므로 @Qualifier가 우선권이 높다






📌애노테이션 직접 만들기


✅문제 상황

만약 다음과 같이 mainDiscountPolicy에 n을 하나 더 쓰면 컴파일시에는 오류를 잡아주지 못하고 실행 후에 오류가 발생한다
이럴 경우를 대비해 애노테이션을 직접 만들어 사용하면 잘못 입력했을시에 컴파일 오류로 잡아줄 수 있다







✅애노테이션 만들기

package Hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

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

public @interface MainDiscountPolicy {
}

다음의 애노테이션을 만든다(참고로 @Documented까지는 @Qualifier의 애노테이션에 커서를 두고 Ctrl + 클릭을 하면 나오는 코드를 복사해 붙여넣은 것이다







다음과 같이 구현체와 생성자에 직접 만든 애노테이션 @MainDisocuntPolicy을 붙여주면 된다







📝참고

애노테이션에는 상속이라는 개념이 없다. 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다. @Qualifier뿐만 아니라 다른 애노테이션 들도 함께 조합해서 사용할 수 있다. @Autowired도 재정의할 수 있다. 물론 스프링이 제공하는 기능을 목적없이 무분별하게 재정의하는 것은 오히려 유지보수에 혼란을 줄 수 있으니 주의하자







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


✅문제 상황

의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있도록 하고싶을 때 다음과 같은 방법을 사용할 수 있다







✅전략

package Hello.core.autowired;

import Hello.core.AutoAppConfig;
import Hello.core.discount.DiscountPolicy;
import Hello.core.member.Grade;
import Hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

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, 20000, "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);
            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);
            return discountPolicy.discount(member, price);
        }
    }
}

  • 로직 분석

    • DiscountService는 Map으로 모든 DiscountPolicy를 주입받는다(fixDiscountPolicy, rateDiscountPolicy 주입)
    • discount() 메소드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행("rateDiscountPolicy"가 넘어오면 fixDiscountPolicy 스프링 빈을 찾아서 실행)
  • 주입 분석

    • Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy타입으로조회한 모든 스프링 빈을 담아줌
    • List<DiscountPolicy> : DiscountPolicy타입으로 조회한 모든 스프링 빈을 담아줌
    • 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입






📝참고 - 스프링 컨테이너를 생성하면서 스프링 빈 등록하기

스프링 컨테이너는 생성자에 클래스 정보를 받는다. 여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 자동 등록된다.
new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);







✅정리

  • new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너 생성
  • AutoAppConfig.class , DiscountService.class를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록






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


✅자동 기능을 사용하는 이유

  • 편리한 자동 기능(@ComponentScan, @Autowired)을 기본으로 사용하는게 좋다

  • 스프링은 @Component 뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다

  • 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다

  • 개발자 입장에서 스프링 빈을 하나 등록할 때 @Configuration설정 정보에 @Bean을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 것 보다는 @Component만 넣어주면 끝나는 자동 빈 등록이 훨씬 편하다

  • 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다







✅수동 빈? 자동 빈?

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

🎈업무 로직 빈과 기술 지원 빈

  • 업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.

  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다







🎈특징

  • 업무 로직 빈 : 숫자도 매우 많고, 한 번 개발해야 하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있다. 이런 경우 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 파악하기 쉬워, 자동기능을 적극 사용하는 것이 좋다

  • 기술 지원 빈 : 업무 로직과 비교해 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 광범위하게 영향을 미친다. 업무 로직 빈에 반해 기술 지원 로직은 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많아 가급적 수동 빈 등록을 사용해 명확하게 드러내는 것이 좋다







🎈다형성을 적극 활용할 때

위에서 말했던

  • DiscountService가 의존관계 자동 주입으로 Map<String, DiscountPolicy>에 주입을 받는 상황에서, 만약 내가 작성한 코드가 아닌 다른 개발자가 작성한 코드라면 어떤 빈들이 주입될 지, 각 빈들의 이름은 무엇일지 코드만 보고 한 번에 쉽게 파악하기 힘들다.
  • 이런 경우 수동 빈으로 등록하거나 자동으로 하면 특정 패키지에 같이 묶어두는게 좋다




🎈다형성 예시

@Configuration
public class DiscountPolicyConfig {

	@Bean
	public DiscountPolicy rateDiscountPolicy() {
		return new RateDiscountPolicy();
	}

	@Bean
	public DiscountPolicy fixDiscountPolicy() {
		return new FixDiscountPolicy();
	}
}
  • 별도의 설정 정보를 만들고, 수동으로 빈을 등록한 코드이다

📝참고
스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 스프링 부트가 의도한 대로 편리하게 사용하면 된다. 반면에 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.







✅정리

  • 편리한 자동 기능을 기본으로 사용하자(자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다)

  • 직접 등록하는 기술 지원 객체는 수동 등록하자

  • 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자

profile
저 커서 개발자가 될래요!

0개의 댓글