자바를 공부할 때, 싱글톤 패턴에 대해서 배웠다. 싱글톤에 대한 설명은 이전 포스트를 참고하자.
싱글톤 패턴이란, 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
스프링을 사용하지 않고, 순수한 자바 코드인 AppConfig 클래스만을 사용하여 객체를 생성한다고 가정해보자.
1000명의 사용자가 동시에 AppConfig.memberServie()
로 MemberService 객체를 요구한다.
➜ 프로그램은 요청을 받을 때마다 객체를 새로 생성한다.
➜ 매우 비효율적이다.
∴ 싱글톤 패턴을 사용해야 한다.
싱글톤 패턴을 적용하면 객체를 공유해서 효율적으로 프로그램을 동작시킬 수 있다. 하지만, 단점도 존재한다.
1. 싱글톤 패턴을 구현하는 코드를 추가해야 한다.
2. DIP를 위반한다. (구체 클래스에 의존하기 때문)
3. 구체 클래스에 의존하므로 OCP를 위반할 가능성이 높다.
4. private 생성자 때문에 자식 클래스를 만들기 어렵다.
순수한 자바 코드 AppConfig 클래스에서 각각을 싱글톤으로 만들어주기 위해서는 어떻게 해야할까?
➜ AppConfig 메소드의 각 리턴 객체를 싱글톤으로 만들어주는 코드를 추가해야한다.
이것은 매우 복잡하고, 귀찮은 작업이 될 것이다.
하지만!!! 스프링 컨테이너는 이 작업을 대신 해준다.
스프링 컨테이너 ApplicationContext는 싱글톤 컨테이너 역할을 한다.
스프링 컨테이너는 객체를 하나만 생성해서 관리한다.
싱글톤 패턴을 구현하기 위해 코드를 추가할 필요 없이, 스프링을 사용하면(Annotation) 스프링 컨테이너가 자동으로 객체를 싱글톤으로 유지시켜준다.
// AppConfig의 일부분
@Bean
public BoardService boardService(){
return new BoardServiceImpl(memberRepository(), boardPolicy());
}
정말로 싱글톤인지 확인해보자.
@Test
@DisplayName("Testing Singleton Container")
void singletonTest(){
ApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
BoardService boardService1 = ac.getBean(BoardService.class);
BoardService boardService2 = ac.getBean(BoardService.class);
스프링 컨테이너가 싱글톤을 유지한다면, getBean의 결과로 리턴된 두 참조 값은 같아야 한다.
System.out.println("boardService1 = " + boardService1);
System.out.println("boardService2 = " + boardService2);
Assertions.assertThat(boardService1).isSameAs(boardService2);
}
결과
boardService1 = boardProject.demo.board.BoardServiceImpl@68759011
boardService2 = boardProject.demo.board.BoardServiceImpl@68759011
Test Passed
결론: 빈은 각각 하나씩만 생성되고 관리된다.
⚠️ 싱글톤은 무상태(stateless)로 설계되어야 한다. ⚠️
잘못된 설계의 예시를 보자
// Stateful 싱글톤 설계
public class StatefulSingleton {
private int timer;
// timer 필드는 누구나 바꿀 수 있다.
public void setTime(String name, int timer){
System.out.println("name = " + name + "time = " + timer);
this.timer = timer;
}
public int getTime(){
return timer;
}
}
위의 StatefulSingleton 클래스에서 timer 필드는 변경 가능하다. 즉, 이 클래스는 stateful 하다.
class StatefulSingletonTest {
// 테스트를 위한 싱글톤 컨테이너 설계
@Configuration
static class TestConfig {
@Bean
public StatefulSingleton statefulSingleton() {
return new StatefulSingleton();
}
}
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new
AnnotationConfigApplicationContext(TestConfig.class);
StatefulSingleton user1 = ac.getBean("statefulSingleton",
StatefulSingleton.class);
StatefulSingleton user2 = ac.getBean("statefulSingleton",
StatefulSingleton.class);
// 싱글톤 객체를 user1 과 user2로 참조함
// Thread1: user1 타이머를 100으로 설정
user1.setTime("user1",100);
// Thread2: user2 타이머를 50으로 설정
user2.setTime("user2",50);
// user1의 타이머 출력 --> 의도와는 달리 50이 출력됨
System.out.println(user1.getTime());
Assertions.assertThat(user1.getTime()).isEqualTo(100);
}
}
각 유저마다 타이머를 설정할 수 있게 만드는 것이 본 프로그램의 의도였다.
하지만, 특정 클라이언트가 싱글톤의 필드를 변경할 수 있기 때문에 프로그램에 오류가 발생했다.
다음과 같이 지역변수를 사용하도록, 즉 무상태로 설계해야 안전하다.
// Stateless 싱글톤 설계
public class StatelessSingleton {
public int setTime(String name, int timer){
System.out.println("name = " + name + "time = " + timer);
return timer;
}
// 함수를 호출한 곳에서 리턴된 값을 알아서 처리한다.
}
다음 예시에서 이상하다고 생각되는 부분이 있는가?
// AppConfig의 일부분
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public BoardService boardService(){
return new BoardServiceImpl(memberRepository(), boardPolicy());
}
memberService와 boardService 빈은 각각 memberRepository() 메소드를 호출하고 있다.
순수한 자바 코드 관점에서 보면, 아무런 조치가 되어있지 않기 때문에 memberRepository() 메소드를 호출할 때마다 MemoryMemberReposioty 객체가 각각 생성되어 반환될 것이다.
➜ 즉 MemoryMemberRepository의 싱글톤 패턴이 깨질 것이다.
하지만!
스프링 컨테이너는 이것마저 처리해준다.
즉, MemoryMemberRepository는 한번만 생성된다.
어떻게 가능한 것일까?
스프링은 AppConfig 클래스를 상속받은 임의의 클래스를 만들어서 사용한다.
조작된 AppConfig 클래스는 다음과 같이 동작할 것이다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository 빈이 등록되어 있음) {
return 등록된 빈;
} else {
//스프링 컨테이너에 없으면
return new MemoryMemberRepository();
}
}
결과적으로 @Configuration이 붙은 스프링 컨테이너의 빈은 모두 싱글톤으로 유지된다.
⚠️ @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장되지 않는다.