이 글은 강의 : 김영한님의 - "스프링 핵심원리 - 기본편"을 듣고 정리한 내용입니다. 😁😁
이번시간부터 싱글톤 컨테이너에 대해 알아볼 것이다.
고객이 요청할 대마다 새로운 객체가 생성되는 문제
우리가 만든 스프링 없는 순수한 DI컨테이너(AppConfig)는 요청할 때마다 새로운 객체를 생성한다.
😄 해결방안 : 해당 객체를 1개만 생성, 공유하도록 설계
이게 바로 싱글톤 패턴
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);
}
}
당연히 결과는 appConfig의 memberService()를 호출해 생성된 MemberServiceImpl 2개는 다른 인스턴스다!
싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
객체 인스턴스를 2개 이상 생성하면 안되므로 private 생성자를 사용해 외부에서 임의로 new 키워드를 사용하지 못하도록 한다.
싱글톤 패턴을 구현하는 방법은 여러가지가 있지만 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 사용했다.
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 키워드로 객체 인스턴스가 생성되는 것을 막는다.
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();
}
}
결과로는 당연히 두 객체는 같은 것을 확인할 수 있다.
싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.
클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
테스트하기 어렵다.
내부 속성을 변경하거나 초기화 하기 어렵다.
private 생성자로 자식 클래스를 만들기 어렵다.
결론적으로 유연성이 떨어진다.
안티패턴으로 불리기도 한다.