📂 목차
📚 본문
프로젝트 진행에 있어 다양한 자바의 내장 클래스들을 살펴보면서 코드를 업그레이드 하기 위해 본다.
ThreadLocal
public class ThreadLocal<T>
- T 타입을 가지는 스레드 로컬 변수를 구현한다.
- 각 스레드마다 독립적인 값을 저장하는 것이 핵심이며,
- 다른 스레드와 공유되지 않는다
이때 스레드마다 하나의 ThreadLocal 을 가진다는 말은 거짓이다. 1:n 의 관계다
Member Variables
private static final boolean TRACE_VTHREAD_LOCALS = traceVirtualThreadLocals();
private final int threadLocalHashCode = nextHashCode();
private static final AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
threadLocalHashCode
: 각 ThreadLocal 인스턴스 별 고유 해시 값 -> 해시 맵에서 key 로 사용하게 된다.nextHashCode
&HASH_INCREMENT
: 해시 충돌 최소화를 위한 고유값을 생성- 여기서
HASH_INCREMENT
의0x61c88647
은 황금 비율 상수이며, 연속 생성되는ThreadLocal
에서도 충돌을 최소화하는 상수이다.
- 여기서
HASH_INCREMENT
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
위 메서드에서 HASH_INCREMENT
를 통해 다음 hashCode
를 가져오는것을 볼 수 있는데 연속되게 생성되는 ThreadLocal
속에서 hashCode
값의 충돌을 최소화 하려면 누적하여 더해지는 값마다 다른 값을 나타내게 해야 하며, 이때 HashMap
에서는 동일 값에 대한 충돌이 없이 잘 퍼지도록 설계할 수 있게 된다.
따라서 ThreadLocalMap
에서 키 값에 대한 황금비를 계속 더하며 슬롯 충돌을 최소화하게 되며, 2의 제곱 크기의 배열에서 고유 ID 가 균등하게 퍼지도록 해준다.
protected T initialValue() {
return null;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
기본 초기값은 null 이 되지만, SuppliedThreadLocal
팩터리 패턴으로 초기화를 할 수 있다. 우리는 withInitial
을 통해 초기화 할 수 있다.
public T get() {
return get(Thread.currentThread());
}
private T get(Thread t) {
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue(t);
}
값 읽기에 해당하는 메서드이다. 두 번째 메서드는 ThreadLocalMap
에서 값 검색을 하며, 없으면 initialValue()
로 초기화 하여 저장하게 된다.
public void set(T value) {
set(Thread.currentThread(), value);
}
private void set(Thread t, T value) {
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value);
else createMap(t, value);
}
현재 스레드의 값을 토대로 ThreadLocalMap
의 값을 변경하게 된다. 여기서 this 가 의미하는 것이 바로 hashCode
가 되게 된다.
결국에는 ThreadLocalMap
이 핵심인데, 이를 더 살펴보자.
ThreadLocalMap
이 클래스는 ThreadLocal
안에 선언되어 있는 static class
이다. 밖에서는 그래서 참조를 할 수 없고, ThreadLocalMap
은 논리 스레드 마다 적어도 하나씩 존재함을 알 수 있다.
ThreadLocalMap
은 핵심 클래스 Entry
를 가진다.
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }
}
엔트리는 ThreadLocalMap
의 슬롯을 추상화한 클래스이며, Key 를 약한 참조를 통해 ThreadLocal 객체가 없다면 null 을 반환한다. 이를 stale entry 라고 한다.
특이한 것은 ThreadLocal
객체 그 자체로 Key 가 됨을 알 수 있다. 그렇다면 이게 어떻게 동작하는지 의문인데, 해당 key 는 ThreadLocal
에서의 hashCode 로 식별함을 볼 수 있었다.
int index = key.threadLocalHashCode & (table.length - 1);
- key.threadLocalHashCode: 스레드 로컬 객체가 가진 고유 해시코드
-
table.length - 1: 스레드 로컬 맵 내부 배열 크기를 통해 해시 값을 배열 인덱스로 변환
- key.threadLocalHashCode & (table.length - 1): % 대신 & 를 쓰는 이유는 table.length 가 2의 제곱이기 때문에 1을 뺀 값은 비트가 전부 1이 되고, 이는 % 의 결과랑 &의 결과랑 같게 된다.
ThreadLocal<String> userId = ThreadLocal.withInitial(() -> "user1");
ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
Thread t = new Thread(() -> {
System.out.println(userId.get()); // "user1"
System.out.println(counter.get()); // 0
userId.set("userA");
counter.set(42);
System.out.println(userId.get()); // "userA"
System.out.println(counter.get()); // 42
});
t.start();
sealed
Java 17 문법에서 도입된 문법으로 봉인된이라는 의미를 가진다.
public abstract sealed class Reference<T>
permits PhantomReference, SoftReference, WeakReference, FinalReference {
클래스 혹은 인터페이스가 상속 또는 구현 될 수 있는 범위를 제한하는 문법이다.
permits
예약어를 통해서 이를 상속하거나 구현하는 클래스의 범위를 제한시켜 명시적으로 지정된 클래스들만이 이를 사용할 수 있게 된다.
Reference
는 GC 와 밀접히 연결된 핵심 클래스라 임의의 사용자가 마음대로 상속해서 써버리게 되면 GC 시스템이 깨질 수 있기에 봉인해놓은 것.
Serializable 의 재현성
Serializable
을 구현한다고 해서, 재현성을 얻는 것은 아니다. 클래스의 구조가 바뀌거나 JVM 이 자동으로 새 serialVersionUID
을 계산하는데, 이 값은 JVM 마다 컴파일 환경마다 달라질 수 있기 때문에 그 때마다 InvalidClassException
의 예외가 발생할 수 있다.
private static final long serialVersionUID = 어떤 값;
해당 값을 멤버 변수로 추가만 해준다면 역직렬화, 직렬화를 재현할 수 있게 되며, 클래스 구조가 조금 바뀌더라도 가능하게 된다.
무조건 static final 로 선언하자.
DTO 와 DAO
DAO
는 Data Access Object
의 약자로, 실제로 DB 에 접근할 수 있는 권한을 가지는 객체이다. 즉, 얘는 실제 Connection
인터페이스를 가지고 있으며, 이 기능들을 제공해주는 애여야 함을 개념적으로 알 수 있다.
DAO
는 보통 infrastructure
패키지에 위치되어서 데이터베이스 접근을 전담하는 객체로 있는게 좋다.
- domain 쪽은 비즈니스 로직의 중심
- infrastructure 는 DB, 외부 API, 메시지 브로커 등 구체적 기술 의존성이 들어가는 곳
DTO
는 Data Transfer Object 이다. 데이터를 전달하기 위한 용도로만 쓰며, 불변이 특징인 객체이다. 따라서 보통 record 로 작성하면 편하며, equals, hashCode 등은 필수이다.
필자는 보통 Service <-> Controller 사이만 DTO 를 사용하고 Service 에서는 비즈니스 엔티티와 DTO 를 혼용하여 사용하였지만,
이는 전역적으로 사용하는 클래스라 따로 빼는게 좋다.
✒️ 용어
Stale Entry
오래되거나 최신 상태가 아닌, 더 이상 유효하지 않은 데이터나 항목을 의미한다.