싱글턴이란

class Logger {
  void print()
  static getInstance()
}

var logger = Logger.getInstance()
logger.print('123')

싱글턴은 해당 클래스의 오브젝트가 하나만 존재해야 하는 경우를 말합니다.
한 클래스의 인스턴스가 여러 개 만들어지면 안 된다는 말입니다.

오브젝트를 하나만 유지하는 방법은 언어마다 환경마다 다릅니다.
광역 변수에 넣어 쓸 수도 있고, static instance() 메서드를 사용할 수도 있습니다.
어노테이션으로 DI 엔진에서 필요한 곳에 알아서 넣도록 할 수도 있습니다.
언어에서 아예 지원하는 경우도 있습니다.
해서 싱글턴을 만드는 디테일은 중요하지 않습니다.
오브젝트가 하나만 존재한다는 사실이 중요합니다.

광역 펑션을 쓰면 되지 않을까

func log() { ... }
log('123')

그런데 굳이 싱글턴을 써야 할까요?
그냥 광역 펑션을 쓰면 되지 않을까요?
만들기도 쉽고 퍼포먼스도 좋을 것 같습니다.

하지만 이러면 문제가 발생합니다.
비즈니스 로직에 광역 펑션에 대한 의존성이 생기기 때문입니다.
비즈니스 로직은 이런 디테일에 의존성이 생기면 안 됩니다.

다형성

interface ILogger {
  void print()
}

class FileLogger : ILogger {
  void print()
  static getInstance()
}

class MemoryLogger : ILogger {
  void print()
  static getInstance()
}

class UseCase {
  void setLogger(ILogger)
}

var u1 = new UseCase()
u1.setLogger(FileLogger.getInstace())

var u2 = new UseCase()
u2.setLogger(MemoryLogger.getInstance())

비즈니스 로직이 저레벨 펑션에 의존성을 갖지 않게 만들려면 적당히 경계를 만들어야 합니다.
경계를 긋는 방법은 언어마다 다릅니다.
OOP에서는 오브젝트의 다형성을 사용합니다.
펑셔널 언어에서는 함수를 사용합니다.

OOP에서는 서로 다른 클래스들이 비슷하게 동작하는 경우가 많습니다.
이것을 다형성, 폴리모피즘이라고 합니다.

다형성을 구현하는 방법은 OOP 언어마다 조금씩 다릅니다.
보통 인터페이스나 추상 클래스를 매계로 계약 관계를 만듭니다.
계약 관계가 만들어지면 다양한 클래스의 오브젝트를 적당히 넣을 수 있습니다.

OOP에서는 다형성의 단위가 오브젝트입니다.
해서 다형성의 장점을 사용하려면 무엇이든 오브젝트로 묶어야 합니다.
오브젝트로 묵어야 하니 날쌩 광역 펑션을 사용하지 못합니다.
이때 사용하는 것이 싱글턴입니다.

OOP에서 싱글턴은 광역 자원의 의존성 역전의 단위가 됩니다.
이렇게 싱글턴 + 의존성 역전으로 금을 그어 놔야 코드 관리가 쉬워집니다.
테스트 돌리기도 쉬워집니다.