하리Hari

1. 싱글톤 패턴 (Singleton)

오직 하나의 객체만 생성하여 상태를 공유하는 싱글톤 패턴을 알아봅니다.

한 줄 요약
프로그램 전체에 딱 하나만 있어야 하는 객체를 클래스가 직접 맡아 두고, 어디서든 같은 문으로만 가져가게 하는 패턴이에요.

문제: 하나여야 하는데 여러 개가 생기는 객체

앱의 설정을 만든다고 해 볼게요. 설정에는 화면에 쓸 언어가 들어 있어요. 화면도, 알림도 이 설정을 봐요. 가장 먼저 떠오르는 방법은 설정이 필요한 곳마다 설정 객체를 새로 만드는 거예요.

[설정]을 설계하자:
    '언어'를 [문자열]인 "한국어"로 정하자

(참고: 화면과 알림이 저마다 설정을 새로 만들어요)
[화면]을 설계하자:
    '설정'을 [설정]인 새로운 [설정]()으로 정하자

    <언어바꾸기>를 만들자 ([문자열]인 '새언어'):
        '나'의 '설정'의 '언어'를 '새언어'로 정하자

    <보여주기>를 만들자 ():
        틀"[화면] 지금 언어: {'나'의 '설정'의 '언어'}"를 출력하자

[알림]을 설계하자:
    '설정'을 [설정]인 새로운 [설정]()으로 정하자

    <보여주기>를 만들자 ():
        틀"[알림] 지금 언어: {'나'의 '설정'의 '언어'}"를 출력하자

'첫화면'을 새로운 [화면]()으로 정하자
'알림창'을 새로운 [알림]()으로 정하자

'첫화면'의 <언어바꾸기>("English")를 실행하자
'첫화면'의 <보여주기>()를 실행하자
'알림창'의 <보여주기>()를 실행하자

실행 결과
[화면] 지금 언어: English
[알림] 지금 언어: 한국어

언어를 영어로 바꿨는데, 알림은 아직 한국어예요. 버그예요.

  • 설정이 두 개 생겼어요. [화면]과 [알림]이 각자 새로운 [설정]()을 했기 때문에, 이름만 같은 서로 다른 설정 객체를 하나씩 들고 있어요. 한쪽을 바꿔도 다른 쪽은 그대로예요.
  • 곳이 늘수록 더 어긋나요. 설정을 쓰는 곳이 열 군데면 설정도 열 개예요. 어느 것이 “진짜 설정”인지 아무도 몰라요.
  • 에러가 나지 않아요. 프로그램은 멀쩡히 돌아가고, 결과만 조용히 틀려요. 그래서 찾기 어려워요.

해결책: 하나만 만들고, 같은 문으로만 가져가기

싱글톤 패턴은 “하나만 있어야 한다”는 규칙을 클래스가 직접 지키게 해요.

  1. 클래스가 자기 객체 하나를 정적 멤버('우리'의)로 맡아 둬요. 처음에는 비어 있어요.
  2. 객체가 필요하면 누구든 <가져오기>라는 같은 문을 지나요. 처음 부르면 그때 하나를 만들어 맡아 두고, 그다음부터는 맡아 둔 그 객체를 돌려줘요.
  3. 누군가 새로운 [설정]()으로 몰래 하나 더 만들려고 하면, 만들어지는 순간 에러를 내서 막아요.

비유: 학교의 교무실 열쇠

교무실 열쇠는 하나뿐이고, 경비실에 맡겨져 있어요. 선생님들은 필요할 때 경비실에서 받아 가고, 문은 언제나 그 하나의 열쇠로 열어요. 선생님마다 열쇠를 복사해 가면, 누가 문을 잠갔는지, 열쇠가 몇 개인지 아무도 모르게 되겠죠. 경비실이 바로 <가져오기>예요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
싱글톤 [설정] 자기 객체 하나를 맡아 두고, 하나 더 만들어지는 것을 막아요
맡아 둔 객체 '우리'의 '_하나' 프로그램 전체가 함께 쓰는 단 하나의 설정이에요
하나뿐인 문 [설정]의 <가져오기>() 처음에는 만들고, 그다음부터는 같은 것을 돌려줘요
쓰는 곳 [화면], [알림] 설정이 필요할 때마다 같은 문으로 가져가요

하리로 만들기

같은 앱을 싱글톤 패턴으로 다시 만들어 볼게요. 마지막에는 몰래 하나 더 만들어 보는 장면도 넣었어요.

(참고: 1단계 — 하나뿐인 설정을 클래스가 직접 맡아 둬요)
[설정]을 설계하자:
    '우리'의 '_하나'를 비어있음으로 정하여 숨기자
    '언어'를 [문자열]인 "한국어"로 정하자

    (참고: 2단계 — 이미 하나 있으면 또 만들지 못하게 막아요)
    처음 만들어질 때 () 다음과 같이 하자:
        만약 ('우리'의 '_하나'가 비어있음과 다르다) 라면:
            새로운 [오류]("설정은 하나만 있어야 해요. [설정]의 <가져오기>()를 쓰세요.")를 발생시키자

    (참고: 3단계 — 어디서든 이 문으로만 가져가요)
    [설정]을 돌려주는 '우리'의 <가져오기>를 만들자 ():
        만약 ('우리'의 '_하나'가 비어있음과 같다) 라면:
            '우리'의 '_하나'를 새로운 [설정]()으로 정하자
        '우리'의 '_하나'를 돌려주자

[화면]을 설계하자:
    <언어바꾸기>를 만들자 ([문자열]인 '새언어'):
        [설정]의 <가져오기>()의 '언어'를 '새언어'로 정하자

    <보여주기>를 만들자 ():
        틀"[화면] 지금 언어: {[설정]의 <가져오기>()의 '언어'}"를 출력하자

[알림]을 설계하자:
    <보여주기>를 만들자 ():
        틀"[알림] 지금 언어: {[설정]의 <가져오기>()의 '언어'}"를 출력하자

'첫화면'을 새로운 [화면]()으로 정하자
'알림창'을 새로운 [알림]()으로 정하자

'첫화면'의 <언어바꾸기>("English")를 실행하자
'첫화면'의 <보여주기>()를 실행하자
'알림창'의 <보여주기>()를 실행하자

일단 해보자:
    '몰래만든설정'을 새로운 [설정]()으로 정하자
오류가 발생했다면 ('에러'):
    ('에러'의 '메시지')를 출력하자

실행 결과
[화면] 지금 언어: English
[알림] 지금 언어: English
설정은 하나만 있어야 해요. [설정]의 <가져오기>()를 쓰세요.

이번에는 화면에서 바꾼 언어가 알림에도 보여요. 둘이 같은 설정 하나를 보고 있기 때문이에요. 그리고 하나 더 만들려고 하면 그 자리에서 막혀요.

단계별로 다시 보기

  • 1단계 — 클래스가 직접 맡아 두기. '우리'의 '_하나'는 객체마다 따로 생기는 값이 아니라 [설정] 클래스에 하나만 있는 값이에요. 숨기자로 만들어서 밖에서는 직접 바꿀 수 없어요.
  • 2단계 — 두 번째 탄생 막기. 생성자는 '_하나'가 이미 차 있으면 에러를 내요. 처음 <가져오기>가 부를 때는 아직 비어 있어서 무사히 만들어지고, 그다음부터는 누가 새로운 [설정]()을 해도 막혀요.
  • 3단계 — 하나뿐인 문. <가져오기>는 처음 한 번만 객체를 만들어요. 필요할 때 처음 만드는 이 방식을 늦은 초기화라고 해요. 설정을 한 번도 쓰지 않는 프로그램이라면 설정을 만들지도 않아요.

하리에서는 생성자를 숨길 수 없나요?
네, 하리에는 생성자를 숨기자로 만드는 방법이 없어요. 그래서 이 페이지처럼 생성자 안에서 검사해 “하나만”을 지켜요. 덕분에 실수로 새로운 [설정]()을 써도 조용히 두 개가 생기지 않고, 무엇을 써야 하는지 알려 주는 에러가 나요.

싱글톤의 그림자, 그리고 더 쉬운 대안

싱글톤은 편하지만, 많이 쓰면 탈이 나는 패턴으로도 유명해요.

  • 어디서든 바꿀 수 있는 값이 돼요. [설정]의 <가져오기>()는 프로그램 어디서나 부를 수 있어서, 누가 언제 언어를 바꿨는지 따라가기 어려워요.
  • 숨은 연결이 생겨요. [알림]의 코드만 봐서는 설정을 쓰는지 알 수 없어요. 안에 [설정]의 <가져오기>()가 숨어 있거든요.
  • 따로 시험하기 어려워요. 하나뿐이라서, “영어 설정일 때 알림”과 “한국어 설정일 때 알림”을 한 프로그램에서 나란히 시험하기 어려워요.

그래서 많은 경우 하나만 만들어서 필요한 곳에 건네주는 방법이 더 깔끔해요. 프로그램 맨 처음에 설정을 하나 만들고, 화면과 알림이 만들어질 때 그 설정을 넘겨주는 거예요.

[설정]을 설계하자:
    '언어'를 [문자열]인 "한국어"로 정하자

(참고: 설정을 직접 만들지 않고, 만들어질 때 건네받아요)
[화면]을 설계하자:
    '설정'을 [설정]인 비어있음으로 정하여 숨기자

    처음 만들어질 때 ([설정]인 '설정') 다음과 같이 하자:
        '나'의 '설정'을 '설정'으로 정하자

    <언어바꾸기>를 만들자 ([문자열]인 '새언어'):
        '나'의 '설정'의 '언어'를 '새언어'로 정하자

    <보여주기>를 만들자 ():
        틀"[화면] 지금 언어: {'나'의 '설정'의 '언어'}"를 출력하자

[알림]을 설계하자:
    '설정'을 [설정]인 비어있음으로 정하여 숨기자

    처음 만들어질 때 ([설정]인 '설정') 다음과 같이 하자:
        '나'의 '설정'을 '설정'으로 정하자

    <보여주기>를 만들자 ():
        틀"[알림] 지금 언어: {'나'의 '설정'의 '언어'}"를 출력하자

(참고: 프로그램 맨 처음에 하나만 만들어서 나눠 줘요)
'공용설정'을 새로운 [설정]()으로 정하자
'첫화면'을 새로운 [화면]('공용설정')으로 정하자
'알림창'을 새로운 [알림]('공용설정')으로 정하자

'첫화면'의 <언어바꾸기>("English")를 실행하자
'첫화면'의 <보여주기>()를 실행하자
'알림창'의 <보여주기>()를 실행하자

실행 결과
[화면] 지금 언어: English
[알림] 지금 언어: English

결과는 싱글톤과 같아요. 하지만 이번에는 [알림]이 “나는 만들어질 때 설정을 받아요”라고 겉으로 드러내고 있어요. 시험할 때는 다른 설정을 만들어 넘겨주기만 하면 되고요.

어느 쪽을 쓸까요?
넘겨줄 곳이 몇 군데 안 된다면 건네주기가 더 읽기 쉽고 고치기도 쉬워요. 프로그램 아주 깊은 곳까지 넘겨줘야 해서 모든 생성자에 설정이 붙는 게 오히려 번거롭다면, 그때 싱글톤을 쓰세요.

언제 쓰면 좋을까요?

  • 프로그램 전체에 정말로 하나만 있어야 하는 것이 있을 때 (설정, 기록장, 연결 하나를 나눠 쓰는 관리자)
  • 그 하나를 여러 곳에서 함께 써야 하는데, 일일이 넘겨주기 어려울 때
  • 만드는 데 시간이 걸려서, 필요할 때 한 번만 만들고 싶을 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
어디서 가져가든 같은 객체라서 값이 어긋나지 않아요 어디서든 바꿀 수 있어서 누가 바꿨는지 따라가기 어려워요
실수로 두 번째를 만들면 바로 알려 줘요 쓰는 쪽 코드에 연결이 숨어 있어요
필요할 때 처음 한 번만 만들어요 하나뿐이라서 다른 값으로 바꿔 끼워 시험하기 어려워요

너무 이른 패턴은 독이에요.
“지금은 하나만 쓰니까”는 싱글톤을 쓸 이유가 아니에요. 하나만 만들어서 넘겨주면 충분한 경우가 훨씬 많아요. 둘 이상 생기면 틀리는 것일 때, 그리고 넘겨주기가 정말 번거로울 때만 싱글톤을 쓰세요.

다른 패턴과의 관계

  • 팩토리 패턴과 함께 쓸 수 있어요. 공장이 설정 같은 값을 기억해야 한다면, 공장 하나를 싱글톤으로 두고 모두가 그 공장에 부탁하게 만들어요.
  • 퍼사드 패턴도 하나만 있으면 충분할 때가 많아요. 복잡한 기능 묶음 앞에 세우는 창구는 보통 하나라서, 싱글톤으로 만드는 경우가 있어요.
  • 옵저버 패턴과 자주 만나요. 프로그램 전체의 소식(로그인, 설정 변경)을 내는 객체를 싱글톤으로 두고, 여러 곳에서 구독하게 해요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.