웹 어플리케이션과 싱글톤이 필요한 이유
보통 여러 클라이언트가 동시에 요청을 하게된다.
DI 컨테이너 (의존성 주입 컨테이너) ==> 우리가 만든 AppConfig
만약에 스프링이 없다면 위의 그림과 같이 3명이 동시에 memberService를 요청했을때
멤버서비스가 3개가 생성될것이다.
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
//1. 조회: 호출할 때 마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회: 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
//참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 != memberService2
assertThat(memberService1).isNotSameAs(memberService2);
}
}
예전에 스프링컨테이너(ApplicationContext)없이 직접 AppConfig 객체를 만들고 AppConfig의 메소드를 이용해서 원하는 객체를 생성했던것처럼 호출할때마다 새로운 멤버서비스를 생성하게 될것이다.
싱글톤 패턴
클래스의 인스턴스가 딱 1개 생성되는것을 보장하는 디자인 패턴이다.
어떻게? -> 2개 이상 생성하지 못하게 막으면 된다.
싱글톤 패턴을 적용한 예제
package com.example.demo.singleton;
public class SingletonService {
private static final SingletonService instance = new SingletonService(); // 자기자신을 내부에 private static으로 가지고있는다.
// static으로 선언했으므로 클래스레벨에 올라가 단 하나만 존재하게 된다.
//객체를 하나만 가지기 위해서 자기자신안에다가 자기자신 객체를 만들었다.
public static SingletonService getInstance() { //만든 단 하나뿐인 객체를 공유해서 쓰려면 가져오는 메소드가 필요하므로 getInstance()를 만든다.
return instance; //만든걸 리턴해준다.
}
//내가 생성한 객체만 가져다 쓰게끔하고 싶은데 , 외부에서 클래스의 생성자를 통해서 객체를 새로 만드려고하는것을 방지하기위해 private를 붙인 생성자를 정의한다.
private SingletonService() {
}
public void logic(){ // 메소드 하나 넣어줬음
System.out.println("싱글톤 객체 로직 호출");
}
}
싱글톤 패턴을 사용하는 테스트 코드
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
//private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
//new SingletonService();
//1. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
//2. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService2 = SingletonService.getInstance();
//참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// singletonService1 == singletonService2
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
[same은 자바에서 ==과 같이 같은 인스턴스인지 체크하는것이고
isEqualTo는 자바에서 equals와 같이 같은 내용인지 체크하는것이다.]
싱글톤 패턴으로 SingletoenSeivice 클래스를 만들었기 때문에 생성자를 사용하려고하면 컴파일오류 발생
객체를 받아와서 사용하는것이므로 같은 객체를 받게 된다.
배운내용을 가지고 AppConfig.java 가서 싱글톤패턴 적용하면 싱글톤이 잘 적용된다.
하지만 그렇게 할필요가없다.
스프링이 있으면 스프링컨테이너에서 객체를 싱글톤으로 관리해준다.
싱글톤 패턴 문제점
자기자신안에 자기자신 객체를 생성하는 코드가 반드시 들어가야하고,
그 객체를 반환해주는 메소드도 필요하고,
생성자를 막기도 해야한다.
그래서 코드 자체가 많이 들어감.
클라이언트가 구체클래스.getInstance()메소드를 사용해야해서 DIP를 위반한다, OCP 위반할 가능성이 높다.
싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서,
객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
스프링 컨테이너를 사용하는 테스트 코드
public class SingletonTest {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("스프링컨테이너와 싱글톤")
void springContainer(){
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//참조값이 같은지 체크
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//검증
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}
싱글톤 방식의 주의점
stateful(상태를 유지) -> 이전 상태를 기억하고 있다.
stateless(무상태) -> http와 같이 이전의 상태를 기록하지 않는 접속
상태를 유지할 경우 발생하는 문제점 예시
StatefulService.java
public class StatefulService { //주문이라는 메소드를 이용해서 가격을 저장하려고 만든 클래스
private int price;// 상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + "price =" + price);
this.price = price; // 여기가 문제!
}
public int getPrice() {
return price;
}
}
라는 서비스클래스가 있고
StatefulServiceTest.java
위의 서비스 클래스를 빈으로 등록하고 테스트해보자.
빈으로 등록하는 순간부터 스프링컨테이너가 싱글톤으로 객체를 생성해서 관리해준다.
class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(
TestConfig.class); //아래에서 임시로 만든 구성파일을 이용해서 스프링컨테이너를 생성해준다.
//스프링컨테이너는 싱글톤을 적용했기에 , 2번뽑아도 같은 객체가 담길것이다.
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA : A사용자 10000원 주문 서비스1를 이용해서
statefulService1.order("userA", 10000);
//ThreadB : B사용자 20000원 주문 서비스2를 이용해서 ( 싱글톤이라 어차피 같은 객체이다)
statefulService2.order("userB", 20000);
//ThreadA : 사용자A 주문금액 조회 -> 예상값은 A사용자가 주문한 만원이 나와야하는데 싱글톤이 적용되어있기때문에 2만원으로 수정된 가격이 반환될것이다.
int price = statefulService1.getPrice(); // 싱글톤이라 같은 객체를 반환받아서 하나뿐인 price변수를 변경하였으니.
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000); //틀린금액으로 반환되는지 검증
}
static class TestConfig{ //빈 메타정보는 StatefulService 메타정보가 생성된다. , statefulService가 빈이름, StatefulService가 빈 타입
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
StatefulService는 Order 클래스 같은 기능을 하는데 , private int price를 이용하여 메소드를 이용하여 값을 설정하고 값을 반환하게끔 설계하였다. 하지만 싱글톤패턴으로 사용되면서 같은 객체를 공유하니까 price 변수도 같이 공유가 된다.
사용자A가 만원을 주문했던값도 price에 적용되고 사용자B가 2만원을 주문했던것도 price에 적용되면서 덮어씌어진다.
그래서 위에서 클라이언트에서 값을 변경할 수 있는 필드가 존재하면 안된다고 한것이다. ( 클래스변수 == 필드)
무상태로 수정해보기
public class StatefulService { //주문이라는 메소드를 이용해서 가격을 저장하려고 만든 클래스
public int order(String name, int price) {
System.out.println("name = " + name + "price =" + price);
return price
}
}
문제를 만들던 필드를 삭제하고, order메소드를 사용하면 입력된 price를 바로 리턴해주었다.
결론 : 스프링빈은 항상 무상태로 설계해야한다.
@Configuration과 싱글톤
구성환경설정파일 AppConfig를 살펴보자.
@Configuration
public class AppConfig { //프로젝트의 객체 생성,구성,주입 환경설정을 하는 중요 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
@Bean
public MemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), DiscountPolicy());
}
@Bean
public DiscountPolicy DiscountPolicy() {
return new RateDiscountPolicy();
}
}
@Bean으로 인해 첫번째 메소드인 memberService()가 실행된다.
그러면서
new MemberServiceImpl(MemberRepository())
가 실행될텐데 그러면서 그안에 있는
MemberRepository()
가 실행될것이다.
@Bean
public MemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
가 실행되면서 메모리멤버리포지토리가 만들어지면서 빈으로 등록될것이다.
그러고 밑에보면
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), DiscountPolicy());
}
오더서비스를 빈으로 등록하는 부분이있다.
여기서도 멤버리포지토리() 메소드를 사용하는데 , 여기서도 실행된다면
@Bean
public MemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
이 부분이 두번실행되면서 멤버리포지토리 빈이 2개 등록되는것이 아닐까?
같은게 2개 등록된다면 싱글톤이 깨지는것이 아닌가? 라고 생각할 수 있다.
테스트해보자.
다른 객체 2개가 생성됬는지 확인해보면된다.
그렇다면 멤버서비스안에 멤버리포지토리와 오더서비스안에 멤버리포지토리가 같은 객체인지 다른객체인지 체크해본다.
멤버서비스임플, 오더서비스임플 ( 각각 구현체임) 에다가 각 클래스가 필드로 가지는 멤버리포지토리를 반환하는 메소드를 만들었다.
public MemberRepository getMemberRepository() {
return memberRepository;
}
이런 메소드이다
get을 이용하면 쉽게 만들 수 있다.
클래스 내부에서 자신의 필드를 get하는 메소드를 만들고싶을때
get이라고 치면 알아서 어떤 필드를 리턴하는 메소드를 만들어줄지 선택할 수 있다.
같은 메모리포지토리인지 테스트 코드
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(
AppConfig.class);
//구현체클래스로 꺼내야한다. 구현체 클래스안에다가 테스트용 메소드를 만들어놨으니까, 원래는 구체타입으로 꺼내는것은 좋지않다.
//
MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean(OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean(MemberRepository.class);
/*
MemberService memberService = ac.getBean(MemberServiceImpl.class); 이렇게 해준다면 멤버서비스Impl에 테스트용으로 만든
getMemberRepository를 사용할수 있을거라 생각했지만 MemberService 인터페이스 변수에 저장하기때문에 구현체가 저장은 되지만
구현체 내부에 만든 메소드는 사용할 수 없었다
MemberServiceImpl memberService = ac.getBean(MemberService.class); 이것또한 컴파일 오류를 발생하였다.
멤버서비스를 상속한 멤버서비스임플이 나오니까 저장될줄 알았는데 되지않았다.
부모는 자식을 품을수있지만, 자식은 부모를 품을수없다. 부모는 자식을 저장할수있지만 , 자식은 부모를 저장할 수 없는 상속개념인것같다.
멤버서비스 인터페이스 변수가 다른 구현체를 저장할 순 있지만 , 멤버서비스의 구현체변수가 다른 멤버서비스구현체를 저장할 수도있는 코드이기때문이다.
MemberService memberService = ac.getBean(MemberService.class);로 바꾸고 저장된 멤버서비스를 출력해보았더니 멤버시버스임플이였다.
*/
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberRepository1 = " + memberRepository1);
System.out.println("memberRepository2 = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
Assertions.assertThat(memberRepository1)
.isSameAs(memberRepository2);
}
}
MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean(OrderServiceImpl.class);
MemberService 타입이 아닌 , MemberServiceImpl 타입을 써준이유?
MemberService를 써주면 memberService 변수로 접근할수있는것은 MemberService 인터페이스에 선언된 것들(재정의한 메소드)에만 접근할수있고, 추가로 MemberServiceImpl 클래스에 선언한것에는 접근할수없다.
그렇기때문에 MemberServiceImpl 타입을 써줘야 memberService 변수로 MemberService 인터페이스에 선언된 것들 + 추가로 MemberServiceImpl 클래스에 선언한것에 접근할수있다.
인터페이스변수가 구현체 어떤것이든 저장하고 , 그 구현체에서 추가로 구현한 메소드까지 사용할 수 있다고 판단하면 안될거같다. ( 상속 공부가 필요)
예전에 테스트할때
AnnotationConfigApplicationContext
는 ac.getBeanDefinition() 메소드를 가지고있었다 그 이유는 AnnotationConfigApplicationContext 내부에서 ApplicationContext 뿐만아니라 다른 인터페이스를 상속받아 구현하고 있었기 때문이다.
그래서 ApplicationContext 인터페이스 변수로 AnnotationConfigApplicationContext를 담더라도,
ApplicationContext 내부에 구현하도록 명시된 메소드만 사용할 수 있었다.
그래서 직접 AnnotationConfigApplicationContext 객체에 담아 주었다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
AppConfig.class);
이렇게
멤버 서비스 구현체도, 오더서비스 구현체도 스프링빈에 등록된 같은 객체를 주입받았다.
생성자가 각각 실행되서 주입됬어도, 다른 객체가 주입된것이 아니라.
같은 객체를 받았음을 확인할 수 있다(싱글톤 적용되어있다)
호출 로그 찍어보기
@Configuration
public class AppConfig { //프로젝트의 객체 생성,구성,주입 환경설정을 하는 중요 역할
@Bean
public MemberService memberService() {
System.out.println("AppConfig.memberService");
return new MemberServiceImpl(MemberRepository());
}
@Bean
public MemberRepository MemberRepository() {
System.out.println("AppConfig.MemberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("AppConfig.orderService");
return new OrderServiceImpl(MemberRepository(), DiscountPolicy());
}
@Bean
public DiscountPolicy DiscountPolicy() {
return new RateDiscountPolicy();
}
}
AppConfig에 호출될때마다 출력을 찍어서 멤버리포지토리 생성이 몇번 되는지 체크해보자.
자바 코드상 보면 멤버서비스 만들때, 오더서비스 만들때, 멤버리포지토리를 만들때 총 3번 메모리멤버리포지토리 생성이 호출되어야할것 같다.
위에서 사용한 테스트코드를 다시돌려보고 로그찍힌걸 확인해보았다.
각 한번씩만 출력된것을 확인할 수 있다. ( 그냥 위에서부터 순서대로 호출되었다.)
어떻게 된일일까? -> 스프링은 어떻게든 싱글톤을 보장해준다.
어떻게 보장해주냐 -> 스프링컨테이너를 생성할때 전달하는 구성설정파일을 상속한 새로운 클래스를 스프링빈으로 등록하는데 그 새로운 클래스안에 CGLIB이라는 바이트코드 조작 라이브러리를 이용해서 싱글톤을 보장해준다.
(이미 생성된 스프링빈이면 그걸 반환하고, 없으면 생성하게끔 해서 여러객체가 생성되는것을 막는다.)
@Configuration과 바이트코드 조작의 마법
스프링컨테이너에 빈으로 등록된 AppConfig 출력 테스트
구성설정파일인 AppConfig도 기본적으로 스프링빈으로 등록된다.
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(
AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class); //구성설정파일도 스프링빈으로 등록되니 꺼내볼수있다.
System.out.println("bean = " + bean.getClass());
}
.getClass()를 이용해서 클래스정보도 받아온다.
스프링컨테이너에 내가 전달한 구성설정파일인 AppConfig를 상속받은 새로운 클래스를 만들고
그 클래스안에서 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 싱글톤을 보장되게끔 한다.
(이미 생성된 스프링빈이면 그걸 반환하고, 없으면 생성하게끔 해서 여러객체가 생성되는것을 막는다.)
반복 실행되야했던 멤버리포지토리 메소드에 저런 로직을 사용해서 여러개의 메모리멤버리포지토리가 생성되는것을 막는다.
@Configuration을 붙여야 CGLIB 기술을 이용해서 스프링컨테이너가 싱글톤패턴을 보장해준다!
@Configuration이 없어도 @Bean이 있으니까 스프링컨테이너에 빈으로 등록이 된다.
@Configuration 없이 테스트
@Configuration 어노테이션을 주석처리하고 다시 테스트를 돌려보자.
스프링컨테이너가 생성되고, AppConfig를 통해 스프링빈이 등록되면서 출력이 된 모습이다.
멤버리포지토리가 3번 호출되는것을 확인할 수 있다 -> 싱글톤아님
그리고 AppConfig의 클래스정보가 일반적인 클래스정보로 나오는것을 확인할 수 있다. (내가 생성한대로)
그리고 @Configuration 없이
멤버서비스 구현체, 오더서비스구현체 , 그리고 멤버리포지토리 자체를 출력해보는 코드를 다시해보면
여기서 마지막꺼인 멤버리포지토리는 스프링빈에서 꺼내온 멤버리포지토리이다.
즉 AppConfig에서
@Bean
public MemberRepository MemberRepository() {
System.out.println("AppConfig.MemberRepository");
return new MemoryMemberRepository();
}
다음 빈 등록 코드로 인해 생성되서 등록된것이다.
그런데
멤버서비스 구현체의 필드속 메모리멤버리포지토리와
오더서비스 구현체의 필드속 메모리멤버리포지토리의 객체가
스프링빈에 등록된것과 다르다.
즉, 구현체내부에서 메모리멤버리포지토리 구현체를 생성해서 주입한것과 같이 컨테이너에서 관리하지않는 객체라는 것이다.
결론 @Configuration을 같이 사용해야 싱글톤이 보장된다. -> 하지만 @Autowired로 해결할 수 있기는하다.
나중에 배울 내용이다.
그냥 스프링 설정 정보는 항상 @Configuration 어노테이션을 사용해야한다.
댓글