스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
본 포스팅은 <스프링 핵심 원리 - 기본편> 강의 내용을 복습하기 위해 간략히 작성된 글이므로 전체 내용은 위 강의에서 확인 가능합니다.
2. 객체지향의 5가지 원칙(SOLID), 관심사의 분리
7. @Autowired 필드명 매칭, @Qualifier, @Primary
제어의 역전(IoC, Inversion of Control)
공연 준비를 한다고 가정해 봅시다. 지금까지는 배우들이 그날의 배역을 맡을 배우를 정했습니다. 배우는 공연하기에도 벅찬데 배역까지 정하느라 힘들었죠. 배우들은 본인들의 역할에 집중하기 위해서는 "공연 기획자"라는 포지션이 필요하다고 생각했고 기획자를 구했습니다. 그제서야 배역을 직접 배우들이 정하지 않고 기획자라는 외부 인물을 통해 정하게 됐습니다. 제어의 역전이란 이와 같이 프로그램의 제어 흐름을 직접 제어하는 게 아니라 외부에서 관리하는 것을 말합니다.
의존관계 주입(DI, Dependency Injection)
의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계를 분리해서 생각해야 합니다.
정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 파악할 수 있습니다. 즉, 애플리케이션을 실행하지 않고도 분석할 수 있다는 것이죠. 하지만 인터페이스까지는 알 수 있지만 실제 구현 객체가 무엇인지는 알 수 없습니다.
아래의 그림에서는 OrderServiceImpl이 MemberRepository와 DiscountPolicy에 의존한다는 건 알고 있지만 실제 구현 객체가 MemoryMemberRepository인지 DbMemberRepository인지 알 수 없습니다.
동적인 객체 인스턴스 의존관계
애플리케이션 실행 시점(런타임)에 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 합니다. 그래서 정적인 클래스 의존관계를 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있습니다.
다양한 의존관계 주입 방법
이제부터는 의존관계 주입의 방법에는 어떤 것들이 있을지 알아보겠습니다.
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
1. 생성자 주입
생성자를 통해서 의존 관계를 주입하는 방식으로 앞선 포스팅에서 모두 생성자를 통한 의존관계 주입을 했습니다. 코드로 과정을 다시 한번 간략하게 정리해 보겠습니다.
@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;
}
}
@Component 애노테이션이 붙은 OrderServiceImpl이 스프링 컨테이너에 빈으로 등록이 될 때 생성자를 호출하게 됩니다. 생성자를 호출할 때 @Autowired가 붙어져 있으면 스프링 컨테이너에서 스프링 빈으로 등록된 MemberRepository와 DiscountPolicy를 꺼내서 주입해 줍니다.
🍪 참고로 생성자가 하나일 때는 @Autowired를 생략해도 무방합니다.
이 생성자 주입에는 특징이 있는데 첫 번째는 생성자 호출 시점에 딱 한 번만 호출되는 게 보장이 된다는 것이고 두 번째는 주로 “불변,필수" 의존관계에 사용된다는 것입니다.
위의 코드는 인스턴스를 변경할 수 있는 코드가 없습니다. 여기서 개발자의 의도는 스프링 컨테이너가 올라갈 때 거기서 의존관계 주입이 모두 끝나고 이후에 변하게 하는 코드를 생성하고 싶지 않은 건데요. 만약 누군가가 setter, getter 메서드를 열어두면 추후에 찾기 어려운 버그가 발생하게 됩니다. 그렇기 때문에 생성자 주입은 “불변" 의존관계에 사용됩니다.
불변은 이제 알겠는데 그럼 필수 의존관계에 사용된다는 건 어떤 의미일까요? 위에 MemberRepository와 DiscountPolicy를 선언하면서 ‘final’을 사용했는데요. 여기에서 ‘final’을 붙인 이유는 무조건 값이 있어야 되기 때문입니다. 문서에 null을 허용한다는 얘기가 따로 없으면 웬만하면 생성자에는 모두 값을 채워넣어야 합니다.
스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장하는 이유는 뭘까요?
대부분의 의존관계 주입이 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없기 때문입니다. 생성자 주입은 객체를 생성할 때 딱 한 번만 호출이 된다고 했었던 거 기억나세요? 그래서 불변하게 설계할 수 있습니다. 변경하면 안되는 메서드를 열어두는 건 좋은 설계 방법이 아니랍니다!
두 번째는 생성자 주입을 사용할 때 필드에 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 키워드를 사용할 수 없습니다.
2. 수정자 주입(setter 주입)
setter를 통해서 의존관계를 주입하는 방식입니다. “선택, 변경" 가능성이 있는 의존관계에 사용합니다. 생성자 주입할 때는 필수값인데 수정자 주입에서는 주입할 대상이 없으면 @Autowired의 required 옵션의 값을 ‘false’로 지정해서 주입할 대상이 없어도 동작하게 만들면 됩니다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
3. 필드 주입
말그대로 필드에 의존관계를 주입하는 방식입니다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
코드가 간결해서 편하긴 한데 외부에서 변경이 불가능해서 테스트하기가 굉장히 어렵습니다. 테스트하려면 결국에는 setter를 만들어야 합니다. 그래서 테스트 코드에서나 @Configuration 같은 곳에서만 사용하는 게 좋습니다.
4. 일반 메서드 주입
일반 메서드를 통해서 주입받을 수 있는 방식으로 일반적으로 잘 사용하지 않습니다.
@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 애노테이션은 required 옵션의 기본값이 true이기 때문에 자동 주입 대상이 없으면 오류가 발생하게 되죠. 이럴 때는 어떻게 해야 할까요?
- @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
- ‘org.springframework.lang.@Nullable’ : 자동 주입할 대상이 없으면 null이 입력됨
- ‘Optional<>’ : 자동 주입할 대상이 없으면 ‘Optional.empty’가 입력됨
// 호출 안됨
@Autowired(required = false)
public void SetNoBean1(@Nullable Member member) { ... }
// null 호출
@Autowired
public void SetNoBean2(@Nullable Member member) { ... }
// Optional.empty 호출
@Autowired(required = false)
public void SetNoBean3(Optional<Member> member) { ... }
롬복과 최신 트렌드
거의 99%를 불변 의존관계라고 생각하면 되는데 생성자 주입을 하면 필드 주입처럼 간단하게 코드를 작성할 수가 없습니다. 어떻게 하면 필드 주입처럼 편리하게 사용할 수 있을까요? 롬복 라이브러리를 사용해 봅시다!
기본 코드
@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;
}
}
롬복 적용 코드
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
롬복 라이브러리가 제공하는 @RequiredArgsConstructor기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어줍니다. 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해주는 겁니다. 실제 class를 열어보면 다음과 같은 코드가 추가되어 있습니다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
DI 컨테이너
객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC컨테이너 또는 DI 컨테이너라고 합니다. 최근에는 주로 DI 컨테이너라고 한다. 조립한다는 의미로 어셈블러 혹은 오브젝트 팩토리 등으로 불리기도 합니다.
'Spring' 카테고리의 다른 글
[Spring] 싱글톤 패턴과 싱글톤 컨테이너 (0) | 2022.09.17 |
---|---|
[Spring] 스프링 컨테이너와 스프링 빈 (0) | 2022.09.14 |
[Spring] 객체지향의 5가지 원칙(SOLID), 관심사의 분리 (0) | 2022.09.14 |
[Spring] 스프링이란 (0) | 2022.09.14 |
[Spring] Spring Batch - 잡과 스텝 이해하기 (0) | 2021.11.10 |