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에 제대로 이해한 것을 다시 정리

  1. 자바 컴파일러가 .java 파일을 컴파일 해주고 바이트 코드(.class 파일)로 변환해주는 것 맞다
  2. 이러한 바이트 코드를 JVM이 읽어드리는 것은 맞으나, 정확히는 JVM의 인터프리터이다
  3. (네이티브 코드가 아닌) 바이트 코드만으로는 CPU가 이해할 수 없기 때문이다
  4. 따라서 바이트 코드를 CPU가 읽어드릴 수 있도록 인터프리터가 바이트 코드를 한 줄씩 읽어서 처리해주는 것이다
  5. JIT 컴파일러는 런타임 과정에서 인터프리터가 자주 읽어드리는 바이트 코드를 모니터링한다 (프로파일링한다)
  6. 그러다 "어? 이놈 봐라? 자주 실행되는 바이트 코드네? 매번 인터프리터가 읽어드리기는 비효율적인데?"라고 판단되면,
    JIT은 해당 바이트 코드를 한 번 네이티브 코드로 변환한 후, JVM의 코드 캐시(Code Cache)에 저장해둔다.
  7. 그리고 다시 똑같은 바이트 코드를 읽어드릴 때가 오면 인터프리터로 그 바이트 코드를 읽는게 아니라,
    JIT에 의해 컴파일되어 캐싱되어있는 네이티브 코드를 활용해 OS를 통해 CPU에 명령을 전달한다
  8. CPU가 읽어드릴 수 있는 코드는 네이티브 코드(=기계어)이나 미리 네이티브로 컴파일하지 않는 이유는 아래 2가지 이유 때문이다
    - 네이티브 코드(=기계어)는 0과1로 이루어진 이진수로 엄청나게 코드가 길어져 메모리를 많이 차지하게되며,
       바이트 코드로 컴파일하는 것보다 컴파일 시간도 그만큼 오래 걸리게 된다
    - 같은 자바 코드라 할지라도, 결국 CPU가 읽어드려야할 네이티브 코드는 다를 수 있다,
       즉, 미리 네이티브 코드로 컴파일 해두면 JVM의 주된 장점 중 하나인 플랫폼으로서 역할을 할 수 없게 된다
  9. 이로써 JIT 컴파일러는 결국 JVM의 성능을 더 높이기 위해 존재하는 것이며,
    어플리케이션 실행과 동시에 그 역할을 한다기 보다,
    어플리케이션 실행 후 런타임 환경에서 어떻게 운영되느냐에 따라 작동 방식이 달라진다
    (같은 바이트 코드가 얼마나 읽어드리느냐)