해당 글은 인프런의 김영한님의 강의인 '스프링 핵심 원리 - 기본편'을 공부하며 작성한 글입니다.
이제 스프링 컨테이너의 컴포넌트 스캔을 활용해보자.
새로운 설정 정보를 나타낼 클래스를 프로젝트 상단의 디렉토리에 생성해서 @ComponentScan
을 붙이면 스프링이 컴포넌트를 찾아서 빈으로 등록한다.
package hello.core;
...
@Configuration
@ComponentScan(
excludeFilters = ... // 컴포넌트 스캔에 제외할 목록
)
public class AutoAppConfig {
...
그리고 빈으로 등록할 클래스마다 상단에 @Component
을 붙이면 해당 클래스를 스프링 빈으로 등록한다.
@Component
public class MemberServiceImpl implements MemberService {
...
그렇다면 이제 의존관계 주입은 어떻게 할까?
기존에는 AppConfig 클래스에서 수동으로 직접 지정해줬지만, 이제는 스프링 컨테이너가 자동으로 주입하도록 해야 한다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Autowired
가 자동 주입된다.@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
@Autowired
를 붙여서 의존관계를 주입시킨다@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
setter
메서드를 public
으로 열어두어야하기 때문에 불변성이 보장되지 않는다.final
키워드를 사용할 수 있으므로, 혹시라도 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 막아줄 수 있다.생성자 주입을 선택하자!
@RequiredArgsConstructor
를 사용하여 필수값(final
)이 붙은 객체를 생성자로 만들어 준다.
즉, 코드가 아래와 같이 더 간결해진다.
@RequiredArgsConstructor
public class ... {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public void customMethod() {
...
...
최근에는 생성자를 1개 두면
@Autowired
가 자동으로 붙기 때문에 생성자를 하나만 두고 lombok을 사용하는 방법이 주를 이루고 있다.
조회 대상 bean이 2개 이상일때 해결 방법
@Autowired
필드 명 매칭@Qualifier
끼리 매칭 -> 빈 이름 매칭@Primary
사용@Autowired
타입이 똑같은 빈을 가져오지만 필드명을 다르게 설정하면, 해당 필드명으로 빈 이름을 매칭한다
@Qualifier
추가 구분자를 붙여주는 방법.
@Qualifier("구분명")
public class BeanName {
...
@Autowired
public ServiceImpl(@Qualifier("구분명") BeanName beanName) {
...
@Primary
우선 순위를 지정해 준다.
public class 결제서비스 {
private final DiscountPolicy discountPolicy;
public int getPrice() {
...
@Component
public class RateDiscountPolicy implements DiscountPolicy {
...
@Primary
@Component
public class FixDiscountPolicy implements DiscountPolicy {
...
의도적으로 해당 타입의 모든 스프링 빈이 다 필요한 경우에는?
예를 들어서 할인 서비스를 제공하는데 클라이언트가 할인의 종류를 선택할 수 있을 경우.
Map 혹은 List 를 통하여 동일한 타입의 빈을 모두 의존성을 주입받을 수 있다.
편리한 자동 기능을 기본으로 사용
OCP (Open Close Principle) : 개방 폐쇄의 원칙
- 확장에는 열려있고, 변경에는 닫혀 있다.
- 기능을 추가하거나 변경하면서 그것을 사용하는 코드는 수정하지 않는다.
어플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
어플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱 설정 정보에 명확하게 나타내는 것이 유지보수 관점에서 이점이 있다.
비즈니스 로직 중에서 다형성을 적극 활용할 때, 여기에 어떤 빈이 주입될지, 각 빈들의 이름은 무엇일지 자동 등록을 사용한다면 한눈에 보고 파악하기가 어렵다. 이런 경우 수동 빈으로 등록하거나 자동으로 하면 특정 패키지에 같이 묶어두는것이 좋다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new fixDiscountPolicy();
}
}
@Controller
: 스프링 MVC 컨트롤러로 인식.@Service
: 특별한 처리는 하지 않으나, 보통 핵심 비즈니스 로직이 여기에 있음을 예상할 수 있다.@Repository
: 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환.@Configuration
: 스프링 설정 정보에서 사용. DBMS와 미리 연결을 해놓을 때 네트워크 소켓 처럼 어플리케이션 시작 지점에 필요한 연결을 미리 해두고, 어플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.
스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음에 데이터를 사용할 수 있는 준비가 완료된다.
따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.
그럼 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있나?
스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.
스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 > 스프링 빈 생성 > 의존관계 주입 > 초기화 콜백 > 사용 > 소멸전 콜백 > 스프링 종료
생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.
반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 함께 하는것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.
물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는 것이 나을 수 있다.
InitializingBean
, DisposableBean
)@PostConstruct
, @PreDestroy
어노테이션 지원초기화, 소멸 인터페이스 단점
인터페이스 방법은 요즘은 거의 사용하지 않는다.
@Bean
의 destroyMethod
는 default 값이 "(inferred)"
(추론)으로 등록되어 있다.close
, shutdown
이라는 이름의 종료 메서드를 사용한다.close
, shutdown
라는 이름의 메서드를 자동으로 호출해준다.destroyMethod=""
으로 지정하면 된다.@PostConstruct
, @PreDestroy
어노테이션 하나만 붙이면 됨.@Bean
의 기능을 사용하면 된다.최신 스프링에서 해당 방법 권장.
빈 스코프란?
스프링 빈이 존재할 수 있는 범위를 뜻한다.
기본적으로 스프링 빈은 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때 까지 유지된다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다.
@PreDestroy
같은 종료 메서드가 호출되지 않는다.Comment : 어디에서 사용될 수 있을까 ...?
프로토타입 스코프를 사용하려고 하면 아마 문제가 생길 가능성이 높다. 어떤 문제일까?
일반적으로 스프링 빈은 싱글톤이다. 이 싱글톤 빈에서 프로토타입 스코프 빈을 주입하여 사용하려고 하는 상황이라면
싱글톤 빈은 생성 시점에 생성되고 의존관계 주입도 이때 발생한다. 이때 프로토타입 빈을 요청하게 되고 스프링 컨테이너는 프로토타입 빈을 생성해서 싱글톤 빈에 반환한다.
그러면 싱글톤 빈은 내부에 프로토타입 빈의 참조값을 가지고있게 된다.
...? 프로토타입은 클라이언트가 빈을 요청하면 생성해야하는데 어플리케이션을 띄우니까 생성되어서 유지되어버린다.
이 문제를 어떻게 해결할까?
가장 간단한 방법은 싱글톤 빈 내부 로직 실행 시에 프로토타입 빈을 생성하는것이다.
지정한 빈을 스프링 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는것이 ObjectProvider
.
이전에 있었던 ObjectFactory
를 상속받으며 편의기능을 추가한 메서드.
implementation 'javax.inject:javax.inject:1'
라이브러리를 추가하여 사용.프로토타입 빈은 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용한다.
하지만 실무에서 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 직접적으로 사용하는 일은 매우 드물다.
그렇다면 실무에서 JSR-330 Provider를 사용할 것인지 스프링이 제공하는 ObjectProvider를 사용할 것인가.
코드를 스프링이 아닌 다른 컨테이너에서 사용할 수 있어야하는것이 아니라면 스프링이 제공하는 ObjectProvider를 사용하면 된다.
웹 스코프 종류
ObjectProvider
를 일일히 써야하고, 대상 Object를 또 찾아야 하는 번거로움을 줄이기 위하여 프록시 모드를 사용할 수 있다.
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
@Scope
에서 proxyMode
를 사용하면 가짜 프록시 클래스를 만들어 두고, HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
TARGET_CLASS
INTERFACES
CGLIB 라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입.
이 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 로직이 들어있다.
프록시(Proxy)란? 대리(행위)나 대리권, 대리 투표, 대리인 등을 뜻한다
싱글톤 처럼 보이기 때문에 주의해서 사용해야 하며, 이러한 싱글톤이 아닌 스코프를 가지는 빈은 최소화 하여 사용하는것이 좋다. 무분별하게 사용하면 유지보수하기가 어려워 진다.