본 시리즈는 우아한형제들 개발 팀장이신
김영한
님의스프링 핵심 원리 - 기본편
강의를 들으며 개인적으로 정리한 내용을 담고 있습니다. 제가 들은 강의는 인프런에 등록되어 있습니다. 모든 다이어그램을 포함한 사진의 출처는 위 강의의 강의록임을 밝힙니다. 개인적으로 정리한 내용이기 때문에 글 내용에 오류가 있을 수 있으며 이에 대한 피드백은 댓글로 부탁드립니다.
참고: 스프링 컨테이너의 생명 주기는 크게 두 단계이다.
- 스프링 빈 생성
- 의존관계 주입
의존관계 주입은 크게 네 가지 방법이 있다.
@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;
}
}
중요 - 생성자가 하나만 있는 경우엔 @Autowired
를 지정하지 않아도 자동 주입 된다. (스프링 빈에만)
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
참고:
@Autowired
는 주입할 대상이 없으면 오류가 발생시킨다. 주입할 대상이 없어도 동작하게 하려면@Autowired(required=false)
로 지정하면 된다.
참고: 자바빈 프로퍼티
자바에서는 과거부터 필드의 값을 직접 변경하지 않고,
setXxx
,getXxx
등의 메소드를 통해 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이다.
@Configuration
같은 곳에서만 특별한 용도로 사용@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
참고: 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌
Member
같은 클래스에서@Autowired
코드를 적용해도 아무 기능도 동작하지 않는다.
어떤 경우에는 주입할 스프링 빈이 없어도 동작해야하는 경우가 있다.
그러나 @Autowired
만 사용하면 자동 주입 대상이 없을 때 오류가 발생한다.
이를 막기 위해 다음과 같은 방법이 있다.
@Autowired(required=true)
: 자동 주입할 대상이 없으면 수정자 메소드 자체가 호출되지 않음org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 null
이 입력된다.Optional<>
: 자동 주입할 대상이 없으면 Optional.empty
가 입력된다. (Java8)코드로 확인해보자
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);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
콘솔 출력은 다음과 같다.
noBean2 = null
noBean3 = Optional.empty
Member
는 스프링 빈이 아니므로 자동 주입이 안된다.setNoBean1()
은 @Autowired(required=false)
이므로 호출 자체가 안된다.참고:
@Nullable
이나Optional
은 스프링 전반에 걸쳐 지원되기 때문에 생성자 자동 주입에서도 특정 필드에만 적용할 수 있다.
과거에는 수정자 주입과 필드 주입을 많이 사용했지만 최근에는 스프링을 포함한 다른 DI 프레임워크도 생성자 주입을 권장한다. 그 이유는 다음과 같다.
public
으로 열어둬야 하는데, 이는 개발자의 실수를 유발할 수 있기에 좋은 설계가 아니다.NullPointerException
이 발생한다.final
키워드를 사용할 수 있다.final
키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final
을 사용할 수 있다.기억하자! 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
실제 개발을 해보면 대부분이 다 불변이고 생성자+final
키워드를 주로 사용하게 된다.
근데 매번 생성자 쓰기가 귀찮다보니 최적화하는 방법이 등장했다.
OrderSerivceImpl
을 예시로 보자. 기존 코드는 다음과 같다.
@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;
}
...
}
생성자가 딱 하나만 있으면 @Autowired
를 생략해도 스프링 컨테이너가 의존성 자동 주입을 해준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
여기서 더 간편하게 작성하는 방법이 있는데, 바로 Lombok 라이브러리를 이용하는 것이다.
build.gradle
에 lombok 관련 의존성을 추가한다.//lombok 설정 추가 시작
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//lombok 설정 추가 끝
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'
}
Enable annotation processing
을 체크해준다.lombok이 잘 설치되면 라이브러리 목록에서 찾을 수 있다!
이제 lombok을 사용한 간소화된 코드를 보자.
@RequiredArgsConstructor
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
...
}
@RequiredArgsContructor
는 final
이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.@Autowired
를 굳이 붙이지 않아도 스프링 컨테이너가 의존관계 자동 주입을 해준다!out
폴더에 있는 .class
파일을 열어보면 생성자가 추가되어 있다.@Autowired
를 생략하는 방법을 주로 사용한다.@RequiredArgsConstructor
와 함께 사용하면 원하는 기능은 다 사용하면서(final
, 생성자 주입으로 인한 불변 효과), 코드는 깔끔하게 사용할 수 있다.@Autowired
는 타입을 기반으로 스프링 빈을 조회한다.
@Autowired private DiscountPolicy discountpolicy
는 ac.getBean(DiscountPolicy.class)
와 유사하게 동작한다.DiscountPolicy
의 경우 FixDiscountPolicy
와 RateDiscountPolicy
가 모두 스프링 빈으로 등록되어 있다고 하면, 다음과 같은 에러가 발생한다.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
이 때 클라이언트 측에서 하위 타입(구현 클래스)으로 지정해서 스프링 빈을 조회할 수도 있지만, 이렇게 되면 DIP를 위배하게 되고 유연성이 떨어진다. (클라이언트가 구현 클래스에 의존하게 된다.) 그리고 완전히 동일한 타입이면서 이름만 다른 두 개의 빈이 있다면 이 방법은 먹히지 않는다.
스프링 빈을 수동 등록해서 문제를 해결할 수도 있지만, 의존 관계 자동 주입에서 해결하는 방법을 알아보자.
@Autowired
가 주입할 스프링 빈을 선택할 때의 두 번째 기준 (링크)해결 방법을 하나씩 알아보자.
조회 대상 빈이 두 개 이상일 때 해결 방법
@Autowired
필드명 매칭@Quailifier
- @Quilifier
끼리 매칭 - 빈 이름 매칭@Primary
사용@Autowired
는 타입으로 먼저 매칭을 시도하고, 여러 빈이 있으면 필드명으로 빈 이름을 추가매칭 한다. 따라서 필드명을 빈 이름으로 맞춰주면 정상적으로 주입된다.
@Autowired
private DiscountPolicy rateDiscountPolicy
필드명 매칭은 타입 매칭 이후에 동작하는 기능이다.
@Quailifier
는 추가 구분자를 붙여주는 방법이다.
빈 등록 시 @Qualifier
를 붙여준다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
주입 시에 @Qualifier
를 붙여주고 등록한 이름을 적어준다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Qualifier
로 주입할 때 "mainDiscountPolicy"
를 못 찾으면 mainDiscountPolicy
라는 이름의 스프링 빈을 찾는다. 그러나 @Qualifier
끼리 매칭시키는 게 좋다.
@Primary
는 우선순위를 지정한다.
예를들어 rateDiscountPolicy
가 우선권을 가지도록 하려면
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscount
이렇게 하면된다.
@Primary
가 간단하기 때문에 보통 @Qualifier
보다 많이 사용한다.
@Primary, @Qualifier 활용
코드에서 자주 사용하는 메인 DB의 커넥션을 얻는 스프링 빈과 가끔 사용하는 서브 DB의 커넥션을 얻는 스프링 빈이 있다고 하면 메인 DB엔 @Primary
를 적용하고 서브 DB를 쓸 땐 @Qualifier
를 지정해서 명시적으로 획득하면 코드를 깔끔하게 쓸 수 있다.
우선순위
@Primary
는 기본값처럼 동작하고 @Qualifier
는 상세하게 동작한다. 스프링은 대개 넓은 범위의 선택권보다는 좁은 범위의 선택권이, 자동보다는 수동이 우선순위가 높기 때문에 @Qualifier
가 우선순위가 높다.
@Qualifier("mainDiscountPolicy")
같은 식으로 string을 직접 적으면 compile time에 type check가 안된다. 이럴 땐 애노테이션을 직접 만들어보자.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
애노테이션을 만들었으니 이제 적용하면 된다.
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
}
@MainDiscountPolicy private final DiscountPolicy 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);
int rateDiscountPolicy = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPolicy).isEqualTo(2000);
}
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);
}
}
}
DiscountService
는 모든 DiscountPolicy
를 주입받는다. 이 때 fixDiscountPolicy
, rateDiscountPolicy
가 주입된다.discount()
메소드는 discountCode
에 따라 어떤 할인 정책을 사용할지 결정하고, 계산한 값을 반환한다.Map<String, DiscountPolicy>
: map의 키에는 빈 이름, 값에는 빈 객체를 담아준다. 모든 스프링 빈을 담아준다.List<DiscountPolicy>
: 모든 스프링 빈을 담아준다.Config
)을 만지는 일 자체가 너무 번거롭다.app은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈: 웹을 지원하는 컨트롤러, 비즈니스 로직이 담긴 서비스, 데이터 계층의 로직을 처리하는 리포지토리가 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가하거나 변경된다.
기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. DB 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술이다.
업무 로직은 숫자도 많고 개발할 게 많으므로 자동 빈 등록을 사용하는 게 좋다.
기술 지원 로직은 수가 적은 편이며 app 전반에 영향을 미치는 편이다. 그리고 문제 발생 시 원인 파악이 어려우므로 가급적 수동 빈 등록을 사용하는 게 좋다.
app에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 딱 나타나게 하는 게 유지보수하기 좋다.
또는 비즈니스 로직에 다형성을 적극 활용하는 경우, 의존 관계 파악을 위해서 수동 설정 정보를 작성하는 게 도움이 되기도 한다. 자동 의존 주입을 사용하게 되면 여러 소스 코드를 봐야하기 때문에 불편하다. 만약 자동으로 쓰고 싶으면 구현체들을 같은 패키지에 모아놓는 게 좋다.
물론 기술 지원 로직이라도 스프링이나 스프링 부트가 직접 자동 등록하는 애들은 그냥 쓰는 게 낫다. 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 빈 등록을 해서 명확하게 드러내는 게 좋다.
Ctrl+Shift+/
: 영역 주석