1. Singleton 패턴이란?
객체의 인스턴스를 최초 1번만 생성하고 이후에는 기존 인스턴스를 호출해서 사용하는 패턴을 말한다.
우린 일반적으로 인스턴스를 생성할 때 new 연산자를 사용하는데, 이 때 메모리에 객체의 인스턴스를 할당하게 된다.
물론 최초 1번의 할당까지는 아무런 문제가 되지 않는다.
하지만 여러 곳에서 필요할 때마다 인스턴스를 생성하고 호출한다면,
이는 메모리를 엄청나게 낭비하는 '비효율적인' 행위가 될 것이다.
따라서 객체가 유일해야할 경우에 싱글톤 패턴을 사용하여 기존에 생성된 인스턴스를 호출해서 사용한다.
2. 언제 사용하는 것이 적절할까?
싱글톤을 적용하지 않고 필요할 때마다 객체를 생성해서 사용한다면 매 생성마다 메모리를 할당받게 되고, 이는 성능 저하를 일으킨다.
하지만 싱글톤 패턴을 사용한다면 기존에 할당받은 메모리를 사용하므로 메모리 낭비를 방지할 수 있게 된다.
그리고 또 다른 이유는 안드로이드 개발자의 관점에서 예시를 들어보겠다.
1. 음악 리스트를 불러오는 작업 도중에 MusicRepository 객체를 새로 생성해서 다시 불러온다면 첫 항목부터 다시 불러오게되는 문제가 생길 수 있다.
2. MediaPlayer로 음악을 재생하는 도중에 새로운 MediaPlayer 객체를 생성한다면, A 음악과 B 음악이 동시에 재생되는 문제가 생긴다.
이와 같이 객체가 유일해야하는 상황에도 유용하게 사용할 수 있다.
3. 여러 방식의 Singleton 패턴에 대하여
가장 기본적인 싱글톤 패턴의 구현 로직은 아래와 같다.
(1) 기본 생성자를 private으로 만들고,
(2) static final 인스턴스 변수를 선언하고, 초기화한다
(3) 이후 해당 클래스의 인스턴스가 필요할 때마다 해당 메서드로 불러 사용한다.
위와 같이 간단해 보이는 싱글톤의 구현에도 여러가지 방법이 있다.
기본형, DCL, Static initialize, Lazy Holder 등 이번 포스팅에서 소개할 유형은 5가지이다.
(1) 기본형
가장 기본적인 싱글톤 구현 방법이다.
구현이 간단하고 직관적이라는 장점이 있으나, 비동기 작업 수행 시 인스턴스가 1개로 보장되지 않는다.
그런 경우가 얼마나 된다고 번거롭게 LazyHolder, DCL 같은 다른 방법을 쓰냐는 의문이 들 수 있다.
하지만 코드는 원래 '만에 하나'라는 예외적인 케이스도 존재하지 않도록 짜야 한다.
소를 잃어버리기 싫다면 외양간을 완벽한 상태로 유지하면 될 것이다.
(2) Syncronized
Syncronized는 클래스의 인스턴스에 lock을 건다.
A스레드가 Z라는 인스턴스를 사용중이면 B스레드는 인스턴스에 접근할 수 없도록 lock을 건다.
여기까지만 들으면 정말 간단하고 좋은 방법 아닌가요? 할 수 있지만
만약 A 스레드가 인스턴스를 사용중일때 7개의 스레드가 해당 인스턴스에 접근하려고 한다면
각각의 스레드에 대해 lock / unlock 처리를 반복처리 해야하므로 성능이 굉장이 떨어지게 된다.
실제로 Syncronized 사용시 다른 방법에 비해 성능이 최대 몇십배 저하되는 경우도 있다고 한다.
(3) Syncronized를 이용한 Thread-safe
.java파일은 컴파일러를 거쳐 .class 파일이 되고,
이후 클래스 로더에서 읽어들인 뒤 메모리에 올라가서
이후 JVM에서 사용할 수 있게 된다.
이번 3번째 방법은 해당 원리를 이용한 방식으로, 클래스가 로딩됨과 동시에 static 객체가 생성되어
객체를 생성하는 시점을 개발자가 정하지 않고, 불러서 쓰기만 하면 된다.
Thread-safe하며 객체 생성에 신경 쓸 필요가 없다는 것이 장점이지만,
해당 클래스를 호출하지 않아도 static 객체가 생성되어 메모리에 상주하기 때문에 비효율적이라는 단점이 있다.
(4) DCL (Double Checking Locking)
getInstance 메소드에서 인스턴스 null 체크를 한 후, null인 상태에서만 동기화한다.
synchronized 블록 안에 null 체크 구문이 하나 더 있는 이유는
만약 서로 다른 두 개의 스레드가 동시에 getInstance에 접근하는 경우에 첫 null 체크 구문은 둘 다 통과하지만,
이후 syncronized 블럭 안으로 들어가면 먼저 들어간 스레드에게만 인스턴스에 대한 접근 권한이 주어지게 하기 위함이다.
syncronized는 큰 성능 저하를 야기하기 때문에 그 전에 null 체크 구문을 한겹 입힘으로써 1차적으로 필터링이 된다.
그리고 변수 선언부에 volatile static으로 선언된 변수가 있는데,
volatile이 붙은 변수는 CPU의 캐시를 거치지 않고 메인 메모리에 직접 read/write를 수행하기 때문에 이를 통해 캐시 관련 문제를 회피하기 위함이다.
하지만 volatile 또한 syncronized와 동일하게 동작하므로 성능 저하를 야기한다.
(5) Lazy Holder
LazyHolder 기법은 현재 싱글톤 구현에 있어 가장 많이 쓰이는방법인데, syncronized의 단점을 보완하면서 Thread-safe하다.
LazyHolder 클래스에서 선언하면서 생성했지만,
스레드에서 Singleton 클래스에 인스턴스를 요청하기 전까지는 JVM에 올라가지 않는다.
앞선 3번 방식과 얼추 비슷해보일 수 있으나, LazyHolder클래스가 이너 클래스이기 때문에
로드타임 동적 로딩(3번)이 아닌 런타임 동적 로딩이다.
로드타임 로딩 : 클래스 내부에 다른 클래스 요소가 있다면 모두 로딩
런타임 로딩 : 클래스 정보에 접근 요청이 오면 로드
게다가 여러 스레드에서 동시에 요청하는 경우에도 JVM이 static 변수의 유일성을 보장해주기 때문에 동기화할 필요도 없다.
4. 싱글톤 패턴의 문제점
가장 큰 문제는 클래스간의 결합도가 높아진다는 것이다.
나도 싱글톤을 공부하고 실무에 적용한지 얼마 안된 시점에 남용했던 적이 있는데,
싱글톤 인스턴스가 지나치게 많은 데이터를 컨트롤해서 코드 수정할 때 번거로웠던 기억이 난다.
이외에도 싱글톤에는 여러 문제점이 있는데,
- 아무 객체나 자유롭게 접근하고 수정할 수 있는 전역적인 상태를 가지게 되는 것은 객체 지향에서 지양해야하는 포인트다.
- private 생성자를 가지기 때문에 상속이 불가능하다. (다형성 위배)
- 서버 환경에서는 싱글톤 객체의 유일성이 보장되지 않는다. (!!reflection!!)
그래서 싱글톤에 대한 좋은 글도 많은 반면, 싱글톤은 '안티패턴'이라고도 불린다.
나도 이 포스팅을 적으면서 싱글톤에 대한 지식을 복기할 수 있었다. 싱글톤은 객체 지향 프로그래밍을 갉아먹기 때문에
아무리 구현이 쉽고 전역적인 상태 관리가 편하다 해도 실무에서 제대로 사용하려면 깊게 공부하고 적용해야할 것 같다.
'개발 > Kotlin' 카테고리의 다른 글
[Kotlin] 테스트 코드 (2) - TDD/BDD와 디자인 패턴 (0) | 2023.07.02 |
---|---|
[Kotlin] 테스트 코드 (1) - 테스트 코드란? (0) | 2023.06.28 |
[안드로이드] 테스트 코드 - JUnit의 예외 처리 (expected, assertThrows, doThrow) (0) | 2023.06.26 |
연산자 개수에 따른 차이(&, |와 &&,||의 차이) (0) | 2022.07.25 |
Serializable Parcelable 차이 (0) | 2022.02.22 |