카테고리 없음

(7)의존 관계 주입

샤샤샤샤 2023. 5. 22. 18:22

의존 관계 주입의 4가지 방법

의존 주입에는 4가지 방법이 존재한다.

1. 생성자 주입

2. 수정자 주입(setter 주입)

3. 필드 주입

4. 일반 메서드 주입

 

 1. 생성자 주입

 생성자 함수를 통한 주입 방법으로, 주입받는 의존관계 객체가 코드가 진행되는 동안 불변하며 필수적일 경우 사용한다.

public class TowAlphabet {

    private final Alphabet first;
    private final Alphabet second;

    @Autowired
    public TowAlphabet(Alphabet a, Alphabet b) {
        first = a;
        second = b;
    }
}

만약 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동주입이 된다.

public class TowAlphabet {

    private final Alphabet first;
    private final Alphabet second;

//    @Autowired
    public TowAlphabet(Alphabet a, Alphabet b) {
        first = a;
        second = b;
    }
}

 

 2. 수정자 주입(setter 주입)

 setter 함수를 통해 주입받는 방법으로 선택, 변경 가능성이 있는 의존관계에 사용한다.

public class TowAlphabet {

    private Alphabet first;
    private Alphabet second;

    @Autowired(required = false) // 주입받을 객체가 없더라도 오류가 발생하지 않게끔 한다.
    public void setFirst(Alphabet a){
        first = a;
    }

    @Autowired(required = false)
    public void setSecond(Alphabet b){
        second = b;
    }
}

 setter 함수를 사용하면 직접 해당 클래스의 코드를 수정하지 않고도 의존 관계 객체 변경이 가능하기에,  의존 주입 객체를 변경해야하는 코드일 경우 사용한다. 그런데 만약 의존 주입받는 빈 객체(위의 코드에서는 a와 b)가 코드 실행시 등록되지 않고 나중에 등록되거나, 애초에 존재하지 않을시 오류가 발생하는 것을 막기 위해 (required = false) 옵션을 붙여줬다.

 

 3. 필드주입

 어떤 메서드도 없이 @Autowired어노테이션만을 통해 주입받는 방식이다.

 코드가 간결하고 깔끔해보이지만, 실제로는 외부에서 변경이 불가능하며 테스트하기 힘들고 프레임워크가 없으면 아무것도 할수 없다는 단점이 존재함으로 사용하지 말자.

public class TowAlphabet {
    @Autowired
    private Alphabet first;
    @Autowired
    private Alphabet second;

}

 

 4. 일반 메서드 주입

 주입받기 위해 새로운 일반 메서드를 만드는 방법이다. 한번에 여러 필드를 주입받을수 있다.

package com.example.demo.noSpringDiTest;

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

public class TowAlphabet {

    private Alphabet first;
    private Alphabet second;

    @Autowired
    public void injection(Alphabet a, Alphabet b){
        first = a;
        second = b;
    }
}

 주입받으려는 객체가 빈으로 등록되지 않았을 수도 있다. 이때 오류가 발생하는 막는 방법은 크게 3가지가 존재한다.

 1. @Autowired(required = false)

 어노테이션에 옵션을 추가하는 방식이다. 만약 주입받으려는 매개변수 객체가 빈으로 등록되어 있지 않다면, 해당 메서드가 실행되지 않는다.

 

 2. @Nullable

    @Autowired
    public void setSecond(@Nullable Alphabet b){
        second = b;
    }

 null 을 허용하게 만드는 어노테이션. 주입할 빈이 없으면 null값이 들어가게 된다.

 

 3. Optional<>사용

 타입을 Optional 래퍼 클래스로 감싸면 주입받을 빈이 없을 경우 Optional.empty가 입력된다.

    @Autowired
    public void setSecond(Optional<Alphabet> b){
        second = b;
    }

 

 

 생성자 주입

 특별한 경우를 제외한 일반적인 상황에서는 생성자 주입을 사용하는 것을 권장한다.

 생성자 주입의 이점은 아래와 같다.

 

 1. 불변

 final 키워드를 사용해서 불변 값으로 만들수 있다. 대부분의 의존 주입 객체는 어플리케이션이 종료시점까지 불변하기 때문에 바뀌면 안된다.

 2. 누락 방지

 @Autowired를 사용하면 저절로 알맞는 객체가 주입되기 때문에 헷갈릴수도 있으나, 만약 프레임워크를 사용하지 않는다고 가정하면 객체 생성시 사용자가 직접 알맞는 객체를 주입해줘야만 한다. 만약 객체를 주입해주지 않았다면 다른 주입 방법들은 어플리케이션이 실행되고 런타임 오류 NPE가 발생하지만, 생성자 주입은 컴파일 과정에서 오류가 발생하기에 개발자의 실수를 줄여줄수 있다.

 

** 컴파일 오류가 가장 바람직한 오류다 **

 

 롬복을 이용한 생성자 주입

 롬복(lombok)은 개발자가 코드를 작성하기 쉽게 해주기 위해 나온 편의기능 라이브러리다.

 롬복에는 @RequiredArgsConstructor 라는 어노테이션이 존재하는, 이는 한국말로 해석하면 '@필수 변수 생성자' 가 된다. 따라서 반드시 초기화되어야 하는(final) 필드의 생성자 함수를 만들어주고, 생성자 주입까지 저절로 해주는 어노테이션이다.

@RequiredArgsConstructor
public class OneAlphabet {

    private final Alphabet a;

//    @Autowired   생략 가능
//    public TowAlphabet(Alphabet a) {
//        this.a = a;
//    }

}

 

 중복된 빈의 자동 주입

 빈의 자동 주입은 컨테이너에서 클래스가 일치하는 빈을 찾아와 주입하는 방식으로 이뤄진다. 따라서 같은 클래스의 빈이 두개 이상 등록되어 있다면 어떤 빈을 주입시켜줘야 하는지 불명확하기에 오류가 발생하게 된다. 이를 해결하는 방법은 다음과 같다.

 

 1. 이름 일치 시키기

 @Autowired의 작동방식은 1. 같은 타입의 빈을 찾아 주입   2. 만약 같은 타입의 빈이 여러개라면 필드 이름과 같은 빈을 찾아 주입   3. 앞의 방식이 안되면 오류 발생  으로 이뤄진다.

 따라서 필드 이름을 빈 이름과 같게 매칭시키면 알아서 알맞은 값이 주입된다.

@RequiredArgsConstructor
public class OneAlphabet {

    private final Alphabet a;  // Alphabet 타입의 빈은 2개. 필드와 이름이 같은 빈이 주입됨
}

 

 2. @Qualifier 사용

@Qualifier 어노테이션은 일종의 식별 코드를 붙여주는 방식이다. 빈의 이름이 변경되지는 않으나, 만약 중복되는 클래스의 빈이 존재할때 어떤 빈을 주입받아야 하는지 구분하는 역할을 수행한다.

 

#A

@Component
@Qualifier("BeanA")
public class A implements Alphabet {

    @Override
    public void className() {
        System.out.println("A");
    }
}

#B

@Component
@Qualifier("BeanB")
public class B implements Alphabet {
    @Override
    public void className() {
        System.out.println("B");
    }
}

#TowAlphabet

@Component
public class TowAlphabet {
    private final Alphabet first;
    private final Alphabet second;

    @Autowired
    public TowAlphabet(@Qualifier("BeanA") Alphabet first, @Qualifier("BeanB") Alphabet second) {
        this.first = first;
        this.second = second;
    }
}

** 이제야 Two 가 Tow로 오타가 났다는 것을 발견했다. 이전 글까지 모두 수정하기는 시간이 오래 걸리니 그냥 이대로 가는걸로....

 

 3. @Primary

 @Primary는 우선권을 지정하는 어노테이션이다. 이를 사용하면 중복되는 빈이 존재할때 어떤 객체가 우선적으로 주입될지 지정해줄수 있다.

 

#A

@Component
@Primary
public class A implements Alphabet {

    @Override
    public void className() {
        System.out.println("A");
    }
}

# OneAlphabet

@RequiredArgsConstructor
public class OneAlphabet {

    private final Alphabet first; // @Primary가 적용된 A의 객체가 주입된다.
}

 

** 만약 A 클래스에 @Primary가 등록되어 있고, B클래스에 @Qualifier가 등록되어 있는데 @Qualifier로 주입받을 객체를 찾으면, B가 주입된다. 즉, 수동이 자동보다 우선권을 가진다. **

 

 

스프링의 어노테이션 상속을 이용하는 방법도 존재한다.

 

# Main

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

# A

@Component
@Main
public class A implements Alphabet {

    @Override
    public void className() {
        System.out.println("A");
    }
}

# TowAlphabet

@Component
public class TowAlphabet {

    private final Alphabet first;
    private final Alphabet second;

    @Autowired
    public TowAlphabet(@Main Alphabet first, @Qualifier("BeanB") Alphabet second) {
        this.first = first;
        this.second = second;
    }
}

 원래 자바는 어노테이션 상속이 불가능할 뿐더러, 불필요하게 코드가 복잡해질수 있어 불가피한 상황이 아니라면 사용하지 말자.

 

 

List, Map으로 모든 빈 조회하기

List나 Map 타입으로 자동주입을 받으면, 제네릭의 타입과 같은 모든 빈을 가져온다.

 

#EveryAlphabet

@Component
public class EveryAlphabet {
    List<Alphabet> alphabetList;
    Map<String, Alphabet> alphabetMap;

    @Autowired
    public EveryAlphabet(List<Alphabet> alphabetList, Map<String, Alphabet> alphabetMap) {
        this.alphabetList = alphabetList;
        this.alphabetMap = alphabetMap;

        System.out.println("list = " + alphabetList);
        System.out.println("map = " + alphabetMap);
    }
}

# 테스트 코드

	@Test
	void everyBean(){
		ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
	}

결과:

map의 경우, 빈 이름이 key가 되었다.

 

 수동 등록과 자동 등록

 업무로직, 즉, 실질적인 서비스를 담당하게 되는 웹 지원 컨트롤러, 핵심 서비스, 레퍼지토리 등은 자동 등록을 사용하고, 기술적인 문제나 공통 관심사(AOP), 비지니스 로직중 다형성을 사용해야하는 경우는 수동 등록을 사용하자.