@Configuration
public Class ExampleConfig {
@Bean
public ExamBean exam() {
return new ExamBean();
}
}
public class SingletonTest {
@Test
void pureContainer() {
ExampleConfig examConfig = new ExampleConfig();
// 호출할 때마다 객체를 생성
ExamBean exam1 = examConfig.exam();
ExamBean exam2 = examConfig.exam();
System.out.println("exam1 = " + exam1);
System.out.println("exam2 = " + exam2);
Assertions.assertThat(exam1).isNotSameAs(exam2);
}
}
exam1 = com.example.demo.ExamBean@26c16c02
exam2 = com.example.demo.ExamBean@52b02322
ExampleConfig
는 요청이 들어올 때마다 새로운 ExamBaen
클래스의 인스턴스를 생성하게 되고 그만큼 JVM의 메모리 사용량이 증가하게 되는 것이다.ExampleConfig
에서 생성한 ExamBean
객체를 공유하면 해결할 수 있다.이번에는 싱글톤 패턴을 적용해보자.
private static final
키워드로 객체를 만들면 외부에서는 해당 클래스의 객체를 새로 생성할 수 없으므로 싱글톤 패턴 조건을 만족하게 된다.public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {
}
}
public static void main(String[] args) {
SingletonService singletonService = new SingletonService();
}
SingletonService
클래스 객체를 getInstance
메서드를 통해서만 접근할 수 있다.@Test
void singletonServiceTest(){
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
static 객체
를 통해서 해당 객체 1개만 생성할 수 있도록 지정한다.private 생성자
를 통해서 외부에서 임의로 new 연산자로 객체를 생성하는 것을 제한한다. getInstance
메소드를 통해서만 조회할 수 있고, 항상 동일한 객체가 반환된다.순수 Java 코드로 싱글톤 패턴을 적용하는 방법은 실제로 사용하기에는 불편하고 실질적인 문제점들이 존재한다.
- 싱글톤 패턴을 구현하기 위한 코드가 늘어난다.
- 의존 관계상 클라이언트가 구체 클래스에 의존한다. (DIP,OCP 위반)
- 내부 속성을 변경, 초기화하기 어렵다.
- private 생성자로 자식 클래스를 생성하기 어렵다.
- 유연성이 떨어진다.
- 테스트마다 데이터를 초기화를 해주어야 하므로 까다롭다.
( DIP(Dependency Inversion Principle, 의존 역전 원칙) : 구현체에 의존하지 말고, 역할(인터페이스)에 의존해야 한다는 원칙)
( OCP(Open Closed Principle, 개방 폐쇄 원칙) : 소프트웨어 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙)
@Test
void springContainer() {
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);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
memberService1 = com.example.springdemostudy.member.MemberServiceImpl@78e16155
memberService2 = com.example.springdemostudy.member.MemberServiceImpl@78e16155
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;
}
}
price
는 공유되는 필드이기 때문에 특정 클라이언트가 값을 변경할 수 있다.class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자 10000원 주문
statefulService1.order("userA",10000);
// ThreadB : B 사용자 20000원 주문
statefulService2.order("userB",20000);
// ThreadA : 사용자 A 주문금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}