소개 및 개요
안녕하세요. 백엔드 데브코스를 진행하게 되면서 싱글톤 패턴에 대해 발표하게 되었습니다. 첫 발표이기도 해서 긴장이 많이 되었지만, 좋은 경험이라 생각하면서 용기를 내었습니다!
싱글톤 패턴 살펴보기
싱글톤 패턴은 한 클래스의 하나만 존재하는 인스턴스에 전역적으로 접근할 수 있는 방법을 제공해야 합니다.
즉, 다음 2가지 목적을 둘 수 있겠죠?
- 인스턴스를 오직 하나만 제공하는 클래스가 필요.
- 하나의 인스턴스에 전역적으로 접근할 수 있는 방법을 제공.
public static void main(String[] args){
Settings s1 = new Settings();
Settings s2 = new Settings();
System.out.println(s1 != s2); //true
}
때문에, 위 코드처럼 하면 안되겠죠?
싱글톤 구현 방법 살펴보기
그러면 먼저 가장 간단하게 싱글톤을 구현해봅시다.
public class Settings{
private static Settings instance;
private Settings() {}
public static Settings getInstance(){
if(instance == null)
instance = new Settings();
return instance;
}
}
위 방식은 과연 Thread-Safe 하다고 말할 수 있을까요?
결론부터 말하면 아닙니다! 즉, 안전하지 못합니다. 왜냐하면 A 쓰레드와 B 쓰레드가 있다고 가정해봤을 때, A 쓰레드와 B 쓰레드가 getInstance()를 호출해서 둘 다 if(instance==null)이 ture가 나온다면? 둘 모두 new 키워드로 생성이 되겠죠?
어떻게 Thread-Safe 하게 구현할 수 있을까?
public static synchronized Settings getInstance(){
return instance==null ? instance=new Settings() : instance;
}
간단합니다! 위처럼 synchronized 키워드만 붙이면 되죠!
더 자세하게 설명하면, 해당 키워드를 붙이는 순간 Lock을 잡아서 해당 Lock을 가진 쓰레드가 해당 영역에 접근할 수 있게 해주기 때문에 동시에 여러 쓰레드가 들어올 수 없게 되죠.
근데.. 그러면 getInstance()를 호출할 때마다 동기화 처리를 하지 않냐고요? 맞습니다. 성능에 불이익이 있습니다...
어떻게, 성능도 챙기고 Thread-Safe 하게 구현해요?
public class Settings{
private static final Settings INSTANCE = new Settings();
private Settings() {}
public static Settings getInstance() {
return INSTANCE;
}
}
이렇게! static final을 이용해 로딩되는 시점에 초기화하면 되죠!
하지만.. 만약에 INSTANCE를 사용하지 않게 되면 괜히 메모리만 잡아먹는거 아닌가요..? 이것도 맞습니다.. 메모리를 잡아 먹어요..
Thread-Safe 하고! 성능도 챙기고! 즉시 로딩이 아닌 지연 로딩이 되는 방법 알려주세요!
public class Settings {
private Settings(){}
private static class SettingsHolder{
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance(){
return SettingsHolder.INSTANCE;
}
}
짠~
아마도 다음과 같이 생각하는 분들도 있을거에요. "static으로 선언된 친구들은 JVM이 올라갈 때 전부 초기화되는 것이 아니었나?", "Static Inner Class로 선언된 것도 올라가는게 아닌가?" 하지만 이것은 틀렸습니다!
즉, getInstance()를 호출할 때, Settings.INSTANCE를 호출하면서 초기화되는 것입니다! 즉, Thread-Safe도 챙기고 성능도 모두 챙길 수 있겠죠?
그리고 하나만 더 말하자면, SettingsHolder.INSTANCE 호출 시에는 Settings 클래스는 로딩되지 않고 내부의 SettingsHolder만 로드 됩니다. 신기하죠? 다음으로 간단하게 클래스 로딩 시점을 정리한 것이니 참고 바랍니다.
클래스 로딩 시점 정리
- 클래스의 인스턴스 생성 시, -> new Settings()
- final 키워드를 사용하지 않은 클래스의 정적 변수 사용 시, -> Settings.value
- 클래스의 정적 메소드 호출 시, -> Settings.getInstance()
싱글톤이 안티 패턴이 될 수 있는 이유..
싱글톤 패턴이 안티 패턴이라는 것은 아닙니다!!
싱글톤은 아래와 같은 이유로 안티 패턴이 될 수 있는데요, 한 번 봐봅시다.
- private 생성자를 갖고 있어 상속이 불가능하다.
: 싱글톤은 자신만이 객체를 생성할 수 있도록 생성자를 private으로 제한하기 때문에, 상속을 통한 다형성을 적용시키기 위해서는 다른 기본 생성자가 필요하므로 객체지향의 장점을 적용할 수 없습니다. - 테스트하기가 힘들다.
: 생성 방식이 제한적이라 Mock 객체로 대체하기가 어려우며 동적으로 객체를 주입하기도 어렵겠죠..? - 서버 환경에서는 싱글톤이 1개만 생성됨을 보장하지 못한다.
: 싱글톤은 일반적으로 단일 JVM 내에서만 싱글톤 인스턴스가 보장되기 때문입니다. 서버 환경에선 여러 JVM 인스턴스가 동작할 수 있어서 싱글톤이 1개만 생성됨을 보장하지 못하겠죠? - 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
: 객체지향적이지 못한 static 필드 혹은 메소드를 사용하면서 아무 객체나 자유롭게 접근/수정이 가능하며 공유되는 전역 상태는 객체지향 프로그래밍에서 권장되지 않습니다.
싱글톤 패턴은 객체를 1번 생성하고 재사용할 수 있어서 필요한 경우가 분명 있습니다. 하지만 싱글톤 패턴을 직접 구현하면 위에서 말한 단점들이 크게 부각되어 활용이 쉽지 않습니다. 여기서 중요한 것은 이런 상황에서 프레임워크에게 싱글톤을 위임할 수 있다면 위와 같은 단점들이 해결되지 않을까요? 계속해서 살펴봅시다.
스프링의 싱글톤!
스프링은 직접 싱글톤 형태의 객체를 만들고 관리하는 기능을 제공하는데, 그것이 바로 Singleton Registry 입니다. 즉!! static, private 키워드를 사용하지 않아서 객체지향적 개발을 할 수 있고 테스트를 하기 편리해졌습니다.
이 Singleton Registry 역할을 하는 것이 바로, 스프링 컨테이너입니다. 스프링 컨테이너는 싱글톤을 생성/관리하고 공급하는 컨테이너이기도 합니다. 예를 들어서 스프링 컨테이너가 스스로 코드를 작성하고 대부분 싱글톤 방식으로 데이터를 자동 저장합니다. 따라서 스프링 컨테이너에서 꺼내 쓰는 데이터는 필요할 때마다 새로운 인스턴스를 생성해서 서로 다른 인스턴스를 가져오는 것이 아닌 하나의 인스턴스를 참조하게 됩니다.
위 그림처럼 만약 수십 ~ 수백만 건의 요청이 발생하는 서비스에서 기존 요청마다 인스턴스를 만드는 멀티톤 방식대로 동작하게 둔다고 생각해봅시다. 이는 초당 생성되는 컨테이너 객체 수를 메모리가 견디지 못하고 서비스는 큰 장애를 발생시키고 먹통이 될 것입니다. 때문에 우리는 단일 인스턴스를 가지는 싱글톤 패턴을 활용해야 합니다.
근데 이 스프링이란 것이 이런 상황을 고려해서 만들어 졌기 때문에 스프링 컨테이너 자체가 위 그림처럼 하나의 인스턴스만을 보장하는 것입니다.
즉, 효율적인 메모리 사용이 가능해졌습니다. 단, 공유 자원을 동시 접근하는 경우에는 당연히 동시성 문제가 발생할 수 있기 때문에 주의해서 설계하도록 합시다.
근데.. Spring Security의 SecurityContext는 싱글톤인가?
발표를 하다보니 또 궁금해지더라구요..?
인증 객체 저장소인 SecurityConext는 동일한 스레드 내에서 여러 개의 컴포넌트 혹은 서비스에서 SecurityContextHolder.getConext()를 호출해도 동일한 SecurirtyContext가 반환되어 일관된 보안 상태를 유지합니다.
final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static SecurityContext contextHolder;
@Override
public SecurityContext getContext() {
if (contextHolder == null) {
contextHolder = new SecurityContextImpl();
}
return contextHolder;
}
}
그리고 공식 문서의 코드를 보면 싱글톤 방식과 동일하게 구현되어 있는 것도 볼 수 있습니다.
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
}
또한 위 코드처럼 ThreadLocal을 통해 관리해서 Thread 별로 객체가 관리되는 것 또한 볼 수 있습니다.
오...! 싱글톤 맞는 것 같죠? ㅎㅎ 싱글톤 아닙니다~ ㅎㅎ
여기서 중요한 것은 우리가 스프링의 싱글톤 개념을 알아야 합니다.
스프링의 싱글톤은 ApplicationConext 내에서 특정 클래스의 인스턴스가 하나인 것을 의미합니다.
즉, 위에서도 말했듯, SecurityConext는 동일 스레드 내에서만 같은 인스턴스를 가집니다.
즉, "싱글톤의 범위는 애플리케이션 전체이지만 SecurityConext는 아니다. 때문에 싱글톤이 아니다."라는 결론을 내릴 수 있습니다.
Reference
'Activity > 데브코스 - 백엔드 엔지니어링' 카테고리의 다른 글
벡엔드 데브코스 TIL - 동시성 이슈 해결하기 (4) | 2023.10.11 |
---|---|
벡엔드 데브코스 TIL - 프로젝트로 단축 URL 알아가기 (0) | 2023.10.04 |
벡엔드 데브코스 TIL - 프록시 패턴 발표 (1) | 2023.07.12 |
백엔드 데브코스 TIL - 동행 스크럼을 하다가 공부하게 된 Process와 Thread (0) | 2023.06.12 |
백엔드 데브코스 TIL - 놓치기 쉬운 JAVA 이야기 (2) | 2023.06.02 |