이 포스팅은 인프런에 있는 김영한 강사님의 스프링 핵심 원리 강의를 들으며 개인 공부를 한 것입니다.
소스코드는 제가 직접 작성하였습니다. (참고 : 깃허브주소)
✅ 다양한 의존관계 주입 방법
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
생성자 주입
- 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다
- 특징
- 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
- 불변, 필수 의존관계에 사용
- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다
- 주로 생성자는 필수값이다.
- 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는게 좋다
수정자 주입 (setter 주입)
- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다
- 특징
- 선택, 변경 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다
- 자바빈 프로퍼티 : setXxx, getXxx 메서드...
- 필수 값이 아닌 경우에는 수정자 주입 방식으로 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다
필드 주입
- @Autowired private TimeSmsFilter timeSmsFilter;
- final 키워드가 없어야 한다
- 특징
- 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 🚫 사용하지 말자
- 애플리케이션의 실제 코드와 관계없는 테스트 코드에서 사용
- 스프링 설정을 목적으로 하는
@Configuration같은 곳에서만 특별한 용도로 사용
일반 메서드 주입
- 특징
- 한번에 여러 필드를 주입 받을 수 있다
- 일반적으로 잘 사용하지 않는다
✅ 옵션처리
- 주입할 스프링 빈이 없어도 동작해야 할 때 처리한다
@Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다Optional<>: 자동 주입할 대상이 없으면Optional.empty가 입력된다
✅ 생성자 주입을 사용해야 하는 이유
불변
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다. (불변해야 한다.)
- 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다
- 누군가 실수로 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불면하게 설계할 수 있다
누락방지
final 키워드
- 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
- 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다
✅ 롬복
- 막상 개발을하면 대부분이 다 불변이고, 생성자 주입을 하다보니 필드에 final 키워드를 사용하게 된다
- 생성자도 만들고, 주입 받은 값을 대입하는 코드도 만들어야하고... 필드 주입처럼 편하게 사용하는 방법이 없을까?
- 롬복 라이브러리가 제공하는
@RequiredArgsConstructor기능으로 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다
롬복 적용된 코드
- 더 간결해졌다
✅ 문제 : 조회 빈이 2개 이상
- @Autowired 는 타입으로 조회한다
- 타입으로 조회하면 선택된 빈이 2개 이상일때 문제가 발생한다.
- 메모리고객저장소, DB고객저장소, 클라우드고객저장소...
- NoUniqueBeanDefinitionException 오류가 발생한다
- 하위 타입으로 지정하는것은 DIP를 위배하고 유연성이 떨어진다. 그리고 이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다
✅ 해결방법 : @Autowired 필드명, @Qualifier, @Primary
- TimeSmsFilter를 상속받는 구현 클래스는 ProdTimeSmsFilter, TestTimeSmsFilter 2개가 있다
@Autowired 필드명 매칭
- @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다
@Component("smsFilter")
public class SmsFilterImpl implements SmsFilter{
@Autowired
private SmsRepository smsRepository;
@Autowired
private CustRepository custRepository;
@Autowired
private TimeSmsFilter prodTimeSmsFilter;
@Qualifier
- 추가 구분자를 붙여주는 방법이다
- 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아니다
@Qualifier는@Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다
// 1. 빈 등록시 @Qualifier를 붙여 준다
@Component
@Qualifier("mainTimeSmsFilter")
public class ProdTimeSmsFilter implements TimeSmsFilter{
. . . . .
// 2. 주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다
@Component("smsFilter")
public class SmsFilterImpl implements SmsFilter{
private SmsRepository smsRepository;
private CustRepository custRepository;
private TimeSmsFilter timeSmsFilter;
@Autowired
public SmsFilterImpl(SmsRepository smsRepository, CustRepository custRepository,@Qualifier("mainTimeSmsFilter") TimeSmsFilter prodTimeSmsFilter) {
this.smsRepository = smsRepository;
this.custRepository = custRepository;
this.timeSmsFilter = prodTimeSmsFilter;
}
@Primary 사용
- 우선순위를 정하는 방법이다
- @Autowired 시에 여러 빈이 매칭되면
@Primary가 우선권을 가진다
@Component
@Primary
public class ProdTimeSmsFilter implements TimeSmsFilter{
...
@Component("smsFilter")
public class SmsFilterImpl implements SmsFilter{
private SmsRepository smsRepository;
private CustRepository custRepository;
private TimeSmsFilter timeSmsFilter;
@Autowired
public SmsFilterImpl(SmsRepository smsRepository, CustRepository custRepository,TimeSmsFilter prodTimeSmsFilter) {
this.smsRepository = smsRepository;
this.custRepository = custRepository;
this.timeSmsFilter = prodTimeSmsFilter;
}
@Primary, @Qualifier 활용
@Qualifier의 단점은 주입 받을 때 모든 코드에@Qualifier를 붙여줘야 한다@Primary를 사용하면 사용하는 곳마다 붙일 필요가 없다- 메인으로 사용해야하는 경우 ->
@Primary사용- 기본값처럼 동작하는 것
- 서브 개념으로 명시적으로 획득 하여 사용하는 경우 ->
@Qualifier사용- 매우 상세하게 동작한다
- 스프링은 자동보다 수동 , 넓은 범위보다 좁은 범위의 선택권의 우선순위가 높기때문에
@Qualifier가 우선권이 높다
✅ 조회한 빈이 모두 필요할 때, List, Map
- 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다
- 예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인 종류를 선택할 수 있다고 가정해보자
- 클라이언트는 할인 폭을 넓은 걸 고르고 싶은 경우...
- sms 상용구 클래스...
public class AllBeanTest {
@Test
void findAllBean() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class, FilterService.class);
FilterService filterService = ac.getBean(FilterService.class);
LocalDateTime ldt = LocalDateTime.of(LocalDate.now(), LocalTime.of(21,00));
boolean testResult = filterService.filter(ldt, "testTimeSmsFilter");
boolean prodResult = filterService.filter(ldt, "prodTimeSmsFilter");
System.out.println("testResult = " + testResult);
System.out.println("prodResult = " + prodResult);
assertThat(testResult).isTrue();
assertThat(prodResult).isFalse();
}
static class FilterService {
private final Map<String, TimeSmsFilter> filterMap;
private final List<TimeSmsFilter> filterList;
public FilterService(Map<String, TimeSmsFilter> filterMap, List<TimeSmsFilter> filterList) {
this.filterMap = filterMap;
this.filterList = filterList;
System.out.println("filterMap = " + filterMap);
System.out.println("filterList = " + filterList);
}
public boolean filter(LocalDateTime ldt, String filterCode) {
TimeSmsFilter timeSmsFilter = filterMap.get(filterCode);
System.out.println("filtercode = " + filterCode);
System.out.println("timeSmsFilter = " + timeSmsFilter);
return timeSmsFilter.isSendable(ldt);
}
}
}
현재 작성한 코드중에 다형성이 적용된 클래스는 TimeSmsFilter라 해당 클래스로 테스트 코드를 구현했다
private final Map<String, TimeSmsFilter> filterMap에 자동으로 key 와 value가 들어갈 수 있는 이유
- key로는 빈 이름이, value로는 해당 타입의 빈 인스턴스가 자동으로 바인딩 된다.
- 스프링의 자동 의존성 주입 기능에 의해 이루어지며, 컨테이너에 등록된 빈 중에서 해당 타입과 일치하는 빈을 찾아 Map에 추가한다.
- 스프링 컨테이너가 빈을 등록할 때 각 빈에 대해 고유한 이름을 할당하기 때문에 key로 사용할 수있다
static inner class로 스프링 컨텍스트에 빈을 등록한 이유
- 일반 내부 클래스는 외부 클래스의 인스턴스에 의존적이므로 스프링이 직접 빈으로 관리할 수 없다
- 스태틱 내부 클래스는 외부 클래스와 독립적으로 존재할 수 있으며, 외부 클래스의 인스턴스에 접근할 수 없다
- 롬복 라이브러리 적용하고 나니
@RequiredArgsConstructor사용 가능
✅ 자동, 수동의 올바른 실무 운영 기준
- 편리한 자동 기능을 기본으로 사용 (컴포넌트 스캔, 자동 주입 사용)
- 업무 로직 빈
- 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다.
- 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다
- 업무 로직은 숫자가 매우 많고 유사한 패턴이 있어 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다
- 업무 로직 빈
- 설정 정보를 통해서 수동으로 빈 등록
- 기술 지원 빈
- 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용
- 데이터베이스 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다
- 기술 지원은 수가 매우 적고 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그래서 적용이 잘되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다
- 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는것이 유지보수 하기 좋다
- 스프링 부트가 아니라 내가 직접 기술 자원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다
- 기술 지원 빈
비즈니스 로직 중에서 다형성을 적극 활용할 때
-> 특정 패키지에 모아서 관리하자
```
'Spring' 카테고리의 다른 글
| Spring Cloud Config Client 설정 후 포트번호가 변경되지 않을 때 (0) | 2025.02.20 |
|---|---|
| 컨트롤러 메서드에서 매개변수 이름을 명시하지 않아서 발생한 IllegalArgumentException (0) | 2025.01.28 |
| 5. 컴포넌트 스캔 (0) | 2025.01.28 |
| 4. 싱글톤 컨테이너 (0) | 2025.01.28 |
| 3. 스프링으로 전환하기 (0) | 2024.11.02 |