728x90

Java를 사용하여 개발을 하기 위해서는 JDK, JRE, JVM을 필요로 한다.

 

오늘은 이것들이 무엇이며, 우리가 프로그램을 개발하고 실행되기까지 어떤 일들이 벌어지는지 알아보도록 하겠다.


1. JDK( Java Deployment Kit )

가장 먼저 알아볼 것은 자바 개발 도구이다.

JDK는 기본적으로 우리가 자바를 사용하여 개발하고, 실행하기 위해 필요한 것들이 담겨있다.

 

자바로 개발된 프로그램을 실행하기 위한 JRE와 필수적 그리고 기본적인 자바 개발 도구들(Java, Javac,...)등이 포함된다.

 

그럼 JDK만 설치되어 있다면 메모장만 사용해도 코드가 돌아가게 할 수 있나요?

 

물론이다. 이를 증명하기 위해 메모장으로 코딩을 진행해 보겠다.

우선, 텍스트 파일에 기본적인 코딩을 해보았다.

MacOs에서 진행

이후 해당 파일을 Java컴파일러로 컴파일하기 위해. java파일로 변경해 준다.

javac를 사용하여 컴파일 결과

javac명령어를 사용하여 컴파일을 진행한다.


왜 컴파일을 진행해야 할까?

Java 코드는 사람이 읽기 쉬운 고급 언어이다.

이는 사람은 이해할 수 있지만 컴퓨터는 이해할 수 없는 언어라는 의미이다.

 

그럼 외국인과 대화를 하기 위해 번역가가 필요하듯이 언어도 번역가가 필요한데 이 역할을 수행하는 것이 컴파일러다.

 

또한 컴파일과정을 거치며, 문법적으로 잘못된 부분까지 체크를 진행하게 되고, 이때 발생하는 오류가 CompileException이다.

즉, 개발을 진행하면서 오류가 발생하였다면 문법이 잘못되었거나 실행할 수 없는 파일이라 보면 된다.

 

자바 컴파일러가 변환작업을 진행하면 자바 코드 소스를 바이트코드로 변환하게 되는데, 이때 확장자는. class고 이때서야 JVM이 해당 소스를 실행시킬 수 있다.


이후 java 명령어를 통해 Class를 실행시키면 정상적으로 작동하는 것을 확인해 볼 수 있다.

 

이렇듯 JDK를 설치한다면 단순한 text로도 개발이 가능한 것을 확인해 볼 수 있다.


2. JRE ( Java Runtime Environment )

우리가 만든 Java프로그램이 동작하려면, 단순히 컴파일을 거쳐. class파일을 만들었다고 실행되는 게 아니다.

실행을 하기 위한 머신, 라이브러리, 파일들이 필요한데, JRE는 이를 모아놓은 패키지이다.

 

위 글에서 보았듯 JRE는 JDK에 포함되어 있어 JDK를 설치하면 함께 설치된다.

( 버전에 따라 다르다. )

 

JRE은 다음과 같은 구성으로 이루어져 있다.

구성요소 설명
JVM 바이트코드를 읽어 실행하는 엔진
Java 클래스 라이브러리 입출력, 네트워크, 컬렉션 등 기본 기능을 제공
Java 클래스 로더 필요한 클래스를 메모리에 동적으로 로드
런타임 라이브러리 및 지원 파일 JVM이 동작하는 데 필요한 추가 리소스 

 

이를 보면 알 수 있지만, JVM은 이론적으로 JRE 없이 단독 설치하는 것은 어려우며, 실용적이지 않다.

왜냐면 JVM은 JRE의 일부분이며, JRE가 JVM이 작동할 때 필요한 기능들을 제공하기 때문이다.


3. JVM ( Java Virtual Machine )

자바 가상 엔진으로 Java로 작성된 프로그램을 실행시키고 관리하는 실질적인 주체입니다.

 

"JVM은 JRE 없이 사용하기 어려우면 JRE가 주체 아닌가요?"

위와 같은 의문이 들었다면 JRE가 무엇인지 다시 확인해 볼 필요가 있습니다.

 

JRE는 실행 환경 전체를 말하는 "패키지"고,

그 안에 포함된 JVM이 실제로 프로그램을 실행하고 메모리를 관리하는 주체로 이해하면 된다.

 

JVM은 운영체제에 영향을 받는데, 이 영향은 JVM에 한정적이기 때문에 Java 프로그램은 모든 플랫폼에서 제약 없이 동작할 수 있다.

이는 Java로 개발된 프로그램의 가장 큰 이점인데, Java로 작성된 코드는 컴파일 후 운영체제와 상호작용을 하는 것이 아닌 JVM과 상호작용을 하고, JVM이 운영체제와 상호작용을 하기 때문에 Java로 개발된 프로그램은 이식성이 좋다.

window JVM, Linux JVM 따로 있다.

위 그림과 같이 컴파일된  바이트코드는 모든  JVM에서 동일하게 인식하기 때문에 운영체제에 맞는 JVM만 설치된다.

  1. 자바 컴파일러가. java를. class로 컴파일(바이트코드).
  2. JVM이 바이트코드를 바이너리코드로 변환.
  3. 변환된 바이너리코드를 CPU에서 실행.

여기까지가 간단하게 Java코드가 실행되기까지의 과정을 간단하게 다뤄보았다.

이제 내부적으로 어떤 일이 일어나는지 알아보도록 하자.


지금부터 알아볼 JVM의 구성요소이다.

  • Class Loader
  • Execution Engine
    • JIT ( Just In Time )
    • Interpreter
    • Garbage Collector
  • Runtime Data Area
    • Method Area
    • Heap
    • PC Register
    • JVM Stack
    • Native Mathod Stack

3.1. Class Loader

클래스 로더는. class 파일을 메모리에 올려 런타임에 사용할 수 있게 만드는 역할을 한다.

  • 클래스를 읽고( Loading )
  • 검증하고 ( Verification )
  • 필드와 메서드를 위한 메모리를 준비하고( Preparation )
  • 심볼릭 참조를 직접 참조로 바꾸고 ( Resolution )
  • 초기화 코드를 실행( Initialization )

여기까지 모든 클래스를 "사용 가능"하게 만드는 전체 파이프라인의 시작점이다.

이 작업들은 아래와 같이 3단계로 구분할 수 있다.

  1. Loading: 클래스 파일을 JVM의 메모리에 로드한다.
  2. Linking: 클래스 파일을 사용하기 위해 검증한다.
    - Verifying: 읽은 클래스가 JVM에 명시된 대로 구성되어 있는지 검사한다.
    - Preparing:
    클래스가 필요로 하는 메모리를 할당한다.
    - Resolving: 
    클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  3. Initializing: 클래스 변수들을 적절한 값으로 초기화한다. (Static 블록 실행 및 필드 초기화)
더보기

용어정리

상수 풀( Constant Pool ): 클래스 파일에 있는 다양한 정보 저장소(클래스명, 메서드명, 필드 정보 등)

심볼릭 레퍼런스( Symbolic Reference ): "이 클래스/메서드/필드는 이름으로 이렇게 적혀있어요"라는 문자열 기반 정보

다이렉트 레퍼런스( Direct Reference): JVM 내부에서 사용하는 메모리 주소 또는 포인터

해당과정이 무작정 이뤄지지는 않는다.

JVM은 클래스를 “필요할 때” Load 하는데 이는 다음과 같은 이유가 존재한다.

  • 메모리 효율성
  • 애플리케이션 실행 시작 시간 단축
  • 모듈화의 유연성 확보

즉, 처음에 모두 메모리에 적재할 경우 비효율성과 유연성을 떨어트리기 때문에 애플리케이션에서 필요로 할 때 메모리에 적재한다.


3.2. Execution Engine

실행엔진은 바이트코드를 해석하고, CPU가 이해할 수 있는 바이너리코드로 변환하여 실행한다.

변환하고 실행할 때 실행엔진은 코드를 실행하기 위해 InterpretrJIT을 사용한다.


3.2.1. Interpreter

  • 바이트코드 명령어를 하나씩 읽어 해석하고 실행한다.

3.2.2 JIT Compiler

  • Interpreter의 중복코드 중복 해석 및 실행에 대응하고자 도입.
  • 바이트코드 전체를 컴파일하여 NativeCode로 변경.
  • NativeCode로 변경한 바이트코드는 Interpreting 되지 않고 캐싱해 두었다 NativeCode로 직접 실행
    NativeCode는 C나 C++, 어셈블리어로 구성된 인터프리터언어를 의미한다.
  • 중복 코드 실행의 경우 계속 Interpreting 하는 것보다 성능이 좋지만, NativeCode로 변경하는데 자원이 소모되기 때문에 Interpreter방식으로 사용하다 일정 기준이 넘어가면 JIT Compile방식 사용.

3.2.3 Garbage Collector

JVM은 Heap 메모리 영역에서 더 이상 참조되지 않는 메모리회수하기 위해 Garbage Collecor를 사용한다.

 

개발자가 직접 사용하는 것이 아닌 자동으로 작동하기 때문에 더욱 손쉽게 메모리 관리를 할 수 있다.

더 이상 사용하지 않는 데이터의 경우 null로 초기화해 주면, 더욱 효율적으로 Garbage Collector를 이용할 수 있다.

 

Garbage Collector는 주기적으로 작동하는 프로세스로서 참조되지 않는 메모리가 언제 해제되는지 알 수 없으며, GC가 작동하게 되면 GC관련 Thread를 제외한 다른 Thread들이 중지되기 때문에 오버헤드가 발생한다.

더보기

STW( Stop The World ): GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상


3.3. Runtime Data Area

 

Runtime Data Area는 JVM 메모리 영역으로 자바 프로그램을 실행할 때 사용되는 데이터를 사용 목적에 맞춰 나누어져 있다.

 

아래와 같이 크게 2가지 특징으로 분리를 해볼 수 있다.

  • Thread-specific ( Thread마다 개별로 존재 )
    • PC Register
    • Stack
    • Native Method Stack
  • Shared (모든 Thread가 공유)
    • Heap
    • Method Area

Thread-specific은 Thread마다 개별로 존재하여 각 Thread 간 메모리 공유가 불가능하다.

Shared는 Thread와 별개로 할당된 메모리 영역으로 각 Thread들이 해당 메모리에 접근한다면 동일한 데이터를 사용할 수 있다.


3.3.1. PC Register

우리가 JVM의 PC Register를 이해하기 위해서는 CPU의 PC Register와 무엇이 다른지 이해해야 한다.

 

CPU의 PC Register는 현재 CPU가 실행할 다음 명령어의 주소를 저장하는 하드웨어 레벨 Register이다.

하지만 JVM의 PC Register는 물리적인 레지스터가 아니라 가상 머신 레벨에서의 명령어 실행 흐름 제어 구조이다.

 

그럼 왜 필요할까?

 

JVM이 바이트코드를 실행할 때 다음 구조를 따른다.

[바이트코드][PC Register (현재 위치)][Opcode 해석기][Operand Stack]

 

아래 코드를 사용하여 한스탭씩 따라가 보자.

int a = 1 + 2;

코드를 바이트코드로 컴파일하면 아래와 같은 결과물이 나온다.

iconst_1
iconst_2
iadd
istore_1

해당 코드를 실행한다면 PC Register는 iconst_1의 위치를 가르치고,

순차적으로 프로그램의 흐름을 추적한다.

 

그럼 PC Register가 가리키는 바이트코드를 Interpreter가 읽어 해당 명령에 대응되는 C/C++ 함수를 실행한다.

이후 Interpreter가 실행한 C/C++함수가 CPU를 작동시키게 된다.

 

이렇듯 Java는 CPU에게 바로 연산시키는 것이 아닌 현재 실행해야 하는 연산을 제공하여야 하며, 이를 위한 버퍼 공간으로 사용하기 위해 PC Register메모리 영역이 존재한다.

 

또한 PC Register는 Thread마다 각각 가지고 있다.

즉, "현재 Thread의 어떤 명령어가 실행 중인가?"에 대한 명령어 주소를 기억하고 있다.


3.3.2. Stack

스택은 원시타입( Primitive type ) 데이터와 참조타입(Reference type)의 주소값, 현재 실행 중인 메서드의 스택 프레임을 저장하는 메모리 영역이다.

 

LIFO구조로 push와 pop기능 방식으로 동작한다.

Thread마다 하나씩 존재하며, Thread가 시작될 때 할당되며, Thread가 종료되면 사라진다.

 

이때, 프로그램 실행 중 할당된 메모리를 벗어난 데이터들이 들어온다면 StackOverFlowError가 발생한다.


3.3.3. Native Method Stack

이는 실제 개발하면서 보기는 힘들고, 내부 라이브러리를 깊게 파보면 native선언을 해둔 것을 확인할 수 있다.

이는 Java로 구현된 것이 아닌 C/C++과 같은 언어로 구현되었다는 의미이며,

예시로 Thread Class를 보면 registerNatives 메서드는 native선언이 되어 있는 것을 확인할 수 있다.

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

 그럼 왜 Java로 작성하지 않고 C/C++로 대신 구현하는 것일까?

  • 시스템 리소스 접근
  • 성능이 중요한 로직을 C/C++로 최적화
  • 기존 C/C++ 라이브러리와 연동

그렇다 컴파일 언인 Java보다 인터프리터언어인 C/C++ 이 성능적으로 앞서 성능 최적화를 한다거나 시스템에 직접 접근하려 할 때 사용하는 것을 확인할 수 있다.

 

Native Method Stack을 통해 C함수 호출 프레임을 관리하여 JVM은 해당 기능을 사용할 수 있는 것이다.


3.3.4. Heap

힙영역은 참조타입( Reference type ) 데이터들이 저장되며, new연산자로 생성되는 Instance들이 저장된다.

 

해당 영역에 존재하는 데이터들은 Stack영역에서 참조하지 않으면 GC대상이 된다.


3.3.5. Method Area

 흔히 ClassStatic으로 선언된 변수 혹은 메서드들이 프로그램이 시작될 때 메모리에 로드되어 프로그램이 종료될 때까지 남아 있다.

 

Heap과 동일하게 모든 Thread가 공유하고, 여러 정보들을 저장하고 있다.

  • Field Info: 멤버 변수명, data type, 접근 제어자의 정보
  • Method Info: 메서드명, return type, 매개변수, 접근 제어자
  • Type Info: Class인지 Interface인지 저장, Type의 속성, Super Class의 이름

즉, 정적 필드와 클래스 구조만을 가지고 있다.(클래스는 new로 생성할 경우 Instance가 Heap에 생성된다.)


3.3.6. JNI

Java Native Interface로 다른 언어로 만들어진 애플리케이션과 상호 작용할 수 있는 인터페이스를 제공한다.


3.3.7. Native Method Library

C/C++로 작성된 라이브러리다.

 

자바에서 native 키워드로 선언된 메서드를 호출할 때, 필요에 따라 JNI를 통해 해당 네이티브 라이브러리를 호출하며, 이 과정에서 Native Method Stack이 사용된다.


4. 글을 마치며

JVM, JDK, JRE에 어떤 것들이 존재하고, 어떤 역할을 하는지에 대해 알아보았다.

이전부터 알고 있던 내용들도 있었지만, 시간을 충분히 투자하며 차근차근 공부해 보니 확실히 모르고 있었던 부분도 많이 보였던 거 같다.

 

"Java개발자가 JVM을 이렇게까지 알아야 하나요? 그냥 코드 잘 짜면 잘 돌아가잖아요."

 

어느 정도 맞는 말인 거 같다.

JVM내부 요소들을 건드리며 개발을 안 하더라도 서버 스펙만 좋다면 아무런 문제 없이 돌아갈 것이다.

개판으로 작성해도 잘 돌아갈 텐데 잘 짰으면 얼마나 잘 돌아갈까.

 

하지만 알고 있다면 메모리를 좀 더 신중하게 사용할 수 있을 것이고, 성능적으로 한 번쯤은 고민해보지 않을까?

그리고 어떤 문제가 발생했다면 이를 해결하기 위해 고민을 할 때 이러한 공부들은 많은 도움이 될 것이다.

 

그리고 도구를 사용하면 어떤 도구인지 알고 있으면 잘 사용하지 않겠나. 


References

https://inpa.tistory.com/entry/JAVA-%E2%98%95-JVM-%EB%82%B4%EB%B6%80-%EA%B5%AC%EC%A1%B0-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%81%EC%97%AD-%EC%8B%AC%ED%99%94%ED%8E%B8#%EB%9F%B0%ED%83%80%EC%9E%84_%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%98%81%EC%97%AD_runtime_data_area

https://d2.naver.com/helloworld/1230

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
728x90

Java에서 제네릭과 메서드 오버로딩이 함께 사용될 때, 우리가 기대했던 것과는 다른 결과가 나오는 경우가 있습니다.

 

이번 글에서는 아래 코드에서 왜 B0가 출력되는지 심층적으로 분석해 보겠습니다.


코드 분석

class Main {
  public static class Collection<T> {
    T value;

    public Collection(T t) {
        value = t;
    }

    public void print() {
       new Printer().print(value);
    }

    class Printer {
      void print(Integer a) {
        System.out.print("A" + a);
      }
      void print(Object a) {
        System.out.print("B" + a);
      }
      void print(Number a) {
        System.out.print("C" + a);
      }
    }
  }
  
  public static void main(String[] args) {
      new Collection<>(0).print();
  }
}

위 코드를 실행하면 B0가 출력됩니다.

하지만 우리는 print(Integer a) 또는 print(Number a)가 호출될 것이라고 예상할 수도 있습니다.

 

그 이유를 하나씩 살펴보겠습니다.


1. 제네릭 타입 결정

new Collection<>(0);

위 코드에서 Collection <T> Collection <T>의 타입 매개변수 TInteger로 추론됩니다.

따라서 Collection <Integer> Collection <Integer>가 됩니다.

 

즉, value의 타입은 Integer가 됩니다.

그런데 print() 메서드를 호출하면 다음 메소드가 작동하게 됩니다.

new Printer().print(value);

 

이때, valueInteger 타입이므로 print(Integer a), print(Object a), print(Number a) 중 하나가 호출될 것입니다.


2. 오버로딩과 정적 바인딩

Java의 메서드 오버로딩컴파일 타임에 정적으로 결정됩니다.

즉, 컴파일러는 print(value)가 호출될 때, value컴파일 시점 타입을 기준으로 가장 적절한 메서드를 선택합니다.

 

하지만 중요한 점은, 제네릭에서 T는 컴파일 시점에는 그냥 타입 매개변수일뿐이며, 구체적인 타입으로 변환되지 않는다는 것입니다.

 

즉, TInteger이지만, 컴파일러는 Type을 Object로 간주하여 처리합니다.

결국, print(value);는 아래와 동일하게 해석됩니다.

new Printer().print((Object) value);

따라서 가장 적합한 메서드는 print(Object a)가 되고, 결과적으로 "B0"가 출력됩니다.


3. 왜 print(Integer a)print(Number a)가 호출되지 않을까?

제네릭 코드에서 타입이 특정 타입으로 지정되었더라도, 컴파일 타임에는 T 자체가 Object로 간주됩니다.

즉, T를 포함한 연산에서는 TObject 타입으로 처리되는 경우가 많습니다.

 

아래 코드로 비교해 보겠습니다.

일반적인 경우:

class Test {
    void print(Integer a) { System.out.print("A" + a); }
    void print(Object a) { System.out.print("B" + a); }
    void print(Number a) { System.out.print("C" + a); }

    public static void main(String[] args) {
        Test test = new Test();
        test.print(0); // "A0" 출력
    }
}

위 코드에서는 0Integer 타입이므로 print(Integer a)가 정확히 호출됩니다.

제네릭을 사용한 경우:

class Collection<T> {
    T value;
    public Collection(T t) { value = t; }
    void print() { new Printer().print(value); }
    
    class Printer {
        void print(Integer a) { System.out.print("A" + a); }
        void print(Object a) { System.out.print("B" + a); }
        void print(Number a) { System.out.print("C" + a); }
    }
}
public class Main {
    public static void main(String[] args) {
        new Collection<>(0).print(); // "B0" 출력
    }
}

여기서 TInteger이지만, 컴파일 타임에는 Object로 간주되므로 print(Object a)가 호출됩니다.


4. 해결 방법

만약 print(Integer a)가 호출되길 원한다면, 다음과 같이 해결할 수 있습니다.

명시적 캐스팅 사용

void print() {
    new Printer().print((Integer) value);
}

위처럼 명시적으로 Integer로 캐스팅하면, print(Integer a)가 호출됩니다.

타입 상한 경계 설정

class Collection<T extends Integer> {
    T value;
    public Collection(T t) { value = t; }
    void print() { new Printer().print(value); }
    
    class Printer {
        void print(Integer a) { System.out.print("A" + a); }
        void print(Object a) { System.out.print("B" + a); }
        void print(Number a) { System.out.print("C" + a); }
    }
}
public class Main {
    public static void main(String[] args) {
        new Collection<>(0).print(); // "A0" 출력
    }
}

이처럼 T extends Integer를 사용하면 T는 최소한 Integer로 간주되므로 print(Integer a)가 호출됩니다.


정리

  1. 제네릭의 타입은 컴파일 타임에 Object로 간주된다.
  2. 오버로딩된 메서드는 컴파일 시점에 정적으로 결정된다.
  3. 명시적 캐스팅 또는 타입 상한 경계 설정을 사용하면 원하는 메서드를 호출할 수 있다.

정처기 실기를 준비 중에 만난 문제에서 그동안 놓쳤던 부분을 알게 되어 작성하게 되었습니다.

 

당연히 Integer를 호출할 것이라 생각하고, Type을 찍어도 Integer를 출력하지만 답은 "B0"가 나오는 상황이 답답하여 정리하게 되었는데요.

이처럼 놓친 내용들이 있는지 확인하며 기본기를 탄탄히 다 저나 갈 생각입니다.

728x90
728x90

Java에서 멀티스레드 환경을 개발하다 보면, 스레드마다 독립적인 데이터를 저장하고 관리해야 할 때가 있습니다.

이럴 때 유용하게 활용할 수 있는 클래스가 바로 ThreadLocal입니다.

 

이번 글에서는 ThreadLocal의 개념, 사용 방법, 그리고 실제 사례를 중심으로 살펴보겠습니다.

ThreadLocal이란?

ThreadLocal은 각 스레드마다 별도의 변수를 저장할 수 있도록 지원하는 Java 클래스입니다.

 

동일한 ThreadLocal 인스턴스를 사용하더라도, 각 스레드가 자신만의 독립적인 값을 가지며, 다른 스레드와 공유되지 않습니다.

즉, 하나의 변수를 스레드 간에 안전하게 관리할 수 있는 메커니즘을 제공합니다.

ThreadLocal의 특징

  1. 각 스레드마다 독립적인 데이터 저장 공간을 제공합니다.
  2. 스레드 간 데이터 공유를 방지하며, 멀티스레드 환경에서 데이터 충돌을 줄입니다.
  3. 특정 스레드에 한정된 데이터를 저장할 때 유용합니다.

ThreadLocal의 사용 방법

ThreadLocal은 주로 다음과 같은 메소드를 사용하여 데이터를 저장하고 접근합니다.

set()

현재 스레드의 ThreadLocal 변수에 값을 저장합니다.

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Thread A's Data");

get()

현재 스레드의 ThreadLocal 변수에 저장된 값을 가져옵니다.

String data = threadLocal.get();
System.out.println(data);

remove()

현재 스레드의 ThreadLocal 변수에 저장된 값을 삭제합니다. 이는 메모리 누수를 방지하기 위해 권장되는 작업입니다.

threadLocal.remove();

initialValue()

ThreadLocal 변수를 초기화할 때 사용됩니다. 기본적으로 null을 반환하지만, 필요하면 오버라이딩하여 초기값을 설정할 수 있습니다.

ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};
System.out.println(threadLocal.get());

사용 시 주의사항

ThreadLocal은 사용 후 반드시 remove() 메소드를 호출하여 데이터를 정리해야 합니다.

이를 소홀히 하면, 스레드가 종료된 후에도 ThreadLocal 객체에 데이터가 남아 메모리 누수가 발생할 수 있습니다.

 

또한, 너무 많은 데이터를 ThreadLocal에 저장할 경우 OOM을 야기할 수 있으니 적절하게 사용하는 것을 권장합니다.

 

ThreadLocal은 부모 Thread가 자식 Thread로 데이터를 전달하지 않습니다.

따라서 데이터를 상속하기 위해서는 ThreadLocal를 상속한 InheritableThreadLocal를 사용하시면 됩니다.


사용해 보기

ThreadLocal 정의

@Getter
@Setter
public class CustomContext {
    private static final ThreadLocal<CustomContext> CONTEXT = ThreadLocal.withInitial(CustomContext::new);

    private Object data;

    public static CustomContext getContext() {
        return CONTEXT.get();
    }

    public static void clearContext() {
        CONTEXT.remove();
    }
}

JWT Token 유저 ID ThreadLocal에 저장 및 사용 후 정리

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;
    private final CustomContext customContext = CustomContext.getContext();;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        if(!(handler instanceof HandlerMethod))
            return true;

        String token = request.getHeader("authorization");
        if(Strings.isEmpty(token)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT token is missing");
            return false;
        }

        token = token.substring(7);
        if(!jwtTokenProvider.validateToken(token)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT Token is invalid");
            return false;
        }
        String userId = jwtTokenProvider.getUserId(token);
        customContext.setData(userId);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        CustomContext.clearContext();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

ThreadLocal 데이터 사용

@Getter
@Setter
public class CreateQuoteRequest {
    transient private String userId;
    private long amount;
    private CurrencyCode targetCurrency;

    public CreateQuoteRequest() {
        CustomContext context = CustomContext.getContext();
        Assert.notNull(context);
        this.userId = (String) context.getData();
    }
}

후기

기존 ID를 넘겨줄 때 Token에서 뽑아 HttpServletRequest에 담거나 하여 사용을 하였지만,

이와 같이 ThreadLocal을 사용하게 된다면 본인 Thread 내에서 데이터를 쉽게 뽑아 사용할 수 있다는 점이 매력적으로 다가온 기술입니다.

 

물론 메모리에 올려두고 사용한다는 점에서 비효율적일 수 있지만, 코드를 생각했을 때 좀 더 보기 편하고 관리하기 편하다는 점이 부정할 수 없다 생각하는데요.

 

추후 좀 더 관리하기 좋은 방법을 찾게 된다면 포스팅하도록 하겠습니다. 

728x90
728x90

컴퓨터 프로그래밍에서 실수 연산은 흔히 오차 문제를 동반합니다.

 

특히 금융, 과학 계산, 그리고 데이터 분석 등 정확한 수치가 중요한 분야에서는 이 문제가 더욱 부각되는 문제인데요.

이번 글에서는 Double의 한계, Float를 사용한 실수 연산의 특징, 그리고 BigDecimal을 활용한 오차 없는 실수 연산 방법을 살펴보겠습니다.


1. Double의 문제: 부동소수점 연산의 한계

Double은 대부분의 프로그래밍 언어에서 기본 실수형으로 사용됩니다.

 

이는 64비트 IEEE 754 표준을 따르며, 빠른 연산 속도와 넓은 범위의 숫자를 처리할 수 있는 장점이 있습니다.

하지만 정확도 면에서는 한계가 있습니다.

public class DoubleExample {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double c = a + b;

        System.out.println("0.1 + 0.2 = " + c);
    }
}

출력 결과.

0.1 + 0.2 = 0.30000000000000004

왜 이런 일이 발생할까요?

  • Double은 2진수 기반의 부동소수점 표현 방식을 사용합니다.
  • 10진수 0.1과 0.2를 정확히 표현할 수 없기 때문에, 이진수로 변환될 때 근삿값으로 저장됩니다.
  • 이로 인해 연산 결과에도 미세한 오차가 발생합니다.

2. Float 사용: 더 작은 범위, 더 큰 오차

Float는 Double보다 작은 32비트 부동소수점 자료형입니다.

메모리 사용량이 적고 연산 속도가 빠르지만, 표현할 수 있는 유효 숫자 범위가 좁습니다.

public class FloatExample {
    public static void main(String[] args) {
        float a = 0.1f;
        float b = 0.2f;
        float c = a + b;

        System.out.println("0.1f + 0.2f = " + c);
    }
}

출력 결과.

0.1f + 0.2f = 0.3

겉보기에는 정확해 보이지만, 실제로는 내부에서 근삿값을 저장하고 있어 더 작은 숫자 범위에서 더 큰 오차가 발생할 가능성이 높습니다.

Float의 주요 특징

  • 적합한 경우: 높은 정밀도가 요구되지 않는 경우 (예: 게임 개발, 그래픽 연산).
  • 부적합한 경우: 금융 계산이나 통계 연산 등 오차가 치명적인 경우.

3. BigDecimal 사용: 실수 연산의 정밀도 보장

BigDecimal은 Java에서 제공하는 클래스 중 하나로, 정확한 실수 연산을 보장하기 위해 사용됩니다.

이 클래스는 실수를 10진법 기반으로 처리하므로, 부동소수점의 근삿값 문제를 해결할 수 있습니다.

import java.math.BigDecimal;

public class BigDecimalExample {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = new BigDecimal("0.2");
        BigDecimal c = a.add(b);

        System.out.println("0.1 + 0.2 = " + c);
    }
}

출력 결과.

0.1 + 0.2 = 0.3

중요 사항

  • new BigDecimal(double)로 생성할 경우 부동 소수점의 오차가 그대로 발생할 위험성이 존재합니다.
  • "scale()"메서드로 소수 자릿수를 설정하여 계산 결과를 정리할 수 있습니다.

그럼 뭘 사용해야 할까?

실수 연산은 문제 되는 상황을 인지하고 있지 않다면 쉽게 간과할 수 있는 중요한 이슈입니다.

 

특히 환율, 이율 등과 같이 금전적인 부분이 연결되어 있다면 이는 서비스의 신뢰도를 낮추게 되는 매우 크리티컬 한 문제가 될 수 있습니다.

 

그렇기 때문에 위에 주어진 Double, Float, BigDecimal들을 잘 활용하여 적재적소에 사용해야 하는 것은 중요한 기본이라 생각합니다.

728x90
728x90

1. Static이란??

Static 키워드를 선언하게 되면 사용할 때만 메모리에 할당하고, 사용 안 할 때는 제거되는 일반 선언들과 달리 프로그램이 종료될 때 까지 메모리에 할당되어 있는 것을 의미합니다.

 

그렇기 때문에 우리가 Static으로 선언한 것들은 따로 생성하지 않고 가져다 사용할 수 있는 것입니다.

 

그렇다면 우리가 흔히 사용하는 Static 변수와 Static 메소드들은 어떤 작동원리를 가지고, 어떤 차이점이 존재하는지 알아보도록 하겠습니다.


2. Static 변수

Static 변수는 클래스 수준에서 선언되며, 인스턴스와 관계없이 모든 객체가 동일한 메모리 공간을 공유합니다. Static 변수는 클래스가 메모리에 로드될 때 한 번만 초기화되며, 프로그램이 종료될 때까지 메모리에 유지됩니다.

Static 변수의 특징

1. 공유 메모리
Static 변수는 클래스 로더가 클래스를 로드할 때 메모리에 할당되며, 해당 클래스의 모든 인스턴스가 이 변수를 공유합니다. 따라서 인스턴스마다 별도의 값을 가지지 않고, 모든 인스턴스가 동일한 값을 참조합니다.

 

2. 클래스 이름으로 접근 가능
Static 변수는 객체를 생성하지 않고, 클래스 이름을 통해 직접 접근할 수 있습니다.

 

3. 메모리 효율성
여러 객체에서 공통으로 사용하는 데이터를 Static 변수로 선언하면, 각 객체가 별도의 메모리를 할당받지 않으므로 메모리 효율성을 높일 수 있습니다.

 

4. 초기화 시점
Static 변수는 프로그램 실행 시점에 클래스가 메모리에 로드될 때 초기화됩니다. 한 번만 초기화되며, 이후 변경된 값은 프로그램 종료 시점까지 유지됩니다.


Static 변수 사용 사례

1. 공유 데이터 관리
객체생성과 관계없이 데이터를 공유하고 싶다면 Static변수를 사용하여 관리할 수 있습니다.

 

2. 상수 선언

변하지 않는 상수 값을 정의할 때 Static 변수를 final 키워드와 함께 사용하여 메모리 낭비를 줄일 수 있습니다.

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
}


3. 유틸리티 클래스에서 공용 변수 사용
특정 설정 값이나 공용 데이터를 유틸리티 클래스에 Static 변수로 선언해 다른 클래스에서 쉽게 접근하도록 합니다.

public final class StandardCharsets {
    private StandardCharsets() {
        throw new AssertionError("No java.nio.charset.StandardCharsets instances for you!");
    }
    public static final Charset US_ASCII = sun.nio.cs.US_ASCII.INSTANCE;
    public static final Charset ISO_8859_1 = sun.nio.cs.ISO_8859_1.INSTANCE;
    public static final Charset UTF_8 = sun.nio.cs.UTF_8.INSTANCE;
    public static final Charset UTF_16BE = new sun.nio.cs.UTF_16BE();
    public static final Charset UTF_16LE = new sun.nio.cs.UTF_16LE();
    public static final Charset UTF_16 = new sun.nio.cs.UTF_16();
}

3. Static 메소드

Static 메소드는 객체를 생성하지 않고도 호출할 수 있는 클래스 수준의 메소드입니다. Static 변수와는 다르게 메소드는 로직을 정의하며, Static 키워드로 선언된 메소드는 클래스의 모든 객체가 동일한 동작을 공유합니다.

Static 메소드의 특징

1. 객체 없이 호출 가능
Static 메소드는 클래스 이름으로 호출할 수 있습니다.

 

2. Static 변수와 연동 가능
Static 메소드는 같은 클래스에 선언된 Static 변수에 직접 접근할 수 있습니다.

 

3. 인스턴스 멤버 사용 불가
Static 메소드 내에서는 클래스의 인스턴스 변수나 인스턴스 메소드를 직접 사용할 수 없습니다. 이는 Static 메소드가 클래스 수준에서 동작하기 때문입니다. 인스턴스 멤버를 사용하려면 해당 메소드에서 객체를 생성하거나 참조를 전달받아야 합니다.

 

4. 상속과 재정의 제한
Static 메소드는 상속받은 클래스에서 오버라이드(재정의)할 수 없습니다. 하지만 동일한 이름으로 정의해 숨길 수는 있습니다.


Static 메소드 사용 사례

1. 유틸리티 메소드 제공
자주 사용되는 공용 로직들을 Static 메소드로 정의해 간편하게 호출할 수 있습니다.

 

2. 팩토리 메소드 구현
특정 객체를 생성하는 로직을 Static 메소드로 제공할 수 있습니다.

 

3. 프로그램의 진입점
Java 프로그램은 main 메소드를 Static으로 선언하여, 객체를 생성하지 않고 프로그램의 실행을 시작합니다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

728x90
728x90

I/O 스트림이란?

I/O 스트림은 Java에서 데이터를 입력(Input)하거나 출력(Output)할 때 사용하는 추상화된 모델입니다.

  • InputStream: 데이터를 읽어오는 데 사용.
  • OutputStream: 데이터를 외부로 쓰는 데 사용.

특징

  • 데이터의 흐름을 Stream으로 간주.
  • Byte 단위 또는 Character 단위로 처리.
  • 데이터 소스: 파일, 네트워크 소켓, 메모리 등.

💡 "Java의 I/O는 Stream 기반이다. 데이터를 한 번에 처리하지 않고, 스트림으로 데이터를 흘려보내면서 효율적으로 작업한다."


InputStream과 OutputStream의 기본 구조

InputStream의 주요 메서드

 
int read() throws IOException  
int read(byte[] b, int off, int len) throws IOException  
void close() throws IOException

OutputStream의 주요 메서드

void write(int b) throws IOException  
void write(byte[] b, int off, int len) throws IOException  
void close() throws IOException
 

주요 I/O 클래스와 사용 사례

파일 읽기: FileInputStream

 
import java.io.FileInputStream;
import java.io.IOException;

public class FileInputExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data); // 데이터를 문자로 변환해 출력
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

파일 쓰기: FileOutputStream

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("example.txt")) {
            String data = "Java I/O Stream 예제입니다.";
            fos.write(data.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🔑 TIP: try-with-resources를 사용하면 스트림을 자동으로 닫아 메모리 누수를 방지할 수 있습니다.


Buffered 스트림과 성능 향상

BufferedInputStream 사용 예

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BufferedInputExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("example.txt"))) {
            int data;
            while ((data = bis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

왜 Buffered 스트림을 사용할까?

  • 기본 스트림은 데이터를 1바이트씩 처리.
  • Buffered 스트림은 버퍼를 사용해 데이터 블록을 한꺼번에 처리 -> 성능 향상.
728x90
728x90

JPA를 사용하여 데이터베이스에 락을 거는 방법은 2가지방법을 뽑아 볼 수 있습니다.

 

테이블에 행하는 행위 ( 조회, 수정, 등록, 삭제 )를 막는 비관적 락과 데이터에 버전을 명시하고 해당 버전을 통해 데이터의 일관성을 보장하는 낙관적 락이 이 경우입니다.

 

비관적 락  - Pessimistic Lock

비관적락은 데이터를 조회하고 특정 작업을 할 때 테이블에 어떤 작업도 일어나면 안되는 상황에서 사용하기 적합하다고 볼 수 있습니다.

이는 데이터의 정확성과 일관성을 보장하는 방법이며, 이 방법은 테이블에 락을 거는 행위로 성능에 큰 영향을 끼칠 수 있습니다.

 

데이터베이스의 종류에 따라 락을 걸었으나 데이터가 조회되는 경우도 있으니 이는 어떤 데이터베이스를 사용하는지를 확인을 하고 잘 선택하기 바랍니다.

public interface LectureJpaRepository extends JpaRepository<LectureEntity, Long> {
    //
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select l from LectureEntity l where l.id = :id")
    Optional<LectureEntity> findByIdForUpdate(long id);
}

사용법은 위와 같이 조회할 때 @Lock 어노테이션을 통해 Lock을 시작하는 Start point를 설정해줍니다.

 

LockkModeType에는 아래와 같은 옵션들을 선택하여 적용할 수 있습니다.

LockModeType Action
READ Entity의 버전을 확인합니다. - jpa1.0 버전 호환
WRITE Entity가 변경이 되지 않더라도 버전을 자동으로 올려줍니다. - jpa1.0 버전 호환
OPTIMISTIC thread가 종료될 때 Entity의 버전을 확인합니다.
OPTIMISTIC_FORCE_INCREMENT Entity가 변경이 되지 않더라도 버전을 자동으로 올려줍니다.
PESSIMISTIC_READ 데이터베이스가 지원하면 비관적락을, 지원하지 않다면 명시적 락으로 통해 읽기는 가능하지만 CUD는 할 수 없어집니다.
PESSIMISTIC_WRITE 해당 쓰레드를 제외 나머지 모든 쓰레드는 lock이 해제될때까지 Block됩니다.
PESSIMISTIC_FORCE_INCREMENT 비관적으로 잠기고, Entity가 변경되지 않더라도 버전이 자동으로 오릅니다.
NONE 잠금을 걸지 않습니다.

 

public class LectureService {

    @Transactional
    public Lecture loadLecture(long lectureId) {
		// 조회 -  Lock시작
        // 작업들
        //종료 - Transaction이 종료될 때 Lock을 풉니다
    }

}

 

비관적 락의 경우 Transaction내에서 유지되며, 해당 영역 내에서 작업을 끝내야 데이터의 일관성을 유지할 수 있습니다.

너무 많은 처리 및 Transaction전파에 의해 Lock이 길어질 수 있어 주의가 필요합니다.

낙관적 락  - Optimistic Lock

Version데이터를 통해 데이터의 일관성을 보장하는 방법입니다.

 

비관적락과 달리 테이블을 잠그거나 하지는 않지만, 버전이 다를 경우 데이터 수정작업 등을 처리하지 않는 것으로 동시성을 처리하였습니다.

 

비관적락에 비해 사용하기 쉽다는 장점이 있으며, 테이블을 잠그지 않기 때문에 성능적인 측면에서 더욱 효율적으로 관리할 수 있는 방법입니다.

단점으로는 버전을 통해 데이터의 일관성을 유지하기 때문에 동시에 여러번의 요청이 들어올 경우 많은 데드락이 발생 할 수 있습니다.

 

아래와 같은 방법으로 Entity에  @Version어노테이션을 사용함으로써 간단하게 명시 할 수 있습니다.

@Entity
public class LectureEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Version
    private int version
}

 

버전의 타입으로는 int, Long, TimeStemp와 같은 자료를 사용할 수 있으며,  TimeStemp의 경우 데이터의 일관성을 보장하기에는 int와 Long타입에 비해 부적절하지만, 상황에 따라 사용하는 경우가 존재한다고 합니다.


데이터베이스에서 락을 거는 만큼 무분별한 락사용과 제대로 된 설계가 아닐 경우 큰 성능적인 이슈를 야기할 수 있습니다.

 

이를 염두하고 적절한 락을 사용하는 것을 권장합니다.

또한 이 블로그글은 Hibernate구현체, Mysql기준으로 작성된 만큼 다른 데이터베이스, 또한 다른 기능들과 같이 사용할 경우 위에 소개한 상황과 다른 상황들이 발생할 수 있으니 확인해보시기 바랍니다.

 

Hibernate공식문서: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#locking

728x90

'Java' 카테고리의 다른 글

[ThreadLocal] Thread영역에 데이터를 저장하고 싶어요  (1) 2025.01.27
오차 없는 실수 연산 어떻게 해야 할까?  (0) 2025.01.10
[Java] Static  (3) 2024.12.26
[ Basic ] I/O Stream이란?  (0) 2024.12.23
JPA는 어떤 기술일까?  (0) 2024.04.12
728x90

JPA에 대하여

 

우리가 JPA를 알기 위해서는 사전 지식으로 ORM이 무엇인지를 알아야 합니다.

이에 이 글은 ORM부터 알아본 후 JPA에 대해 다뤄보도록 하겠습니다.

ORM이란?

ORM( Object Ralational Mapping )은 단어를 풀어 해석하면 '객체 관계형 연결'이 됩니다.

이 기술은 애플리케이션과 데이터베이스 연결 시 기존에는 SQL언어를 애플리케이션 서버에서 직접 작성하였지만, 이를 서버에서는 객체로 정의하여 행위에 대한 Action을 하면  정의된 객체를 해석하여 행위에 필요한 SQL문을 작성하여 데이터베이스로 전달하는 말 그대로의 Mapping역할을 합니다.

 

이러한 ORM은 기존 Mybatis와 같은 기술을 사용하던 것을 '객체 지향'적으로 사용하기 위해 나온 기술이라고 봐도 무방 할 것이라 생각하는데, 어떤 탄생 배경이 있는지 알아보도록 하겠습니다.

ORM 탄생 배경

우리가 개발할 때 객체 지향 언어를 사용하여 개발을 하고, 관계형 데이터베이스를 사용하게 되면 프로그램 내에서 데이터베이스와 연결하여 SQL문을 직접 작성을 해줘 데이터를 얻거나 작업을 할 수 있습니다.

 

이때 우리는 객체 지향 언어의 데이터 표현 방법관계형 데이터베이스의 데이터 표현 방법의 차이를 확인해 볼 필요가 있습니다.

 

객체 지향 언어은 데이터를 객체에 상호 연결된 그래프로 표현을 하고, 관계형 데이터베이스는 데이터를 표형식으로 표현을 하게 됩니다.

이렇게 표현 방식이 다른 두 개념을 한 프로그램에서 사용을 하려면 표형식 표현방법을 객체에 상호 연결된 그래프 방식으로 변경하는 작업을 하게 되는데, 이는 개발자에게 번거로움과 오류 발생의 원인을 제공하게 됩니다.

 

이리하여 관계형 데이터베이스와 연결하여 SQL문을 작성하는 부분을 추상화시켜버리게 되는데 이것이 ORM입니다.

ORM의 장단점

위 설명만 보면 ORM을 만능으로 볼 수도 있습니다.

개발자의 번거로움을 줄여주고!! 오류 발생의 원인을 없애 주다니!! 하고 말입니다.

하지만 대부분의 기술이 그렇듯 장단점이 존재합니다.

장점

  • 사용하기 편하다.  - 강점 -
  • 직접적인 SQL문 작성이 없는 만큼 신경써야하는 부분이 줄어든다.
  • 객체를 정의하여 사용하고 얼마든지 수정 및 확장이 쉬워 유지보수가 편하다.

단점

  • ORM만 사용하여서는 복잡한 작업( 조회 혹은 수정 등 )을 대응하기 힘들다.
  • 객체 지향적이라 객체로 관리하게 되었다고 설계를 신경써서 하지 않으면 어떤 프로그램보다 복잡하고 힘든 프로그램이 만들어진다.

데이터 베이스를 객체 지향적으로 접근할 수 있는 만큼 설계의 중요성은 이루 말할 수 없을 정도이며,

사용하다 보면 그냥 SQL사용해서 개발하고 싶다라는 생각이 들 수도 있습니다.

다만 적절히 비즈니스 로직을 나눠 분석하여 사용할 경우보다 쉽게 개발할 수 있는 기술인 것에는 틀림없다 생각합니다.


JPA이란?

JPA(Java Persistent API)는 말그대로 JAVA Application에서 '객체와 관계형 데이터베이스 간의 매핑을 위한 API' 인터페이스의 모음입니다.

이 모음에는 3가지 구현체가 존재하는데, Hibernate, EclipseLink, DataNucleus가 있습니다.

Hibernate

하이버네이트는 JDBC API를 사용하는 JPA인터페이스 구현체입니다.

이는 무슨 뜻이냐 우리가 객체로 데이터를 CRUD 하게 되면 JPA 즉 하이버네이트 내부에서는 해당 객체를 분석하여 데이터베이스로 SQL문을 보내주게 되는데 이때 사용하는 API가 JDBC API라는 것입니다.

EclipseLink

EclipseLink는 JPA인터페이스 구현체입니다.

또한 JAXB, SDO를 구현한 포괄적인 오픈소스 프레임워크로, 다양한 기능과 확장성을 강력하게 제공하고 있습니다.

DataNucleus

DataNucleus는 JPA 인터페이스 구현체입니다.

EclipseLink와 마찬가지로 다른 데이터베이스들 또한 지원하고 있습니다.

Hibernate를 왜 많이 사용하는가

글을 작성하면서 의문이 들었던 점이 왜 Hibernate는 많이 사용하거나 본 거 같은데, EclipseLink와 DataNucleus는 다루는 글을 많이 못 본 것 같다는 생각을 하였습니다. 그리고 설명들을 보면 Hibernate는 JPA 인터페이스를 구현한 가장 대표적인 오픈소스라고 하는데 EclipseLink와 DataNucleus 또한 강력한 기능과 확장 가능성을 지원해 준 것을 확인할 수 있었습니다.

 

우선 찾아보며 생각한 것은 Hibernate의 강력한 커뮤니티와 그로 인해 나온 많은 선례, 또한 만들어진 지 오래된 만큼 안정성을 보장하기 때문이라 생각합니다.

JPA주요 특성

ORM

엔티티 클래스와 데이터베이스 테이블 간의 매핑을 지원합니다.

영속성 컨텍스트

데이터를 영속성 컨텍스트에 저장하고 지속적으로 추적하게 되는데, 이 데이터가 수정될 경우 트랜젝션이 걸려있지 않으면 바로 반영이 됩니다. 이게 무슨 의미인가 만약 데이터를 조회하여 엔티티로 받아서 바로 엔티티 속성을 수정한다면 실제 SQL문이 바로 날아가 변경된다는 뜻입니다.

 

즉 commit을 안 했는데 commit이 되는 현상으로 이러한 상황을 방지하기 위해서는 Transaction처리를 해주거나, 엔티티를 사용하지 말고 데이터를 수정할 때 사용하는 객체를 따로 만들어 해당 객체로 데이터 값들만 복사하여 사용하는 것이 안전한 사용법인 것 같습니다.

캐싱

영속성 컨텍스트는 한번 조회된 데이터들은 가지고있는데, 만약 조회되는 데이터가 영속성 컨텍스트에 저장되어있는 데이터라면 데이터베이스를 조회하지 않고 해당 데이터를 반환해줍니다. 이는 1차 캐시의 개념이며, 만약 1차 캐시에 없을 경우 2차 캐시를 조회하게되고, 거기에도 없다면 데이터베이스를 조회하게 됩니다.

 

이때 말하는 1차 캐시는 Hibernate에서 제공하는 영속성 컨텍스트 캐시이고, 2차 캐시는 Session Factory 캐시입니다.


Reference

 

Understanding EclipseLink

This chapter describes how to set up your JPA applications to work with a non-relational data source. There are many types of non-relational data sources. These include document databases, key-value stores, and various other non-standard databases, such as

eclipse.dev

 

 

JPA Getting Started Guide (v5.2)

Developing applications is, in general, a complicated task, involving many components. Developing all of these components can be very time consuming. The Java Persistence API (JPA) was designed to alleviate some of this time spent, providing an API to allo

www.datanucleus.org

 

 

What is Object/Relational Mapping? - Hibernate ORM

Idiomatic persistence for Java and relational databases.

hibernate.org

 

캐싱 참조자료: https://www.baeldung.com/hibernate-second-level-cache

 

728x90

+ Recent posts