당니의 개발자 스토리
싱글톤 패턴 본문
싱글톤 패턴
이번 시간에는 싱글톤 패턴에 대해서 알아보겠습니다.
디자인 패턴에 있는 싱글톤 패턴으로 앞서 있었던 문제를 해결해 볼 거에요.


싱글톤 패턴은 클래스 인스턴스가 딱 한 개만 생성되는 것을 보장하는 디자인 패턴이에요.
무슨 말이냐면, 지금 내 JVM, 한 자바 서버 안에서는 그 객체 인스턴스가 하나만 딱 생성이 되는 거예요. 절대 두 개가 생성되지 않도록 만드는 디자인 패턴이에요. 한 개만 생성하려면 어떻게 해야 되냐? 방법이 여러 가지가 있는데요.
결론은 똑같은 객체 타입의 인스턴스를 두 개 이상 생성하지 못하도록 막으면 됩니다.
그래서 private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하게 막아야한다. 라고 하는데, 말로 해서는 안 와닿고 코드로 보겠습니다.

test 밑에 singleton 에다가 만들겠습니다.

여기에다가 SingletonService 라는 클래스를 만들 거에요. 이건 테스트 케이스가 아니지만,

여기 순수한 애플리케이션에 영향을 안 주고, 그냥 싱글톤만 만들어서 테스트 하는 거를 보여드릴려고 test 폴더에 넣었습니다.

먼저 SingletonService 라고 자기 자신을 선언합니다. 그리고 변수명은 instance로 많이 씁니다.

이렇게 자기 자신을 내부의 private으로 하나 가지고 있는데, static으로 가지고 있기 때문에, 클래스 레벨에 올라가서 딱 하나만 존재하게 됩니다.
static으로 되어있으면, static 영역에 딱 하나만 만들어져서 올라갑니다. 이렇게 해놓고, 그 다음에 필요하면 얘를 조회를 해야겠죠.

이렇게 하면 JVM, 자바가 뜰 때 바로 SingletonService의 static 영역에다가 new SingletonService()를 그냥 자기가 내부적으로 실행하고 SingletonService 객체, 즉 자기 자신을 생성해서, 변수 instance에 참조값을 넣어놔요.
그러면 자기 자신을 인스턴스 객체로 하나만 딱 생성해서 instance 안에만 들어가 있는거죠.
그렇게 해놓고 조회 할 때는

얘를 쓰면 돼요. 그 다음에 이게 중요한데,

이렇게 하면 어떻게 되나요?

위에도 new SingletonService()이 있고, 아래에도new SingletonService()이 있죠. 이런 식으로 내가 private 붙여서 아무리 열심히 내부에 감추려고 해도,

이렇게 누가 new로 임의로 생성할 수 있단 말이에요. 여긴 class 안이어서 private 생성자 만들어도 호출할 수 있지만, 저 psvm 안에 있는 코드가 바깥에서 실행되면 안되는 거죠.
그래서 private 생성자를 씁니다.

이렇게 생성을 막아버리면, 바깥에서 호출했을 때 안될 겁니다. 한번 바깥에서 호출해보면,

임의로 아무 데서나 psvm 해서 생성자를 호출하면, 빨간 줄 쳐지고 안됩니다.

private access 해서 안된다고 나옵니다. 컴파일 오류가 납니다. 다시 지우고요.
SingletonService로 돌아와서, 로직을 하나 만들게요.

이렇게 하겠습니다.

이렇게 하면 완벽한 싱글톤이 됩니다. 자바가 뜨면서,

이 static 영역에 있는 instance를 초기화하면서 new로 생성해서 가지고 있고요. 이 instance의 참조를 꺼낼 수 있는 방법은

이것밖에 없습니다. 그리고

SingletonService를 생성할 수 있는 곳은 아무데도 없어요.
이제 설명을 해드리면,

먼저 static 영역에 객체 instance를 미리 하나 생성해서 올려둡니다.

java 뜰 때 바로 딱 얘가 생성이 돼서 올라갑니다.
그 다음에 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있어요. 이 method를 호출하면 항상 같은 인스턴스를 반환하죠.
그리고 딱 한 개의 객체 인스턴스만 존재하기 때문에 생성자를 private으로 막아야 돼요. 그래서 혹시라도 초보 개발자가 외부에서 new 키워드로 객체 인스턴스를 생성하는 걸 막아야 됩니다. 이게 진짜 중요해요.
우리한테 제일 좋은 오류는 컴파일 오류입니다. 진짜 잘 설계한 객체는 컴파일 오류만으로 웬만한 오류가 다 잡히도록 설계하는 게 진짜 잘 설계한 거거든요.
생성자를 딱 private으로 막아버리면, 누군가는 이걸 보고 "어? 이게 왜 private이지? 아 싱글톤이구나~ 꺼내려면 어떻게 해야하지?" 하고 생각하겠죠. 만약에 public으로 되어 있으면, 그냥 생성해서 쓰나 보다" 하고 생성 해버리겠죠. 그럼 SingletonService가 2, 3개 되어버리는 거에요.
그러니까 내가 아무리 의도가 좋게 public으로 선언을 하거나, 주석을 달아서 얘가 싱글톤이예요! 라고 해도, new로 다른 개발자들이 생성해버리면 끝납니다.
우리가 만들었던 SingletonTest에 테스트를 하나 더 만들어 보겠습니다.

이렇게 만들고,

new 해서 SingletonService를 생성하려고 하면 안되겠죠.

'private access 라서 안돼!' 하고 컴파일 오류를 띄워줍니다.
그럼 기존과 비슷하게 조회를 해보겠습니다.

이렇게 하고, 하나 더 만들어서 singletonService2를 만들겠습니다.
그럼 이제 검증은 호출할 때 마다 객체를 생성하는지, 안하는지 보면 되겠죠? 우리가 원하는 건 한번만 생성해서, 그 뒤부턴 같은 객체 인스턴스를 호출하는 거였습니다.

돌려보면,

드디어 같은 객체 인스턴스가 반환이 됐죠.
java가 뜰 때,

instance를 생성해놓은 걸 getInstance()로 불러내면서, 그걸 계속 가져다 쓰는 거에요.
눈으로 출력해서 계속 볼 수는 없으니까 검증을 해 보겠습니다.

isSameAs는 isEqualTo와 뭐가 다를까요?
same은 진짜 java에서의 == 비교입니다. 객체 인스턴스와 같은 "==" 참조어를 비교하는 거에요.
Equal은 Java에서 Equals 메서드로 비교하는 것처럼, 내용을 비교하는 겁니다.
same과 eqauls의 차이

p1 == p2 라고 하는 동등비교 연산자는 p1과 p2가 "같은 곳을 가리키고 있냐"
==(동등비교연산자)은 "같은 곳에 있냐"를 따져보는 것이고, equals는 "내용이 같은지"를 따지는 취지로 만들어진 메소드다.

따라서, 우리는 참조 값을 비교하는 거기 때문에 IsSameAs 메서드를 써서 검증해보겠습니다.

테스트를 통과했습니다.
자 이렇게 해서 싱글톤 패턴을 구현하는 걸 봤어요.

그러면 여기 AppConfig 있죠?

AppConfig의 이것들을 하나하나 다 싱글톤 패턴으로 다 넣어가지고 바꾼 다음에, getInstance() 해가지고 다 반환하도록 하면 되겠죠. 그러면 싱글톤이 다 적용이 됩니다.
그런데 그렇게 할 필요가 없어요. 왜냐?
스프링 컨테이너를 쓰면, 스프링 컨테이너가 기본적으로 객체를 다 싱글톤으로 만들어서 관리를 해줍니다. 기가 막히죠.

이 싱글톤 패턴이 딱 적용되면 어떻게 됩니까?
이전처럼 고객 요청 100개가 와서 초당 100개의 요청을 처리해야 돼도, 객체를 하나도 안 만들어요. 있는 객체를 그냥 그대로 재활용 하는 거예요. 그러면 성능이 상당히 좋아지겠죠.
이렇게 해서 지금까지 배운 내용을 정리해보면,

private으로 new 키워드를 막았죠. 그래서 new SingletonService() 이게 안 됐구요.
호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있습니다.
그리고 참고로 이 싱글톤 패턴을 구현하는 방법은 정말 많아요.
우리가 했던,

이 방법은 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택한 거예요.
싱글톤 패턴에는 실제 getInstance()를 호출하는 타이밍에 객체가 생성이 되어있으면, 생성이 되어있는 걸 쓰고,
만약 객체가 생성이 안 되어있으면, 그제서야 객체를 생성해서 지연해서 처리하는 방법도 있거든요.
여러가지 방법이 있는데, 사실 이런 객체 인스턴스가 메모리를 많이 잡아먹는 게 아니면, 우리가 했던 것처럼 로딩할 때 하는

이 방법이 제일 간편하고 제일 안전합니다.
나머지 싱글톤 패턴 구현 방법은 한번 찾아보세요. 어차피 스프링을 쓰면 스프링이 다 해결해주기 때문에 크게 고민 안 해도 됩니다.

싱글톤 패턴을 적용하면 고객의 요청이 올 때마다 객체를 생성하는 게 아니죠. 이미 만들어진 객체를 공유해서 굉장히 효율적으로 사용할 수 있습니다.
하지만 싱글톤 패턴은 다음과 같은 수많은 문제들을 가지고 있어요.

첫 번째 문제는 이 코드를 일단 기본적으로 넣어야 됩니다.

내가 지금 하고 싶은건,

SingletonService의 한 줄인데, 이 한 줄을 하기 위해선

이걸 다 넣어야 되는 거죠. 그래서 싱글톤 패턴을 구현하는 데에는 여러가지 단점이 있지만, 코드 자체가 많이 들어가는 게 참 싫더라구요.

그리고 사실 이게 의존관계상 문제가 있어요. 클라이언트가 구체 클래스에 의존해야 돼요.
getInstance() 메서드를 구체 클래스 안에 만들어서 불러와야 하기 때문에 DIP를 위반합니다.
예를 들어서, 우리가 AppConfig에서 MemberServiceImpl을 불러오려고 하면,

이런 식으로 꺼내야 되거든요. 그래서 클라이언트가 구체 클래스.getInstance() 이렇게 해서 이걸 꺼내야 돼요. 그러니까 구체 클래스에 의존하게 되고 DIP를 위반하게 됩니다.
그리고 클라이언트가 구체 클래스에 의존하기 때문에 OCP를 위반할 가능성이 굉장히 높겠죠.
또 싱글톤의 문제는 테스트하기가 되게 어려워요. 싱글톤은 내가 딱 지정해서 가져오잖아요. 싱글톤은 인스턴스를 미리 박아가지고 설정이 다 끝나버리기 때문에, 유연하게 테스트하기 되게 어렵습니다.
그리고 내부 속성을 변경하거나 초기화 하기도 되게 어렵구요. private 생성자를 쓰죠.

private 생성자를 쓰면, 자식 클래스를 만들기 되게 어려워져요.
결론적으로 유연성이 떨어집니다. Dependency Injection 이런 걸 적용하기가 참 어려워요. 왜냐면 구체 클래스에다가 getInstance()를 해줘야 된단 말이에요. 그래서 유연성이 굉장히 떨어지구요.
그래서 안티 패턴으로 불리기도 합니다.
물론 장점도 있죠. share를 할 수 있고 확실하게 객체 하나가 있다는게 보장이 되지만, 이런 수많은 단점들을 갖고 있어요.
자 그런데 스프링 프레임워크는 이 싱글톤 패턴의 문제점을 전부 다 해결하고, 그러니까 싱글톤이 가진 단점은 다 제거하면서, 객체를 싱글톤으로 관리해줍니다.
이제 스프링 컨테이너를 싱글톤 컨테이너라고 하기도 하는데요.
다음 시간에는 스프링 컨테이너, 즉 싱글톤 컨테이너의 역할에 대해서 알아보겠습니다.
'스프링 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
| 싱글톤 방식의 주의점 (0) | 2024.01.23 |
|---|---|
| 싱글톤 컨테이너 (0) | 2024.01.23 |
| 웹 애플리케이션과 싱글톤 (0) | 2024.01.23 |
| 스프링 빈 설정 메타 정보 - BeanDefinition (0) | 2024.01.23 |
| 다양한 설정 형식 지원 - 자바 코드, XML (0) | 2024.01.22 |