- 싱글턴: 객체의 인스턴스가 오직 1개만 생성되는 패턴을 의미
ex) 함수와 같은 stateless Object(무상태 객체), 설계상 유일해야 하는 시스템 컴포넌트- stateless Object(무상태 객체): 인스턴스 변수가 없는 객체
1 2 3 4 5
public class Car { void Car() { System.out.println("I'm a car!"); } }
아래와 같이 컴파일 타임에 정의되어 있고 변경되지 않는 상수도 stateless Object이다.
1 2 3 4 5 6 7
public class Car { static final String MESSAGE = "I'm a car!"; void Car() { System.out.println(MESSAGE); } }
stateless Object와 연관된 헷갈릴 수 있는 개념으로 Immutable Object가 있다. 객체 지향 프로그래밍에서 Immutable Object란 상태를 바꿀 수 없는 객체이다.
1 2 3 4 5 6 7 8 9 10 11
public class Car { private static String message; void Car(String message) { this.message = message; } public void printMessage() { System.out.println(message); } }
이와 같이 불변 객체는 상태가 한번 지정되면 바뀔 수 없지만 컴파일 시점에 값이 정의되는 것이 아니기때문에 stateless Object는 아니다.
- stateless Object(무상태 객체): 인스턴스 변수가 없는 객체
클래스를 싱글턴으로 만들면 클라이언트 코드를 테스트하기가 어렵다. 싱글톤이 인터페이스를 구현한게 아니라면 싱글턴 인스턴스를 mock 객체 구현으로 대체할 수 없기 때문이다.
- Mock 객체: 실제 객체를 다양한 조건으로 인해 제대로 구현하기 어려울 경우 가짜 객체를 만들어 사용하는데, 이를 Mock 객체라 한다.
Mock 객체가 필요한 경우,- 테스트 작성을 위한 환경 구축이 어려운 경우
- 테스트가 특정 경우나 순간에 의존적인 경우
- 시간이 걸리는 경우
싱글턴을 만드는 방식은 두가지가 있으며 공통점이 있다.
- 생성자는 private으로 감춰둔다.
- 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 마련해둔다.
1. public static final 필드 방식의 싱글턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package item03;
public class Singleton1 {
//instance는 static이니까 클래스가 로딩될 시점에 한번만 초기화됨.
//초기화될 때 new를 사용하기 때문에 그때 처음 만들어진 instance가 계속해서 재사용하게됨
public static final Singleton1 instance = new Singleton1();
static int cnt;
private Singleton1() {
cnt++;
if(cnt != 1){//리플렉션을 막기 위한 방법
throw new IllegalStateException("this object should be singleton");
}
}
}
public static final 필드인 Singleton1.instance 초기화 할 때 private 생성자는 딱 한번만 호출되고 Singleton1은 싱글턴이 된다.
예외로는 리플렉션 API를 사용해 private 생성자를 호출할 수 있지만 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지면 이러한 공격에 대한 방어가 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package item03;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class SingleTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//private이므로 Singleton1 밖에서 생성 불가
//Singleton1 singleton1 = new Singleton1();
Singleton1 singleton2 = Singleton1.instance;
System.out.println(singleton2); //item03.Singleton1@4eec7777 - 주소 잘 가져옴
//아래와 같은 방식으로 리플렉션 API를 이용하여 private 생성자 호출 가능. But
Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton1 singleton3 = constructor.newInstance(); //Caused by: java.lang.IllegalStateException: this object should be singleton - 에러발생
System.out.println(singleton3);
}
}
public static final인 instance를 호출하는 singleton2는 정상적으로 객체를 잘 가져오지만,
리플렉션 API를 활용하여 private 생성자를 직접 호출 하는 singleton3는 private 생성자에 추가된 리플렉션 방어 로직에 의하여 this object should be singleton
에러가 발생한다.
장점
- static 팩토리 메소드를 사용하는 방식보다 간단하다.
- 해당 클래스가 싱글턴임이 API에 명백히 드러난다.
2. static 팩토리 메소드 방식의 싱글턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package item03;
public class Singleton2 {
private static final Singleton2 instance = new Singleton2();
private Singleton2(){
}
public static Singleton2 getInstance(){
return instance;
}
}
장점
- public static factory 메소드를 호출하는 클라이언트 코드를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
- 처음엔 싱글턴을 쓰다가 나중에 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.
- static factory를 Generic 싱글턴 팩토리로 만들 수 있다.
- static factory method 참조를
Supplier<Singleton2>
와 같이 공급자로 사용할 수 있다.public static Singleton2 getInstance()
자체가 Supplier 인터페이스의 구현체처럼 쓰일 수 있는 메소드다
interface Supplier
JAVA 8부터 등장한 개념인 Supplier란 메서드를 ‘get()’ 하나만 가지고 있고 아무타입이나 리턴하는(T) 인터페이스이다.
1
2
3
4
5
6
7
public class supplierExam {
public static void main(String[] args){
final Supplier<String> helloSupplier = () -> "Hello";
System.out.println(helloSupplier.get() + "World!");
}
}
위는 supplier 예제 소스코드이다. 스트링 타입의 Supplier를 helloSupplier라는 이름으로 생성하여 “Hello” 값을 담는다.
그리고 나중에 “Hello”라는 값을 얻고 싶을 때 helloSupplier.get()
메소드를 사용하여 호출 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package item03;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class supplierExam {
public static void main(String[] args){
long start = System.currentTimeMillis();
printIfValidIndex(0, ()-> getVeryExpensiveValue());//3초
printIfValidIndex(-1, ()-> getVeryExpensiveValue());//0초
printIfValidIndex(-2, ()-> getVeryExpensiveValue());//0초
System.out.println("It took "+((System.currentTimeMillis() - start) / 1000) + " seconds."); //9초 -> 3초
}
//아래구문이 많은 CPU 연산을 필요로 한다고 가정해보자!
private static String getVeryExpensiveValue(){
try{
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
return "Kevin";
}
private static void printIfValidIndex(int number, Supplier<String> valueSupplier){
if(number>=0){
System.out.println("The Value is"+valueSupplier.get()+".");
}else{
System.out.println("Invalid");
}
}
}
getVeryExpensiveValue가 굉장히 많은 CPU 연산을 필요로 한다고 가정해보자. main에서 printIfValidIndex 메소드 호출 시 첫 번쨰 number가 0인 경우를 제외하고 -1, -2인 경우는 getVeryExpensiveValue를 수행할 필요가 없다.
왜냐? number가 0 이상일 때에만 valueSupplier 값이 필요하기 때문이다.
getVeryExpensiveValue가 많은 연산을 필요로 하지 않는다면 불필요하게 호출된다해도 큰 차이는 없겠지만, 그 반대의 경우 Supplier를 활용하여 매우 유용하게 쓸 수 있다.
- int형의 number, valueSupplier는 빈 값을 주어 printIfValidIndex 호출 (Supplier는 입력 매개변수가 필요 없음)
- printIfValidIndex 메소드 내부 로직 수행
2-1. number가 0인 경우, valueSupplier.get()을 통해 getVeryExpensiveValue 수행됨.
2-2. number가 0미만인 경우, 출력 후 종료 (getVeryExpensiveValue 수행하지 않음)
3. Enum 방식의 싱글턴
직렬화/역직렬화 할 때 코딩으로 문제를 해결할 필요도 없고, 리플렉션으로 호출되는 문제도 고민할 필요도 없고, 코드도 간결한 방법이 있다.
1
2
3
4
5
6
7
public enum Singleton3 {
INSTANCE;
public String getNmae(){
return "Ara";
}
}
위 처럼 선언한 뒤 객체를 가져올 때 아래와 같이 가져다 쓰면 된다.
1
String name = Singleton3.INSTANCE.getNmae();
코드는 좀 불편하게 느껴지지만 싱글턴을 구현하는 최선의 방법이다. 하지만 이 방법은 Enum말고 다른 상위 클래스를 상속해야한다면 사용할 수 없다. 그러나 인터페이스는 구현할 수 있다.