저번 시간 컴포넌트 스캔에 대해 알아보면서 Component Scan이 어떻게 스프링 빈을 동록하고 관리하는지, 어떻게 사용하는지 알아보았다.
그리고 @Autowired라는 애노테이션을 사용해서 자동 의존관계 주입을 사용해 보았는데, 이번 시간에는 자동 의존관계 주입에 대해서 더 자세히 일아보도록 하겠다.
의존관계 주입 방법
의존관계를 주입하는 방식은 크게 4가지가 있다.
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
생성자 주입은 지금까지 우리가 사용하던 의존관계 주입 방법이다.
생성자를 통해서 의존관계를 주입하는 것으로 생성자 위에 @Autowired 를 붙여주면 된다.
그런데 생성자가 하나라면 @Autowired를 생략 가능하다.
생성자 주입
생성자 주입의 특징은
1. 생성자 호출시점에 딱 1번만 호출 되는 것이 보장된다.
2. 불변, 필수 의존관계에 사용한다.
@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라고 불리는 메서드를 사용해 필드 값을 수정하고 이를 통해 의존관계를 주입하는 것이다.
특징으로는
1. 선택, 변경 가능성이 있는 의존관계에 사용함
2. 자바빈 프로퍼터 규약의 수정자 메서드를 사용하는 방법
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
이때 필드 값은
private MemberRepository memberRepository ;
private DiscountPolicy discountPolicy;
final을 제거해줘야 한다.
그래서 선택, 변경 가능성이 있는 의존관계에 사용하는 것이다.
외부에서 메서드를 통해 값을 변경할 수 있다.
필드 주입
이름 그대로 필드에 바로 주입하는 방법이다.
코드가 간결해 이전에는 자주 사용하였지만, 외부에서 변경이 불가능해 테스트하기 힘들다는 치명적인 단점을 가지고 있다.
또한 DI 프레임 워크가 없으면 아무것도 할 수 없다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
따러서 요즘에는 사용하지 않고 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.
일반 메서드 주입
일반 메서드를 통해서 의존관계를 주입받을 수 있다.
특징은
1. 한번에 여러 필드를 주입 받을 수 있음
2. 일반적으로 잘 사용하지 않음
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
이 방법 또한 필드에 final을 제거해줘야 한다.
옵션 처리
주입할 스프링 빈이 아직 없다면 어떨까?
@Autowired에 required 옵션의 기본 값은 ture로 되어 있는데 이는 자동 주입 대상이 없을 때 오류를 발생시킨다.
자동 주입 대상을 옵션으로 처리하는 방법은 3가지가 있다.
- Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 되지 않음
- org.springfamework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력됨
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
자동 의존관계 주입을 사용할 때는 생성자 주입 방식을 선택하는 것이 좋다.
대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료까지 변해서는 안된다. 즉, 불변 상태를 유지해야 한다.
생성자 주입 방식은 불변하게 설계 할 수 있도록 돕는다.
또한 다른 의존관계 주입 방식을 사용하면 의존관계 주입이 누락될 가능성이 있다.
하지만 생성자 주입 방식은 final 키워드를 필드에 사용할 수 있어 누락도리 가능성을 막아준다.
생성자 주입 방식은 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살려주는 방법이다.
또한 필요하다면 기본으로 생성자 주입 방식을 채택하돼, 필수 값이 아닐 때 수정자 주입 방식을 선택해 동시에 사용할 수도 있다.
롬북과 최신 트랜드
하지만..
생성자 주입 방식은 생서자도 만들고 생성자 코드도 만들어야 하니 필드 주입보다 불편한 것은 사실이다.
그래서 요즘에는 롬복을 사용해서 코드를 최적화한다.
build.gradle 파일을
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
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'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
다음과 같이 변경한다.
그럼 OrderServiceImpl 파일을
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository ;
private final DiscountPolicy discountPolicy;
}
이렇게 최적화시켜 바꿀 수 있다.
의존관계 주입 조회 문제
Autowired는 의존관계를 주입할 때 Type을 통해서 조회한다.
마치 .getBean()메서드를 사용해 스프링 빈을 조회하는 것처럼 말이다.
그런데 이번 스프링 빈 조회 문제 때 배웠 듯 @Autowired를 사용해도 타입으로 조회시 선택된 빈이 2개 이상일 때는 문제가 발생한다.
조회대상이 되는 빈이 2개 이상일 때는 다음과 같은 방법으로 해결한다.
- @Autowired 필드 명 매칭
- @Qualifier -> Quialifer끼리 매칭 -> 빈 이름 매칭
- @Primary 사용
먼저 Autowired의 필드명을 구체적인 파라미터 이름으로 바꿔주는 것이다.
@Autowired
private DiscountPolicy discuontPolicy-> rateDiscountPolicy
이러면
먼저 타입 매칭을 시도하고
그 결과 여러 빈이 있을 때 파라미터 명으로 빈 이름을 매칭하게 된다.
@Qualifer는 추가 구분자를 붙여주는 것이다.
@Qualifer("mianDiscountPolicy")
이렇게 빈 등록시 추가해 둔 후 생성자 주입이나 수정자 주입 시, 생성자의 파라미테어 @Qualifer를 추가해주면 된다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
이러면 먼저 @Qualifer끼리 매칭을 시도하고
찾을 수 없다면 빈 이름으로 매칭을 시도한다
그래도 찾을 수 없다면 NoSuchBeanDefinitionException 예외를 발생시킨다.
@Primiary는 우선순위를 정하는 방법이다.
여러 빈이 매칭될 때 우선권을 설정해두는 것이다.
이러면 여러 빈이 조회되어도 @Primiary가 붙은 빈을 우선적으로 등록하게 된다.
@Primary와 @Qualifer를 동시에 사용한다면, 우선권은 @Qualifer가 갖게 된다. 더 구체적인 설정 정보를 입력할 수 있기 때문이다. 따라서 함께 사용하돼 적절한 곳에 배치해야 한다.
애노테이션을 직접 만들 수도 있는데
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
이렇게 하면 애노테이션 을 만들어 @MainDiscountPolicy를 사용하고, 빈의 우선순위를 정할 수 있다.
그런데 가끔 해당 타입의 스프링 빈이 모두 다 필요한 경우가 있다.
package hello.core_review.autowired;
import hello.core_review.AppConfig;
import hello.core_review.AutoAppConfig;
import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.member.Grade;
import hello.core_review.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
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, 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;
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);
}
}
}
이렇게 진행하면, Map에 등록된 스프링 빈의 DiscountPolicy 중 discountCode를 직접 입력 해 discountPolicy를 받아 사용할 수 있다.
자동과 수동의 기준
지금까지 ComponentScan과 Autowired와 같이 자동으로 스프링 빈을 등록해주고, 의존관계를 설정해주는 방법을 알아보았다.
수동보다는 자동으로 설계하는게 개발자 입장에서는 부담이 적을 수밖에 없다. 그렇다고 언제나 자동을 사용하는 것이 맞을까? 어떤 방법을 사용해야 할까?
애플리케이션은 크게 업무 로직 빈과 기술 지원 빈으로 나눌 수 있다.
업무 로직은 웹을 지원하는 컨트롤러나 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
기술 지원 빈은 기술적인 문제나 공통 관심사를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
업무로직은 숫자도 많고, 유사한 패턴을 자주 사용한다. 이런 경우 자동 기능을 적극 활용하는 것이 좋다.
하지만 기술 지원 로직은 수도 적고, 어플리케이션 전반에 광범위한 영향을 미친다. 또한 기술 로직은 업무로직에 비해 어디가 문제인지 명확하게 드러나지 않는다. 따라서 가급적 수동 빈 등록을 통해서 명확하게 드러내는 것이 좋다.
또한 다형성을 활용할 수 있는 비즈니스 로직은 수동 등록을 사용하는 것이 더 명확하고 좋다.
하지만 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외이다. 스프링 자체를 이해하고 의도대로 활용하는 것이 가장 좋기 때문이다.
'Spring' 카테고리의 다른 글
[Spring 핵심 원리 이해] 8. 빈 스코프 (1) | 2025.01.13 |
---|---|
[Spring 핵심 원리 이해] 7. 빈 생명주기 콜백 (1) | 2025.01.07 |
[Spring 핵심 원리 이해] 5. 컴포넌트 스캔 (0) | 2024.12.28 |
[Spring 핵심 원리 이해] 4. 싱글톤 컨테이너 (0) | 2024.12.27 |
[Spring 핵심 원리 이해] 3. 스프링 컨테이너와 스프링 빈 (4) | 2024.12.20 |