역할과 구현으로 구분
역할: 줄리엣
구현: 김태희, 송혜교
장점: 유연해지고 변경에 유리
MemberService는 memberRepository에서 save라는 함수를 클라이언트가 알기만 하면되지, 실제로 구현되어있는 MemoryMemberRepo,JpaMemberRepo의 save메서드가 어떻게 구현되어있는지 전혀 알필요가 없다.
다형성의 본질
인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 할 수 있다.
즉, 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경 할 수 있다.
MemberServiceForMemory memberService1 = new MemoryMemberRepository();
MemberServiceForJdbc memberService2 = new JdbcMemberRepository();
이렇게 클라이언트를 여러개 안만들고
//private MemberRepository memberRepository = new MemoryMemberRepository();
//private MemberRepository memberRepository = new JdbcMemberRepository();
내가 원하는대로 바꿔 끼울 수 있다.
그런데 해당 코드는 OCP, DIP 원칙을 못지키고 있음
왜냐하면 new JdbcMemberREpository()라는 구현체에 의존하고 있기때문이다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final MemberReposiroty memberRepository = new MemoryMemberRepository();
}
해당 코드는 마치 구현체만 바꿔끼우면 되는 다형성을 잘 활용한거 같지만,
OrderServiceImpl이 DiscountPolicy 인터페이스에 의존하면서도, FixDiscountPolicy,RateDiscountPolicy 같은 구체 클래스에도 의존하고 있다. -> DIP위반
FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간, OrderServiceImpl의 소스코드도 변경해야한다. -> OCP위반
항상 구체클래스가 아니라 인터페이스에 의존해야한다.
방법은 뒤의 new를 없애면 된다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy;
당연히 discountPolicy의 구현체가 없으니까 NullpointException이 터질거다.
누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현객체를 대신 생성하고 주입해줘야한다.
관심사 분리
구현객체를 생성하고 주입해주는 역할을 AppConfig가 맡게 된다.
@Configuration
public class AppConfig{
@Bean
public OrderService orderService(){
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
OrServiceImpl에서는 이제 생성자 주입으로 받는다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberReposiroty memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
해당 OrderServiceImpl이라는 클라이언트는 오직 인터페이스에만 의존하고 있다.
memberRepository와 discountPolicy에 어떤 구현체가 들어올지 아무도 모른다.
AppConfig에서 구현체인 OrderServiceImple을 생성과 동시에 memroyMemberRepository와 FixDiscountPolicy라는 구현체를 만들어서 주입까지 해준다.
만약 Appconfig에서 discountPolicy를 anotherDiscountPolicy로 바꾸고싶으면 어떻게 될까?
@Configuration
public class AppConfig{
@Bean
public OrderService orderService(){
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
//이부분만 바꿔주면된다.
return new anotherDiscountPolicy();
}
}
클라이언트인 OrderServiceImpl을 포함해서 어떠한 사용영역의 코드도 변경할 필요가 없다.
그렇다면 AppConfig는 어떻게 사용될까?
원래는 주석처리부분처럼, AppConfig 생성자 호출로 인스턴스 생성하고, 메서드를 호출하는 방식을 사용했었다.
그러나, AnnotaionConfigApplicationContext를 통해 인자로 AppConfig.class를 넘겨주면,
ApplicationContext(스프링컨테이너)에서 getBean() 메서드 호출로 빈으로 설정해둔 인스턴스를 반환 받을 수 있다.
@Configuration애노테이션이 붙었으면 @Bean애노테이션이 붙은 모든 메서드를 호출해서 반환된 객체를 스프링 컨테이너인 ApplicationContext에 저장해둔다.
이러면 Key가 메서드명, value가 new로 생성한 객체가 된다.
좋은 객체 지향 설계의 5가지 원칙
solid
SRP 단일 책임 원칙 -- 하나의 클래스는 하나의 책임을 가져야한다.
이전에는 OrderServiceImpl에서 직접 new DiscountPolicy()를 통해 구현객체를 생성하고, 연결하고, 실행하는등 다양한 책임이 있었다.
이제는 AppConfig가 구현객체를 생성하고 연결하는 책임을 담당한다.
클라이언트 객체인 OrderServiceImpl은 비즈니스 로직 실행만 담당하면된다.
DIP 의존관계 역전 원칙 - 추상화에 의존해야지 구체화에 의존해서는 안된다.
OrderServiceImpl이 DiscountPolicy 추상화 인터페이스에 의존하는것 처럼 보이지만 RateDiscountPolicy라는 구현체에도 의존했었다.
이제는 DiscountPolicy라는 인터페이스에만 의존하고, 필요한 구현체는 AppConfig가 생성후 주입해줬다.
OCP - 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야한다.
애플리케이션을 사용영역 service와 구성영역 AppConfig로 나눔.
그러면 AppConfig의 의존관계를 Fix에서 Rate로 변경해서 주입한다고 한들, 클라이언트의 코드는 병경하지 않아도됨.
소프트웨어 요소를 새롭게 확장해도, 사용영역인 Service에서는 그 어떤 코드도 수정하지 않아도됨
LSP
다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야한다.
ISP
특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스 분리
정비 인터페이스 변경되어도, 운전 인터페이스에 영향 주지않음
IoC,DI,컨테이너
일반적인 프로그램에서는 객체의 생명주기(생성,소멸,삭제)를 내(개발자)가 관리했다.
IoC는 이러한 제어 권한을 개발자로부터 빼앗아, 프레임워크나 라이브러리 같은 외부 요소에 위임한다.
프레임워크 vs 라이브러리
테스트에서 Junit프레임워크 사용시 개발자가 Test메서드를 직접 작성하지만, 우리가 Test메서드를 호출하지 않는다. 우리는 @Test 에노테이션만 붙이면, Junit프레임워크가 이 Test메서드를 호출한다. 이처럼 테스트 메서드를 호출하는 프로그램을 짜지 않았음에도, Junit 프레임워크로 제어권이 역전되어서 실행되는것이다.
내가 작성한 코드를 다른게 제어하고 실행
반면에 라이브러리는 vector라이브러리를 개발자가 #include하면 main메서드 내에 vector 라이브러리에 대한 객체 생성, 메서드 호출등 내가 컨트롤한다.
내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리이다.
DI
정적인 클래스 의존관계는 import를 통해서 쉽게 알아차릴수있다.
반면에 동적인 객체 인스턴스 의존관계는 애플리케이션 실행 시점에 외부에서 실제 구현객체를 생성하고 클라이언트에게 전달하여 클라이언트와 서버의 실제 의존관계가 연결된다. 이러한 과정을 DI라고 한다.
의존관계 주입을 사용하면, 정적인 클래스 의존관계를 변경하지 않고(orderServiceImpl에서 코드 수정없이), 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
이처럼 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는것을 IoC컨테이너 또는 DI컨테이너라고 한다.
지금까지 AppConfig에 설정정보를 바탕으로 스프링 컨테이너에 빈을 등록해서 DI해서 사용하였다.
그렇다면 스프링컨테이너에 어떤식으로 등록이 되는걸까?
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class)
ApplicationContext는 인터페이스이다. 구현체 부분에는 Xml형식, 애노테이션 기반 자바 설정 클래스로도 구현가능,
우리는 AppConfig에서 애노테이션 기반으로 만들었으므로 AnnotationConfigApplicationContext를 사용하였다.
참고: 스프링컨테이너를 부를때, BeanFactory와 ApplicationContext를 구분해서 말하는데, 직접 BeanFactory를 사용하는 경우가 없으므로(ApplicationContext에 기능이 더 많음) 일반적으로 ApplicationContext를 스프링 컨테이너라고 한다.
컨테이너 생성방식
Bean붙은 메서드를 최초로 호출하면서, key value로 이름을 메서드명, 빈객체를 반환값으로 넣어둔다.
여기서 Bean name을 직접 부여할수 있다. => @Bean(name = "memberServiceV2")
스프링 컨테이너가 만들어진 빈객체를 가지고 설정정보를 참고하여서 의존관계 주입을 한다.
원래 스프링에서는 빈을 생성하고, 의존관계를 주입하는 단계가 나뉘어져 있지만, 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계주입도 한번에 처리된다.
스프링 빈 조회방법
1. ac.getBean(빈이름,반환타입)
2. ac.getBean(반환타입), 반환타입 MemberService.class
3. 만약 조회대상 없으면 NoSuchBeanDefinitionException발생
참고: 반환타입을 구체클래스로 설정하지 않기, MemberServiceImpl.class로 해도 스프링컨테이너에 등록되어있다면 조회가 가능하지만, 구체클래스에 의존하는것은 좋지 않다.
2번에서 만약 key가 memberRepository1, memberRepository2가 있는데 value가 동일하게 MemoryMemberRepository로 동일하다면,
ac.getBean(MemberRepository.class)로 조회하면, NoUniqueBeanDefifnitionException발생
고로 키에 해당하는 메서드명과 같이 getBean을 호출하는게 좋다.
만약 특정 타입으로 모두 조회를 하려면
Map<String, MemberRepository>
beanOfType = ac.getBeansOfType(MemberRepository.class);
어? 그러넫 생각해보면 나는 구체클래스로 MemoryMemberRepository 객체를 등록했는데 왜? MemberRepository.class라는 인터페이스(부모)로 찾았는데 찾아지는걸까?
스프링 빈 조회 - 상속관계
빈을 조회할때, 부모타입으로 조회하면, 자식 타입도 함께 조회한다.
만약 DiscountPolicy 인터페이스에 구현체가 RateDiscountPolicy, FixDiscountPolicy 두개가 있다면,
ac.getBean(DiscountPolicy.class)를 호출하면 당연히 둘중 어떤걸 가져와야할지 모르니까 NoUniqueBeanDefinitionException이 터진다.
부모타입으로 조회시, 자식이 둘이상 있으면, 자식이름을 포함해서 찾던가, ac.getBean("rateDiscountPolicy",DiscountPolicy.class);
ac.getBeansofType(DiscountPolicy.class)를 통해 부모타입으로 전부 조회하는 방식을 선택해야한다.
BeanFactory와 ApplicationContext
앞에서 BeanFactory와 ApplicationContext는 인터페이스라고 하였다.
BeanFactory
ApplicationContext
고로 ApplicationContext는 BeanFactory 기능을 상속받는다.
ApplicationContext는 빈관리 기능 + 편리한 부가기능을 제공한다.
BeanFactory보다는 기능이 많은 ApplicationContext를 주로 사용하므로 이걸 스프링 컨테이너로 부른다.
결국 인터페이스에대한 구현체가 있어야하는데 여러가지 형식이 존재하는데 xml기반, 애노테이션 기반 등등
우리는 @Configuration,@Bean으로 스프링 컨테이너에 빈을 등록하였으므로, AnnotationConfigApplicationContext를 구현체로 사용하였다.
그렇다면 AnnotationConfigApplicationContext는 어떻게 Appconfig에서 빈을 등록시켜주는것일까?
BeanDefinition을 빈 설정 메타정보라 한다.
@Bean당 각각 하나의 메타 정보가 생성되는데, 스프링 컨테이너가 이 메타정보를 기반으로 스프링 빈을 생성한다.
BeanDefinition 메타정보를 기반으로 빈을 생성해서 컨테이너에 등록하는데
메타정보에는 BeanClassName,factoryBeanName(appConfig),factoryMethodName(memberService),Scope,layInit(스프링 컨테이너를 생성할때 빈을 생성하는게 아니라, 실제 빈을 사용할때 생성하는것),InitMethodName(빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드명),DestoryMethodName빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드명)
그렇다면 컨테이너는 해당 빈들을 어떤식으로 관리할까?
웹을 생각해보면 다수의 사용자가 요청을 보낼것이다.
클라이언트마다 memberService를 요청하면 계속 새로운 memberService를 new해서 반환해야할까? -> 매번 요청마다 인스턴스 생성후 반환하면 비용이 엄청 클것이다.
AppConfig를 봐보자.
전부다 new를 통해서 인스턴스를 생성하는것을 볼 수 있다.
아 그럼 memberService()를 호출할때마다 return new MemberServiceImpl을 해주나?
아니다.
스프링 컨테이너는 싱글톤 패턴으로 빈들을 관리하다.
싱글톤 패턴이란, 클래스의 인스턴스가 딱 1개만 생성하는것을 보장하는 디자인 패턴이다.
즉 인스턴스를 1개만 생성해서 서로 공유하는 방식을 사용한다.
그렇다면, 자바 코드에서는 이런방식을 어떻게 사용할까?
이런식으로 static영역에 필요한 객체 singletonService인스턴스를 미리 생성해놓고, statitc으로 공유하는 전역변수로 설정, final이므로 재할당을 불가능하게 하면될것이다.
또한 1개만 생성됨을 보장하기위해서 private생성자로 막으면될것이다.->외부에서 new못함
스프링 컨테이너는 앞에서처럼 getInstance메서드만들고, private생성자 막고 이런걸 내가 직접 안해도 된다.
애초에 스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리한다.
그래서 우리는 스프링 컨테이너에 있는 공유하는 1개의 인스턴스를 가져다가 쓰면 된다.
결극 싱글톤 컨테이너는 객체의 인스턴스를 하나만 생성해서 공유하는 방식을 사용하므로 stateful하게 설계하면 안된다.
무상태 설계가 필요하다.
상태를 유지하는 price필드가 있는것을 볼 수 있다.
설정정보를 넣고
현재 StatefulService인스턴스를 satefulService1,2가 공유해서 사용하고 있기 때문에 상태를 유지하는 필드가 있다면, 2가 price를 2000으로 바꾸면 1의 price도 동일한 인스턴스이므로 2000으로 바뀌게 된다.
해결방법 -> 지역변수 이용
Configuration
앞서서 스프링 컨테이너는 생성자 호출과 동시에 의존관계 주입까지 한다고 했다.
어떻게 가능한걸까?
우선 AppConfig도 스프링 빈으로 등록이 된다.
왜냐하면 AnnotationConfigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 등록되기 때문이다.
이거 bean.getClass()하면 AppConfig가 아니라 $$EnhanceBySpringCGLIB어쩌구가 나온다.
바로, 스프링이 AppConfig를 상속받는 임의의 appConfig를 따로 만들고 실제 AppCofig를 등록하는 것이 아닌, 이 프록시를 스프링 빈으로 등록해놓은것이다.
고로 스프링컨테이너에 appConfig는 CGLIB인 프록시가 등록되어있다.
이 내부의 AppConfig CGLIB인스턴스는 그냥 Appconfig와 달리 로직이 추가되어있는데
기존에 컨테이너에 등록되어있는 빈이 있다면 스프링컨테이너에서 해당 빈을 찾아서 반환하는 로직이 담겨있다.
그래서 싱글톤이 보장되고,
AppConfig를 등록할때, memberService()에서 memberRepository를 호출해서 등록하였다면
orderService()를 호출할때는 또다시 memberRepository를 호출하는것이 아닌 컨테이너에서 이미 memberService()호출시에 등록한 memberRepository빈을 찾아서 가져오는것이다.
이것은 AppConfig을 @Configuration으로 등록하여서 사용했기 때문이다.
해당 어노테이션을 붙여야 AppConfig가 프록시로 컨테이너에 등록이 된다.
만약 AppConfig에 @Configuration을 사용하지 않는다면 순수한 AppConfig가 등록이 될것이고, 여기서는 메서드를 전부 호출하기때문에 memberRepository가 두개가 만들어지고 당연히 인스턴스도 다르다.
고로, Configuration이 없어도 Bean으로 등록은 되지만, 싱글톤을 보장하지 않는다. 결국 싱글톤 방식으로 스프링컨테이너를 사용하려면, Configuration,Bean을 둘다 써야한다.
컴포넌트 스캔이란?
우리는 지금까지 AppConfig를 만들고, @Bean을 이용해서 빈을 등록하는 방식을 수행하였다.
그런데, 프로젝트가 커지면 @Bean을 까먹을 수 도 있을것이다.
그래서 자동적으로 스프링 컨테이너에 등록해주는게 없을까? 컴포넌트 스캔이다.
@Configuration은 동일한데, @ComponentScan애노테이션이 추가로 달려있다.(참고로 excludeFilters는 현재 AppConfig에도 @Configuration이 있고, Configuration안에 @Component가 있으므로 스프링컨테이너가 올라오면서 AppConfig도 등록하기 때문에 사용함)
그다음에 등록하고싶은 클래스에 @Component를 붙여주면된다.
@Component
public clas MemoryMemberRepository implemnets MemberRepository{
...
}
@Component
public class MemberServiceImpl implements MemberService{
...
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implments DiscountPolicy{
...
}
@Component
public class MemberServiceImpl implements MemberService{
...
}
이제는 빈 이름 memberServiceImpl 빈 객체 MemberServicecImple@0x01 이런식으로 컨테이너가 만들어진다.
그런데 AppConfig에는 의존관계주입이 명시되어있다.
그러나, AutoAppConfig에는 이러한 정보가 하나도 없다.
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
@Autowired 애노테이션을 통해서 스프링 컨테이너에서 의존관계 주입을 생성자를 통해 받게된다.
그러면 어? memberRepository는 컴포넌트 스캔을 안했는데 어떻게 컨테이너에서 가져오지? 할 수 있다.
왜냐하면 컴포넌트 스캔이후 스프링컨테이너가 아래와 같을 것이기 때문이다.
그러나, 이전에 스프링 컨테이너는 Type으로 조회한다는 것을 배웠다.
부모타입으로 조회하면 자식까지 다튀어나온다.
그래서 memberRepository를 상속받고있는 memoryMemberRepository(자식)까지 다 뒤져서 해당하는 동일 type의 객체 인스턴스를 자동적으로 의존관계 주입을 해준다.
그러면 또 이생각이 들수있다. 만약 memoryMemberRepository와 dbMemberRepository등으로 두개가 있으면 뭘넣지? 라는 궁금증이 생길 수 있다.
즉 여기까지 정리해보면,
직접 빈 애노테이션으로 Configuration,Bean을 이용해 AppConfig에서 사용하기 힘드니,
직접 스프링 컨테이너에 등록하고싶은 class를 @Component 애노테이션으로 등록 후, 필요한 의존관계주입은 @AutoWired를 사용해서 한다는 것을 알았다.
그런데, 생각해보면 AutoAppConfig에서 @Bean이 없으므로 @Configuration은 작성할 필요가 없다.
그럼 @Component가 달려있는 class를 위한 @ComponentScan은 있어야되는거아니냐?
아니다.
그러면 @Configuration과 @Component 둘다 필요없으면 AutoAppConfig는 필요없는거아니냐?
맞다.
@SpringBootApplication에 @ComponentScan이 있다.
그러면 xxxApplication의 패키지가 시작위치가 되고, 해당 패키지 하위를 모두 뒤져서 스프링 컨테이넌에 등록시켜준다.
컴포넌트의 스캔 기본대상은 꼭 @Component뿐만아니라, @Controller,@Service,@Repository,@Configuration등이 있다.
각 애노테이션을 까보면 @Component를 포함하는 것을 확인 할 수 있다.
중복 등록과 충돌
1번의 경우를 보자.
컴포넌트 스캔에 의해서 자동으로 스프링 빈이 생성되는데 이름이 같으면 그냥
ConflictingBeanDefinitionException 예외가 발생한다.
2번의 경우는 좀 다른데
@Component
public class MemoryMemberRepository implements MemberRepository{}
이렇게 이미 컨테이너에 memoryMembeRepository를 등록했는데
@Configuration
@ComponentScan
public class AppConfig{
@Bean(name = "memoryMemberRepository)
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
지금 Appconfig에도 @Bean을 통해서 등록을 하고 있다.
이러면, 수동빈이 우선권을 가진다.
수동빈이 자동빈을 오버라이드 하는것이다.
최근 스프링부트에서는 자동빈 등록과 수동 빈 등록이 충돌이나면, 오류가 발생하도록 바꾸었다.
다양한 주입방식
@Component가 있으면,
빈으로 등록한다.
만약 의존관계 주입이 필요하면, 스프링컨테이너에서 Type에 해당하는 인스턴스를 찾아와서 콱 주입해준다.
생성자주입
생성자를 통해서 의존관계 주입을 한다.
특징: 생성자 호출 시점에 딱 1번 호출되는것이 보장된다.
다른곳에서 OrderServiceImpl을 변경하기 위해서 아무리 생성자 호출을 해도 스프링 컨테이너에 바뀌지 않는다.
불변,필수 의존관계에 사용한다.
@Component를 보고 어? 이거 빈등록해야하네 하고 new를 통해서 인스턴스를 생성해서 등록해야하니까, 생성자를 호출한다.
@Autowired가 있으니까 현재 컨테이너에 등록된 Type의 인스턴스를 찾아서 콱 주입해준다.
그런데 중요한점은 해당 의존관계가 private final로 되어있다.
고로, 생성자 호출시점에 딱 한번 초기화가 되면, 더이상 변하지 않는다.(외부에서 set메서드로 변경하려해도 이미 생성자에서 초기화를 해서 할당했으므로 변경이 불가능하다.)
외부에서 당연히 OrderServiceImpl ofrder = new OrderServiceImpl();이런식으로 order객체를 만들수는 있으나, 이건 그냥 java.class를 만든거고, 스프링컨테이너에 있는 OrderServiceImpl인스턴스랑은 아에 다른거다.
생성자가 딱 1개있으면 @Autowired를 생략해도 된다. 2개 이상이면 당연히 어떤 생성자를 사용해 인스턴스를 등록할지 모르니까 오류가난다.
수정자 주입
setter라는 필드값을 변경하는 수정자 메서드를 통해 의존관계를 주입한다.
선택,변경 가능성이 있는 의존관계에서 사용한다.(final이 없다.)
필드 주입
이름그대로 필드에 바로 주입한다.
사용하지 말자.
일반메서드 주입
생성자주입이 있는데 굳이 비슷한 이걸 쓸 필요가 없다.
또한 public메서드이므로 외부에서 수정도 가능하다. 좋지 않은 방식이다.
옵션처리
가끔 주입해야할 스프링 빈이 없는데도 동작해야 할때가 있다.
즉, 의존관계를 주입할 대상이 없는데도 동작해야할 때를 말하는것이다.
@Autowired(required = false)
public void setNoBean(Member member){
sout(member);
}
}
Member member를 스프링컨테이너에 찾아서 없어도 required false라 오류가 발생하지 않는다.
@Autowired
public void setNoBean(@Nullable Member member){
sout(member);
}
}
@Autowired
public void setNoBean(OOptional<Member> member){
sout(member);
}
}
생성자 주입을 사용해야하는이유
RequiredArgsConstructor사용
@RequiredArgsConstructor을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
조회 빈이 2개이상
ac.getBean(DiscountPolicy.class)인데
만약 RateDiscountPolicy,FixDiscoutnPolicy 두개를 빈으로 등록해놨다면, 오류가 발생한다.
왜냐하면 DiscountPolicy를 Rate와 Fix가 상속받았고, 부모타입으로 조회시 자식까지 전부다 뒤져서 가져오기 때문이다.
@Qualifier
@Component
@Qulifier("mainDiscountPolicy")
public class RateDiscountPolicy implments DiscountPolicy{
...
}
@Component
@Qulifier("fixDiscountPolicy")
public class FixDiscountPolicy implments DiscountPolicy{
...
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qulifier("mainDiscountPolicy") DiscountPoicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Primary
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
Primary가 붙은 빈을 인젝션한다.
@Qulifier의 단점
@Component
@Qulifier("mainDDDDDiscountPolicy")
public class RateDiscountPolicy implments DiscountPolicy{
...
}
잘못 Qulifier 이름으로 등록하면 OrderServiceImpl을 빈으로 등록하기 위해서
mainDiscountPolicy라는 이름의 빈을 찾으려면 없으니까 오류가 발생할것이다.
현재 컨테이너에는 mainDDDDDiscountPolicy가 등록되어있기 때문이다.
이건 컴파일 시점이 아닌, 실행시점에 아는것이다.
이런식으로 애노테이션을 만들고 @Qulifier를 붙여주면
앞으로는 아래와 같이 쓰면된다.
@Component
@MainDiscountPolicy
public class RateDsicountPolcy...
MainDDiscountPolicy 이런식으로 써도 컴파일 타임에 잡을 수 있다.
조회한 빈이 모두 필요할때, List,Map
의도적으로 코드를 짤때 해당 타입의 스프링 빈이 모두 필요한 경우도 있다.
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String,DiscountPolicy> policyMap,List<DiscountPolicy>poliicies){
this.policyMap=policyMap;
this.policies = poliicies;
}
}
스프링이 해당 DiscountPolicy타입의 하위인 RateDiscountPolicy와 FixDiscountPolicy를 찾아서 Map과 List에 주입해준다.
자동,수동의 올바른 기준
결론적으로 스프링이 나오고 시간이 갈수록 자동을 선호하게한다.
사실, 어차피 스프링 빈을 등록할때 @Component만 넣어서 자동 빈으로 등록하더라도, OCP,DIP를 지키면서 사용할 수 있는데
AppConfig를 만들고 @Configuration과 @Bean을 적고, 주입할 대상을 일일히 적는건 굉장히 번거롭고 부담이 된다.
다만 애플리케이션에 광범위하게 영향을 미치는 기술 지원객체는 수동 빈으로 등록해서, 딱 설정 정보에 바로 나타나게 해서 유지보수하는것이 좋다.
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/your_database");
dataSource.setUsername("your_username");
dataSource.setPassword("your_password");
return dataSource;
}
}
빈의 생명주기
데이터 베이스 커넥션 풀처럼, 객체를 생성하는 부분과 초기화하는 부분을 나눠서, 커넥션을 생성해뒀다가, 뒤에 커넥션을 얻으면 초기화를 해야하는 경우를 생각해보자.
스프링 빈의 이벤트 라이프 사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소면전 콜백 -> 스프링 종료
싱글톤빈은 스프링 컨테이너가 관리하므로, 스프링컨테이너가 종료될때 싱글톤 빈들도 함께 종료되므로, 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다.
생명주기가 짧은 빈들도 존재 할 수 있느데 이런 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 먼저 소멸전 콜백이 일어난다.(스코프)
인터페이스-InitializationBean,DisposableBean
afterPropertiesSet,destory메서드를 오버라이드 해야한다.
의존관계 주입까지 끝나면 자동적으로 스프링이 afterPropertiesSet을 호출하여 초기화하고, 종료할때 destory 메서드를 호출한다.
빈 등록 초기화, 소멸 메서드 지정
@Bean(initMethod = "init",destoryMethod ="close") 초기화,소멸 메서드를 지정가능하다.
@PostConstruct,@PreDestory
초기화, 종료 메서드에 해당 애노테이션을 붙이면 된다.
패키지를 보면 javax.annotation.PostConstruct이다.
스프링에 종속적인 기술이 아닌 JSR-250 이라는 자바 표준 기술이다.
고로 스프링 뿐만아니라, 자바 EE기술을 지원하는 다른 컨테이너 환경에서도 동작 가능하다.
예를들어, Java EE 서버인 WildFly나 GlassFish등에서도 적용이 가능하다.
다만, 외부라이브러에는 적용 할 수 없다. 내가 외부라이브러리 코드를 까서 여기다 애노테이션을 적을 수 없기 때문이다.
그래서 코드를 고칠 수 없는 외부라이브러리에서 초기화,종료가 필요하다면 @Bean의 initMethod,destoryMethod를 사용해야한다.
스코프
지금까지 우리는 스프링 빈이 스프링 컨테이너 시작과 함께 생성되어서 스프링 컨테이너가 종료 될때까지 유지하고, 스프링 컨테이너가 이 빈을 관리 한다고 했다.
싱글톤 이외에도, 스프링은 다양한 스코프를 지원한다.
프로토 타입 스코프
싱글톤 스코프의 빈을 조회하면, 스프링컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
그러나 포로토 타입 스코프 빈을 스프링 컨테이너에 조회하면, 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
프로토 타입 빈을 관리할 책임은 클라이언트에게있다, 고로 빈생성과 의존관계주입, 초기화 까지만 처리하고, PreDestory는 해주지 않는다.
클라이언트가 해야한다.
@Bean
@Scope("prototype")
public class OrderServiceImpl{
...
}
OrderServiceImpl order1 = ac.getBean(OrderServiceImpl.class);
OrderServiceImpl order2 = ac.getBean(OrderServiceImpl.class);
싱글톤처럼 같은 빈을 반환하는게 아니라 order1과 order2는 참조값이 다르다.
문제는 프로토타입 스코프와 싱글톤 빈을 함께 사용하는게 문제라는거다.
만약, 클라이언트가 싱글톤 빈한테 요청을 보내고, 해당 빈이 프로토타입빈을 새로 만들어서 반환한다고 쳐보자.
싱글톤 빈(ClientBean)은 프로토타입 빈을 만들어 달라는 요청만 처리하는 빈이라고 생각
Clinet A 가 싱글톤한테 요청을 보냄 -> 싱글톤이 프로토타입빈을 만들어서 반환
Client B 가 싱글톤한테 프로토타입빈 하나 달라고 요청 보냄 -> 싱글톤이 프로토타입 빈을 만들어서 반환
@Bean
@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean){
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Bean
@Scope("prototype")
static class PrototypeBean{
int count = 0;
...
]
근데 이거 PrototypeBean의 스코프를 prototype으로 설정하여도 계속 동일한 프로토타입빈을 반환한다.
ClientBean clientA = ac.getBean(ClientBean.class);
int cnt1= clinetA.logic();//1
ClientBean clientB = ac.getBean(ClientBean.class);
int cnt2= clinetB.logic();//2 1이아님
왜냐하면 당연히 ClientBean을 등록할때 의존관계 주입시 한번 PrototypeBean을 생성해서 주입받고, 우리는 ClientBean에서 getBean을 하므로 PrototypeBean이 ClientBean을 사용할때 새로 생성되지 않는다.
당연히 ClientBean이 아닌 xxxBean에서 PrototypeBean을 사용하면 주입받는 시점에 각각 새로운 프로토 타입 빈이 생성된다.
해결방법
싱글톤 빈과 프로토타입 빈을 함께 사용할때는 Provider 또는 ObjectFacotry를 사용한다.
@Bean
@Scope("singleton")
static class ClientBean{
@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Bean
@Scope("singleton")
static class ClientBean{
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
ClientBean clientA = ac.getBean(ClientBean.class);
int cnt1= clinetA.logic();//1
ClientBean clientB = ac.getBean(ClientBean.class);
int cnt2= clinetB.logic();//1
clientA와 clientB는 동일한 싱글톤 빈이다.
그러나 여기서 logic()메서드를 호출 할 때마다.
Factory,Provider에서 새로운 PrototypeBean을 생성해서 반환한다.
정리하자면, 각 클라이언트마다 ClientBean(싱글톤) 한테 프로토타입빈을 달라고 요청을 보냈지만, 싱글톤 빈에서 프로토타입 빈을 DI받아버리면 해당 프로토타입 빈까지 싱글톤으로 관리되므로,
ClientBean에서 프로토타입빈을 DI하는것이 아닌 Provider를 사용해서 providert.get()으로 계속 생성받는게 맞다.
웹스코프
웹스코프는 웹 환경에서만 동작하고, 스프링이 해당 스코프의 종료시점까지 관리한다.
request: HTTP 요청 하나가 들어오고 나갈때까지 유지되는 스코프, 각각의 HTTP요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
만약 각 클라이언트가 HHTP요청을 보낼때마다. userid,message등 로그를 찍고 싶다고 치자.
Scope가 request이다.
HTTP요청이 들어오면 init으로 초기화 메서드 실행 -> uuid 생성한다.
해당 url로 요청이 들어오면 myLogger의 url을 초기화하고, logic()을 호출한다.
그런데 생각해보면, 이건 실행이 안된다. 왜냐하면, MyLogger스코프는 request이므로, Http Request가 들어와야 MyLogger가 생성된다.
그런데 컨테이너를 띄울때 Controller를 빈으로 등록할때 MyLogger를 DI해줘야하므로 컨테이너에 현재 MyLogger가 없으므로 에러가 생긴다.
즉 이럴때는 MyLogger를 프록시로 등록하고, 실제 필요할때 생성되게 해야한다.
2가지 방법
ObjectProvider
스프링 컨테이너를 띄울때 MyLogger가 의존성 주입이 되는게 아니라,
ObjectProvider.getObject()될때까지 지연생성이 가능하다.
프록시
만약 MyLogger를 DI해야하는 상황이면 프록시 MyLogger를 만들고 주입한다.
그러면 싱글톤 빈으로 프록시 MyLogger가 등록이 된다.
중요한것은, 가짜 프록시 객체에는 실제 MyLogger가 아닌, 요청이 들어오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
고로 요청이 들어오면 어? scope가 request네? MyLogger를 새로 만들어서 로직을 처리한다.
고로 중요한건 Provider를 사용하던, 프록시를 사용하던 진짜 객체 조회를 꼭 필요한 시점까지 지연 처리한다는것이다.