Java Spring Core

DI

스프링에서는 DI를 수동 주입하거나 자동으로 주입 받을 수 있습니다. 수동으로 하는 방식은 기존에 AppConfig Class을 이용해서 직접 모든 의존성을 주입해주는 방식이고, 자동으로 주입하는 방식은 Component Scan 방식으로 spring이 spring container에 있는 spring bean을 이용해 의존성이 필요한 부분에 적용시켜주는 것이다.

DI Types

이렇게 자동으로 의존관계를 주입하는 방식에는 크게 4가지가 있다.

Constructor

생성자를 이용해서 자동으로 주입받는 방식이다. 아래와 같이 생성자 함수에 @Autowired을 표기를 통해 해당 객체가 생성될때 DI가 이루어진다.

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired //아래와 같이 생성자가 1개인 경우에 대한 DI에서는 @Autowired 생략가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

생성자 함수를 이용한 DI는 변수를 private, final로 설정함으로써, 외부에서 변경을 하지 못하도록 막을 수 있으며 누락을 방지 할 수 있다.

Setter

private 변수에 대한 Setter 함수를 정의해서 해당 setter 함수를 통해서 DI를 적용받을 수도 있다.

private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRespository(MemberRepository memberRepository){
    this.memberRepository=memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy){
    this.discountPolicy=discountPolicy;
}

대신 setter 함수를 사용할 때는 final 선언문을 빼줘야한다. –> final은 변경의 여지가 없고, 생성당시 바로 초기화 해줘야하는 조건이 있다.

나중에 의존관계를 변경해줘야하는 경우 setter함수를 활용한다.

Field

일반 field즉 변수에 바로 DI를 적용하는 방식이다.

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

이렇게 바로 필드에 @Autowired을 적용해서 자동으로 DI가 이루어지도록 할 수 있다.

하지만 이는 Spring이 없는 순수 자바 코드 환경에서는 활용(테스트)할 수 없으며 외부에서 의존관계 관련 수정을 할 수 가 없다.

일반적으로 DI는 constructor나 setter함수를 이용해서 진행하는 것이 좋다.

field DI를 사용하는 제한적인 상황은 아래와 같이 spring container가 활용되면서, 수동 Spring Bean을 등록할때 DI가 필요한 경우에 사용되기도 한다.

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

@Bean
OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy
discountPolicy) {
new OrderServiceImpl(memberRepository, discountPolicy)
}

Method

일반 메소드에 @Autowired 선언을 통한 DI 주입

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

일반적인 경우에서는 Constructor 방식과 Setter 함수 방식을 이용하는 것이 좋다.

Options

DI를 통해 객체 생성을 해야하는데, 만약 해당 객체가 Spring Bean과 연관이 없는 일반 객체라면 어떻게 될까?

Example

@Autowired
public void setNoBean(Member noBean){
    System.out.println("noBean = " + noBean);
}

가령 위와 같은 코드가 있다고 가정하자.

Member 객체는 Spring Bean과 관련 없는 일반 모델 객체이다. 해당 메소드를 실행하게 되면

method 'setNoBean' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException

NoSuchBeanDefinitionException이 발생해서 실행이 되지 않는다.

하지만 옵션을 부여해서 해당 코드가 DI를 받지 않은 상태로 실행되도록 할 수 있다.

@Autowired(required = false)

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

@Autowired annotation의 required option을 false 처리 해주게 되면 Spring Bean이 아닌 DI에 대해 애초에 method 호출이 되지 않도록 막는다.

@Nullable

@Nullable annotation을 객체 옆에 명시해준다.

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

@Optional<>

Optional class로 한 번 wrapping 해서 객체로 넘기면 null 값이 와도 실행이 가능하다.

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

Best DI => Constructor

앞서 설명한 4가지 DI 방법 중에서 Constructor을 이용하는 것이 가장 좋은 방법이다.

Constructor만이 가지는 몇가지 장점들이 있는데, 우선

  1. 변수에 final 선언을 한다.

final 선언을 통해 추후에 변경을 하지 못하도록 강제하며, 생성당시에 초기화를 보장 받을 수 있어, DI가 누락되는 경우가 발생하지 않음

가령, setter을 이용해서 DI를 하게 되면 아래와 같은 오류가 발생한다

private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRespository(MemberRepository memberRepository){
    this.memberRepository=memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy){
    this.discountPolicy=discountPolicy;
}

Test

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

위의 코드는 에러가 발생하는데, 왜냐하면 의존 관계에 있는 객체들을 초기화해주지 않았기 때문이다.(MemberRepository,DiscountPolicy) 하지만, Setter 함수를 이용해서 DI 설계를 하게 되면 이 오류를 실행을 해야 파악할 수 있다.(코드상에는 문제가 없기 때문에) 그래서 오류를 파악하는데 시간이 오래 걸리게 된다.

하지만, Constructor을 활용하게 되면, new를 통한 객체 선언 당시에 해당 DI를 넘겨주지 않으면 컴파일 에러를 발생시켜서 오류를 쉽게 파악할 수 있다.

이처럼, Constructor 이외의 DI 방법들은 final 처리를 하지 않아 외부에서 DI를 넣도록 코드를 설계해야하는 부분으로 인해 이렇게 누락되도록 설계되는 문제가 발생할 수 있다.

사실 final 선언을 통해 얻을 수 있는 가장 큰 장점은 외부에서의 의존관계 변경을 허용하지 않는 다는 점이다.

  1. 또한, Constructor을 통한 DI가 보장되어 있기 때문에, 순수 자바 코드 환경에서 테스트를 하더라도, new을 통해서 DI를 주입하는 것이 가능해서 테스트를 수행할 수 있다.

field DI는 Spring Container가 올라와있지 않은 경우에서는 테스트를 하는 것이 불가능하다.(애초에 외부에서 DI 주입하는 것이 불가능하도록 설계되어 있기 때문.)

Lombok

프로젝트를 진행하다 보면 매번 모든 객체에 생성자나, setter,getter을 설계 해야하는 부분은 어쩌면 큰 수고가 되는 경우가 많다. 물론 소규모 프로젝트의 경우 괜찮지만, 객체가 수백개~ 수천개까지 있는 대규모 프로젝트에서는 이런 코드를 짜는 것 자체가 시간이 많이 소모된다.

이럴때, 자바에서는 annotation processor을 통한 함수 생성을 자동화해주는 라이브러리를 제공한다.

Lombok이 이러한 기능을 제공해준다.

Installation

build.gradle


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

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

다음과 같이 gradle 파일에 필요한 설정들을 추가해준다.

OrderServiceImpl class

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

Test

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
OrderServiceImpl orderService = ac.getBean(OrderServiceImpl.class);
System.out.println("orderService = " + orderService);

Results

orderService = hello.core.order.OrderServiceImpl@45312be2

생성자 함수가 자동으로 생성되었으며, Spring에서 DI를 자동적으로 진행하는 것을 확인할 수 있다.

생성자 함수를 하나만 둬서 @Autowired을 생략하고, @RequiredArgsConstructor을 이용해서 생성자 함수도 생략해서 코드를 최대한 간략하게 구현할 수 있다.

Duplicate Beans

만약, DI를 하려고 하는데, 같은 타입의 객체가 2개이면 어떻게 될까?

가령 OrderServiceImpl이 DiscountPolicy를 DI로 받는데, DiscountPolicy를 구현한 구현 클래스가 RateDiscountPolicy, FixDiscountPolicy 이렇게 2개가 있다면, Spring 에서는 DI 과정에서

NoUniqueBeanDefinitionException 에러를 출력하게 된다.

그래서, 설계 당시에 이런 중복되는 Bean에 대한 처리를 해야한다.

@Autowired 활용

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

위와 같이 있을 되어 있을때, @Autowired는 우선 DI를 위해서 Type을 먼저 확인한다. 그런데, 이때 Type이 같은 객체가 여러개 있는 경우, 그 다음 parameter name을 읽는다.

그래서 만약 아래와 같이 해놓게 되면

@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository;
    private final DiscountPolicy ㅇiscountPolicy;

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

DiscountPolicy에 rateDiscountPolicy가 주입되게 된다.

Test

@Test
void orderServiceImplTest(){
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
    OrderServiceImpl orderService = ac.getBean(OrderServiceImpl.class);
    System.out.println("Discount policy = " + orderService.getDiscountPolicy());
}

Results

Discount policy = hello.core.discount.RateDiscountPolicy@3d3e5463

이렇게 RateDiscountPolicy 객체를 주입받을 것을 확인할 수 있다.

@Qualifier

@Qualifier을 통한 매칭을 시켜줄 수 있다.

FixDiscountPolicy

@Component
@Qualifier("mainDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{

OrderServiceImpl

@Component
public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
   @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

Test Results

Discount policy = hello.core.discount.FixDiscountPolicy@3d3e5463

위와 같이 이번에는 FixDiscountPolicy 객체가 들어가는 것을 확인할 수 있다.

@Primary

@Primary 선언을 해놓은 Bean에 더 우선순위를 해줌으로써 해당 객체를 DI로 입력받게한다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Primary vs @Qualifier

우선순위는 @Qualifier가 @Primary보다 높다. @Qualifer은 수동으로 개발자가 직접 설계를 하는 느낌이 강하고, @Primary는 자동으로 설계를 하는 느낌이 강하다.

메인으로 돌아가는 로직에는 주로 @Primary을 통해 기본적으로 적용을 하고, 특수하게 돌아가야 되는 마이너한 부분에 @Qualifier을 부여함으로써 코드를 유지한다.

Customized Annotaion

@Qualifer(““)안에는 String형 데이터가 들어가는데, 컴파일러는 해당 String 데이터에 오류가 있는지 파악을 할 수가 없다 그래서 @Qualifer을 이용하는 것 보다 Annotation을 설계해서 그 안에 @Qualifier을 넣어서 활용하는 것이 좋은 방법이다.

@MainDiscountPolicy

package hello.core.annotataion;
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)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

RateDiscountPolicy

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

OrderServiceImpl

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

이렇게 하게되면 Annotation이 잘못되는 경우 컴파일러가 감지할 수 있기 때문에 프로그래머의 실수를 미연에 방지 할 수 있다.

이렇게 Annotation이 포함되었는지 여부를 판단하는 기능은 Spring의 고유한 기능이다.

Multiple Beans

만약 여러개의 Spring Bean을 등록받아서 사용해야된다면 어떻게 해야될까?

가령, 사용자로부터 RateDiscountPolicy를 사용받을지, FixDiscountPolicy를 받을지 선택할 수 있도록 하려면 어떻게 해야될까??

DiscountService

아래와 같이 DiscountPolicy을 여러 개 받아서 저장할 수 있는 DiscountPolicy가 있다고 하자.

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);
            return discountPolicy.discount(member,price);
        }
    }

Test

@Test
void findAllBean(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
    DiscountService discountService = ac.getBean(DiscountService.class);
}

Results

policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@5a755cc1, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@7ae42ce3}
policies = [hello.core.discount.FixDiscountPolicy@5a755cc1, hello.core.discount.RateDiscountPolicy@7ae42ce3]

이처럼, Map 과 List에 FixDiscountPolicy, RateDiscountPolicy 가 모두 들어가지는 것을 확인할 수 있다. 이처럼, Map, List을 이용해서 자동으로 여러개의 DI를 입력받도록 할 수 있다.

여러개의 DiscountPolicy을 활용해서 필요할 때마다 꺼내서 원하는 DiscoutPolicy을 활용하면 된다.

Map<String,DiscountPolicy>
List<DiscountPolicy>

map 에는 bean 이름과 함께, Spring Bean이 입력되어서, bean 이름을 통한 Spring Bean을 선택할 수 있다.

list에는 DI들이 연속적으로 리스트에 저장되어있다.

Using Multiple Discount Policies


Member member=new Member(1L,"userA", Grade.VIP);
//FixDiscountPolicy 사용
int fixDiscountPrice=discountService.discount(member,10000,"fixDiscountPolicy");

assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(fixDiscountPrice).isEqualTo(1000);
//RateDiscountPolicy 사용
int rateDiscountprice=discountService.discount(member,10000,"rateDiscountPolicy");

assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(rateDiscountprice).isEqualTo(1000);

Auto DI vs Manual DI

@Component 표시를 통한 Component Scan을 이용한 자동 DI가 있고,

@Configuration, @Bean 표시를 통한 수동 DI 설정 방식이 있는데, 어떻게 구분지어서 써야될까?

Auto DI

Controller, Repository, Service 와 같은 비지니스 로직에는 Auto DI를 이용해서 자동으로 DI를 하도록한다. 비지니스 로직간에 구현해야할 컨트롤러,리포지토리, 서비스가 많기 때문에 annotation을 이용한 자동 의존성 주입을 활용한다.

Manual DI

AOP, DB Connection과 같은 기술 로직은 비지니스 로직과 달리 개수가 적고, 프로젝트 전 범위에 사용되는 공통 기술이다. 이러한 로직들은 비지니스 로직과 달리 표면상에 오류들이 들어나지 않기 때문에 수동 DI를 통해 명확하게 표현될 수 있도록 한다. 추가적으로, 위의 예제에서 활용한 다형성을 이용한 로직 설계를 진행할때에는 수동 DI를 통해 코드의 가독성을 높일 수 있다.

DiscountPolicy Config

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

DiscountPolicy에 여러개의 Spring Bean이 올 수 있는 경우, 다음과 같은 Config 파일을 구성함으로써 어떤 Bean들이 등록될 수 있는지 파악하기 쉽다.

References

link: inflearn

link:springcore

link:spring_framework

댓글남기기