728x90
0. 자세한 내용
- 1~2 chapter: 스프링 핵심 원리 이해 2 - 객체 지향 원리 적용 , 스프링 핵심 원리 이해 3 - 스프링 컨테이너와 스프링 빈
- 3 chapter: 스프링 핵심 원리 이해 4 - 싱글톤 컨테이너
- 4 chapter: 스프링 핵심 원리 이해 5 - 컴포넌트 스캔
- 5~6 chapter: 스프링 핵심 원리 이해 6 - 의존관계 자동 주입
- 7 chapter: 스프링 핵심 원리 이해 7 - bean 생명주기 callback
- 8 chapter: 스프링 핵심 원리 이해 8 - bean scope
1. appConfig
- srp, dip, ocp 지키는 방법
- 수동 빈 등록 방법
- 종류
- appConfig.java
- appConfig.xml
- @Configuration
- @ComponentScan이 읽고 springContainer에 넣는다.
- 가짜 객체를 통해서 싱글톤 및 싱글톤의 문제점을 해결
- @Bean
- @Bean으로 등록되는 type : interface
- @Bean으로 반환하는 객체: interface의 구현체
- DI하는 코드: 반환하는 객체 내부의 생성자 초기화 매개변수
- @Bean의 이름은 겹치면 난리나니깐 무조건 다르게 지정하기
1.1. config.java 예시 코드
@Configuration // spring 설정정보
public class AppConfig {
@Bean // spring container에 넣기
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
// service
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
1.2. config.xml 예시 코드
- spring 레거시 프로젝트때는 xml 로 bean 등록을 권장
- 직접 빈을 등록하기 때문에 @Configuration 같은 과정을 거칠 필요가 없다.
- 필요하면 https://spring.io/projects/spring-framework 이 문서 찾아보기
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="memberService" class="hello.core.member.MemberServiceImpl">
// id : bean 이름
// class : 해당 id의 실제 bean 객체
<constructor-arg name="memberRepository" ref="memberRepository11"/>
// constructor-arg : DI 해야 할 bean
// ref : DI할 bean
// name :생성자의 매개변수와 일치하는 이름
</bean>
<bean id="memberRepository11" class="hello.core.member.MemoryMemberRepository"/>
<bean id="orderService" class="hello.core.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
<constructor-arg name="discountPolicy" ref="discountPolicy"/>
</bean>
<bean id="discountPolicy" class="hello.core.discount.FixDiscountPolicy"/>
</beans>
2.Spring Container 및 bean 조회
- 실제 code에선 잘 사용하지 않지만 TDD에 사용하므로 알아둘 필요가 있다.
2.1. SpringContainer
- SpringContainer : applicationContext
- SpringContainer 구현체 : annotationConfigApplicationContext(요즘), GenericXmlApplicationContext(xml)
2.2. bean 조회
- 핵심 조회
- bean 1개 조회
- Object getBean(classType);
- Object getBean(String beanName);
- 여러 bean 조회
- Map<String, classType> getBeansOfType(classType)
- bean 1개 조회
- 빈 조회 method() 들
String[] getBeanDefinitionNames(); // spring 등록된 All bean 조회
Object getBean(String beanName); // 등록된 spring bean의 instance 반환
getBeanDefinition(String beanName); // 해당 bean instance의 meta 정보 반환
getRole(); meta 정보 반환된 객체에서 사용 가능, bean meta 정보 중 어디서 등록된 빈인지 정보 반환
// 상수
ROLE_APPLICATION // 직접 등록한 빈
ROLE_INFRASTRUCTURE // 스프링이 내부에서 사용하는 빈
// 사용 예
beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE // BeanDefinition : 해당 상수가 존재하는 class
classType getBean(classType); // 등록된 spring bean 중 해당 class의 bean instance 반환
classType getBean(String beanName, classType); // 해당 클래스에 여러개의 bean 1개의 bean만 반환
Map<String,classType> getBeansOfType(classType); // 해당 클래스에 여러개의 bean 존재시 모든 빈 반환
// classType도 구체타입, 추상타입 다 가능하지만 구체타입은 유연성이 떨어지므로 권장 안함
추상타입 : service.class
구체타입 : serviceImple.class
// classType으로 bean 호출 시 중복으로 bean이 나오는 경우
1. 해당 bean type이 동일할 때
2. classType의 자손 class type들이 등록 된 경우
- 모든 빈 조회 tdd code
class ApplicationContextInfoTest1{
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
void findAllBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + "object = " + bean);
}
}
@Test
@DisplayName("app bean만 출력하기")
void findApplicationBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName: beanDefinitionNames){
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// getBeanDefinition: getRole()로 구분하기 위해 필요한 Bean에 관한 meta data 정보 반환
// Role ROLE_APPLICATION: 직접 등록한 애플리케이선 빈
// Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + "object = " + bean);
}
}
}
}
3. singleton Container
- java로 생성한 singleton Container 단점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다.
- DIP를 위반한다. ex) getInstance()로 직접 구현체 가져옴
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 유연성이 떨어진다.
- springContainer는 위의 문제점을 다 해결한 singleton container를 만들었다.
- @Configuration이 java singleton의 문제점을 해결하기 위해 사용하는 것, cglib라는 proxy객체를 사용하게 한다.
- 주의점
- 싱글톤이란 것은 주 목적은 공유객체이다.
- 그래서 singleton은 최대한 stateless하게 설계해야한다.
싱글톤인 class가 존재시, 어지간하면 method내의 지역변수를 통해서 무언가를 수행하고 정~~~말 아닐 경우 잘 생각하고 iv를 생성한다. iv가 결국 stateful한 것이기 때문
4. 자동 bean 등록방법
- springboot, spring 레거시 동일
- 차이점이라고 하면 springboot는 @ComponentScan이란 annotation으로 @Componet를 찾고 spring 레거시는 xml 파일 componentScan 을 등록한다.
- @Component
- @Autowired : getBean(classType)으로 찾기 때문에, 동일 type이 2개가 존재시 예외 발생
- @ComponentScan 범위
- default: @ComponentScan 붙은 설정 정보 class 패키지가 시작위치
- 수동 : @ComponentScan(basePackages + "탐색패키지.하위프로젝트명")
- springRegacy 경우: 수동의 방식으로만 등록할 수 있다.
- @ComponentScan 필터
- include, exclude가 있는데 있다는 정도로만 알기
- 중복과 충돌
- 스프링: 수동 빈 등록이 우선권을 가진다. 경고 문구만 날림(중복시 수동빈을 덮어쓰도록 의도하면서 코드를 작성하지 않으므로 주의 필요)
- 부트: 에러 터뜨림 (최고방법)
- 스프링: 수동 빈 등록이 우선권을 가진다. 경고 문구만 날림(중복시 수동빈을 덮어쓰도록 의도하면서 코드를 작성하지 않으므로 주의 필요)
5. 의존관계 자동 주입
- 권장
- 생성자 주입 99%
- 수정자 주입 1%
- 필드, 메서드 주입 (권장 안함)
5.1. 주입할 빈이 없는 경우
- 옵션
- 자동 의존관계 주입을 하려는 주입할 스프링 빈이 없을 경우 동작시키려고 하는 옵션 3가지
- @Autowired(required=false): 자동 주입 대상이 없을 시, 메서드 자체가 호출이 안된다.
- @Nullable : 자동 주입할 대상이 없으면 null이 입력
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력
- 자동 의존관계 주입을 하려는 주입할 스프링 빈이 없을 경우 동작시키려고 하는 옵션 3가지
- lombok
- 코들 간략화 최신 트랜드
- getter, setter, 생성자, toString, requiredArgsConstructor, 등등
5.2. 조회 bean이 2개 이상
- 예외 발생
- @Autowired private DiscountPolicy discountPolicy
- NoUniqueBeanDefinitionException
- 해결 - 3가지 방법
- @Autowired 필드 명 - 거의 안쓰는 듯
- @Qualifier : sub (서브 설정 파일일 경우 사용)
- @Primary : main (중요 설정 파일 경우 사용)
- @Autowired 필드 명
- 타입 매칭 을 우선 수행
- 동일 타입 빈이 2개 이상일 때, 필드명, 파라미터 명과 동일 한 빈을 매칭
@Autowired
private DisountPolicy discountPolicy;
//@Autowired 필드 명
@Autowired
private DiscountPolciy rateDiscountPolicy;
//@Autowired 파라미터 명
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy){
this.memberRepository = memberRepository;
this.discoutnPolicy = ratediscountPolicy;
}
- @Qualifier
- 추가 구분자를 붙여주는 방법
- 등록위치
- @Component - @Autowired 자동 주입
- 생성자 주입
- 수장자 주입
- AppConfig - @Bean에 주입
* 1번에서 끝나야지 2번까지 넘어가면 실수로인한 잡기 힘든 에러가 될 가능성이 높다.
- @Component - @Autowired 자동 주입
- annotaion 직접 만들어서 에러 줄이는 방법 존재
1. @Component - @Autowired 자동 주입 - 생성자 주입
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// 어노테이션 만든 경우
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// 어노테이션 만들기
//@Qualifier에서 가져온 정보
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 앞으로 사용할 임의의 애노테이션
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy { }
2. @Bean에 주입
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy(){
return new ...
}
- @Primary
- @Bean의 우선순위 정하는 방법
- @Autowired시 여러빈이 매칭되면 @Primary를 가진 @Bean이 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
// 아래는 원래 주입코드와 동일함
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
6. 조회한 빈이 모두 필요할 때 - List, Map <중요>
- 전략패턴 = 모든 빈 조회 방법 (tdd때 방법 말고 실제 spring main code에서 모든 빈 조회 방법)
- Map<String, BeanType>
- Map key = spring bean name
- Map values = bean 객체
- List<DiscountPolicy>
- bean 객체 나열
- Map<String, BeanType>
- 사용 방법, 주석이 핵심 내용
public class AllBeanTest{
@Test
void findAllBean(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
// fixDiscountPolciy라는 매개변수는 front단에서 받아올 것이다.
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
asserThat(discountPrice).isInstanceOf(DiscountService.class);
asserThat(discountPrice).isEqualTo(1000);
}
static class DiscountService{
// bean 등록 DI 시, 전체 단위로 받는다.
private final Map<String, DisountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies){
this.policyMap = policyMap;
this.policies = policies;
}
public int discount(Member member, int price, String discountCode){
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member,price);
}
}
}
7. 빈 생명주기 callback
- spring bean life cycle
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 소멸전 콜백 -> 스프링 종료
# 초기화 콜백 : DI까지 끝내고 외부 호출 전 미리 수행하고 싶은 method 수행
# 소멸전 콜백 : spring 종료 직전 해당 작업이 먼저 안전히 종료하고 싶은 method() 수행
- 의존관계 주입이 끝난 후 요청이 와야지 작업이 수행되지만 초기화 콜백 문법에 맞는 코드가 존재시, di까지 완료 후 초기화 콜백을 수행한다.
- 소멸전 콜백 또한 마찬가지로 스프링이 끝나기 전에 소멸전 콜백에 맞는 문법이 적용이되었을 경우, 소멸전 콜백이 수행된다.
7.1. 초기화 콜백, 소멸전 콜백 문법
- 인터페이스 - 안씀
- @Bean - 3.번으로 해결 안될 경우 사용
- @PostConstruct, @Predestroy - 제일 많이 사용 (하지만 외부 library를 초기화, 소멸전 콜백 할 시 2번 사용 필요)
- bean
// NetworkClient 같은 경우는 직접 만든 class 이지만 외부 library를 가지고 올 경우도 있다.
// 이때 외부라이브러리 중 초기화 콜백할 method의 명만 알고 설정파일에 @Bean option에 넣어주면 된다.
public class NetworkClient {
private String url;
public NetworkClient(){
System.out.println("생성자 호출, url = " + url);
}
// 의존관계 주입
public void setUrl(String url){
this.url = url;
}
// 초기화 콜백 하고 싶은 method를 init() 메서드 내부에 넣기
// -> 설정 파일 bean에서 초기화 콜백 method 명시
public void init(){
System.out.println("DI까지 끝나면 호출한다는 method - InitiallizingBean");
connect(); // 초기화 콜백 하고 싶은 method
call("초기화 연결 메시지"); // 초기화 콜백 하고 싶은 method
}
// 소멸전 콜백하고 싶은 method를 close() 메서드 내부에 넣기
public void close(){
System.out.println("spring container 작업이 종료되면 소멸시키는 method - DisposableBean");
disConnect(); // 소멸전 콜백 하고픈 method
}
}
// 설정 파일
@Configuration
Static class LifeCycleConfig{
// 설정 파일 bean에서 초기화 콜백, 종료 콜백 method 명시
// 초기화 할 callback method명을 "init"처럼 string으로 넣어주면된다.
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
- @PostConstruct, @PreDestroy
제일 많이 사용하지만 외부 라이브러리에는 적용하지 못한다.
직접 bean으로 등록될 method에서 초기화 콜백, 소멸전 콜백 어노테션을 붙인다. = DI까지 완료한 후 초기화 콜백을 수행
public class NetworkClient {
private String url;
public void setUrl(String url){
this.url = url;
}
public NetworkClient(){
System.out.println("생성자 호출, url = " + url);
}
// 초기화 콜백
@PostConstruct
public void init(){
connect();
call("초기화 연결 메시지");
}
// 소멸전 콜백
@PreDestroy
public void close(){
disConnect();
}
// 서비스 시작 ~ 서비스 종료 까지의 코드
public void connect(){ System.out.println("connnect: " + url); }
public void call(String message){ System.out.println("call: " + url + ", message = " + message); }
public void disconnect(){ System.out.println("close: " + url); }
}
// TEST
@Configuration
Static class LifeCycleConfig{
@Bean
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
8. bean scope
- singleton
- prototype
- web-scope
- request
- session
- application
- scope 등록 방식
// 자동 등록
@Component
@Scope("prototype")
public class HelloBean{}
// 수동 등록
@Bean
@Scope("prototype")
public class HelloBean{}
핵심
- prototype scope
- prototype scope + singleton
- request Scope
- 2, 3 핵심의 문제는 prototype은 singleton과 엮여도 항상 새로운 객체가 생성되길 바라고 request Scope이 di로 들어가야 되는 경우 요청이 오기전에 주입 되야하므로 문법적 오류가 발생한다.
- 해결
- DI 대신, DL이란 개념을 이용한다. - 의존관계 조회(탐색) 수행
- DL이란 가짜 객체를 일단 주입 하고 실제로 필요할 때 진짜 객체를 넣어주는 방식
8.1. DL 방식
- spring way : object provider
- java way : provider
- spring way : object provider
@Scope("singleton")
static class ClientBean {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
@Autowired
public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); // 내부에서 스프링 컨테이너를 통해서 해당 빈을 찾아서 반환
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
- java way : provider
# gradle
jakarta.inject:jakarta.inject-api:2.0.1
@Scope("singleton")
static class ClientBean {
private final Provider<PrototypeBean> provider;
@Autowired
public ClientBean( Provider<PrototypeBean> provider) {
this.provider = provider;
}
public int logic() {
PrototypeBean prototypeBean = provider.get(); // 내부에서 스프링 컨테이너를 통해서 해당 빈을 찾아서 반환
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
- 참고 : web scope - request를 provider보다 더 편한걸로 사용하는 방식 - 필요시 사용