(인프런) 김영한님의 스프링 핵심 원리-기본편을 공부하고 리뷰한 글입니다.
1. 웹 애플리케이션과 싱글톤
1) 스프링: 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
2) 대부분의 스프링 애플리케이션 = 웹 애플리케이션 (웹이 아닌 애플리케이션 개발도 가능함)
3) 웹 애플리케이션: 보통 여러 고객이 동시에 요청을 한다.
- 고객이 요청할 때마다 새로운 객체가 생성되는 문제
1. 스프링 없는 순수한 DI 컨테이너 테스트
우리가 만든 스프링 없는 순수한 DI 컨테이너(AppConfig)는 요청을 할 때 마다 새로운 객체를 생성한다.
- 고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성, 소멸된다 -> 메모리 낭비 심함
(해결방안) 해당 객체를 1개만 생성, 공유하도록 설계 -> 싱글톤 패턴
1) SingletonTest 코드 작성
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
// 1. 조회: 호출할 때 마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
// 2. 조회: 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
// 위에서 생성한 객체 2개의 참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 != memberService2 (두 객체가 다른지) 검증
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
}
- Assertions.assertThat(actual).isNotSameAs(expected): 실제값(actual)이 기대값(exptected)와 다른 객체인지 검증
2) 결과
appConfig의 memberService()를 호출해 생성된 MemberServiceImpl 2개는 다른 인스턴스다.
2. 싱글톤 패턴
싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
객체 인스턴스를 2개 이상 생성하면 안되므로 private 생성자를 사용해 외부에서 임의로 new 키워드를 사용하지 못하도록 한다.
1. 싱글톤 패턴을 적용한 예제 코드
싱글톤 패턴을 구현하는 방법은 여러가지가 있지만 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 사용했다.
package hello.core.singleton;
public class SingletonService {
// 싱글톤 패턴 구현: 객체를 미리 생성해두는 가장 단순, 안전한 방법
// 1. static 영역에 객체를 딱 1개만 생성한다. (외부에서 접근하지 못하도록 private으로)
private static final SingletonService instance = new SingletonService();
// 2. Singleton 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 public으로 선언한다.
public static SingletonService getInstance() {
return instance;
}
// 3. private 생성자(외부에서 new로 객체를 생성하는 것을 막기 위함)
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
1) SingletonService 클래스 static 영역에 객체 instance 1개를 미리 생성해둔다.
2) SingletonService 객체 인스턴스가 필요하면 public으로 선언된 getInstance()를 통해서만 조회할 수 있다. 이 메서드는 항상 같은 인스턴스를 반환한다.
3) 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 선언해 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
2. 싱글톤 패턴을 사용하는 테스트 코드
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class SingletonTest {
...
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
// private으로 생성자를 막아둠(외부에서 생성하면 컴파일 오류)
// new SingletonService();
// 1. 조회: 호출할 때 마다 같은 객체 반환
SingletonService singletonService1 = SingletonService.getInstance();
// 2. 조회: 호출할 때 마다 같은 객체 반환
SingletonService singletonService2 = SingletonService.getInstance();
// getInstance()로 얻은 객체 인스턴스 2개의 참조값이 같은지 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// singletonServcie1 == singletonService2 (두 객체 인스턴스가 같은지 검증)
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
}
- Assertions.assertThat(actual).isSameAs(expected): 실제값(actual)이 기대값(exptected)와 같은 객체인지 검증
- SingletoneService의 생성자가 private이기 때문에 외부에서 인스턴스를 new로 생성X
2) 결과
getInstance()로 얻은 객체 인스턴스가 같은 것을 확인할 수 있다.
3. 싱글톤 패턴 문제점
(참고) 테스트 코드에서 검증할 때 assertThat().isEqualTo()가 아닌 isSameAs() / isNotSameAs() 를 사용한 이유가 궁금하다면, 여기 글을 참고하자.