자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. ex) InputStream, OutputStream, DB Connection ...

item8에서 알 수 있듯이 안전망으로 finalizer를 활용할 수 있으나, finalizer는 확실하게 믿을 순 없다

 

이를 보완하기 위해 try-with-resources를 사용하도록 하자

// 둘 이상의 자원에서 try-finally: 중첩 되어 지저분해 보이고, exception이 일어난 경우 디버깅이 어렵다
static void copy(String src, String dst) throws IOException {
	InputStream in = new FileInputStream(src);
	try{
		OutputStream out = new FileOutputStream(dst);
		try{
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while((n=in.read(buf))>=0){
				out.write(buf, 0, n);
			}
		} finally {
			out.close();
		}
	} finally {
		in.close();
	}
}

위 예시에서 예외는 try 블록과 finally 블록 모두에서 발생할 수 있다.

예를 들어 FileOutputStream 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패할 때,

두 번째 예외가 첫 번째 예외를 완전히 집어삼켜 버린다. => 스택 추적 내역에 첫 번째 예외에 관한 정보X

 

// try-with-resources: 자원을 회수하는 올바른 선택, 코드는 더 짧고 분명해지며, 예외 케이스도 유용하게 볼 수 있다.
static void copy(String src, String dst) throws IOException {
	try(
			InputStream in = new FileInputStream(src);
			OutputStream out = new FileOutputStream(dst);
	){
			byte[] buf = new byte[BUFFER_SIZE];
			int n;
			while((n=in.read(buf))>=0){
				out.write(buf, 0, n);
			}
	}
}

Java7 부터 Closeable리소스 와 관련하여 아래와 같이 키워드 뒤의 괄호 안에 리소스를 만들 수 있다.

try (initialize resources here) {
   ...
}

그리고 코드 블록이 완료되면 자동으로 닫히기 때문에 finally가 필요 없다 .

 

이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다

AutoCloseable은 단순히 void를 반환하는 close 메서드 하나만 덩그러니 정의한 인터페이스다.

이 경우는 try-finally와는 다르게 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 맨 처음에 발생한 예외가 기록된다.

단, 이렇게 숨겨진 예외들도 그냥 버려지지는 않고, 스택 추적 내역에 ‘숨겨졌다 (suppressed)’는 꼬리표를 달고 출력된다.

 

또한, 자바 7에서 Throwable에 추가 된 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있다.

catch (Throwable e) { 
	Throwable[] suppExe = e.getSuppressed(); 
	for (int i = 0; i < suppExe.length; i++) { 
		System.out.println("Suppressed Exceptions:"); 
		System.out.println(suppExe[i]); 
	} 
} 


보통의 try-finally에서처 럼 try-with-resources에서도 catch 절을 쓸 수 있다. catch 절 덕분에 try 문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있다.

 

 

참고로, 스트림을 닫을 때에는 IOUtils를 쓰는 방법도 있다. (대신 이 방법을 사용할 경우 Exception이 제대로 나오지 않음)

commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/IOUtils.html#closeQuietly(java.io.InputStream)

 

IOUtils (Apache Commons IO 2.8.0 API)

Skips characters from an input character stream. This implementation guarantees that it will read as many characters as possible before giving up; this may not always be the case for skip() implementations in subclasses of Reader. Note that the implementat

commons.apache.org

closeQuietly는 Closeable의 varargs 타입을 파라미터로 받고, 이 closeable들을 모두 닫는다.


 

  • try-catch-finally
package com.example.sypark9646.item09;

import static org.apache.tomcat.util.http.fileupload.IOUtils.copy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class TryCatchFinallyTest {

		private static String root =
				"/Users/soyeon/Documents/GitHub/Effective_Java_Study/sypark9646/src/main/java/com/example/sypark9646/item09";
		private static String inputFileName = root + "/testin.txt";
		private static String outputFileName = root + "/testout_try_catch_finally.txt";

		public static void main(String[] args) throws IOException {

				try {
						final InputStream in = new FileInputStream(inputFileName);
						try {
								final OutputStream out = new FileOutputStream(outputFileName);
								try {
										copy(in, out);
										out.flush();
								} finally {
										out.close();
								}
						} finally {
								in.close();
						}
				} catch (IOException exc) {
						throw new IOException(exc);
				}
		}
}

 

  • try-with-resources
package com.example.sypark9646.item09;

import static org.apache.tomcat.util.http.fileupload.IOUtils.copy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class TryWithResourcesTest {

		private static String root =
				"/Users/soyeon/Documents/GitHub/Effective_Java_Study/sypark9646/src/main/java/com/example/sypark9646/item09";
		private static String inputFileName = root + "/testin.txt";
		private static String outputFileName = root + "/testout_try_with_resources.txt";

		public static void main(String[] args) throws IOException {

				try (
						final InputStream in = new FileInputStream(inputFileName);
						final OutputStream out = new FileOutputStream(outputFileName);
				) {
						copy(in, out);
						out.flush();
				} catch (IOException exc) {
						throw new IOException(exc);
				}
		}
}

자바는 두 가지 객체 소멸자는 finalizer와 cleaner가 있다.

이때, finailzer는 예측할 수 없고, 상황에 따라 위험할 수 있어 쓰지 않는 것이 좋다. (오동작, 낮은 성능, 이식성 문제)

cleaner 또한 finalizer 보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

 

1) finalizer와 cleaner는객체에 접근을 할 수 없게 된 후, 이 것이 실행되기까지 얼마가 걸릴지 알 수 없다. 심지어 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다.

=> 즉, 제때 실행되어야 하는 작업은 절대 할 수 없다. (ex. file close, db 공유자원의 영구 lock 해제 등의 인스턴스 자원 회수)

왜냐면 finalizer와 cleaner은 GC에 의해 실행되고, GC 알고리즘마다 다르게 실행되기 때문이다.

 

자바 언어 명세는 어떤 스레드가 finalizer를 수행할지 명시하지 않는 반면, cleaner는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금 낫다. 하지만 여전히 백그라운드에서 수행되며 가비지 컬렉터의 통제하에 있으니 즉각 수행되리라 는 보장은 없다.

 

item07에서 예시로 사용해 보았던

`System.gc` 와 `System.runFinalization` 또한 gc를 건드려서 finalizer와 cleaner가 실행 될 가능성을 높여줄 수는 있으나, 보장해주진 않는다.

(단, `System.runFinalizersOnExit`와 그 쌍둥이 인 `Runtime. runFinalizersOnExit` 다. 하지만 이 두 메서드는 심각한 결함 때문에 수십 년간 지탄받아 왔다.[ThreadStop])

  • System.gc
    • 명시적으로 가비지 컬렉션이 일어나도록 하는 코드이다
    • 모든 스레드가 중단되기 때문에 코드단에서 호출하면 안된다
  • System.runFinalization
    • method는 종료 대기중인 모든 개체의 종료 방법을 실행하지만, 언제 실행될지/실행될지 여부는 보장하지 못한다
  • System.runFinalizersOnExit & Runtime.runFinalizersOnExit
    • 프로세스가 끝날 때 수거하지 않은 개체를 수거하도록 설정한다

 

 

2) finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남았더라도 그 순간 종료된다

=> 만약 다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없다.

일반적으로는 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력하지 않는다.

c.f.) cleaner는 자신의 스레드를 통제하기 때문에 이 문제에 대해서는 괜찮다.

 

 

3) finalizer와 cleaner는 심각한 성능 문제도 동반한다

=> finalizer가 가비지 컬렉터의 효율을 떨어뜨리기 때문이다

 

 

4) finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.

finalizer 공격 원리는 생성자나 직렬화 과정 (readObject와 readResolve 메서드)에서 예외가 발생하면,

이 생성이 되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다.

=> 이 경우 finalizer는 정적 필드에 자신의 참조를 할당하여 가비 지 컬렉터가 수집하지 못하게 막을 수 있다.

해결 방법1 : 클래스를 final로 만들자(final 클래스들은 그 누구도 하위 클래스를 만들 수 없기 때문)
해결 방법2: final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언

 

 


finalizer나 cleaner를 대신 할 수 있는 방법은 무엇이 있을까?

 

AutoCloseable 을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다

(일반적으로 예외가 발생해도 제대로 종료되도록 try-withresources를 사용해야 한다. 아이템 9 참조).

추가적으로 각 인스턴스는 자신이 닫혔는지 추적하는 것이 좋다. 즉, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지록 하자.


그렇다면 finalizer이나 cleaner를 써야하는 경우는 언제일까?

1) 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할로서 사용할 때

cleaner나 finalizer가 즉시 (혹은 끝까지) 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다는 낫기 때문이다. 그래도 이런 안전망 역할 의 finalizer를 작성할 때는 그럴만한 값어치가 있는지 생각해 보아야 한다.

 

ex) 안전망 역할의 finalizer: FileInputStream, FileOutputStream, ThreadPoolExecutor

(finalize는 java9부터 deprecated 되어서 java11을 사용하는 나는 메소드 내부는 볼 수 없었다)

  • FileInputStream & FileOutputStream: Ensures that the `close` method of this file stream is called when there are no more references to it.

  • ThreadPoolExecutor: Invokes shutdown when this executor is no longer referenced and it has no threads.

@Override
protected void finalize() throws Throwable {
	try {
		... // cleanup subclass state
	} finally {
		super.finalize();
	}
}

기본적으로는 deprecated된 Object.finalize()를 위와 같이 오버라이드 하고 있다.

docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--

 

Object (Java SE 9 & JDK 9 )

Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. The general contract of fi

docs.oracle.com

 

 

c.f. finalizer 와 달리 cleaner는 클래스의 public API에 나타나지 않는다

// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스
public class Room implements AutoCloseable {
	private static final Cleaner cleaner = Cleaner.create();

	/* static으로 선언된 중첩 클래스인 State는 cleaner가 방을 청소할 때 수거할 자 원들을 담고 있다. */
	// 청소가 필요한 자원.절대 Room을 참조해서는 안 된다! 만약 참조한다면 순환참조가 생김
	private static class State implements Runnable {
		int numJunkPiles; // 방(Room) 안의 쓰레기 수 => 수거할 자원

		State(int numJunkPiles) {
			this.numJunkPiles = numJunkPiles;
		}

		// close 메서드나 cleaner가 호출한다. => cleanable에 의해 딱 한 번만 호출된다
		@Override
		public void run() {
			System.out.println("방 청소");
			numJunkPiles = 0;
		}

	}

	// 방의 상태. cleanable과 공유한다.
	private final State state;

	// cleanable 객체. 수거 대상이 되면 방을 청소한다. => Room 생성자에서 cleaner에 Room과 State를 등록할 때
	private final Cleaner.Cleanable cleanable;

	public Room(int numJunkPiles) {
		state = new State(numJunkPiles);
		cleanable = cleaner.register(this, state);
	}

	@Override
	public void close() {
		cleanable.clean();
	}

}

run 메서드가 호출되는 상황은 둘 중 하나다. 

1 - 보통은 Room의 close 메서드를 호출할 때이다. close 메서드에서 Cleanable의 clean을 호출하면 이 메서드 안에서 run을 호출

2 - 가비지 컬렉터가 Room을 회수할 때 까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State.run 메서드를 호출

 

이 때, State가 정적 중첩 클래스인 이유는, 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되기 때문에 GC가 회수할 수 없다.

이와 유사하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.

 

/* 올바른 예제 */
class Adult {
	public static void main(String[] args) {
		try (Room myRoom = new Room(7)) { // "안녕" "방청소" 순서대로 출력
			System.out.println("안녕〜");
		}
	}
}

/* 틀린 예제 - System.exit을 호출할 때의 cleaner 동작은 구현하기 나름이다. 청소가 이뤄질지는 보장 하지 않는다 */
public class Teenager {
	public static void main(String[] args) {
		new Room(99);
		System.out.println("아무렴"); // "아무렴" 출력 이후 아무 일도 일어나지 않는다 => 예측할 수 없는 상황
	}
}

 

 

2) 네이티브 피어와 연결된 객체에서 사용할 때(단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않은 경우)

  • 네이티브 피어(native peer)
    • 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체
    • 네이티브 피어는 자바 객체 가 아니니 가비지 컬렉터슨 그 존재를 알지 못한다. 그 결과 자바 피어를 회수 할 때 네이티브 객체까지 회수하지 못한다.

=> 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면 `close` 메서드를 사용해야 한다.

 

 

자바는 가비지 컬렉터를 가지고 있어서 다 쓴 객체를 알아서 회수 해 간다.

처음엔 나도 그래서 메모리 관리에 전혀 신경을 쓰지 않았는데, 직접 배포를 하고 메모리 사용률 등을 보니 그렇지 않다는 것을 알게 됐던 기억이 있다..

 

 

메모리 관리에 대한 내용을 알아보기 전에 자바의 가비지 컬렉터에 대한 내용을 먼저 알아두는 것이 좋다.

2021/02/13 - [Java (+ Spring)] - JVM과 Garbage Collector

 

JVM과 Garbage Collector

자바를 이용하여 개발하는 개발자라면 누구나 자바 바이트코드가 JRE 위에서 동작한다는 사실을 잘 알고 있습니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java V

sysgongbu.tistory.com

 

메모리 누수를 주의 깊게 보아야 하는 경우 아래 3가지 정도가 있다. = 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있는 경우

 

1) 아래 코드는 스택을 구현한 코드인데, pop()을 보면 스택에서 꺼내진 객체를 더 이상 사용하지 않는데도 회수하지 않고 있다.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    ...
    
    public Object pop() {
        if(size == 0){
            throw new EmpthStackException();
        }
        
        return elements[--size]; // 메모리 누수: elements[size] = null을 해 주어야
    }
}

가비지 컬렉터는 객체 참조 하나 뿐만 아니라, 이 객체가 참조하는/참조되는 모든 객체들을 회수하지 못하게 된다.

그래서 elements[size] = null 을 해 주는 부분이 필요하다. (이 부분을 추가할 경우 외부 영역 접근 시 NullPointerException을 던지게 됨으로써 오류를 조기에 발견할 수 있게 된다)

 

하지만, 모든 곳에 이 것을 해 줄 필요는 없다.

객체 참조를 꼭 null 처리 해 주어야 하는 경우는 위 스택 예제와 같이 자기 메모리를 직접 관리하는 클래스를 구현할 때이다.

위 스택 예제에서는 객체 자체가 아닌 객체 참조를 담는 elements 배열로 저장소 pool을 만들어 원소들을 관리한다.

 

 

 

2) 객체 참조를 캐시에 두고 계속 캐시에 두는 것 또한 메모리 누수를 일으키는 주범이다. 해결 방법은 아래와 같다.

- WeakHashMap을 사용하여 캐시를 만들면 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.

package com.example.sypark9646.item07;

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapTest {

		public static void main(String[] args) {
				MyKey k1 = new MyKey("Hello");
				MyKey k2 = new MyKey("World");
				MyKey k3 = new MyKey("Java");
				MyKey k4 = new MyKey("Programming");

				Map<MyKey, String> wm = new WeakHashMap<>();

				wm.put(k1, "Hello");
				wm.put(k2, "World");
				wm.put(k3, "Java");
				wm.put(k4, "Programming");
				k1 = null;
				k2 = null;
				k3 = null;
				k4 = null;
				System.gc(); // 강제로 GC 발생
				System.out.println("Weak Hash Map :" + wm.toString()); //Weak Hash Map :{}
		}
}

 

- 캐시를 만들 때 ScheduledThreadPoolExecuter 등을 이용하여 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.

- LinkedHashMap을 사용할 경우에는 removeEldestEntry 메소드를 사용해서 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.

- java.lang.ref 패키지를 활용하여 만드는 방법

java.lang.ref 패키지는 전형적인 객체 참조인 strong reference 외에도 soft, weak, phantom 3가지의 새로운 참조 방식을 각각의 Reference 클래스로 제공합니다. 이 3가지 Reference 클래스를 애플리케이션에 사용하면 앞서 설명하였듯이 GC에 일정 부분 관여할 수 있고, LRU(Least Recently Used) 캐시 같이 특별한 작업을 하는 애플리케이션을 더 쉽게 작성할 수 있습니다. 이를 위해서는 GC에 대해서도 잘 이해해야 할 뿐 아니라, 이들 참조 방식의 동작도 잘 이해할 필요가 있습니다.

 

WeakReference<Sample> wr = new WeakReference<Sample>(new Sample());  
Sample ex = wr.get(); // 그림 1
// ...
ex = null; // 그림 2

https://d2.naver.com/helloworld/329631 그림 1
https://d2.naver.com/helloworld/329631 그림 2

 

Strong 레퍼런스를 Weak 레퍼런스로 감싸면 Weak 레퍼런스가 된다.

Garbage Collection 대상이 되려면 해당 객체를 가리키는 레퍼런스가 전부 없어져야 한다.

Weak 레퍼런스는 Strong한 레퍼런스가 없어지면 Weak 레퍼런스도 GC의 대상이 될 수가 있다.

 

즉, 정리하자면, 원래 GC 대상 여부는 reachable/unreachable인가로만 구분하였고 이를 사용자 코드에서는 관여할 수 없었다.

그러나 java.lang.ref 패키지를 이용하여 reachable 객체들을 strongly reachable, softly reachable, weakly reachable, phantomly reachable로 더 자세히 구별하여 GC 때의 동작을 다르게 지정할 수 있게 되었다. = GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있다

https://d2.naver.com/helloworld/329631 Reachability

녹색으로 표시한 중간의 두 객체는 WeakReference로만 참조된 weakly reachable 객체이고, 파란색 객체는 strongly reachable 객체이다. GC가 동작할 때, unreachable 객체뿐만 아니라 weakly reachable 객체도 가비지 객체로 간주되어 메모리에서 회수된다.

root set으로부터 시작된 참조 사슬에 포함되어 있음에도 불구하고 GC가 동작할 때 회수되므로, 참조는 가능하지만 반드시 항상 유효할 필요는 없는 LRU 캐시와 같은 임시 객체들을 저장하는 구조를 쉽게 만들 수 있다.

 

위 그림에서 WeakReference 객체 자체는 weakly reachable 객체가 아니라 strongly reachable 객체이다. 또한, 그림에서 A로 표시한 객체와 같이 WeakReference에 의해 참조되고 있으면서 동시에 root set에서 시작한 참조 사슬에 포함되어 있는 경우에는 weakly reachable 객체가 아니라 strongly reachable 객체이다.

 

GC가 동작하여 어떤 객체를 weakly reachable 객체로 판명하면, GC는 WeakReference 객체에 있는 weakly reachable 객체에 대한 참조를 null로 설정한다. 이에 따라 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되고, 가비지로 판명된 다른 객체들과 함께 메모리 회수 대상이 된다.

 

참고) Strengths of Reachability

  • strongly reachable: root set으로부터 시작해서 어떤 reference object도 중간에 끼지 않은 상태로 참조 가능한 객체, 다시 말해, 객체까지 도달하는 여러 참조 사슬 중 reference object가 없는 사슬이 하나라도 있는 객체
  • softly reachable: strongly reachable 객체가 아닌 객체 중에서 weak reference, phantom reference 없이 soft reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • weakly reachable: strongly reachable 객체도 softly reachable 객체도 아닌 객체 중에서, phantom reference 없이 weak reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • phantomly reachable: strongly reachable 객체, softly reachable 객체, weakly reachable 객체 모두 해당되지 않는 객체. 이 객체는 파이널라이즈(finalize)되었지만 아직 메모리가 회수되지 않은 상태이다.
  • unreachable: root set으로부터 시작되는 참조 사슬로 참조되지 않는 객체

 

 

3) Listener와 Callback 등록 후 해지 하지 않는 것도 메모리 누수의 주범이 될 수 있다.

이 경우 마찬가지로 콜백을 약한 참조로(WeakHashMap에) 저장하면 가비지 컬렉터가 즉시 수거해 갈 수 있다.

1. 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다.

 

불변 객체 String 예시를 보면 아래와 같다.

String s = new String ("effective java"); // (1)실행될 때마다 String 인스턴스를 생성
String s = "effective java"; // (2)새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용

새로운 인스턴스를 만드는 대신 하나의 인스턴스를 재사용하는 2번 방식의 경우,

똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

 

 

생성자 대신 정적 팩터리 매서드(아이템 1)를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.

(생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 재사용하기 때문)

불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경 되지 않을 것임을 안다면 재사용할 수 있다.

 

 

2. 생성 비용이 비싼 경우 + 반복해서 필요한 경우 객체를 캐싱하여 재사용하는 것이 좋다.

ex) Pattern: 입력받은 정규표현식에 해당하는 유한 상태머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.

따라서 필요한 정규표현식을 표현하는 불변 Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고,

나중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스를 재사용하도록 하는 것이 좋다.

public class RomanNumber {

    private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }

//  static boolean isRomanNumeral(String s) { 
//      // 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 됨
//      return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
//  }
}

 

c.f. 클래스가 초기화된 후 ROMAN 필드를 사용하지 않으면 낭비이기 때문에 지연 초기화(lazy initialization, 아이템 83) 하는 방법도 있다.

하지만 지연 초기화는 코드를 복잡하게 만들고 성능을 크게 개선시키지는 않는다고 한다(아이템 67)

 

 

객체가 불변이라면 재사용하면 되지만 불변이 아닌 상황은 어떨까?

3. Adapter Pattern에서 Adapter는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체다.

즉, 어댑터는 뒷단 객체만 관리하면 되기 때문에, 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분하다.

 

ex) Map::keyset - Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다.

keyset을 호출할 때마다 새로운 Set 인스턴스가 만들어지지 않고, 매번 같은 Set 인스턴스를 반환한다. 따라서 keySet 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다.

//HashMap.java
    transient Set<K>        keySet;
    
    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

 

 

4. 오토박싱(auto boxing): 오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.

불필요한 박싱/언박싱은 불필요한 메모리 할당, 재할당을 반복할 수 있으며 성능상 좋지 않기 때문에 꼭 박싱 타입이 필요한 경우가 아니라면 기본타입을 사용하는 것이 좋다(아이템 61)

 

 

 

주의할 점은 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 점이다.

즉, 이번 아이템의 주의점은 "객체 생성은 비싸니 피해야 한다" (X)

=> 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 객체 풀(pool)을 만드는 것이 좋은것만은 아니다. 일반적으로는 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨린다.

따라서 언제 객체를 재사용하는 것이 좋고 이를 실제 코드에 적용해야할 것인지는 많은 생각을 해 봐야겠다.

 

 

용어

  • 리터럴: 소스 코드의 고정된 값을 대표하는 용어다. 리터럴과 대조적으로, 고정된 값을 가질 수 있는 변수나 변경되지 않는 상수가 있다.
  • Adapter pattern
  • 방어적 복사(아이템50): 생성자를 통해 초기화 할 때, 새로운 객체로 복사해주는 방법

 

많은 클래스가 하나 이상의 자원에 의존한다.(컴포지션)

이 의존하는 클래스를 정적 유틸리티 클래스(아이템 4)로 구현하게 되면 유연하지 않고 테스트 하기 어려운 구조가 된다.

 

  • 정적 유틸리티의 안좋은 예- 유연하지 않고 테스트 어렵다.

public class SpellChecker {

  private final Lexicon dictionary = ...;

  private SpellChecker() { }
  public static SpellChecker INSTANCE = new SpellChecker();

  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... }
}

 

  • 싱글톤의 안좋은 예- 유연하지 않고 테스트 어렵다.
public class SpellChecker {

  private final Lexicon dictionary = ...;

  private SpellChecker() { }
  public static SpellChecker INSTANCE = new SpellChecker();

  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... }
}

 

  • SpellChecker가 여러 사전을 사용할 수 있도록 하기 위해서 좋지 않은 방법 - 멀티스레드 환경X
public class SpellChecker {

  private Lexicon dictionary = ...;

  public static void changeDictionary(){
		this.dictionary = ...
  }
}

 

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준 다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 

이 자원들을 클래스가 직접 만들게 해서도 안 된다. 

 

 

대신 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 사용한다.(불변 보장)

이는 의존 객체 주입의 한 형태로, 객체를 생성할 때 의존 객체를 주입해주면 된다. => c.f. Strategy Pattern

public class SpellChecker {

  private final Lexicon dictionary;

  public SpellChecker(Lexicon dictionary) { // 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨준다
    this.dictionary = dictionary;
  }

  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { }

}

 

 

이 패턴의 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다.(Factory Method Pattern)

팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다.

 

ex) Supplier<T> interface(java8)

Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입을 사용해 팩터리의 타입 매개변수를 제한해야 한다. 

이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다. 

// 클라이언트가 제공한 팩터리가 생성한 타일(Tile)들로 구성된 모자이크 (Mosaic)를 만드는 메서드
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

 

예를 들어 어떤 사람의 이름과 생일을 입력해두고 getAge()로 나이를 가져오는 Person 클래스를 만든다고 하자.

package com.example.sypark9646.item5;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class Person {

	final String name;
	private final LocalDate dateOfBirth;
	private final LocalDate currentDate;

	public Person(String name, LocalDate dateOfBirth) {
		this(name, dateOfBirth, LocalDate.now());
	}

	public Person(String name, LocalDate dateOfBirth, LocalDate currentDate) {
		this.name = name;
		this.dateOfBirth = dateOfBirth;
		this.currentDate = currentDate;
	}

	long getAge() {
		return ChronoUnit.YEARS.between(dateOfBirth, currentDate);
	}

	public static void printAge(PersonSupplierConstruct person) {
		System.out.println(person.name + " is " + person.getAge());
	}
}

위 방법의 경우, getAge ()는 현재 날짜가 아닌 Person 객체가 생성 된 시기를 기반으로 한다.

이 문제는 Supplier <LocalDate>를 사용하면 해결된다. 현재 시간을 Supplier를 이용하여 주입하는 것이다.

package com.example.sypark9646.item5;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.function.Supplier;

public class PersonSupplierConstruct {

	final String name;
	private final LocalDate dateOfBirth;
	private final Supplier<LocalDate> currentDate;

	public PersonSupplierConstruct(String name, LocalDate dateOfBirth) {
		this(name, dateOfBirth, LocalDate::now);
	}

	public PersonSupplierConstruct(String name, LocalDate dateOfBirth, Supplier<LocalDate> currentDate) {
		this.name = name;
		this.dateOfBirth = dateOfBirth;
		this.currentDate = currentDate;
	}

	public long getAge() {
		return ChronoUnit.YEARS.between(dateOfBirth, currentDate.get());
	}

	public static void printAge(PersonSupplierConstruct person) {
		System.out.println(person.name + " is " + person.getAge());
	}
}

 

 

용어

  • 한정적 와일드카드 타입(bounded wildcard type, 아이템 31)
item3 -
싱글턴을 만드는 방식
기본적으로 생성자는 private 으로 감춰두고, => 인스턴스 생성 불가, 서브클래스 생성 불가
유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버 를 하나 마련해둔다.

 

정적 메서드와 정적 필드만을 담은 클래스를 만드는 경우

  • 기본 타입 값이나 배열 관련 메서드들을 모아놓는 경우: java.lang.Math, java.util.Arrays
  • 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드/팩터리 메소드들을 모아놓는 경우: java.util.Collections 
  • final 클래스와 관 련한 메서드들을 모아놓는 경우(final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가능)

생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자(매개변수 없는 public 기본 생성자)를 만들어준다.

그런데 읽다가 이런 이야기가 있었다.

추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
아래와 같이 하위 클래스를 만들어 인스턴스화하면 그만이다.
이를 본 사용자는 상속해서 쓰라는 뜻으로 오해할 수 있으니 더 큰 문제다. (아이템 19)

 

무슨 말일까... 자바 공식 문서를 한번 보자

docs.oracle.com/javase/tutorial/java/IandI/abstract.html

 

Abstract Methods and Classes (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritanc

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 Abstract classes cannot be instantiated, but they can be subclassed.

여기서 나온 예제를 직접 한 번 구현해 보았다.

 

Shape는 추상 클래스로 두고, Circle과 Rectangle은 이를 extends 해서 구체화 하여 구현할 수 있다.

package com.example.sypark9646.item4;

public abstract class Shape {

	protected int x, y;

	public Shape() {
		System.out.println("Shape 호출");
	}

	public Shape(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public abstract String getName();

	public void drawCenter() {
		System.out.println("x = " + x + ", y = " + y);
	}
}
package com.example.sypark9646.item4;

public class Circle extends Shape {

	int radius;

	public Circle() {
		// super(); 묵시적 호출
		System.out.println("Circle 호출");
	}

	public Circle(int x, int y, int radius) {
		super(x, y);
		this.radius = radius;
	}

	@Override
	public void drawCenter() {
		super.drawCenter();
		System.out.println("radius = " + radius);
	}

	@Override
	public String getName() {
		return "circle" + this.hashCode();
	}
}
package com.example.sypark9646.item4;

public class Rectangle extends Shape {

	int row, col;

	public Rectangle() {
		// super(); 묵시적 호출
		System.out.println("Rectangle 호출");
	}

	public Rectangle(int x, int y, int row, int col) {
		super(x, y);
		this.row = row;
		this.col = col;
	}

	@Override
	public void drawCenter() {
		super.drawCenter();
		System.out.println("row = " + row + ", col = " + col);
	}

	@Override
	public String getName() {
		return "rectangle" + this.hashCode();
	}
}

 

추상클래스의 경우 아래와 같이 그냥 Shape 그 자체를 인스턴스화 할 순 없지만,

Circle과 Rectangle은 생성자를 통해 인스턴스화가 가능하고, 이 생성자들을 부르게 되면 상위 클래스 Shape의 생성자를 호출하게 된다.

package com.example.sypark9646.item4;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class AbstractInstantiateTest {

	@Test
	@DisplayName("추상클래스 인스턴스화 테스트")
	public void testInstantiateShape() throws InterruptedException {
		Shape circle = new Circle();
		Shape rectangle = new Rectangle();

		circle.drawCenter();
		rectangle.drawCenter();
	}
}

위와 같이 Circle, Rectangle 인스턴스를 생성하기 위해서는 반드시 생성자를 호출해야 한다.

하지만 생성자는 상속되지 않고 멤버만 상속된다. 대신 자식 클래스로 인스턴스를 생성하게 되면 부모 클래스의 생성자를 super로 호출한다.

즉, 부모 클래스의 생성자가 호출되어야 자식 클래스를 인스턴스화할 수 있다.

 

 

 

그래서 어떤 클래스의 인스턴스화를 확실히 막는 방법은 private 기본 생성자를 추가하는 것이다.

위에서 제시한 java.lang.Math, java.util.Arrays, 그리고 java.util.Collections 또한 private 생성자로 인스턴스화를 막고 있는 것을 확인할 수 있었다.

public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections() {}
}

public final class Math {

    /**
     * Don't let anyone instantiate this class.
     */
    private Math() {}
}

 

만약 유틸리티 클래스를 만들게 된다면 아래와 같이 만들면 된다.

public class Utilityclass {
    // 기본 생성자가 만들어지는 것을 막는다, 인스턴스화 방지용
    private Utilityclass() { throw new AssertionError(); }
}

생성자를 명시적으로 private이니 클래스 바깥에서는 접근하지 못하도록 하고,

꼭 Assertion Error를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 해준다.

즉, 어떤 환경에서도 클래스가 인스턴스화되는 것을 막아 준다.

추가적으로 사용자가 이해하기 쉽도록 생성자가 존재하는데 호출할 수 없다는 내용을 주석으로 달아주면 더 좋다.

이 방식은 상속을 불가능하게 하는 효과도 있다. 

Shape 예제에서 볼 수 있듯이 모든 생성자는 명시적이든 묵시적이든 부모 클래스의 생성자를 호출하게 되는데, 

이를 private으로 선언 하게 되면 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막혀버리기 때문에 컴파일 타임에 상속이 불가능하다는 것을 알 수 있다.

 

 

Abstract class vs Interface

인스턴스화를 막는 방법으로서 private 기본 생성자를 추가하는 예시를 자바 컬렉션에서 찾아보다가 궁금한 점이 생겼다

HashSet의 경우 AbstractSet 추상클래스를 상속하고 있는데, Set 인터페이스또한 implement하고 있다.

그런데 AbstractSet은 Set을 implement하고 있다.

그렇다면 HashSet에서는 Set을 이미 implement하고 있는 AbstractSet만 상속하여 구현하면 될텐데 왜 굳이 두가지를 모두 상속하고 구현한 것일까?

 

public class HashSet<E> extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{...}

public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {...}

public interface Set<E> extends Collection<E> {...}

 

 

Abstract class가 Interface를 implement하는 이유는 무엇이고 어떻게 동작하는 것일까?

이와 관련된 질문을 스택오버플로우에서 찾을 수 있었다.

출처: https://stackoverflow.com/questions/49757423/what-happens-when-an-abstract-class-implements-an-interface-in-java
What exactly happens when an abstract class implements an interface? Does this work like inheritance, i.e. all interface methods belong also to the abtract class eventhough they are not implemented in it? Or will only the implemented methods belong to the abstract class? So is there any difference between implements and extends, except that one is used to implement interfaces and the other one is used for inheritance?

이에 대한 답변은 정리하자면 아래와 같다.

if you have an abstract class and implement an interface with it, you have two options for the interface methods.

예를 들어 소리를 낼 수 있는지 여부를 나타내는 인터페이스 CanMakeNoise, 동물을 나타내는 추상클래스 Animal이 있다고 하자

package com.example.sypark9646.item4;

public interface CanMakeNoise {

	void makeNoise();
}

 

1. implement them in the abstract class

package com.example.sypark9646.item4;

public abstract class Animal implements CanMakeNoise {

	public abstract void jump();

	@Override
	public void makeNoise() { // interface 함수 구현
		System.out.println("animal noise");
	}
}

abstract class에 interface에서 정의한 함수를 구현하면 구체클래스에서는 이를 재 정의할 필요 없다

package com.example.sypark9646.item4;

public class Dog extends Animal implements CanMakeNoise{

	@Override
	public void jump() {
		System.out.println("dog jumps");
	}

//	@Override
//	public void makeNoise() {
//		System.out.println("dog noise");
//	}
}

 

물론 makeNoise 함수를 Override하여 구현 해줘도 되긴 하다. 이럴 경우 메소드가 오버라이드 되어서 "dog noise"가 나온다.

 

2. you leave them abstract, but then some of your more concrete children need to implement it.

만약 abstract class에서 interface 함수를 구현하지 않고 implement만 한다면

package com.example.sypark9646.item4;

public abstract class Animal implements CanMakeNoise {

	public abstract void jump();
}

구체 클래스 Dog에서는 두 abstract 함수를 필수적으로 모두 구현해 주어야 한다.

package com.example.sypark9646.item4;

public class Dog extends Animal implements CanMakeNoise {

	@Override
	public void jump() {
		System.out.println("dog jumps");
	}

	@Override
	public void makeNoise() {
		System.out.println("dog noise");
	}
}

 

이 예제의 경우는 Animal은 특정 동물이 어떻게 makeNoise 하는지 알 수 없기 때문에 함수의 구현을 구체 클래스로 남겨 두는 것이 좋다.(2번 방법)

 

 

 

이와 반대로, 인터페이스의 중복된 구현을 추상클래스로 빼서 중복을 방지하는 디자인 패턴이 있을 수 있다.(1번 방법)

 

출처: effectiveprogramming.tistory.com/entry/interface-abstract-class-concrete-class-%ED%8C%A8%ED%84%B4
 

interface -abstract class - concrete class 패턴(인터페이스 구현 중복 해결 패턴)

interface - abstract class - concrete class 패턴은 인터페이스 구현 시 자주 발생하게 되는 중복 구현을 방지하는 패턴이다. 해결하고자 하는 문제 - 구현해야 할 클래스에 대한 인터페이스가 이미 정해진

effectiveprogramming.tistory.com

 

 

딴길로 좀 샌거같은데..

자바 컬렉션에서는 Abstract class가 Interface를 implement하는 이유는 정리해 보자면

클래스가 실제로 해당 인터페이스를 구현한다는 것을 기억하기 위해서라고 한다. 즉, 주어진 클래스의 전체 계층 구조를 거치지 않고 코드를 이해하는 데 도움이 될 수 있으며 문서화 할 때 가독성이 좋기 때문이다.

 

또한 이펙티브 자바에서는 인터페이스와 함께 사용할 abstract skeletal 구현 클래스를 추가하여 인터페이스와 추상 클래스의 장점을 결합 할 수 있다고 했다.

인터페이스는 유형을 정의하여 기본 메서드를 제공하는 반면, skeletal 클래스는 기본 인터페이스 메서드 위에 남아있는 기본이 아닌 인터페이스 메서드를 구현한다.

skeletal 구현을 확장하면 인터페이스 구현에서 대부분의 작업이 필요한데, 이것이 템플릿 방법 패턴이다.

관례에 따라 skeletal 구현 클래스는 AbstractInterface라고 하고, 여기서 Interface는 구현하는 인터페이스의 이름이다.

예로는 아래의 추상 클래스들이 있다.

AbstractCollection
AbstractSet
AbstractList
AbstractMap

 

 

인터페이스를 명시적으로 구현하는 것과 상속으로 구현하는 것은 분명 다르긴 하다.

extends AbstractSet, implements Set이라고 되어있지만, 아래와 같이 리플렉션을 통해 보면...

소스에 작성된 순서대로 HashSet에 의해 명시적으로 구현 된 인터페이스만 표시한다는 것을 알 수 있다.

 

for (Class<?> c : ArrayList.class.getInterfaces())
    System.out.println(c);

// interface java.util.List
// interface java.util.RandomAccess
// interface java.lang.Cloneable
// interface java.io.Serializable

ArrayList 또한 마찬가지이다. 

출력에는 super class에 의해 구현 된 인터페이스 또는 포함 된 super interface인 인터페이스가 포함되지 않는다.

public interface List<E> extends Collection<E> {
	...
}
public interface Collection<E> extends Iterable<E> {
	...
}

특히, ArrayList가 암시적으로 구현하더라도 Iterable과 Collection은 위에서 누락되었다는 것을 알 수 있다.

Collection과 Iterable을 찾으려면 클래스 계층 구조를 재귀 적으로 반복해야한다.

그렇지만 다행히 이 차이는 `new ArrayList <> () instanceof Iterable` 및 `Iterable.class.isAssignableFrom (ArrayList.class)`은 올바르게 ​​true로 나온다.

 

 

마지막으로 추상 클래스와 인터페이스의 특징에 대해 각각 알아보자면,

  • 추상클래스가 인터페이스보다 빠르다 (그렇지만 별 차이 없는 정도이다)
  • 인터페이스는 다중 상속이 가능하지만, 추상클래스는 최대 1개만 가능하다.
  • 추상클래스는 모든 추상메소드를 재구현(Override)해야하지만, 인터페이스는 필요한 것만 구현해도 된다.
  • 인터페이스에는 접근 제어자가 없다. 인터페이스 내부에서 선언된 모든 것들은 public만 가능하다. 반면 추상클래스는 접근제어자가 가능하다.
  • 인터페이스의 경우 다양한 하위 구현 클래스들이 같은 메소드 특징을 공유할 때 사용한다. 추상클래스는 주로 동일한 종류의 다양한 구현이 공통 동작을 공유 할 때 사용한다.
  • 인터페이스는 데이터 필드를 가질 수 없지만 추상 클래스는 가질 수 있다.
  • 인터페이스는 생성자가 없으며, 추상클래스는 생성자가 있다.
  • 인터페이스의 경우 java8부터 default 메소드를 통해 메소드 내부 구현이 가능하다.

 

싱글턴(singleton): 인스턴스를 오직 하나만 생성할 수 있는 클래스, 2021/01/19 - [책읽기/Design Patterns] - Singleton Pattern

 

클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.

타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면, 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.

 

싱글턴을 만드는 방식

기본적으로 생성자는 private 으로 감춰두고, => 인스턴스 생성 불가, 서브클래스 생성 불가

유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버 를 하나 마련해둔다.

 

 

1. Eager Initialization

가장 간단한 형태의 구현 방법으로, 인스턴스를 클래스 로딩 단계에서 객체를 프로그램 시작과 동시에 초기화하는 방법이다.

package com.example.demo.item03;

public class EagerInitializationSingleton {

	private static final EagerInitializationSingleton instance = new EagerInitializationSingleton();

	private EagerInitializationSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	public static final EagerInitializationSingleton getInstance() {
		return instance;
	}
}

private 생성자는 private static final 필드인 instance를 초기화할 때 딱 한 번만 호출되고, getInstance는 항상 같은 객체의 참조를 반환하므로, EagerInitializationSingleton 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

기본 생성자에서 예외를 리턴하는데, 클라이언트가 자바 리플렉션 API(아이템 65)를 사용하여 private 생성자를 호출할 수도 있다.

이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.

package com.example.demo.item03;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class SingletonCreateSecondExceptionTest {

	@Test
	public void testEagerInitializationSingletonException() throws IllegalAccessException, InvocationTargetException, InstantiationException {
		Constructor<?>[] constructors = EagerInitializationSingleton.class.getDeclaredConstructors();
		Constructor theConstructor = constructors[0];
		theConstructor.setAccessible(true);
		EagerInitializationSingleton singleton1 = (EagerInitializationSingleton) theConstructor.newInstance();
		EagerInitializationSingleton singleton2 = (EagerInitializationSingleton) theConstructor.newInstance();

		System.out.println(singleton1);
		System.out.println(singleton2);

		Assertions.assertSame(singleton1, singleton2);
	}
}

 

이 방법의 장점은 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점이다. 또한, 두 번째 장점은 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다는 점이다(아이템 30). 세 번째 장점은 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다는 점이다. 가령 Singleton::get Instance를 Supplier<Singleton>로 사용하는 식이다(아이템 43, 44). 

 

자세한 클래스 로딩 시점은 2020/12/18 - [작성중...] - JVM의 구조와 array, hashmap max size에 대한 생각 - 작성중...에서 볼 수 있다.

인스턴스를 사용하지 않더라도 인스턴스를 생성하기 때문에 낭비가 될 수 있다.

즉, File System, Database Connection 등 객체 생성에 많은 리소스를 사용하는 싱글톤을 구현할 때는 직접 사용할 때까지 싱글톤 인스턴스를 생성하지 않는 방법이 더 좋다.

//예시 java.util.HashSet
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

 ...
 }

 

2. Static Block Initialization

Eager Initialization과 유사하지만 static block을 통해서 Exception Handling을 제공한다.

Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하므로, 큰 리소스를 다루는 경우에는 적절하지 않다.

package com.example.demo.item03;

public class StaticBlockInitializationSingleton {

	private static StaticBlockInitializationSingleton instance;

	private StaticBlockInitializationSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	static {
		try {
			instance = new StaticBlockInitializationSingleton();
		} catch (Exception e) {
			throw new RuntimeException("Creating of this object is not allowed.");
		}
	}

	public static StaticBlockInitializationSingleton getInstance() {
		return instance;
	}
}

 

3. Lazy Initialization > single thread 에서만 사용

앞선 두 방식과는 달리 나중에 초기화하는 방법이다.

public 메소드 getInstance()를 호출할 때 인스턴스가 없는지 확인하고, 없다면 생성한다. 따라서 인스턴스 낭비를 막을 수 있다.

package com.example.demo.item03;

public class LazyInitializationSingleton {

	private static LazyInitializationSingleton instance;

	private LazyInitializationSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	public static LazyInitializationSingleton getInstance() {
		if (instance == null) {
			instance = new LazyInitializationSingleton();
		}
		return instance;
	}
}

하지만 multi-thread 환경에서 instance==null인 시점일 때 여러 쓰레드가 동시에 getInstance() 를 호출 한다면

thread-safe 하지 않을 수 있다는 치명적인 단점이 있다.

	@Test
	public void testLazyInitializationSingleton() throws InterruptedException {
		int numberOfThreads = 500;
		ExecutorService service = Executors.newFixedThreadPool(500);
		CountDownLatch latch = new CountDownLatch(numberOfThreads);

		HashSet<LazyInitializationSingleton> singletonHashSet = new HashSet<>();

		for (int i = 0; i < numberOfThreads; i++) {
			service.execute(() -> {
				LazyInitializationSingleton lazyInitializationSingleton = LazyInitializationSingleton.getInstance();
				singletonHashSet.add(lazyInitializationSingleton);
				latch.countDown();
			});
		}
		Assertions.assertEquals(singletonHashSet.size(), 1);
		latch.await();
	}

lazy initialization singleton 방식만 multi-thread 테스트를 통과하지 못함

 

4. Thread Safe Singleton

Lazy Initialization의 thread-safe 문제를 해결하기 위한 방법으로, getInstance() 메소드에 synchronized를 걸어두는 방식이다.

synchronized 키워드는 임계 영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해 준다.

package com.example.demo.item03;

public class SynchronizedSingleton {

	private static SynchronizedSingleton instance;

	private SynchronizedSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	public static synchronized SynchronizedSingleton getInstance() {
		if (instance == null) {
			instance = new SynchronizedSingleton();
		}
		return instance;
	}
}

getInstance() 메소드 내에 진입하는 쓰레드가 하나로 보장받기 때문에 멀티 쓰레드 환경에서도 정상 동작하게 된다.

그러나 synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은 경우 성능이 떨어지게 됩니다.

 

5. Double checked Locking Singleton

synchronized 키워드가 성능상으로 좋지 않기 때문에,

getInstance() 메소드 수준에 lock을 걸지 않고 instance가 null일 경우에만 synchronized가 동작하도록 한다.

package com.example.demo.item03;

public class DoubleCheckingLockingSingleton {

	private static DoubleCheckingLockingSingleton instance;

	private DoubleCheckingLockingSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	public static DoubleCheckingLockingSingleton getInstance() {
		if (instance == null) {
			synchronized (DoubleCheckingLockingSingleton.class) {
				if (instance == null) {
					instance = new DoubleCheckingLockingSingleton();
				}
			}
		}
		return instance;
	}

}

 

6. Bill Pugh Singleton Implementation

Bill Pugh가 고안한 방식으로, inner static helper class를 사용하는 방식이다.

앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식으로, 현재 가장 널리 쓰이는 싱글톤 구현 방법이다.

package com.example.demo.item03;

public class InitializationOnDemandHolderIdiomSingleton {

	private InitializationOnDemandHolderIdiomSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	public static InitializationOnDemandHolderIdiomSingleton getInstance() {
		return SingletonLazyHolder.INSTANCE;
	}

	private static class SingletonLazyHolder {

		private static final InitializationOnDemandHolderIdiomSingleton INSTANCE = new InitializationOnDemandHolderIdiomSingleton();
	}
}

private inner static class를 두어 싱글톤 인스턴스를 갖게 하는데,

inner class인 SingletonHelper 클래스는 Singleton 클래스가 로드 될 때가 아닌, getInstance()가 호출됐을 때

비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 된다.

(+ synchronized를 사용하지 않기 때문에 성능 문제가 해결된다.)

 

7. Enum 방식

앞선 1~6 방식은 자바 리플렉션 api를 이용하여 싱글톤을 파괴할 수 있다.

package com.example.demo.item03;

public enum EnumSingleton {
	INSTANCE;
}

enum 방식은 이와 달리 간결할 뿐만 아니라, 쉽게 직렬화할 수 있고, 제2의 인스턴스를 생성하는 리플렉션 공격도 완벽히 막아준다.

조금 부자연스러워 보일 수는 있으나 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법 일 수 있다.

단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다).

그러나 이 방법 또한 1, 2번과 같이 클래스를 사용하지 않을 경우 메모리 낭비를 한다는 단점이 있다.

 

싱글턴 클래스의 직렬화

싱글턴 클래스를 직렬화하려면(12장) 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다.

package com.example.demo.item03;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class SingletonSerializeTests {

	@Test
	public void testEagerInitializationSingleton() throws IOException, ClassNotFoundException {
		EagerInitializationSingleton singleton1 = EagerInitializationSingleton.getInstance();
		ObjectOutputStream obs = new ObjectOutputStream(new FileOutputStream("filename1.ser"));
		obs.writeObject(singleton1);
		obs.close();

		ObjectInputStream objInputStream = new ObjectInputStream(new FileInputStream("filename1.ser"));
		EagerInitializationSingleton singleton2 = (EagerInitializationSingleton) objInputStream.readObject();
		objInputStream.close();

		Assertions.assertSame(singleton1.getClass(), singleton2.getClass()); // true
		Assertions.assertSame(singleton1, singleton2); // false
		Assertions.assertSame(singleton1.hashCode(), singleton2.hashCode()); // false
	}
}

모든 인스턴스 필드를 일시적 (transient)이라고 선언하고 readResolve 메서드를 제공해야 한다(아이템 89).

이렇게 하지 않으면 직렬화된 인스턴스를 역직렬화할 때 마다 새로운 인스턴스가 만들어진다. 가짜 Singleton 클래스 생성을 예방하고 싶다면 싱글톤 클래스에 다음의 readResolve 메서드를 추가해야 한다.

package com.example.demo.item03;

import java.io.Serializable;

public class EagerInitializationSingleton implements Serializable {

	private static EagerInitializationSingleton instance = new EagerInitializationSingleton();

	private EagerInitializationSingleton() {
		if (instance != null) {
			throw new InstantiationError("Creating of this object is not allowed.");
		}
	}

	public static EagerInitializationSingleton getInstance() {
		return instance;
	}

	private Object readResolve() {
		return instance;
	}
}
package com.example.demo.item03;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class SingletonSerializeTests {

	@Test
	public void testEagerInitializationSingleton() throws IOException, ClassNotFoundException {
		EagerInitializationSingleton singleton1 = EagerInitializationSingleton.getInstance();
		ObjectOutputStream obs = new ObjectOutputStream(new FileOutputStream("filename1.ser"));
		obs.writeObject(singleton1);
		obs.close();

		ObjectInputStream objInputStream = new ObjectInputStream(new FileInputStream("filename1.ser"));
		EagerInitializationSingleton singleton2 = (EagerInitializationSingleton) objInputStream.readObject();
		objInputStream.close();

		Assertions.assertSame(singleton1.getClass(), singleton2.getClass()); // true
		Assertions.assertSame(singleton1, singleton2); // true
		Assertions.assertSame(singleton1.hashCode(), singleton2.hashCode()); // false
	}
}

같은 객체이기 때문에 hashcode 또한 같은 값을 리턴해야 한다고 생각했는데 다른 값을 리턴하는 것을 확인하고 이슈에 질문을 올렸다.

github.com/dolly0920/Effective_Java_Study/issues/9

 

[item 3] hashcode · Issue #9 · dolly0920/Effective_Java_Study

singleton 객체에 readResolve 함수를 구현하고 serialize & deserialize 해 보았는데 Assertions.assertSame(singleton1, singleton2)가 true를 리턴하는 것을 보아 같은 인스턴스로 직렬화 & 역직렬화가 된 것 같긴 한데 Ass

github.com

알고보니 Assertions.assertSame의 특성을 잘 이해하지 못하고 사용해서 문제가 생겼던 것이다.

assertSame 는 객체가 같은지 비교하는 가정문이다.

Assert.assertSame(1000, 1000); // false
Assert.assertSame(Integer.valueOf(1000), Integer.valueOf(1000)); // false
Assert.assertSame(new Integer(1000), new Integer(1000)); // false

assertSame 은 참조형(Reference Type)인 경우에만 사용해야 한다.
만약 기본형(Primitive Type)을 비교해야 한다면 값을 비교하는 가정문 assertEquals 을 사용해야 한다.

assertSame(Object arg, Object arg2) 의 파라미터 형은 Object 이기 때문에,
autoboxing이 일어나서 새로운 객체가 생성되어 같은 값이라도 hashCode 가 다르다 -> false 리턴

추가로,
Assert.assertSame(127, 127); // true
자바에서 -128 ~ 127 사이의 값은 미리 저장된 값을 이용하기 때문에 (새로운 객체를 생성하지 않아서) true를 리턴한다

 

용어

  • 무상태 (stateless) 객체: 
    •  인스턴스 변수가 없는 객체
    • Stateless object is an instance of a class without instance fields (instance variables). The class may have fields, but they are compile-time constants (static final).
    • A very much related term is immutable. Immutable objects may have state, but it does not change when a method is invoked (method invocations do not assign new values to fields). These objects are also thread-safe.
    • 장점: 다른 클래스에 종속적이지 않음, thread-safe, 인터페이스화 하기 용이하다
    • 공부해 볼 것: 
  • supplier: 인자는 받지않으며 리턴타입만 존재하는 메서드를 갖고있다. 순수함수에서 결과를 바꾸는건 오직 input parameter 뿐이다. 그런데 input이 없다는건 내부에서 랜덤함수같은것을 쓰는게 아닌이상 항상 같은 것을 리턴하는 메서드라는걸 알 수 있다.
  • synchronized: 여러개의 스레드가 한개의 자원을 사용하고자  , 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근할 수 없도록 막는다.
  • 임계영역: 둘 이상의 스레드가 동시에 실행될 경우 생길 수 있는 동시 접근 문제를 발생시킬 수 있는 코드 블록

정적 팩터리 메소드와 생성자는 선택적 매개변수가 많을 떄 적절하게 대응하기 어렵다는 단점이 있다.

만약 있을 수도 없을 수도 있는 필드가 많은 객체가 있다고 가정하자.

 

1. 옛날에는 이러한 클래스에서 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다.

이 패턴은 필수 매개변수를 받는 생성자와, 선택 매개변수를 하나씩 늘여가며 생성자를 만드는 패턴이다.

=> 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다

class Person {

	private String name; // 필수
	private int age; // 필수
	private String job; // 선택
	private String hobby; // 선택

	public Person(String name, int age, String job, String hobby) {
		this.name = name;
		this.age = age;
		this.job = job;
		this.hobby = hobby;
	}
	
	// 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 
	// 어쩔 수 없이 hobby 매개변수에도 ""라는 값을 지정해줘야 한다.
	public Person(String name, int age, String job) {
		this(name, age, job, "");
	}

	public Person(String name, int age) {
		this(name, age, "", "");
	}
}

코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 것이고, 매개변수가 몇 개인지도 주의해서 세어 보아야 한다.

또한 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어 질 수 있다.

클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러 는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 된다(아이템 51).

 

 

2. 위 문제점에 대한 대안으로는 자바빈즈 패턴(JavaBeans pattern)이 있다. 

매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.

@Setter
class Person {

	private String name = ""; // 필수
	private int age = 0; // 필수
	private String job = ""; // 선택
	private String hobby = ""; // 선택

	public Person() {
	}
}

public class DemoApplication {

	public static void main(String[] args) {
		Person person=new Person();
		person.setName("soyeon");
		person.setAge(25);
		person.setJob("student");
		person.setHobby("exercise");
	}

}

자바빈즈 패턴의 경우 장점은 코드가 길어지긴 했지만 인스턴스를 만들기 쉽고, 그 결과 더 읽기 쉬운 코드가 되었다는 점이다.

하지만 단점은 객체 하나를 만들기 위해 많은 메서드를 호출해야 하며, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다. => 버그가 존재하는 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅이 어려워진다.

 

점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것이다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변(Item17)으로 만들 수 없으며 스레드 안전성이 없다는 문제도 있다.

 

 

3. 마지막 대안은 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattem)[Gamma95]이다.

클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자 또는 정적 팩터리 메서드를 호출해 빌더 객체를 얻는다.

클라이언트는 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.

마지막으로 매개변수가 없는 build 메서드를 호출해 드디어 우리에게 필요한 객체를 얻는다.(이 객체는 일반적으로 불변객체이다.)

빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어 두도록 한다.

package com.yapp.crew.domain.model;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookMark extends BaseEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(nullable = false)
	@Setter(value = AccessLevel.PRIVATE)
	private User user;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(nullable = false)
	@Setter(value = AccessLevel.PRIVATE)
	private Board board;

	public static BookMarkBuilder getBuilder() {
		return new BookMarkBuilder();
	}

	public static class BookMarkBuilder { // 정적 멤버 클래스
		private User user;
		private Board board;

		public BookMarkBuilder withUser(User user) {
			this.user = user;
			return this;
		}

		public BookMarkBuilder withBoard(Board board) {
			this.board = board;
			return this;
		}

		public BookMark build() {
			BookMark bookMark = new BookMark();
			bookMark.setUser(user);
			bookMark.setBoard(board);

			return bookMark;
		}
	}
}
	private void saveBookMark(Board board, User user) {
    	// 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다.(method chaining)
		BookMarkBuilder bookMarkBuilder = BookMark.getBuilder();
		BookMark bookMark = bookMarkBuilder
				.withUser(user)
				.withBoard(board)
				.build();

		user.addBookMark(bookMark);
		board.addBookMark(bookMark);

		bookMarkRepository.save(bookMark);
	}

이때, 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식 (invariant)을 검사하도록 하면 더 좋다. 공격에 대비해 이런 *불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다(Item50).

검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IllegalArgumentException을 던지면 된다(Item75).

 

 

빌더 패턴은 계충적= 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하자.

추상 클래스는 추상 빌더를, 구체 클래스(concrete class)는 구체 빌더를 갖게 한다.

다음은 피자의 다양한 종류를 표현 하는 계층구조의 루트에 놓인 추상 클래스다.

 

 

 

용어

  • 불변 vs 불변식
    • 불변(immutable, immutability): 어떠한 변경도 허용하지 않는다는 뜻으로, 주로 변경을 허용하는 가변(mutable) 객체와 구분하는 용도로 쓰인다. ex) String 객체
    • 불변식(invariant): 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 뜻한다. 즉, 변경을 허용할 수는 있으나 주어진 조건 내에서 만 허용한다는 뜻이다.(아이템 50)
      • ex) 리스트의 크기는 반드시 0 이상 = 만약 한순간 이라도 음수 값이 된다면 불변식이 깨진 것이다.
      • ex) 기간을 표현하는 Period 클래스에서 start 필드의 값은 반드시 end 필드의 값보다 앞서야 = 두 값이 역전되면 불변식이 깨진 것이다


따라서 가변 객체에도 불변식은 존재할 수 있으며, 넓게 보면 불변은 불변식의 극단적 인 예라 할 수 있다.

+ Recent posts