이 글은 인프런 - 스프링 핵심 원리 기본편을 보고 공부한 것을 정리한 글입니다.
이름 그대로 생성자로써 의존관계를 주입하는 것
특징으로 생성자 호출 시점에 딱 한 번만 호출되는 것이 보장
따라서 불변, 필수 의존관계에 사용
참고로 생성자가 딱 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라 불리는 수정자 메서드를 통해 의존관계를 주입하는 방법
특징으로 선택, 변경 가능성이 있는 의존관계에 사용
자바빈 프로퍼티 규약의 수정자 메서드 방식(setter)을 사용하는 방법
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired(required = false)
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
@Autowired
의 기본 동작은 주입할 대상이 없으면 오류가 발생. 하지만 @Autowired(required = false)
해주면 대상이 없어도 동작
참고로, 스프링은 크게 두 가지 라이프사이클로 나뉜다.
여기서 의존관계 주입 시, @Autowired
를 통해 의존관계 주입
생성자 => 객체를 생성 시 생성자 호출. 때문에 빈 등록 시 자동주입. 1번 단계에 가까움
수정자 => 생성자 이후에 호출되어 의존관계 주입. 2번 단계에 가까움
결국, 순서를 따지자면 생성자 -> 수정자 순서
그리고 setter 쓰면 생성자 굳이 필요 x
생성자는 필수성, 불변성, 수정자는 선택성, 변경성
요즘은 생성자를 주로 사용하지만, 가끔 선택 및 변경 가능성이 있을 경우 setter도 사용
이름 그대로 필드에 @Autowired
를 붙여서 바로 주입하는 방법
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
코드가 간결하다는 장점이 있음
하지만 외부에서 변경이 불가능하여 테스트 하기 힘들다는 치명적 단점이 있다. 따라서 사용하지 말자!
다만, 어플리케이션 실제 코드와 상관없는 테스트 코드인 경우나, 스프링 설정을 목적으로 하는 @Configuration
같은 곳에서만 특별한 용도로 사용하자
일반 메서드를 통해 주입하는 방법
특징으로, 한번에 여러 필드를 주입받을 수 있지만 일반적으로 잘 사용하지 않는다.
@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
만 명시하면 default 값으로 require = true
로 설정되어 오류가 발생한다.
이 문제를 해결하기 위해 아래 세 가지 방법이 있다.
@Autowired(required = false)
: 자동 주입 대상 없으면 호출되지 않는다.org.springframework.lang.@Nullable
: 자동 주입 대상 없으면 null이 입력된다.Optional<>
: 자동 주입 대상 없으면 Optional.empty가 입력된다.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); // 호출 x
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2); // noBean2 = null
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3); // noBean3 = Optinal.empty
}
}
}
의존관계 주입 방법 네 가지 중 가장 좋은 방법은 생성자 주입이다.
그 이유로 첫 번째, 불변성을 지킬 수 있다.
웬만한 어플리케이션의 의존관계는 초기에 한 번 설정되면 종료시점까지 변경되면 안된다.
따라서 setter나 다른 메서드로 public하게 열어두어 변경 가능성을 애초에 배제시킬 수 있다.
두 번째, 누락을 막을 수 있다.
생성자를 사용하면 필드에 final을 사용할 수 있다.
final을 사용하면 초기화가 되지 않는 것을 막을 수 있고, 값이 한 번 들어오면 더 이상 변경할 수 없게 된다.
생성자 주입 방식을 택하는 이유에 여러가지가 있지만, 프레임워크에 의존하지 않고 순수 자바 언어의 특징을 잘 살리는 방법이다.
따라서, 기본으로 생성자 주입 방법을 사용하고, 필수 값이 아닌 경우엔 수정자 주입 방식을 옵션으로 설정하여 생성자 주입, 수정자 주입을 동시에 사용할 수 있다.
이 생성자 주입을 더 간단하게 해줄 수 있는 라이브러리가 있다.
바로 Lombok 라이브러리이다.
우선 이 롬복 라이브러리를 사용하기 위해 해줘야 할 설정이 있다.
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
간단한 롬복 사용 예시는 아래 코드와 같다.
@Getter
@Setter
@ToString
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setAge(11);
helloLombok.setName("hongdo");
System.out.println("age: " + helloLombok.getAge() + " name: " + helloLombok.getName());
System.out.println("helloLombok = " + helloLombok);
}
}
// age: 11 name: hongdo
// helloLombok = HelloLombok(name=hongdo, age=11)
위와 같이 어노테이션으로 간단히 getter, setter, toString과 같은 메서드를 알아서 만들어 사용할 수 있게 해준다.
이 롬복을 이용하여 생성자 의존관계 주입을 간단히 해보자.
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@RequiredArgsConstructor
를 명시하면, 인자가 필요한 모든 필드에 대한 생성자를 알아서 만들어준다.
만약 이름이 다르고 타입은 똑같은 두 개의 스프링 빈이 동시에 의존관계에 주입된다고 가정해보자.
이러한 상황을 만들고 테스트하면 NoUniqueBeanDefinitionException
이 터진다.
이를 해결하기 위한, 3가지 방법이 있다.
@Autowired
필드명 매칭@Quilifier
-> @Quilifier
끼리 매칭 -> 빈 이름 매칭@Primary
사용@Autowired
는 타입 매칭을 우선적으로 시도하고, 이때 여러 빈이 있을 시, 필드 이름, 파라미터 이름으로 매칭한다.
이 점을 이용해서
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
필드 명을 적어주는 방법이 있다.
@Quilifier
는 추가 구분자를 붙여주는 방법이다.
의존관계 주입 시 메타 정보를 제공하는 식이다.
@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;
}
주입 시, @Qualifier를 붙여주고 등록한 이름을 적어준다.
만약 @Qualifier("mainDiscountPolicy")
를 못찾으면 어떻게 될까? 그러면 mainDiscountPolicy라는 스프링 빈을 찾는다.
그럼에도 찾지 못할 경우, NoSuchBeanDefinitionException
예외가 발생한다.
추가로, 직접 스프링 빈 등록시에도 @Qualifier("mainDiscountPolicy")
를 동일하게 적용할 수 있다.
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {}
이름 그대로 우선순위를 정해주는 방법이다.
의존관계로 등록하려는 구현체에 @Primary
를 달아주자.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
1, 2, 3번 방법 중 대부분의 경우 사용하는 구현체에 @Primary
를 달아주고, 가끔가다 사용하는 테스트용? 구현체는 직접 @Qualifier
를 통해 주입해주자.
@Primary
보다 @Qualifier
의 우선순위가 더 높다.
@Qualifier
가 매우 상세하게 동작하기 때문 -> 스프링에서는 자동보다는 수동, 넓은 범위보단 좁은 범위의 선택권이 더 높은 우선순위를 가진다.
@Qualifier("mainDiscountPolicy")
이렇게 문자를 적으면 컴파일 시 타입 체크가 안된다.
아래와 같이 어노테이션을 직접 만들어 해결할 수 있다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented // 여기까지는 모두 @Qualifier에 있는 어노테이션 가져온 것
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
이와 같이 어노테이션을 직접 만들어 이용하면 오타 발생 시 컴파일 오류가 발생해 쉽게 파악할 수 있다.
의도적으로 해당 타입의 빈이 모두 필요한 경우가 있다.
만약 클라이언트가 할인 서비스의 종류를 선택할 수 있다고 가정해보자.
이때, 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
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;
@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 = this.policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@6bea52d4, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@11981797}
policies = [hello.core.discount.FixDiscountPolicy@6bea52d4, hello.core.discount.RateDiscountPolicy@11981797]
discountCode = fixDiscountPolicy
discountPolicy = hello.core.discount.FixDiscountPolicy@6bea52d4
discountCode = rateDiscountPolicy
discountPolicy = hello.core.discount.RateDiscountPolicy@11981797
이와 같이 Map 혹은 List에 원하는 타입의 구현체를 모두 보관하고, 유동적으로 구현체를 선택할 수 있다.
웬만하면 자동 기능을 기본으로 사용하자.
그럼 어느 경우에 수동 빈 등록을 하면 좋을까?
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 지원 로직.
보통 비즈니스 요구사항을 개발할 때 추가 & 변경된다.
숫자도 많고, 한번 개발하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있다. 이러한 경우엔 자동 기능을 적극 사용하자.
기술 지원 빈: 기술적인 문제나 AOP를 처리할 때 주로 사용.
DB연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
수가 적고, 보통 애플리케이션 전반에 걸쳐 영향을 미친다. 문제가 생겼을 때 어디가 문제인지 명확하게 파악하기 힘들다.
그래서 이러한 기술 지원 로직은 가급적 수동 빈 등록으로 명확하게 들어내는 것이 중요하다.
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 보이게 하는 것이 유지보수하기 좋다.
그리고, 비즈니스 로직 중 다형성을 적극 활용할 때이다.
위 List, Map 코드를 봤을 때, 개발자가 아닌 사람이 코드를 봤을 때 Map<String, DiscountPolicy>
만 있을 때 DiscountPolicy에 어떤 구현체들이 있는지 금방 파악하기 어렵다.
때문에 이럴 때는 따로 DiscountPolicyConfig.java
같이 별도의 설정 정보를 만들어 한눈에 보고 파악하며 관리할 수 있도록 하는 것이 좋다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
혹은 특정 패키지에 같이 두는 것이 좋다.