김영한 강사님이 제공하시는 인프런 - "스프링 입문 - 스프링 핵심 원리 - 기본편" 강의를 듣고 정리한 내용입니다
강의 링크 김영한 - 스프링 핵심 원리 - 기본편 (유료강의)
생성자 주입
src/main/java/hello.core/order/OrderServiceImpl에 생성자 주입 코드 작성
package hello.core.order;
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자 주입
@Autowired //의존 관계 자동 주입
// ac.getBean(MemberRepository.class), ac.getBean(DiscountPolicy.class)
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
스프링 빈에서만 생성자가 딱 1개있으면 @Autowired를 생략해도 자동 주입된다
수정자 주입(setter 주입)
필드의 값을 변경하는 수정자 메서드(setter)를 통해 의존관계를 주입하는 방법
특징
@Autowired
의 기본 동작은 주입할 대상이 없으면 오류가 발생@Autowired(required - false)
를 통해 주입할 대상이 없어도 동작 가능src/main/java/hello.core/order/OrderServiceImpl에 수정자 주입 코드 작성
package hello.core.order;
@Component
public class OrderServiceImpl implements OrderService{
// 수정자를 만들기 위해 final 제거
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
// 수정자 주입
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
System.out.println("memberRepository = " + memberRepository);
this.memberRepository = memberRepository;
}
// 수정자 주입
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
System.out.println("discountPolicy = " + discountPolicy);
this.discountPolicy = discountPolicy;
}
...
}
setter 에 @Autowired를 붙였더니 memberRepository, discountPolicy 에 자동으로 주입된 것을 AutoAppConfigTest를 실행한 결과 확인 할 수 있다.
자바빈 프로퍼티 규약 예시
class Data {
private int age;
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
자바빈 프로퍼티 규약: 자바에서 필드의 값을 직접 접근하지 않고, setXxx, GetXxx 라는 메서드를 통해 값을 읽거나 수정하는 규칙
이름 그대로 필드에 바로 주입하는 방법.
특징
DI 프레임워크가 없으면 아무것도 할 수 없다. (순수 자바 코드로 테스트 불가능)
사용하지 않아야 하지만, 사용해도 되는 경우
src/main/java/hello.core/order/OrderServiceImpl에 필드 주입 코드 작성
package hello.core.order;
@Component
public class OrderServiceImpl implements OrderService{
// 필드 주입
@Autowired private final MemberRepository memberRepository;
@Autowired private final DiscountPolicy discountPolicy;
...
}
순수 자바 테스트 코드에서는 @Autowired가 동작 X.
⇒ @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능
package hello.core.order;
@Component
public class OrderServiceImpl implements OrderService{
// 일반 메서드에서 주입하기 때문에 final을 빼야 함
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
// 일반 메서드 주입
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
- 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작!
- 스프링 빈이 아닌
Member
같은 클래스에서@Autowired
코드를 적용해도 아무 기능도 동작 X
주입할 스프링 빈이 없어도 동작해야 할 때 존재
→ 자동 주입할 대상이 없을 때, @Autowired만 사용하면 오류 발생!
⇒ 자동 주입 대상을 옵션으로 처리
@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 null이 입력.Optional<>
: 자동 주입할 대상이 없으면 Optional.empty 가 입력src/test/java/hello.core/autowired/AutoWiredTest에서 확인
public class AutoWiredTest {
@Test
void AutoWiredOption() {
// TestBean 스프링 빈 등록
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
// 자동으로 주입할 대상이 없으면, 수정자 메서드 호출X
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
// 자동으로 주입할 대상이 없으면, null 호출
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
// 자동으로 주입할 대상이 없으면, Optional.empty 호출
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
AutowiredTest 실행 결과 Member는 스프링 빈이 아니므로, 자동 주입할 대상이 없다
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장
WHY?
1. 불변
@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
가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 지금은 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행 class OrderServiceImplTest {
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
}
그러나 실행하면 NPE(NullPointerException) 예외가 발생 @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;
}
...
}
src/test/java/hello.core/order/OrderServiceImplTest class OrderServiceImplTest {
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
}
생성자 주입을 사용하면 주입 데이터를 누락 했을 때 컴파일 오류가 발생
⇐ IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있음
컴파일 오류를 해결하기 위해 직접 MemoryMemberRepository, Member, FixDiscountPolicy 인스턴스를 생성해서 주입
class OrderServiceImplTest {
// 순수 자바로 OrderServiceImpl 테스트
@Test
void createOrder() {
// 자바코드로 테스트를 하기 위해 MemoryMemberRepository, Member, FixDiscountPolicy 인스턴스 생성
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
memberRepository.save(new Member(1L, "name", Grade.VIP));
OrderServiceImpl orderService = new OrderServiceImpl(memberRepository, new FixDiscountPolicy());
Order order = orderService.createOrder(1L, "itemA", 10000);
assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
final
키워드
생성자 주입을 사용하면 필드에 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;
}
//...
}
위의 코드에서 discountPolicy
에 값을 설정해야 하는데, 이 부분이 누락되어 있다.
→ 자바는 컴파일 시점에 java: variable discountPolicy might not have been initialized
오류를 발생시킨다
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용 X
오직 생성자 주입 방식만 final 키워드를 사용 O
정리
개발을 하면 대부분 불변이므로 필드에 final 키워드를 사용
⇒ 생성자 주입 방식은 생성자도 만들어야하고 주입 받은 값을 대입하는 코드도 만들어야 함
⇒ 필드 주입처럼 편하게 사용하기 위해 롬복 라이브러리 사용
기본 코드
@Component
public class OrderServiceImpl implements OrderService{
// final 키워드 사용
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자 만들기
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
// 주입받은 값 대입
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
생성자가 딱 1개만 있으면 @Autowired
생략 가능
롬복 라이브러리 적용 방법
plugins {
id 'org.springframework.boot' version '2.6.7'
id 'io.spring.dependency-management' version '1.0.11.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'
}
tasks.named('test') {
useJUnitPlatform()
}
gradle 파일을 변경한 후에는 코끼리를 클릭하거나 Gradle → reload최종 결과 코드
롬복 라이브러리가 제공하는 @RequiredArgsConstructor
기능을 사용하면 final
이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. (코드에는 보이지 않지만 실제 호출 가능)
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성
실제 class
를 열어보면
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
이 코드가 추가되어 있는 거 확인 가능
@Autowired
는 타입(Type)으로 조회
@Autowired
private DiscountPolicy discountPolicy
타입으로 조회하기 때문에, ac.getBean(DiscountPolicy.class)
코드와 유사하게 동작. (실제로는 더 많은 기능을 제공)
스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
// FixDiscountPolicy 스프링 빈 등록
@Component
public class FixDiscountPolicy implements DiscountPolicy {...}
// RateDiscountPolicy 스프링 빈 등록
@Component
public class RateDiscountPolicy implements DiscountPolicy {...}
그리고 DiscountPolicy에 의존관계 자동 주입을 실행하면@Autowired
private DiscountPolicy discountPolicy
NoUniqueBeanDefinitionException
오류가 발생의존 관계 주입 시 조회 빈이 2개 이상일 때 문제 해결 방법
@Autowired 필드 명 매칭
@Autowired
는 타입 매칭을 시도하고 해당 타입의 조회 대상 빈이 2개 이상이면, 필드 이름(파라미터 이름)으로 빈 이름을 추가 매칭
기존 코드
@Autowired
private DiscountPolicy discountPolicy
필드 명을 빈 이름인 rateDiscountPolicy로 변경
@Autowired
private DiscountPolicy rateDiscountPolicy
기존 코드에서는 DiscountPolicy로 매칭한 결과가 fixDiscountPolicy, rateDiscountPolicy 2개여서 문제가 발생했으므로, 필드 명 매칭을 통해 문제 해결
@Autowired
매칭 정리@Qualifier 사용
@Qualifier
는 추가 구분자를 붙여주는 방법
주입 시 추가적인 방법을 제공하는 것. 빈 이름 변경 X
빈 등록시 @Qualifier를 붙여 준다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
주입시에 @Qualifier
를 붙여주고 등록한 이름을 적는다
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
return discoutnPolicy;
}
@Qualifier
로 주입할 때 @Qualifier("mainDiscountPolicy")
를 찾지 못하면 mainDiscountPolicy
라는 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 명확하고 좋다
@Qualifier
정리@Primary 사용
@Primary는 우선순위를 정하는 방법
@Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다
rateDiscountPolicy가 우선권을 가지도록 변경
⇒ RateDiscountPolicy 앞에 @Primary를 붙여주고, FixDiscountPolicy는 그대로 둔다
@Component
@Primary // 우선권 부여
public class RateDiscountPolicy implements DiscountPolicy{...}
@Component
public class FixDiscountPolicy implements DiscountPolicy{...}
사용 코드
우선권을 가질 빈의 클래스 앞에 @Primary를 붙여주면, 생성자나 수정자 주입 코드는 수정할 필요 없이 자동으로 우선권을 가진 빈이 의존 관계 주입 된다
// 생성자
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// 수정자
@Autowired
public DiscountPolicy setDiscountPolicy(DiscountPolicy discountPolicy) {
return discountPolicy;
}
@Qualifier | @Primary |
---|---|
주입 받을 때 모든 코드에 @Qualifier를 붙여야 한다는 단점 | 주입 받을 때 기존 코드를 건드릴 필요 X |
서브 데이터베이스의 커넥션을 획득하는 스프링 빈에서 @Qualifier 지정해서 명시적으로 획득 가능 | 메인 데이터베이스의 커넥션을 획득하는 스프링 빈에서 @Qualifier 지정 없이 편리하게 조회 가능 |
@Qualifier()
안에 문자는 컴파일 시 타입 체크가 안된다. 이는 다음과 같은 애노테이션을 만들어서 문제를 해결할 수 있다.
src/main/java/hello.core/annotation
1. @MainDiscountPolicy 애노테이션 만들기
```java
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
```
RateDiscountPolicy에 @MainDiscountPolicy 애노테이션 적용
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{...}
OrderServiceImpl 의존 관계 주입 코드 수정
//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
return discountPolicy;
}
애노테이션에는 상속이라는 개념 X
→ 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능
(@Qulifier 뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용 가능)
의도적으로 해당 타입의 스프링이 모두 필요한 경우 존재
예를 들어, 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정할 때 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
⇒ 빈을 동적으로 선택해야할 때, Map으로 모든 빈을 조회해서 사용하면 다형성 코드를 유지하면서 빈을 동적으로 사용 가능
src/test/java/hello.core/autowired/AllBeanTest
public class AllBeanTest {
@Test
void findAllBean() {
//AutoAppConfig, DiscountService 스프링 빈 등록
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");
// DiscountService 스프링 빈 등록 확인
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
//모든 DiscountPolicy 주입. 생성자 1개이므로 @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) {
//key(스프링 빈 이름) = discountCode인 할인 정책(DiscountPolicy) 조회
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
로직 분석
fixDiscountPolicy
, rateDiscountPolicy
가 주입fixDiscountPolicy
스프링 빈을 찾아서 실행 (rateDiscountPolicy도 마찬가지)주입 분석
Map<String, DiscountPolicy>
: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.List<DiscountPolicy>
: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다⚠️ 스프링 컨테이너를 생성하면서 스프링 빈 등록하기
스프링 컨테이너는 생성자에 클래스 정보를 받는다. 여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 자동 등록된다.
new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
new AnnotationConfigApplicationContext()
를 통해 스프링 컨테이너 생성AutoAppConfig.class, DiscountService.class
를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록
→ 스프링 컨테이너를 생성하면서, 해당 컨테이너와 동시에 AutoAppConfig, DiscountService를 스프링 빈으로 자동 등록한다.
편리한 자동 기능을 기본으로 사용하자
컴포넌트 스캔과 자동 주입 vs. 설정 정보로 수동으로 빈 등록, 의존관계 수동 주입
⇒ 점점 자동을 선호하는 추세
@Component
@Controller
@Service
@Repository
그러면 수동 빈 등록은 언제 사용하면 좋을까?
업무 로직 빈 | 기술 지원 빈 |
---|---|
웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직.보통 비즈니스 요구사항을 개발할 때 추가되거나 변경 | 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용. 데이터베이스 연결, 공통 로그 처리 등 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들 |
수도 많고, 한번 개발하면 컨트롤러, 서비스, 리포지토리처럼 어느 정도 유사한 패턴 존재 ⇒ 자동 기능 사용 | 업무 로직에 비해 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 광범위하게 영향을 미침. 업무 로직과 달리 적용이 잘 되는지조차 파악하기 어려운 경우가 많음. ⇒ 수동 빈 등록 사용 |
즉 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.
비즈니스 로직 중에서 다형성을 적극 활용할 때
8강 조회한 빈이 모두 필요할 때, List, Map을 다시 보자
DiscountService
가 의존관계 자동 주입으로 Map<String, DiscountPolicy>
에 주입을 받는 상황에서 코드만 보고 어떤 빈들이 주입될지? 각 빈들의 이름은 무엇일지? 한번에 파악 하기 어렵다
⇒ 이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다
이 부분을 별도의 설정 정보로 만들고 수동으로 등록하면
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
한눈에 빈의 이름과, 어떤 빈들이 주입될지 파악이 가능하다.
빈 자동 등록을 사용하고 싶으면 파악하기 좋게 DiscountPolicy 의 구현 빈들만 따로 모아서 특정 패키지에 모아두자
스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외!
이런 부분들은 스프링 자체를 잘 이해하고 스프링의 의도대로 잘 사용하는게 중요
→ 스프링 부트의 경우 DataSource 같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 매뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용 가능
→ 반면에 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록했다면 수동으로 등록하여 명확하게 드러내는 것이 좋다