스프링...정리...하자링
스프링 정리
예전에 전공 수업들을 때 (전자회로 였었나...) 교수님이 항상 하셨던 말씀이 있는데
"님들 시간 지나면 머릿속에 나머지는 다 까먹고 0.7v 만 기억 날거에용"
그렇다 @Autowired 빼고는 기억이 나지 않는다. 그래서 스프링을 한번 정리해 볼까 하는데... 시간 날 때마다 인프런 강의 들으면서 조금씩 정리를 해볼까?
예제 코드 작성(순수 java)
회원 서비스 설계
회원서비스를 개발을 할건데 데이터를 어떻게 저장 할지 정해지지는 않았다. 우선은 MemberRepository 라는 인터페이스를 만들어서 join & save 기능을 정의해 놨다.
MemberService의 구현 소스는 아래와 같은데 다음과 같이 코드를 작성했을때, 2가지 문제를 가지고 있다.
- DIP : MemberSerivceImpl 회원 저장소의 구체적인 구현체에도 의존하게 된다.
- OCP : 만약 데이터 저장을 DB에다가 넣도록 변경이 일어난다면, MemberServiceImpl 에도 소스 수정이 필요하다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
주문 서비스 설계
클라이언트가 주문을 하면 주문 서비스는 회원 저장소에서 회원을 조회하고 할인 대상인지를 체크해서 주문결과를 반화시켜 준다. 이떄 할인 정책은 계속 바뀔 수가 있다.
회원 서비스 설계에서와 마찬가지로 회원 저장소의 구체적인 구현체에 의존을 한다는 문제점과, 할인정책의 구체적인 구현체에 의존한다는 문제점을 가지고 있다.
프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다..!!"
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
새로운 할인 정책 적용시 문제점
새로운 할인 정책을 적용한다고 해보자, OrderServiceImpl 에서 기존에 있던 FixDiscountPolicy -> RateDiscountPolicy 로 변경이 필요하다.
새로운 할인 정책을 추가를 했는데... 주문서비스는 할인정책 인터페이스 뿐만 아니라 구체적인 할인정책 구현체에도 의존을 하고 있어서 OrderServiceImpl 의 소스코드까지 수정이 필요하다. OCP & DIP 위반!!
그렇다면 구현체를 지워버리고 인터페이스만 선언해두면 되는거 아닌가? 하지만 안타깝게도 nullPointerException이 발생하게 된다ㅠㅠ 도대체 DIP를 어떻게 적용해야 하는걸까~? 에 대한건 아래에서 계속~~!!
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
//private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
AppConfig 등장 (관심사의 분리)
개발하는 애플리케이션을 하나의 공연이라고 생각해보자. 로미오와 줄리엣이라고 한번 해보자 줄리엣 역할이 있을것이고, 줄리엣 역할의 배우로는 아이유 & 유인나가 할 수도 있고 로미오는 장동건 & 원빈이 할 수도 있다.
인터페이스를 공연의 역할이라고 하고 구현체를 공연의 배우라고 해보자 현재 우리 코드를 보면 줄리엣 역할의 배우인 아이유 (MemberServiceImpl) 가 로미오 역할 (MemberRepository)의 배우로 장동건 (MemoryMemberRepository)을 고른거다. 배우가 직접 상대 배우를 고르는거다..
이게 맞나? 감독이 정해줘야 하는거 아닌가? 배우는 자신의 배역을 수행하는거만 집중을 하면 된다. 공연날에 장동건이 배탈이나서 무대에 서지 못하면 원빈을 투입시키면 된다.
AppConfig 클래스를 만들어서 AppConfig를 통해서 서비스들을 호출할 거다. 코드를 보면 AppConfig를 통해서 MemberService를 호출하면 MemberServiceImpl 객체를 생성해서 반환한다. 근데 이때 객체를 생성할떄 회원 저장소의 구현체인 MemoryMemberRepository 객체를 생성을해서 생성자 매개변수로 넘겨준다.
public class AppConfig {
/**
* 어딘가에서 AppConfig를 통해서 memberService를 호출하면
* 이때 MemberServiceImpl 객체를 생성한다. 근데 MemberServiceImpl 에서 사용되는
* 회원 저장소의 구현체 = MemoryMemberRepository 객체도 생성을 해서
* MemberServiceImpl의 생성자의 매개변수로 넘겨준다.
*
* 이렇게 코드를 작성하면 MemberServiceImpl에 가면 회원저장소의 인터페이스에만 의존하게 된다.
* 이런걸 생성자 주입이라고 한다.
*/
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}
}
다시 MemberServiceImpl 코드를 보면 생성자를 통해서 회원저장소의 구현체를 주입시켜 줬다. 이로인해서 MemberServiceImpl 은 회원저장소 인터페이스인 MemberRepository 에만 의존하게 된다.
이제 MemberServiceImpl 은 회원저장소로 메모리를 쓰던 DB를 쓰던 관심이 없다. 그냥 인터페이스의 기능만 가져다가 쓰면 된다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
AppConfig 를 사용해서 한번 회원 서비스를 이용해보면 아래코드와 같이 이용할 수 있다. 기존에는 memberService 객체를 직접 생성해서 사용했다면. 현재는 AppConfig 객체를 생성해서 memberservice를 호출하면 된다.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
//MemberService memberService = new MemberServiceImpl();
Member memberA = new Member(1L, "memberA", Grade.VIP);
memberService.join(memberA);
Member findMember = memberService.findMember(1L);
System.out.println("findMember = " + findMember);
System.out.println("new memberA = " + memberA);
}
}
AppConfig 리팩토링
AppConfig에서 역할이 좀더 잘보이게 리팩토링을 해보자 DiscountPolicy 와 memberRepository 를 분리함으로써 서비스에 필요한 역할들이 한눈에 들어온다. 추가적으로 할인정책이 변경되거나 데이터 저장방식이 바뀌면 해당 역할에서 return 하는 부분만 수정하면 된다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), DiscountPolicy());
}
private DiscountPolicy DiscountPolicy() {
return new FixDiscountPolicy();
}
private MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
제어의 역전 : IOC
기존 프로그램에서는 MemberServiceImpl 이 MemoryMemberRepositoryImpl 를 직접 생성하고 사용했는데, AppConfig 가 등장한 이후에는 어떤 구현체가 올지 모른다. 단순히 MemberRepository 인터페이스가 있다는거만 알고 있다. AppConfig 가 프로그램의 제어 흐름을 가지고 있다.
스프링으로 전환
기존의 AppConfig 클래스에 @Configuration 과 각각의 메서드에 @Bean을 작성해주자. 이렇게 되면 프로그램이 실행될때 @Bean이 붙은 메서드에서 return 하는 new 객체 (구현체) 들을 스프링 Container 에 등록을 해주게 된다. 이때 메서드의 이름을 가지고 Container에 등록이 된다. (Map 같이 키벨류로 들어가나?)
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), DiscountPolicy());
}
@Bean
public DiscountPolicy DiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
기존에는 appConfig.memberService() 와 같이 AppConfig 에 직접 접근해서 구현체를 뽑아왔는데, 이제는 스프링 컨테이너인 ApplicationContext 에 접근해서 Bean 들을 뽑아온다. 이제부터 빈으로 등록된 구현체들은 스프링 컨테이너가 관리하게 된다.
아래 로그를 보면 막~ 뭐라고 적혀있는데 빈팩토리 어쩌고~ 들이 있고 우리의 구현체들이 싱글톤으로 생성되었다는걸 알려주는것 같다..!
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
웹 애플리케이션과 싱글톤
자바 소스로 구현한 싱글톤
웹 애플리케이션은 고객의 요청이 많이 발생하겠지? 이때마다 객체를 생성한다? -> 아무리 jvm이 좋아졌다고 해도 굳이 일부러 그렇게 할 필요가 없다. 싱글톤 패턴을 통해서 하나의 객체만을 생성하고 서비스 요청이 올때마다 같은 객체를 공유해서 사용한다.
객체를 미리 생성하고 해당 객체를 getInstance() 로 가져온다. 근데 이렇게 싱글톤으로 생성을 하게되면 단점이 존재하는데 "개발자는 구현체에 의존하면 안된다" 를 어기게 된다. -> DIP위반, OCP 위반 가능성
또한 private 생성자로 인해서 상속이 불가능하다는 점이 있다.
하지만 싱글톤 컨테이너를 사용하면 스프링이 DIP, OCP, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있게 도와준다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();//static 영역에 대해서 한번 공부해보자
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {
}
}
static & this 로 인한 현타
객체가 호출할때마다 호출된 숫자를 구하고 싶었음 근데 처음에 private int cnt = 0; 을 하고 getInstance() 에서 값을 증가시켜주려고 하니까 non-static 멤버변수를 사용할 수 없다고 했다. 그래서 static 변수로 설정을 해주니까 cnt++ 은 된다.
근데 this.cnt++ 은 안되네? 왜이러지? 우선은 스태틱영역에서는 this 를 호출할 수 없다.. 왜일까..? this는 인스턴스(객체)를 가리키는건데 객체는 힙 영역에 선언이 되고 static 변수는 스태틱 영역에 선언되어 있으니까?
실제로 cnt를 객체.getCnt() 를 호출하면 안되고 클래스.getCnt() 를 호출하면 되는걸 보면.. 일리가 있나?
public class SingletonService {
private static final SingletonService instance = new SingletonService();//static 영역에 대해서 한번 공부해보자
//private int cnt = 0;
private static int cnt = 0;
public static SingletonService getInstance() {
this.cnt++; //여기서 에러 발생
return instance;
}
private SingletonService() {
this.cnt++;
}
public void logic(){
this.cnt++;
}
싱글톤 방식 주의점
싱글톤 패턴이 적용되었을 때 주의할 점이 있는데, 여러 클라이언트가 같은 인스턴스를 공유하니까 이로인해서 같은 멤버변수를 사용할 때 문제가 발생할 가능성이 있다.
클라이언트 요청이 동시에 들어온 상황이라고 가정했을때 각각의 클라이언트 들이 공유하는 price라는 멤버변수 때문에 A사용자가 주문한 가격이 B사용자가 주문한 가격으로 바껴있다. 이런 문제때문에 싱글톤 패턴이 적용되어 있을때는 가급적 읽기만 가능하도록 해야하고, 멤버변수를 사용함에 있어서 조심해야 한다.
본인말고 다른 사람의 정보가 노출될수 있다던가... 다른사람이 주문한 가격으로 결제가 된다거나...
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//쓰레드A : A사용자가 10000 원 주문
statefulService1.order("userA", 10000);
//쓰레드B: B사용자가 20000 원 주문
statefulService2.order("userB", 20000);
//쓰레드A: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
}
@Configuration & 바이트코드 조작
근데 AppConfig 클랙스를 보면 orderService() , memberService() 에서 모두 new 로 객체를 생성을 하는데 어떻게 싱글톤이 지켜지는걸까~? 는 바로 @Configuration 에 정답이 있었다. @Configuration을 사용하면 CGLIB (Code Generator Library) 라이브러리가 조작을 통해서 객체를 관리한다. 각각의 빈들을 꺼낸다음에 memberRepository를 찍어보면 다음과 같이 같은 이름을 가진 객체가 나오는걸 확인할 수 있다.
@Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
MemoryMemberRepository memberRepository3 = ac.getBean("memberRepository", MemoryMemberRepository.class);
System.out.println("memberService -> memberRepository1 = " + memberRepository1);
System.out.println("memberService -> memberRepository2 = " + memberRepository2);
System.out.println("MemoryMemberRepository -> memberRepository3 = " + memberRepository3);
}
한번 AppConfig를 찍어보자 로그에 보면 AppConfig 뒤에 이상한 값들이 붙어있는데 이게 CGLIB 에 의해서 생성되어진 거다. @Configuration 을 빼고 돌려보면 순수한 AppConfig 가 찍히는걸 볼 수 있다. 하지만 이렇게 하면 싱글톤은 깨지게 되고 memberRepository는 여러번 객체를 생성하게 된다..!
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
컴포넌트 스캔
위에서 AppConfig 클래스에다가 @Bean 들을 정의해서 컨테이너에 빈들을 생성을 했다. 근데 이렇게 하면 누락을 할 수도 있고 작업량이 증가하게 된다. 이런 문제 때문에 좀더 간편하게 자동으로 의존관계들을 주입하는 방법을 개발자님들이 구현을 해놨다.
@ComponentScan 을 붙혀준 AutoAppConfig.class 를 만들어주자. 여기에는 아~~무것도 작성된게 없는 클래스이다. 그리고 나서 각각의 구현체에 변경이 필요하다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
//@Configuration 소스를 열어보면 Component 가 있기때문에 스캔 할 때 자동으로 빈 등록이 된다
//지금 AppConfig 가 하나 더있으니까 그거는 제외시키려고!! & 테스트 코드에서 configuration 을 쓴게 더 있구나!!
)
public class AutoAppConfig {
}
우리가 구현한 구현체 (기존의 AppConfig에서 의존관계를 주입해 주었던) 소스로 이동한 다음에 @Component 를 붙혀준다. 이렇게 하면 @ComponentScan 을 할 때 @Component 들을 찾아서 스프링 컨테이너에 등록을 시켜준다.
근데 여기서 한가지... 그럼 의존관계를 어떻게 주입하지..? 기존 AppConfig에서는 메서드를 통해서 각각의 빈들의 의존관계를 설정해줬는데..!?
정답은 @Autowired 이다. 해당 어노테이션을 생성자 위에 작성을 해주면 스프링 컨테이너에 있는 빈들을 조회해서 의존관계들을 적용해준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
수동으로 @bean 을 사용해서 등록하는거와 동시에 @Autowired가 충돌이 일어날 수도 있는데,이때는 스프링부트가 알아서 오류를 발생시켜준다. → 가장좋은 에러 = 런타임 에러
실제로 운영을 하다보면 이렇게 꼬이는 문제들 때문에 시간을 많이 소모한다고 한다. 그래서 스프링이 디폴트로 에러를 띄우는듯
의존관계 자동 주입
생성자 주입
생성자가 딱 한개만 있으면 @Autowired 생략 가능 -> 요걸 요즘에 많이 사용한다고 한다. 얘는 라이프사이클이 맨처음 객체가 생성될떄 의존관계 주입이 일어난다. 생성자니까..!! 그래서 얘가 먼저 일어나고 이후에 set등이 일어난다
수정자 주입 (setter)
- set메서드를 통해서 의존관계 주입
- required = false를 입력하면 주입할 대상이 없어도 된다는 의미! 중간에 배역을 바꾸고 싶어 이럴때 외부에서 set호출
-> @Autowired 는 기본적으로 주입할 대상이 없으면 어류가 발생한다. - 많이 쓰이지는 않는듯
@Autowired(required = false)
public void setDiscountPolicy(DiscountPolicy discountPOlicy){
this.discountPolicy = discountPolicy
}
필드 주입
- 필드주입은 권장되지는 않고 많이 사용하지 않는다.
- 외부에서 변경이 불가능하기 때문에 테스트할때 변경이 불가능하다. 예를들어 테스트할떄 더미데이터를 넘기려고 메모리로 바꾸고 싶은데 이런걸 하기가 슆지가 않다. 이러면 결국 setter 를 만들어야 하는데 이럴바에는 setter에다가 @Autowired를 쓰는게 좋다
@Autowired private MemberRepository memberRepository
lombok 라이브러리 사용
lombok 라이브러리를 사용하면 @RequiredArgsConstructor 를 사용할 수 있는데, 이걸 사용하면 생성자를 자동으로 생성해주고 의존관계를 주입해준다. 요렇게 만 하면 생성자 + @Autowired 를 해준다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
의존관계 주입시 여러개의 빈이 존재 할 때
@Autowired 필드 명
스프링은 기본적으로 컨테이너에 빈을 등록할 때 클래스 명을 가지고 등록하는데 여러개가 등록 될 때가 있을것이다. 이럴 때는 변수 명을 구체적으로 적어줘서 해당 빈을 의존관계를 주입시켜줄 수도 있다.
@Autowired 가 적용되어 있다고 할 때
private final DiscountPolicy rateDiscountPolicy;
-
@Qualifier
@Component 가 달려 있는 각각의 구현체에 @Qulifier("특정 이름") 어노테이션을 통해서 별명같은거를 붙혀줄 수 있다. 얘는 모든곳에 @Qulifier 를 붙혀줘야 되어 조금 귀찮은게 있는듯
-
@Primary
이건 우선순위를 지정하는 방법이다. 단순히 구현체에다가 @Primary를 붙히면 얘가 우선순위를 가지게 된다.
@Qulifier & @Primary 중에는 @Qulifier 가 더 우선순위가 높다. 세세한 것이 우선순위가 더 높다
조회한 빈이 모두 필요할 때
할인 정책을 고객이 선택 하고 싶을 때도 있을것이다. 그럼 이때 어떻게 하느냐~ 한번 테스트 코드를 작성해 보자
우선 아래와 같이 코드를 작성한 후에 제대로 빈들이 등록이 되었는지 확인해보면 다음과 같이 테스트를 하기 위한 DiscountService 객체가 잘 등록된걸 확인할 수 있다.
Map 에 있는 key 값들을 한번 확인해 보니 rateDiscountPolicy 만 나왔는데, 기존 fixDiscountpolicy 구현체 부분에 @Component 를 안해줬었다. 그래서 어노테이션을 추가해줬고 대신에 rateDiscountPolicy 부분에 @Primary를 붙혀줘서 중복빈 등록 문제를 해결했다.
public class AllBeanTest {
@Test
void findAllBean(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
}
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
//모든걸 찾아서 map 이나 list에 넣어서 반환을 해주는듯
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
}
}
참고 : new AnnotationConfigApplicationContext(xxx.class) 를 하게 되면 해당 클래스를 컴포넌트 스캔의 대상이 되는듯
우선 여기까지 구현을 하고 위에 만들어둔 DiscountService 클래스 (얘가 컨테이너에 새로운 서비스 객체로 등록이 되는거다) 에다가 다음 메서드를 추가해보자
//key값을 넘겨받아서
public int discount(Member member , int price , String dcPolicy){
DiscountPolicy discountPolicy = policyMap.get(dcPolicy);
return discountPolicy.discount(member, price);
}
그리고 아래 테스트 코드를 실행해보면 매개변수로 넘겨준 할인 정책 문자열에 따라서 할인정책이 다르게 적용되는걸 확인할 수 있다.
@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);
int fixDiscountPolicy = discountService.discount(member, 20000, "fixDiscountPolicy");
System.out.println("fixDiscountPolicy = " + fixDiscountPolicy);
int rateDiscountPolicy = discountService.discount(member, 20000, "rateDiscountPolicy");
System.out.println("rateDiscountPolicy = " + rateDiscountPolicy);
}
빈 생명주기 콜백
데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작시점에 필요한 연결을 미리 해두고 애플리케이션이 종료될 때 모든 연결을 종료하는 진행을 할때 다음 두개의 어노테이션을 사용한다.
@PostConstruct @PreDestroy 를 사용하면 초기화 및 소멸전 콜백을 통해서 init() 또는 close() 메서드들을 실행시켜줄 수 있다.
위에 두가지 방법을 사용하되 외부라이브러리에는 적용이 안된다고 한다. 이럴때는 @Bean 등록을 할 때 @Bean(initMethod = "init" , destroyMethod = "close") 를 사용해서 적용하면 된다.
스프링 빈의 이벤트 라이프 사이클
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료
빈 스코프
프로토타입 스코프
얘 같은 경우는 클라이언트들이 요청을 할 때마다 스프링 컨테이너에서 빈을 새로 생성하고 의존관계를 주입 초기화만 한 후에 클라이언트에게 던지고 관리를 하지 않는다. 종료메서드를 클라이언트가 해줘야 한다. 예를 들면 @PreDestroy 를 스프링 컨테이너가 관리를 하지 않기 때문에 클라이언트가 직접 종료해줘야 한다.
싱글톤 & 프로토타입 이 같이 사용되는 경우
아래 코드에서 싱글톤으로 생성된 ClientBean 이 있다. 여기에는 prototypeBean 이 의존관계 주입이 돼있는데 ClientBean 에서 logic() 을 실행하면 prototypeBean 의 addCount() 를 실행하는거다. 근데 ClientBean 을 여러 클라이언트가 사용을 하면 이때는 어떻게 될까? 카운트가 계속 1을 가질까? 아니다 ClientBean이 생성 될때 이미 prototypeBean 이 생성되어서 의존관계가 주입이 되었기 때문에 addCount를 하면 계속 값이 증가할 것이다.
@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean;//clientbean 이 생성될 때 얘도 이미 생성이 되고 주입이 된거다.
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
}
그럼 prototypeBean 이 종속되지 않게 하려면 어떻게 해야할까? 매번 새로 만들고 싶다. AnnotationConfigApplicationContext 를 객체를 의존관계를 아예 넣어버리고 logic() 을 호출할 때마다 매번 getBean()을 하는것도 방법이다.
@Scope("singleton")
static class ClientBean{
//private final PrototypeBean prototypeBean;//clientbean 이 생성될 때 얘도 이미 생성이 되고 주입이 된거다.
private final AnnotationConfigApplicationContext ac;
@Autowired
public ClientBean(PrototypeBean prototypeBean, AnnotationConfigApplicationContext ac) {
this.ac = ac;
//this.prototypeBean = prototypeBean;
}
public int logic(){
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
int count = prototypeBean.getCount();
return count;
}
}
ObjectProvider & Provider
스프링의 OjbectProvider & javax의 Provider 를 사용해서 getBean 으로 직접 가져오지 않고 dependency lookup 으로 빈들을 가져올 수 있다. Provider 를 사용하려면 gradle 에 라이브러리를 추가해줘야 한다.
웹 스코프 (request)
@Scope(value = "request") 를 써주면 해당 객체의 스코프는 request 요청이 들어올 때 생성되고 이후 고객의 요청이 끝나면 종료된다.
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
근데 이렇게 하면 컨테이너를 생성할때 문제가 생기는데 이를 해결하기 위해서 ObjectProvider 를 사용할 수 있다.
MyLogger 의 스코프는 request 요청이 들어올 때 생성이 되어지는데 의존관계를 주입하려고 하니까 에러가 발생한다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "ok";
}
}
근데 아래처럼 프로시모드를 @Scope 에 추가해주면 스프링이 가짜 객체를 의존관계로 주입을 해준다. 이후에 실제로 request 가 들어올 때 스프링이 내부에서 진짜 빈을 찾아 요청하는 로직이 들어가 있다고 한다. 단지 애노테이션 설정만 변경했을 뿐인데 코드를 수정하거나 할 필요가 없다(ObjectProvider 를 안써도 된다.) 하지만 이러한 특별한 스코프는 꼭 필요한 곳에서만 최소화 해서 사용해야 한다고 한다. 유지보수 하기 어려워 질 수 있다고 한다. 다른 개발자가 보고 "아 그냥 싱글톤인가 보네~" 하고 사용할 가능성이 있는거 같음
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestURL;
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
Author And Source
이 문제에 관하여(스프링...정리...하자링), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@baek_su/스프링-정리좀-해라-제발저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)