F-LAB
멘토링 1회차 - 인사이트
Pleasant Pain
2025. 3. 5. 17:52
인사이트 정리
- 싱글턴 패턴은 멀티 스레드 환경에서 올바르게 구현하여야 함.
- 자바의 정적 변수는 프로그램 실행 시 메모리에 로드되며 가비지 컬렉션의 대상이 아님.
- 스프링에서 사용할 수 있는 롬복의 빌더는 직접 생성과 빌더를 동시에 사용할 수 있도록 설계되어 있음.
- JVM의 저스트 인 타임 컴파일러는 코드 사용 빈도에 따라 실행되며, 메모리 효율성을 높임.
- 자바 타입 언어의 특성과 인터프리터 타입 언어의 차이를 명확히 이해해야 함.
- 정적 팩토리 메소드를 사용하면 기능을 보다 명확히 표현할 수 있음.
- JPA NTT 객체 생성 시 null을 허용하는 Reference 타입의 사용이 데이터베이스와의 호환성을 높임.
📌 1. 싱글턴 패턴과 멀티 스레드
🔹 기존에 알고 있던 내용
- 싱글턴 패턴은 synchronized 키워드를 사용해서 동기화해야 한다.
❌ 잘못 알고 있었던 점
- synchronized만 사용하면 스레드 안전한 싱글턴이 된다고 생각했다.
- 메모리 가시성 문제를 고려하지 않았다.
✅ 정확한 개념 정리
- JVM 메모리 모델에서 각 스레드는 자신의 캐시 메모리를 사용하므로, 공유된 static 변수를 읽더라도 최신값이 아닐 수 있다.
- 이를 해결하려면 volatile 키워드를 사용하거나, 클래스 로더 특성을 이용한 안전한 싱글턴 패턴을 적용해야 한다.
🔹 코드 예제
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
📌 2. static 변수의 메모리 적재와 GC
🔹 기존에 알고 있던 내용
- static 변수는 "프로그램 실행과 동시에" 메모리에 올라간다.
❌ 잘못 알고 있었던 점
- static 변수는 실행과 동시에 메모리에 올라가는 것이 아니라 클래스 로딩 시점에 적재됨.
- static 변수도 GC의 대상이 될 수 있음.
✅ 정확한 개념 정리
- static 변수는 클래스 로더가 클래스를 처음 로드할 때 메모리에 할당됨.
- GC의 대상이 아예 안 되는 것은 아니며, 클래스가 언로드될 때 같이 해제될 수 있음.
- static 변수는 JVM의 메타스페이스 영역에서 클래스 정보가 로드될 때 함께 저장됨.
- 다만 힙 영역에서 관리되므로, 모든 스레드에서 공유됨.
🔹 코드 예제
class Test {
static int count = 0;
public static void main(String[] args) {
count++;
System.out.println(count);
}
}
➡ 위 코드에서 count는 힙 영역에 저장되며, 여러 스레드가 공유하게 된다.
📌 3. JIT 컴파일러의 최적화 과정
🔹 기존에 알고 있던 내용
- JIT 컴파일러는 성능을 높이기 위해 존재한다.
❌ 잘못 알고 있었던 점
- JIT 컴파일러가 실행될 때 모든 바이트 코드를 즉시 네이티브 코드로 변환한다고 생각했다.
✅ 정확한 개념 정리
- JVM은 처음에는 인터프리터 방식으로 코드를 실행한다.
- 하지만 자주 실행되는 코드를 감지하여 JIT 컴파일러가 네이티브 코드로 변환하여 캐싱한다.
- 이렇게 하면 같은 코드가 실행될 때 인터프리터를 거치지 않고 빠르게 실행된다.
🔹 코드 예제
public class JITExample {
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) {
add(5, 10);
}
long end = System.nanoTime();
System.out.println("Execution Time: " + (end - start));
}
public static int add(int a, int b) {
return a + b;
}
}
➡ 위 코드는 반복적으로 실행되며, JIT 컴파일러가 최적화를 수행하여 실행 속도가 빨라짐.
JIT에 제대로 이해한 것을 다시 정리
- 자바 컴파일러가 .java 파일을 컴파일 해주고 바이트 코드(.class 파일)로 변환해주는 것 맞다
- 이러한 바이트 코드를 JVM이 읽어드리는 것은 맞으나, 정확히는 JVM의 인터프리터이다
- (네이티브 코드가 아닌) 바이트 코드만으로는 CPU가 이해할 수 없기 때문이다
- 따라서 바이트 코드를 CPU가 읽어드릴 수 있도록 인터프리터가 바이트 코드를 한 줄씩 읽어서 처리해주는 것이다
- JIT 컴파일러는 런타임 과정에서 인터프리터가 자주 읽어드리는 바이트 코드를 모니터링한다 (프로파일링한다)
- 그러다 "어? 이놈 봐라? 자주 실행되는 바이트 코드네? 매번 인터프리터가 읽어드리기는 비효율적인데?"라고 판단되면,
JIT은 해당 바이트 코드를 한 번 네이티브 코드로 변환한 후, JVM의 코드 캐시(Code Cache)에 저장해둔다. - 그리고 다시 똑같은 바이트 코드를 읽어드릴 때가 오면 인터프리터로 그 바이트 코드를 읽는게 아니라,
JIT에 의해 컴파일되어 캐싱되어있는 네이티브 코드를 활용해 OS를 통해 CPU에 명령을 전달한다 - CPU가 읽어드릴 수 있는 코드는 네이티브 코드(=기계어)이나 미리 네이티브로 컴파일하지 않는 이유는 아래 2가지 이유 때문이다
- 네이티브 코드(=기계어)는 0과1로 이루어진 이진수로 엄청나게 코드가 길어져 메모리를 많이 차지하게되며,
바이트 코드로 컴파일하는 것보다 컴파일 시간도 그만큼 오래 걸리게 된다
- 같은 자바 코드라 할지라도, 결국 CPU가 읽어드려야할 네이티브 코드는 다를 수 있다,
즉, 미리 네이티브 코드로 컴파일 해두면 JVM의 주된 장점 중 하나인 플랫폼으로서 역할을 할 수 없게 된다 - 이로써 JIT 컴파일러는 결국 JVM의 성능을 더 높이기 위해 존재하는 것이며,
어플리케이션 실행과 동시에 그 역할을 한다기 보다,
어플리케이션 실행 후 런타임 환경에서 어떻게 운영되느냐에 따라 작동 방식이 달라진다
(같은 바이트 코드가 얼마나 읽어드리느냐)