Spring

스프링 기본편 - 6

woohap 2025. 1. 19. 21:20

의존관계 자동 주입

- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입

생성자 주입

- 생성자 호출할 때, @Autowired를 보고 
  스프링 컨테이너에서 스프링빈을 꺼내서 주입해준다.
- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. (스프링 빈에만 해당)

[특징]
- 생성자 호출시점에 딱 1 번만 호출되는 것을 보장 
  인스턴스를 설정하고 변경하지 못하도록 막을 수 있다.

- 주로 불변, 필수 의존관계에 사용
  생성자는 두 번 호출되지 않음 
  따로 수정하는 메서드를 만들지 않는 한 불편

- 불변 - 의존관계가 변할 필요가 없을 때 주로 사용 
  final이란 한 번만 초기화 할 수 있고 상수로 설정하겠다는 의미

- 필수 - 의존관계가 반드시 설정되어야 하는 경우 사용 
  private 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;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        // 주문 서비스 입장에서 할인에 대한 것 모르겠다. discountPolicy 니가 알아서 해줘 - 단일 책임 원칙을 잘 지킨 것
        // 즉, 할인 변경을 수행할 때, 할인 쪽만 고치면 됨
        // member 전체를 넘겨도 되고, 등급만 넘겨도 된다. - 상황에 맞게 구현하면 됨
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
-------------------------------------------------

- 생성자 주입은 그냥 빈을 등록하면서 의존관계 주입도 같이 실행 
  (Life Cycle과 달리 생성자 주입은 빈 등록시 어쩔 수 없이 자동 주입이 일어남)

[기타 팁]
좋은 개발은 한계점이랑 제약이 있어야 한다.
가급적 생성자에 값을 넣고 Setter 메서드를 안 만드는게 좋다.
→ 버그가 줄어든다.

관례상 생성자의 인자는 웬만하면 값을 다 넣어야 한다.

수정자 주입

- Setter 메서드를 활용하여 의존관계 참조변수 필드에 의존관계를 주입하는 방법

[특징]
- 주로 선택, 변경 가능성이 있는 의존관계에 사용

- 의존관계를 선택적으로 넣어줘야 할 때 ( 안 넣어줘도 되는 경우가 있을 수 있음 )
  단, @Autowired(requied = 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;
        }

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        // 주문 서비스 입장에서 할인에 대한 것 모르겠다. discountPolicy 니가 알아서 해줘 - 단일 책임 원칙을 잘 지킨 것
        // 즉, 할인 변경을 수행할 때, 할인 쪽만 고치면 됨
        // member 전체를 넘겨도 되고, 등급만 넘겨도 된다. - 상황에 맞게 구현하면 됨
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
-------------------------------------------------

[참고]
- 스프링 컨테이너는 두 가지 LifeCycle으로 나눠져 있다.
  1. 스프링 빈들을 등록한다. 
  2. 의존관계를 자동으로 주입 (@Autowired가 붙으면 자동으로 주입)

- 생성자, 수정자 둘 다 @Autowired가 붙어 있는 경우
  생성자를 통한 의존관계 주입이 우선 수행 됨

중요 !!
- @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다.
  주입할 대상이 없어도 동작하게 하려면 @Autowired(requied = false)로 지정

필드 주입

- 필드에 바로 주입하는 방법

[특징]
- 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다.

- DI 프레임워크가 없으면 아무것도 할 수 없음

- 사용하지 말자 !!
    = 애플리케이션 실제 코드와 관계 없는 테스트 코드
      테스트를 스프링 컨테이너에서 할 때
    = 스프링 설정을 목적으로 하는 @Configration 같은 곳에서만 특별한 용도로 사용

-------------------------------------------------

@Component
public class OrderServiceImpl implements OrderService{
        // 필드에 바로 주입 
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        // 주문 서비스 입장에서 할인에 대한 것 모르겠다. discountPolicy 니가 알아서 해줘 - 단일 책임 원칙을 잘 지킨 것
        // 즉, 할인 변경을 수행할 때, 할인 쪽만 고치면 됨
        // member 전체를 넘겨도 되고, 등급만 넘겨도 된다. - 상황에 맞게 구현하면 됨
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

-------------------------------------------------

@Bean
OrderService orderService(MemberRepository memberRepoisitory, DiscountPolicy
discountPolicy) {
return null;
}


[참조]
- 직접 참조를 통해 객체를 생성하는 경우 Autowired가 되지 않음 
    Ex) `OrderServiceImpl orderService = new OrderServiceImpl();`
         또한 테스트할 때, 구현객체 교체가 불가능 
    Ex) 스프링을 사용하지 않고 테스트할 때 
          더미 데이터를 갖고 있는 메모리 저장소를 사용하여 테스트 하고 싶은데 
          필드에 주입해줄 방법이 없음 (의존관계 필드들이 모두 NullPointerException)

- 이 경우 Setter를 사용해야 한다.

- 순수한 자바 코드로 OrderServiceImpl 처럼 테스트 하는 경우가 많음 
  필드 주입을 사용할 경우 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;
    }
}

- 수정자 주입과 비슷한 타이밍에 의존관계를 주입해준다.

참고

- 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
  스프링 빈이 아닌 Member 같은 클래스에서 @Autowired를 적용해도 아무 기능도 동작하지 않음

옵션 처리

- 주입할 스프링 빈이 없어도 동작해야할 때가 있음
- @Autowired만 사용하면 required 옵션의 값이 true이므로, 자동 주입 대상이 없으면 오류 발생

[자동 주입 대상을 옵션으로 처리하는 방법]

여기서 Member는 스프링 빈이 아닌 자바 순수 객체

- @Autowired(required=false) - 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안 됨 
  setNoBean1() 메서드가 자체가 호출되지 않음

// 의존관계가 없으면 해당 메서드 자체를 호출하지 않음
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
}

- org.springframework.lang@Nullable - 주입 대상이 없으면 null이 입력된다.
  호출은 되지만 Null로 들어온다.

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

- Optional<> - 주입할 대상이 없으면 Optional.empty가 입력된다.
  스프링 빈이 없는 경우 Optional.empty로 반환

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

Optional<> 결과 
noBean2 = null
noBean3 = Optional.empty

[참고]

- @Nullable, Optional은 스프링 전반에 걸쳐서 지원
  예를 들어 생성자 자동 주입에서 특정 의존관계를 null로 하고 싶을 때도 가능

- @Autowired 옵션으로 생성자 주입이 불가능한 이유
  스프링은 보통 “주 생성자”를 통해 의존성을 필수로 주입한다고 가정. 
  required = false가 붙었다고 해서, 생성자 인자에 해당하는 빈이 없을 때 
  null을 대입하고 넘어가는 식의 로직은 기본적으로 제공되지 않음

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

- 최근에는 DI 프레임워크 대부분이 생성자 주입을 권장한다.

[불변]
- 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없음
  오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 된다.(불변해야 함)

- 수정자 주입을 사용하면, setXxxx 메서드를 public으로 열어두어야 한다.

- 누군가 실수로 변경할 수도 있고, 
  변경하면 안 되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.

- 생성자 주입은 객체를 생성할 때, 딱 1 번만 생성되기 때문에
  이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.

[누락]
- 프레임워크 없이 순수한 자바 코들르 단위 테스트 하는 경우가 많음
  수정자 의존관계를 사용하면 테스트를 실행할 수 있다.
  하지만 막상 테스트를 실행하면 NullPointException 발생한다.

- 생성자 주입을 사용하면 데이터를 **누락했을 때, 컴파일 오류가 발생**한다.
  또한 IDE에서 어떤 값을 필수로 주입해야 하는지 알 수 있다.

[final 키워드 사용]
- 생성자를 사용하면 final 키워드를 사용하기 때문에
  한 번 결정되면 바꿀 수 없으며
  생성자에서만 값을  설정할 수 있다.

- 코드가 누락된 경우 컴파일러가 누락됐음을 알려준다.

[참고]

- 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다. !!

- 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로
  필드에 final 키워드를 사용할 수 없다.
  오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.

정리

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

롬복

[@Getter @Setter @toString]
- 클래스에 해당 애너테이션을 붙이면 Getter, Setter, toString을 자동으로 만들어준다.

[@RequiredArgsConstructor]
- final이 붙은 필드에 대한 생성자를 만들어진다.
- 의존관계 추가할 때 간단하다.

정리

- 최근에는 생성자를 딱 한 개를 두고, @Autowired를 생략하는 방법 많이 사용
- 여기에 Lombok 라이브러리의 @RequiredArgsConstructor 함께 사용하면 
  코드를 깔끔하게 사용할 수 있다.

롬복 라이브러리 적용 방법

- build.gradle에 라이브러리 및 환경 추가


groovy

plugins {

id 'org.springframework.boot' version '2.3.2.RELEASE'

id 'io.spring.dependency-management' version '1.0.9.RELEASE'

id 'java'

}

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'

}

}

test {

useJUnitPlatform()

}

1. Preferences(윈도우 File Settings) plugin lombok 검색 설치 실행 (재시작)
2. Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)
3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인

조회할 빈이 두 개 이상인 경우

@Autowired
private final DiscountPolicy discountPolicy;

- 타입으로 조회하기 때문에 ac.getBean(DiscoutPolicy.class) 와 유사하게 동작

[타입으로 조회하면 선택된 빈이 2개 이상일 때, 문제가 발생]

- DiscountPolicy 타입으로 빈을 검색하면 
  FixDiscountPolicy와 RateDiscountPolicy 둘 다 검색되어 
  `NoUniqueBeanDefinitionException` 예외 발생

- 하위 타입으로 지정해서 해결할 수 있지만 
  하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.

- 스프링 빈을 수동 등록해서 문제를 해결해도 되지만, 
  의존관계 자동 주입에서 해결하는 여러 방법이 있다.

Autowired 필드명, @Qualifier, @Primary

[조회 대상 빈이 2개 이상일 때, 해결방법]
- @Autowired 필드명 매칭
- @Quilifier - @Quilifier끼리 매칭 → 빈 이름 매칭
- @Primary 사용

@Autowired 필드명 매칭

- @Autowired는 타입 매칭을 시도하고
  이때 여러 빈이 있으면 필드 이름(파라미터 이름)으로 빈 이름을 추가 매칭한다.
  ** 생성자인 경우 생성자의 파라미터 이름으로 빈 이름을 추가 매칭

@Autowired 
private DiscountPolicy rateDiscountPolicy;

- 필드명이 rateDiscountPolicy 이므로 정상 주입
- 필드명 매칭은 먼저 타입 매칭을 시도하고 
  그 결과에 여러 빈이 있을 때, 추가로 동작하는 기능

정리

1. 타입 매칭
2. 타입 매칭의 결과가 2개 이상일 때는 필드 명 또는 파라미터 명으로 빈 이름 매칭

에러 참고

1. Build → Execution → Deployment → Compiler → Java Compiler
2. Addtional command line parameters 항목에 -parameters 추가
3. out 폴더 제거 후 실행 (제거하지 않으면 컴파일이 일어나지 않음)

스프링에서 생성자 주입 시, 매개변수 이름을 활용해 빈을 찾으려면(즉, 파라미터 이름과 빈 이름을 매칭하려면) 자바 컴파일 시 실제 파라미터 이름이 .class 파일에 저장되어 있어야 합니다. 그러나 기본적으로 자바 컴파일러는 매개변수 이름을 .class 파일에 저장하지 않기 때문에, -parameters 옵션을 붙이지 않으면 스프링은 생성자 파라미터의 실제 이름을 알 수 없어 빈 이름 매칭을 해주지 못합니다.

1. 기본적으로 매개변수 이름 미저장
  - 자바 컴파일러는 기본 설정으로 매개변수 이름을 .class 파일에 저장하지 않고, 
    arg0, arg1 등 임의의 이름으로 대체합니다.
  - 따라서 스프링이 리플렉션(Reflection)으로 생성자 파라미터를 확인해도 진짜 이름을 알 수 없습니다.

2.    -parameters 옵션의 역할
  - javac -parameters를 사용하면 컴파일 시 메서드(및 생성자) 파라미터의 실제 이름을 
    .class 파일에 저장합니다.
  - 이 정보가 .class에 있어야 스프링이 파라미터 이름을 인식하고, 
    해당 이름과 동일한 빈을 찾을 수 있습니다.

3.    스프링의 매개변수 이름 인식
  - Spring 4.3 이상에서는 생성자 주입 시 파라미터 이름으로 
    빈을 탐색할 수 있는 기능이 들어있습니다(주석 없이도 가능).
  - 하지만 이를 쓰려면 .class에 실제 파라미터 이름 정보가 필요하기 때문에, 
    [ -parameters 옵션이 필수적으로 요구됩니다. ]

@Qualifier

- @Qualifier는 추가 구분자를 붙여주는 방법
  주입시 추가적인 방법을 제공하는 것이지, 빈이름을 변경하는 것은 아니다.

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

// 수정자 자동 주입 예시
@Autowired
public DiscountPolicy setDiscountPolicy(
                @Quilifier("mainDiscountPolicy") DiscountPolicy discountPolicy){
            return discountPolicy;
}

- @Qualifier로 주입할 때, @Quailifier(”mainDiscountPolicy”)를 못 찾으면 어떻게 되는가
  추가적으로 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
  경험상 @Qualifier는 @Qualifier는 찾는 용도로만 사용하는게 명확하고 좋다. 
  실패하는 것까지 고려하지 마라 - 사용하기 헷갈림

- @Bean을 직접 등록할 때도 @Qualifier 사용 가능

정리

1. @Quilifier끼리 매칭
2. 빈 이름 매칭
3. `NoSuchBeanDefinitionException` 예외 발생

자주 사용 - @Primary

- Primary는 우선순위를 정하는 방법

- @Autowired 시에 여러 빈이 매칭되면 
  @Primary가 우선순위 최상위로 설정됨

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { }

@Primary, @Qualifier

- @Qualifier의 단점은 주입 받을 때, 모든 코드에 @Qualifier를 붙여줘야 한다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
            @Qualifier("mainDiscountPolicy) DiscountPolicy discountPolicy) {}

- 반면 @Primary는 붙여줄 필요가 없어 단순하다.

[대표적인 사용사례]

- 코드에서 자주 사용하는 메인 데이터 베이스의 커넥션을 획득하는 스프링 빈이 있고
  특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 가정

- 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서
  @Qualifier 지정 없이 편리하게 조회

- 서브 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Qualifier를 지정해서
  명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.

- 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해줘도 상관 없다.

우선순위

- @Primary는 기본값처럼 동작하는 것이고

- @Qualifier는 매우 상세하게 동작한다.
  이런 경우 어떤 것이 우선권을 가져가는가 ???

- 스프링은 항상 자세한 것이 우선순위가 높다.

- 스프링은 자동보다는 수정이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 
  우선순위가 높다.

- 여기서는 @Qualifier가 우선권이 높다.

애너테이션 직접 만들기

- @Qualifier(”mainDiscountPolicy”) 처럼 
  문자를 적으면 컴파일시 타입 체크가 안 된다.
- 애너테이션을 만들어서 문제를 해결할 수 있다.
- 애너테이션을 사용함으로써 잘못 작성 할 경우 컴파일 에러가 발생하도록 함
- 인텔리제이에서 코드 추적하기도 편하다.

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


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

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

조회한 빈이 모두 필요할 때, 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");

        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
        Assertions.assertThat(discountPrice).isEqualTo(1000);

        discountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");

        Assertions.assertThat(discountPrice).isEqualTo(2000);
    }

    // 구성 정보
    static class DiscountService {

        private final Map<String, DiscountPolicy>  policyMap;
        private final List<DiscountPolicy> policyList;

        @Autowired
        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
            this.policyMap = policyMap;
            this.policyList = policyList;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policyList = " + policyList);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

[로직 분석]
- DiscountService는 Map으로 모든 DiscountPolicy를 주입 받는다.
  이 때, fixDiscountPolicy와 rateDiscountPolicy가 주입된다.
- discount() 메서드는 discountCode로 fixDiscountPolicy가 넘어오면 
  map에서 fixDiscountpolicy 스프링 빈을 찾아서 실행한다.

[주입 분석]

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

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

[편리한 자동 기능을 기본으로 사용하자]
- 스프링이 나오고 실간이 갈수록 점점 자동을 선호하는 추세

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

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

- 설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게
  나누는 것이 이상적이지만 개발자 입장에서 스프링 빈을 하나 등록할 때, 
  @Component만 넣어주면 끝나는 일을 @Configuration 설정 정보에 가서 
  @Bean을 적고 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정은 상당히 번거롭다.
  또 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담

- 결정적으로 **자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

[수동 빈은 언제 사용]
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
- 비즈니스 업무 로직 빈:앱을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스
  데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직
  보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.

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

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

- 기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서
  광범위하게 영향을 미친다. 그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지
  명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 
  어려운 경우가 많다. 
  그래서 기술 지원 로직은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.

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

[비즈니스 로직 중에서도 수동 빈 등록이 유리한 경우도 있다.]
- 비즈니스 로직 중 다형성을 적극 활용할 때

static class DiscountService {
    private final Map<String, DiscountPolicy>  policyMap;
}   

- DiscountPolicy에 어떤 빈들이 주입될지, 각 빈들의 이름은 무엇일지 현재 코드만 보고
  한 번에 쉽게 파악하기 어려움
- 다른 개발자에게 전달하거나 내가 받을 때, 코드를 파악하기 위해 여러 코드를 찾아야 한다.

@Configuration
public class DiscountPolicy{
        @bean
        public DicsountPolicy discountPolicy() {
            return new RateDiscountPolicy();
        }

        @bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
}

- 수동으로 빈을 등록하면
  설정 정보만 봐도 한 번에 빈의 이름은 물론이고, 어떤 빈들이 주입될지 파악하기 쉽다.

- 그래도 빈 자동 등록을 사용하고 싶다면 
  파악하기 좋게 DiscountPolicy의 구현 빈들만 따로 모아서 특정 패키지에 모아두자.
  → 적어도 인터페이스와 구현체가 특정 패키지에 모여 있어야 함

[즉, 딱 보고 이해할 수 있도록 코드를 작성하는 것이 좋다.]

[참고]
- 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외
  이런 부분들은 스프링 자체를 잘 이해하고 스프링의 의도대로 잘 사용하는게 중요

- 스프링 부트의 경우 DataSource같은 데이터베이스 연결에 사용하는 기술지원 로직까지
  내부에서 자동으로 등록
  이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.
  → 필요한 경우에만 커스텀해서 직접 스프링 빈으로 등록하는게 좋음

- 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 
  수동으로 등록해서 명확하게 들어내는 것이 좋다.

[정리]
- 편리한 자동 기능을 기본으로 사용하자
- 직접 등록하는 기술 지원 객체는 수동 등록
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록 고민

내용 출처

인프런 - 스프링 핵심 원리 (기본편)
강사명 - 김영한

노션 링크

https://earthy-grouse-d42.notion.site/6-17b5723e06ee8078a90fe255ff643914

'Spring' 카테고리의 다른 글

스프링 기본편 8  (0) 2025.01.21
스프링 기본편 7  (0) 2025.01.20
스프링 기본편 5  (0) 2025.01.14
스프링 기본편 4  (0) 2025.01.13
스프링 기본편 3  (0) 2025.01.11