김영한 님의 스프링 핵심 원리 - 기본편 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard
다형성을 이용하면 부품을 갈아 끼우듯이 컴포넌트를 쉽고 유연하게 변경할 수 있다
부모(인터페이스)로 선언하고 구체화는 클래스의 생성자를 이용한다
public class MemberService {
private MemberRepository m = new MemoryMemberRepository();
}
하지만 위처럼 작성하게 되면, 구현 객체 변경을 위해 클라이언트 코드를 변경해야하기 떄문에 OCP를 위반하게 된다
또한 인터페이스에만 의존해야하는데 인터페이스에 의존하면서 동시에 구현 객체에도 의존하고 있기 때문에 DIP를 위반하게 된다
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
< AppConfig >
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
그러므로 구현 클래스를 작성할 때 인터페이스에만 의존하도록 변경하고, 의존 관계가 있는 객체는 생성자를 통해 외부에서 주입받아야 한다
AppConfig가 객체의 생성과 연결을 설정하며 프로그램의 제어 흐름을 관리하는데 설정 파일임을 알 수 있도록 @Configuration
을 붙인다
이처럼 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 하고, 외부에서 구현 객체를 주입받는 것을 DI(의존관계 주입)라고 한다
MemberService도 다른 곳에서 의존하고 있을 수도 있기 때문에 MemberService 인터페이스를 통해 구현 객체(MemberServiceImpl)가 생성될 수 있도록 한다
스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리
여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 무상태(stateless)로 설계해야 한다
클라이언트에 의존적인 필드나, 클라이언트가 값을 변경할 수 있는 필드가 존재하면 안된다
@Bean
만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다 ( 객체가 여러 개 생성된다 )
설정 정보에 @Configuration
를 붙여줘야 바이트코드를 조작하는 CGLIB 기술을 사용해 싱글톤을 보장할 수 있다
public class MemberApp {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
}
}
AnnotationConfigAPplicationContext
을 통해 스프링 컨테이너를 생성하였다
생성 시 설정 정보인 AppConfig 파일을 넘겨주었다
설정 파일에 명시된 빈들이 등록되면 getBean()
메서드를 통해 빈을 꺼낼 수 있다
BeanFactory : 스프링 컨테이너의 최상위 인터페이스 ( 빈을 관리하고 조회하는 기능을 가짐 )
ApplicationContext : BeanFactory 의 기능을 상속받아 제공
AnnotationConfigApplicationContext : ApplicationContext 의 구현 클래스
ApplicationContext 는 스프링 컨테이너를 생성할 때 다양한 형식의 설정 정보를 받을 수 있다
어노테이션 기반 자바 코드
xml 문서 기반
임의로 구현한 설정 정보
3-1 번에서 스프링 컨테이너를 생성할 때 AppConfig 를 넘겨주었는데 이는 어노테이션 기반 자바 코드이기 때문에 AnnotationConfigApplicationContext
클래스를 사용했다
xml 은 GenericXmlApplicationContext
3-3에서 설명한 것처럼 스프링이 다양한 형식의 설정 정보 ( 자바 코드, xml )을 받아들일 수 있는 이유 : BeanDefinition
BeanDefinition : 빈 설정 메타정보로 @Bean
, <bean>
마다 하나씩 메타 정보가 생성된다
ApplicationContext ( 스프링 컨테이너 인터페이스 )는 BeanDefinition( 인터페이스 )에만 의존하고 이 메타 정보를 기반으로 스프링 빈을 생성하기 때문에 다양한 형식의 설정 정보를 받아들일 수 있는 것이다
즉, 스프링 컨테이너 구현 객체가 설정 정보를 읽어서 BeanDefinition 을 생성하고 스프링은 이 BeanDefinition를 기반으로 스프링 빈을 생성한다
설정 정보 ( 어노테이션 기반 자바 코드, xml 문서 )를 받아 스프링 컨테이너를 생성하는데 바로 이 설정 정보에 스프링 빈에 대한 정보가 있다
강의에서는 AppConfig라는 설정 정보 클래스에서 객체 생성과 연결을 담당하는데 @Configuration
과 @Bean
을 이용해서 빈을 수동으로 등록하였다
컴포넌트 스캔 : 설정 정보에 직접 빈을 등록하는 것이 아닌 자동으로 스프링 빈을 등록하는 방식
컴포넌트 스캔을 사용하려면 @ComponentScan
을 설정 정보에 붙여야 하는데 이는 @Component
를 가진 클래스를 찾아서 자동으로 스프링 빈으로 등록해준다
@Controller
, @Repository
등의 어노테이션이 붙은 클래스도 스프링 빈으로 등록하는데 그 이유는 위 어노테이션들이 @Component
이 붙어있기 때문이다
컴포넌트 스캔 사용 시, 의존관계 주입을 위해 생성자에 @Autowired
붙여주면 스프링이 타입에 맞는 객체를 자동으로 주입해준다
@Configuration
static class SameBeanConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
MemberService 가 스프링 빈으로 등록될 때 MemberService 가 빈의 타입이 되고, 메서드 이름인 memberService가 빈의 이름이 된다
MemberRepository 는 두 개 다 MemberRepository 로 타입은 동일하지만 빈의 이름이 memberRepository1, memberRepository2 로 다르다
빈 이름 + 타입으로 조회 : getBean("memberService", MemberServiceImpl.class);
타입으로 조회 : getBean(MemberRepository.class);
@Autowired
를 통해 의존관계를 주입받는다
일반적인 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다
생성자 의존관계 주입 방법은 스프링 빈을 등록하면서 자동으로 의존관계까지 주입되는 방식이다
생성자 주입 외에도 setter 주입, 필드 주입, 일반 메서드 주입이 있다
@Autowired(required = false)
public void setNoBean1(Member noBean1) { ... }
@Autowired
public void setNoBean2(@Nullable Member noBean2) { ... }
@Autowired
public void setNoBean3(Optional<Member> noBean3) { ... }
@Autowired
에는 required 속성이 있는데 default가 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다
오류가 발생하지 않도록 하기 위해 required 속성 이용, @Nullable
혹은 Optional<>
를 사용하는 방법이 있다
@Nullable
: 자동 주입할 대상이 없으면 null
이 입력된다
Optional<>
: 자동 주입할 대상이 없으면 Optional.empty
가 입력된다
@Autowired
는 타입에 맞는 객체를 자동으로 주입해주는데 이는 빈을 타입으로 조회한다는 의미이다
타입으로 조회했을 때, 동일한 타입의 스프링 빈이 2개 이상이라면 NoUniqueBeanDefinitionException
오류가 발생한다
즉, 동일한 타입이 2개 이상이라면 의존관계 자동 주입이 이루어지지 않는다
// 오류
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// 해결
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
@Autowired
로 조회 시, 결과가 두 개 이상인 경우 필드 이름, 파라미터 이름으로 빈 이름을 추가로 매칭한다
타입이 DiscountPolicy 인 FixDiscountPolicy , RateDiscountPolicy 가 둘 다 등록되어 있는 경우 첫 번째 코드는 오류가 발생
두 번째 코드에서 필드명 매칭은 결과가 두 개 이상인 경우 빈 이름을 추가로 매칭하기 때문에 rateDisCountPolicy 를 찾아서 의존관계를 주입한다
// 스프링 빈 등록
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { ... }
// 의존관계 주입
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { ... }
스프링 빈 등록 & 의존관계 주입 시 @Qualifier
를 사용해서 추가 구분자를 붙여주는 방식
@Qualifier
로 주입할 때 @Qualifier("mainDiscountPolicy")
를 못찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다
// 스프링 빈 등록
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { ... }
// 의존관계 주입
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { ... }
스프링 빈을 등록할 때 @Primary
로 우선순위를 지정한다
위의 방식처럼 따로 생성자 코드를 맞추지 않아도 @Primary
가 붙은 객체가 잘 주입된다
스프링 빈( 싱글톤 )의 이벤트 라이프사이클
스프링 빈의 초기화 작업을 수행하려면 스프링 빈이 생성되고, 의존관계 주입까지 끝난 후에 진행해야한다
의존관계 주입이 끝난 시점을 알 수 있도록 스프링은 스프링 빈에게 초기화 콜백 메서드롤 통해 초기화 시점을 알려준다
또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다
스프링은 인터페이스, 메서드, 어노테이션 3가지 방법으로 초기화 콜백, 소멸 콜백을 지원한다
초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
소멸 전 콜백: 빈이 소멸되기 직전에 호출
가장 간단한 방법은 @PostConstruct
, @PreDestroy
어노테이션을 사용하는 것이다
스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프
의존관계를 주입하고 초기화 메소드까지 호출하지만 @PreDestroy
와 같은 종료 메서드가 호출되지 않는다
요청을 할 때마다 새로운 프로토타입 빈이 생성되어 반환된다
단> 의존관계 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 프로토타입 빈을 사용 할 때마다 새로 생성되는 것이 아니다
지정방법 : @Scope("prototype")
스프링 웹과 관련된 기술이 들어가야 사용할 수 있는 스코프
request
웹 요청이 들어오고 나갈때 까지 유지되는 스코프
요청이 들어올 때 생성, 응답이 나갈 때 destroy
각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성된다
application : 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger; // 이 부분이 오류
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
...
}
}
@RequiredArgsConstructor
가 있기 때문에 스프링 컨테이너가 생성될 때 MyLogger 객체 의존관계 주입이 자동으로 이루어진다
but> MyLogger 는 request 스코프이기 때문에 고객의 요청이 들어올 때까지 생성되지 않기 때문에 없는 객체를 주입하려고 해서 오류가 발생하한다
이를 해결하기 위해 ObjectProvider 나 프록시를 이용할 수 있는데 이들의 핵심은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다