싱글턴 논의는 객체를 하나만 만드는 문제와 그 객체를 안전하게 공개하고 종료하는 문제를 분리해서 시작해야 합니다. 인스턴스 수가 하나여도 내부 증가가 원자적이라는 뜻은 아니며, 반대로 공유할 필요가 없는 설정은 조립 지점에서 주입하는 편이 수명과 테스트를 더 잘 드러냅니다.
Singleton 가변 상태와 원자적 증가
thread A와 thread B가 singleton.counter=10을 각각 읽고 1을 더한 뒤 저장하면, 두 증가를 수행했는데도 최종값은 11입니다. 이 read-modify-write는 읽기·계산·쓰기를 묶은 하나의 동작이 아니므로, 인스턴스가 하나라는 사실만으로 원자성이 생기지 않습니다. 반대로 불변 설정의 여러 복사본은 공유 변경이 없어서 안전할 수 있으므로, 유일성·전역 접근·가변 상태를 같은 문제로 취급하지 않아야 합니다.
프로세스 안에서 한 객체를 만들고 필요한 곳에 전달하면 전역 locator 없이 공유할 수 있습니다.
상태를 따라가면 첫 접근 두 개가 동시에 들어올 때 create → fully initialized → publish가 한 번만 일어나야 하고, 그 뒤 counter++는 별도의 원자성 계약을 가져야 합니다. 생성이 파일 열기에서 실패하면 실패를 캐시할지 다음 접근에서 재시도할지 정한 뒤, 재시도 시 이미 생성된 외부 효과를 중복 적용하지 않는지 확인합니다. 종료 때는 새 작업 접수를 멈추고 진행 중 작업을 끝낸 다음 pool과 logger를 닫아, “객체가 존재한다”와 “사용 가능한 수명이다”를 혼동하지 않습니다.
간단한 검증은 첫 접근을 여러 thread에서 동시에 실행하고 인스턴스 수·field 초기화·예외 후 다음 접근 결과를 기록하는 것입니다. 그 시험은 언어 구현의 모든 메모리 모델을 증명하지 않으므로, Java/C++의 보장과 직접 작성한 동기화 코드를 별도로 심사해야 합니다. 다른 process·class loader 등은 또 다른 범위이므로 process singleton이 분산 시스템의 유일한 소유권을 만들지 않습니다.
완전 초기화와 안전한 객체 공개
첫 접근이 겹칠 때 중복 생성이나 반쯤 초기화된 객체 노출을 막으려면 언어가 검증한 static/lazy 초기화 기능을 우선합니다. 직접 double-checked locking을 작성하려면 atomic/volatile, 메모리 순서, 객체 수명에 관한 언어별 전제를 모두 맞춰야 하므로, 하나라도 빠지면 안전한 공개가 되지 않습니다. 생성 중인 this를 callback이나 전역 저장소에 넘기지 않아야 다른 코드가 불완전한 객체를 보지 않습니다.
그림을 글로 읽기
- 동시 첫 접근·초기화
- 완성 후 안전한 공개
- 내부 변경 동기화·snapshot
- 사용 작업 종료·자원 close
- 동시 첫 접근·초기화 → 완성 후 안전한 공개: 실패 정책 확인
- 완성 후 안전한 공개 → 내부 변경 동기화·snapshot: 읽기·쓰기 계약
- 내부 변경 동기화·snapshot → 사용 작업 종료·자원 close: 명시적 owner
생성 예외와 초기화 재시도 정책
C++ 함수 local static은 초기화 함수가 예외로 끝나면 초기화 완료로 기록되지 않아 이후 접근에서 다시 시도할 수 있습니다. 반면 Java class initialization이 실패하면 해당 class가 erroneous 상태가 되고, 같은 class loader에서 단순 접근할 때마다 초기화가 성공할 때까지 반복하는 모델이 아닙니다. 별도 lazy wrapper를 쓰면 실패를 캐시하는지 다시 시도하는지 정책이 달라질 수 있으므로, 장애 시 다음 접근의 결과를 구현별로 확인해야 합니다.
재시도되는 초기화가 외부 파일·network 효과를 이미 수행했다면 중복 효과를 처리해야 합니다. 초기화 함수의 자기 재진입·서로 의존하는 singleton cycle은 deadlock·잘못된 재귀 등 문제를 만들 수 있어 의존을 끊고 실패 상태를 명시합니다. 느린 I/O를 숨겨 첫 사용자 요청의 지연을 갑자기 키우지 않습니다.
| 단계 | 별도 설계 |
|---|---|
| 생성 | 한 번 또는 명시적 재시도·부분 자원 정리 |
| 공개 | 완전 초기화와 memory visibility |
| 변경 | lock·atomic·불변 snapshot 교체 |
| 종료 | 사용자가 먼저 종료·자원 나중 close |
| 테스트 | 새 객체 그래프·전역 상태 누수 차단 |
공유 객체 Owner와 조립 지점의 수명 관리
GlobalConfig.instance()를 모든 정책이 직접 찾으면 테스트 설정·초기화 순서·종료 의존이 숨습니다. 시작점에서 만들어 주입하고 요청별 주체·취소 상태는 요청 수명으로 둡니다. 테스트마다 전역을 reset하면 병렬 test와 충돌할 수 있어 새 객체 그래프가 더 명확합니다.
종료 경로에서는 작업 정리→pool close→log flush 순서를 실제로 실행해, 이미 닫힌 logger를 호출하는 일이 없는지 확인합니다. 정상 초기화·동시 접근·생성 실패·cycle·설정 교체·shutdown을 각각 실제 경로로 실행하고, 생성 실패에서는 다음 접근이 재시도되는지까지 봅니다. 이 노트는 언어와 수명 계약을 설명하며 해당 lazy 실패 예제를 이 작업에서 별도로 실행한 결과는 아닙니다.