싱글톤!
웹 어플리케이션은 대게 한번에 여러 요청이 들어오게 된다. 이때 클라이언트와 관계 없는 획일화 된 서비스를 제공 한다고 하자. 이럴 때 마다 서비스 객체를 계속 새로 만들어서 제공하는 것은 정말 멍청한 짓이지 않을 까? 그래서 이러한 서비스들에 대해 딱 하나의 객체만 만들어서 모든 클라이언트에게 제공하도록 하는 개념이 싱글톤 패턴이다. 오늘은 이것에 대해 알아보자 !@!
1. 순수 자바 코드로 싱글톤을 만들어 보기
SingletonService라는 객체가 있다고 하면 단순히 생성자 함수를 private으로 선언하면 된다. 그 외에도 몇 가지 추가 설정이 필요한데 코드로 보여줄께요~
public class SingletonService {
//static을 붙이면 class level에 올라가서 하나의 인스턴스만 만들 수 있음. 자바 실행 될 때 클래스가 객체 자체가 됨. new보다 1000배 이득
private static final SingletonService instatnce = new SingletonService();
public static SingletonService getInstance(){
return instatnce;//항상 같은 객체가 호출됨 -> 싱글톤
}
private SingletonService(){
}//생성자 함수를 private으로 만들기. 외부에서 new 호출 불가
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
첫번째 줄을 보면 클래스 명으로 클래스 객체를 선언한다. static은 꼭 붙여야 한다! 생성자 함수가 private인데 어떻게 외부에서 쓰냐고?
SingletonService singletonService = SingletonService.getInstance();
그냥 클래스 명이 객체 자체로 사용될 수 있어서 getInstance()로 객체를 가져올 수 있다. 여기서 흥미로운 것은 자바가 처음 컴파일 할때 아예 singletonService객체를 만들어 놓고 getInstance()를 처음 호출 한다고 생성하는 것이 아닌 그냥 이미 만들어진 singletonService객체를 가져다 쓰는 것이다.
또한 주석에도 달려있듯이 new를 이용하는 것 보다 싱글톤을 이용하는게 거의 천 배 가까이 효율적인 이득이 있다고 한다.(?측정법 몰라 ㅎㅎ. 걍 카더라~)
1-1. 문제점
이렇게 순수 자바 코드로 싱글 톤을 유지하기 위할려고 한다면,, 공유 가능한 모든 클래스들은 위 코드 처럼 처리해주어야 한다. 또한 기존에는 인터페이스로 의존관계들을 묶어서 DIP를 지켰지만 위 처럼 싱글톤을 만들어 주어 버리면 DIP를 위배하게 될께 뻔하다. (getInstance()로 객체를 가져와 버리니깐!) 이에 따라 자연스럽게 코드 객체 바꿔 끼울 일 생기면 OCP도 위배할 가능성이 현저히 높다. 또한 생성자 함수가 private이니 상속 관계도 만들어 내기 어렵다. 이것 말고도 더 많은 단점들이 있다. 그럼 이거 어떡하냐,,, 그냥 new 쓰면서 감수 해야하나..
2. 혜성처럼 등장한 스프링 컨테이너
유~~어~~ 새이비어~~ 이스~ 히어얼!!!
스프링 컨테이너는 사실 모든 스프링 빈들을 싱글톤으로 관리 한다. 즉 우리는 저번에 만들어 놓은 코드를 저언혀어 고칠 필요가 없어졌다! 즉, 즉, 즉, 알고보니,, 우린 싱글톤 꽤 잘 쓰고 있잖아..?
void sprintContainer(){
ApplicationContext aC = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = aC.getBean("memberService",MemberService.class);
MemberService memberService2 = aC.getBean("memberService",MemberService.class);
//참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 != memberService2
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
AppConfig라는 클래스에 @Configuration 어노테이션 붙이고 MemberService객체를 반환하는 memberService함수가 존재하고 이 함수 또한 @Bean어노테이션 붙여져 있어야 한다. 이후에 위와 같은 테스트 코드가 있다고 하면 memverService1과 memberService2는 같은 참조값을 사용하고 있다.(same as는 일종의 자바의 == 같은 역할) 엇, 그럼, 잠시만, 그럼 싱글톤을 사용하는 객체 인스턴스는 서로 공유하니깐 클라이언트가 다른 클라이언트에게 영향력을 행사 할 수 있는 거 아냐?! 맞다.. (뭐라고?)
3. 싱글톤 방식의 주의점
싱글톤 방식은 싱글톤 객체의 상태를 유지하게 설계하면 안된다. 즉, 서로에게 영향력을 행사하지 않게 코드를 잡는다.
private int price; //상태를 유지하는 필드
public void order(String name, int price){
System.out.println("name = " + name + "price = " + price);
//return price;
}
public int getPrice(){
// return price;
}
위 코드를 가진 클래스를 싱글톤으로 만들어 버린다고 하자. 보나마나 A가 price를 10으로 order() 함수를 호출해 설정 해 놓았어도 B가 5로 바꾸어 버리면 그냥 바뀌어 버린다. 만약 이게 결제 서비스에서 이런 일이 일어난다면 회사에서 옷 벗어야 한다. 위 코드는 간단하지만 대게 상속에 쓰레드까지 들어가면 꽤 찾기 어렵다고 한다.
이에 해결법은 단순하다. 필드 값들을 어떤 클라이언트가 접근하지 못해야 하겠지! 그냥 읽기만 가능하도록 코드를 짜고, 자바에서 공유 되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용한다. (ThreadLocal은 뭐지..ㅎ 쓰레드 공부해야 겠다.)
4. 그렇다면 AppConfig안에서 같은 생성자 함수를 여러번 부른다면..?
@Bean
public MemberRepository memberRepository(){
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
위에서 말한 AppConfig에 다음과 같은 함수들이 존재한다. 만약 memberRepository() 부르고 memberService()부르고 orderService() 부르게 되면 memberRepository()는 총 3번이 불리게 된다. 정말..?
스프링 컨테이너에서 스프링 빈들에 대해 싱글톤을 유지해 준다고 했다. 즉, 스프링 컨테이너에 처음 생성되는 스프링 빈 생성자 함수는 1문단에서 설명한 것과는 다르게 첫 호출 시에 객체를 생성하고 반복되게 생성자 함수를 호출하면 생성자 함수 자체를 실행 하지 않는다. 그래서 만약, 저 세가지 생성자 함수를 다 호출한다 해도 call AppConfig.memberRepository 문구 출력은 단 한번만 일어나게 된다.
4-1. but, how?
사실 우리는 @Configuration 어노테이션을 붙여버리면 AppConfig를 우리는 사용하는 것이 아닌 AppCongfig의 자식인 AppConfig@(뭐시기뭐시기)CGLIB를 사용하게 된다. 웃긴건 스프링 컨테이너에 등록된 이 자식의 이름은 appConfig이름으로 부모 행세를 하고 다닌다. 밑에 사진처럼 말이다. (물론 @Bean어노테이션 뿐만 아니라 @Configuration 어노테이션을 붙이면 그 클래스 자체도 스프링 컨테이너에 들어가 있다!)
그리고 이제 AppConfig@CGLIB 안에는 대강 이러한 코드가 존재 할 것이다.
즉, 이러한 방식으로 스프링은 우리에게 스프링 빈으로 등록된 객체들에 대해 싱글톤을 정말정말 정갈하게 유지해준다고 할 수 있다.
4-2. 만약 @Configuration 어노테이션을 빼고 @Bean만 붙여 놓는 다면?
당연히 스프링 컨테이너에는 AppConfig 자체가 들어가게 되고 Bean으로 선언한 함수들은 스프링 빈으로 등록 되기는 하나 싱글톤을 보장 받지못한다.
추가로 @Configuration 어노테이션 없이도 싱글톤을 유지하는 방법이 있는데 @Autowired라는 어토네이션 을 이용하는 방법이 있지만 다음에 차피 자세히 다룰 예정!
5. 느낀점
저번 글에서 객체들을 스프링 컨테이너에 스프링 빈으로 등록하면 순수 자바 코드에 비해 어떠한 장점이 있는지 어렴풋이 밖에 알지 못했는데 오늘 하나 배웠다... 자동 싱글톤 유지.. 상당히 고맙다!!
추가적으로 싱글톤으로 등록된 객체들의 싱글톤 성격을 풀 수도 있다고 한다. 이것 또한 기대 되지만 상당히 쓰임이 드물기도 하고,, 기회 되면 이야기 나올 것 같다!