내가 아는 파이썬의 특징 중 GIL만큼 신기한 특징도 없다.

 

 파이썬에서 하나의 스레드에 모든 자원을 할당하고 다른 스레드에서 접근할 수 없도록 Lock을 건다. OS 공부하며 본 그 뮤텍스와 똑같다. 

이번 포스팅은 Python, Java 두가지 언어로 Single, Multi Thread 각각의 상황에서 무거운 연산을 진행하며 시간을 측정하고 시간 차이를 통해 GIL을 확인해보고 GIL의 존재 이유를 설명하려 한다. 

 

들어가기에 앞서 현재 내 컴퓨터 환경은 아래와 같다.

JDK 8.0, pypy 3.10 을 사용했다. 

 

멀티 스레드 연산 실험 

실험은 1 ~ 2000000000(20억)의 값을 더하는 속도를 비교할 것이다. 

멀티스레드 환경, 단일 스레드 환경의 연산속도를 Java와 Python에서 각각 비교해보겠다. 

 

Java에서 단일스레드

비교를 위한 자바에서의 연산 실험이다.

먼저 단일 스레드에서의 연산을 실험할 코드는 아래와 같다. 

/**
 * 단일 스레드에서의 연산
 * sum : 1~20억을 더한 결과값  
 */
long start = System.currentTimeMillis();
long sum = 0;
for(int i =0; i < 2000000000; i++) {
	sum+=i;
}
long end = System.currentTimeMillis();
System.out.println("Thread");
System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
System.out.println("result : " + sum);

 

내 훌륭한 노트북은 자바, 단일스레드로 1부터 20억까지 더하는데 0.68초가 걸리는걸 확인했다.

단일 스레드

Java에서의 멀티스레드 

다음은 멀티 스레드에서의 연산을 실험할 코드이다. 

/*
* sum  (long)  : 1부터 20억을 더한 결과
* start(int)   : 나누어진 구간의 시작
* end  (int)   : 나누어진 구간의 끝
*/
public class MyThread extends Thread{
	public static long sum; 
	private int start;
	private int end;
	
	public MyThread(int start, int end) {
		this.start = start;
		this.end = end;
	}
	/**
    	* start ~ end의 값을 모두 더함 
    	*/
	@Override
	public void run() {
		long temp = 0; 
		for(int i = start; i <= end ; i ++) {
			temp+=i;
		}
		sum+=temp; 
	}

}
/**
 * 멀티스레드에서의 연산 
 * target(int) 	  : 1부터 더할 범위의 수 == 20억 
 * tharedCnt(int) : 사용될 스레드의 수 
 * MyThread[]     : 스레드를 담을 변수. 
 * Thread.start() : Thread에서 Thread.run()을 호출함 -> 각 스레드에 할당된 범위를 더
 * Thread.join()  : Thread가 종료될때까지 기다릴때 사용
 *
 */
start = System.currentTimeMillis();
int threadCnt = 8;
int target = 2000000000;
MyThread[] threads = new MyThread[threadCnt];
for(int i = 0 ; i < threads.length; i++) {
	threads[i] = new MyThread(i*(target/threadCnt), (i+1)*(target / threadCnt)-1);
	threads[i].start();
}
for(int i = 0 ; i < threads.length; i++) {
	threads[i].join(); 
}
end = System.currentTimeMillis();
System.out.println("Multi Thread");
System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
System.out.println("result : " + MyThread.sum);

각 쓰레드는 20억에서 threadCnt개의 범위를 나누어 할당받은 후 static으로 선언된 MyThead.sum의 값에 더하게 된다. 중요한건 결과니깐 결과를 보자.

멀티 스레드

0.68초 -> 0.232초로 빨라졌다.  

 

전체 코드

더보기
JAVA
public class MultiThread {
	public static void main(String[] args) throws InterruptedException {
		/**
		 * 단일 스레드에서의 연산
		 * sum : 1~20억을 더한 결과값  
		 */
		long start = System.currentTimeMillis();
		long sum = 0;
		for(int i =0; i < 2000000000; i++) {
			sum+=i;
		}
		long end = System.currentTimeMillis();
		System.out.println("Thread");
		System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
		System.out.println("result : " + sum); 
		
		/**
		 * 멀티스레드에서의 연산 
		 * target(int) 	  : 1부터 더할 범위의 수 == 20억 
		 * tharedCnt(int) : 사용될 스레드의 수 
		 * MyThread[]     : 스레드를 담을 변수. 
		 * Thread.start() : Thread에서 Thread.run()을 호출함 -> 각 스레드에 할당된 범위를 더
		 * Thread.join()  : Thread가 종료될때까지 기다릴때 사용
		 *
		 */
		start = System.currentTimeMillis();
		int threadCnt = 8;
		int target = 2000000000;
		MyThread[] threads = new MyThread[threadCnt];
		for(int i = 0 ; i < threads.length; i++) {
			threads[i] = new MyThread(i*(target/threadCnt), (i+1)*(target / threadCnt)-1);
			threads[i].start();
		}
		for(int i = 0 ; i < threads.length; i++) {
			threads[i].join(); 
		}
		end = System.currentTimeMillis();
		System.out.println("Multi Thread");
		System.out.println("연산 시간 : " + (end-start)/1000.0 +"초");
		System.out.println("result : " + MyThread.sum); 
		
	}
}

 

/*
* sum  (long)  : 1부터 20억을 더한 결과
* start(int)   : 나누어진 구간의 시작
* end  (int)   : 나누어진 구간의 끝
*/
public class MyThread extends Thread{
	public static long sum; 
	private int start;
	private int end;
	
	public MyThread(int start, int end) {
		this.start = start;
		this.end = end;
	}
	
	@Override
	public void run() {
		long temp = 0; 
		for(int i = start; i <= end ; i ++) {
			temp+=i;
		}
		sum+=temp; 
	}

}

 

Python에서의 단일스레드 

 정확히는 Pypy를 사용했다. 파이썬의 경우 20억까지 더하는데 예상보다 더 걸린다... pypy는 JIT 컴파일러를 사용하는데 jvm도 JIT 컴파일러를 쓴다. JIT 컴파일러에 대해서는 다음에 자세히 포스팅하기로 하고 아무튼 반복문, 함수 등에 대해 최적화 해주는 친구라고 생각하자.

# start부터 end까지 result에 더해주는 함수
def work(start: int,end: int) -> None:
    global result
    for i in range(start,end):
        result += i

멀티 스레드, 단일스레드도 이 함수를 사용해서 실험한다. 

단일 스레드의 코드는

result = 0
start_time = time.time()
work(1,2000000000)
end_time = time.time() 
print("Single-Thread")
print(f"연산 시간: {(end_time-start_time)/1000}")
print(f"result: {result}")

 

파이썬의 단일 스레드

결과는 위와 같다. 

Python에서의 멀티스레드 

똑같이 작성해놓은 work 함수를 사용할 것이다.

result = 0
start_time = time.time()
threads = []
thread_cnt = 8
target = 2000000000
for i in range(thread_cnt):
    threads.append(threading.Thread(target=work(i * (target // thread_cnt), (i + 1) * (target // thread_cnt))))
    threads[-1].start()

for t in threads:
    t.join()
end_time = time.time()
print("Multi-Thread")
print(f"연산 시간: {(end_time-start_time)/1000}")
print(f"result: {result}")

자바에서 멀티스레드와 동일한 로직으로 동작한다. 

파이썬의 멀티 스레드

결과는 위와 같다. 

 

3.9 -> 4.0초로 더 느려졌다. 자바에서의 결과와 반대로 더 느려졌다.

 

결과

왼쪽 : Java의 단일 스레드(상), 멀티스레드(하)  오른쪽 : Python의 단일 스레드(상), 멀티스레드(하)

자바는 빨라졌지만, 파이썬은 멀티스레드 환경에서 오히려 더 느려졌다. 

 

GIL - Global Interpreter Lock

앞에서 말했지만 파이썬의 경우 하나의 스레드에 자원을 할당하고 Lock을건다. 즉 파이썬 인터프리터는 하나의 스레드만 하나의 바이트 코드를 실행 시킬 수 있다.

 

코어가 8개인 환경에서 스레드 8개가 작업을 한다고 가정해보자. 

파이썬의 멀티 스레드 동작 흐름

000,000,000~250,000,000 까지 더하는 스레드

250,000,001~500,000,000 까지 더하는 스레드

500,000,001~750,000,000 까지 더하는 스레드 

 ... 

1,750,000,000 ~ 2,000,000,000 까지 더하는 스레드

파이썬의 경우 각 스레드는 위 그림과 같이 동작한다. 실험에 빗대어 보면 단일스레드가 3.8초 동안 연산한걸, 8개의 스레드가 4.06초동안 번갈아가며 연산한다. 오히려 context switch에 비용이 추가적으로 발생하며 싱글스레드보다 느린 결과를 보여준다. 자바의 경우엔 이 모든 스레드가 0.232초 사이에 동시에 동작하며 단일 스레드 연산 시간인 0.677초보다 빠르게 해결할 수 있다. 

 

왜 귀도 반 로섬씨는 이런 비효율적으로 보이는 락을 만들었을까. 

 

파이썬은 문자열부터 정수, 실수, 뭐 죄다 객체다. 심지어 함수도 객체다. 

이게 진짜 객체지향이지 ㅋㅋ..

그래서 위 사진처럼 함수도 변수에 저장할 수 있고, 매개변수로도 넘길 수 있다. (이걸 어따쓰냐 궁금하다면.. Decorator 등 한 번 알아봐라)이렇게 모든게 객체인 파이썬은 문제가 조금 있다. 바로 메모리 관리다.

 

 Call By Value랑 Call By Reference도 헷갈려했던 기억이 있는데 파이썬은 Call By Object-reference라는 호출 방식을 사용하고, 알아보니 또 헷갈려 죽으려하는 내가 보인다. 이렇게 헷갈리는데 모든게 객체다. 이때 사람이 메모리 할당과 해제를 직접 한다면 파이썬이 이렇게까지 인기를 끌 수 없었을거다. 아무튼 귀도반로섬씨께서는 메모리 관리 방법으로 reference counting을 사용했다. 예를들면 a = 999라는 코드라면 999라는 객체의 reference counting은 1이다. 이때, 특정 객체를 참조하는 변수의 개수가 0이라면 메모리에서 해제하면 되는데 몹시 간단하고 직관적이다. 물론 단일스레드에서의 얘기다.

 

멀티스레드 환경에서 reference counting은 critical section으로 여러 스레드가 이 영역에 진입하게 되면 Race Condition으로 동기화 문제가 발생한다. 그 결과 값을 참조하고 있음에도 불구하고 reference counting이 0이 되어 메모리에서 삭제될 수도 있고, 아무도 참조를 안하고 있음에도 메모리에 남아있을수도 있다!(파이썬에선 GIL때매 있을 수 없는 상황이으로 극단적인 예시이며 순환참조의 경우는 GC가 세대별로 확인하며 열심히 지울거다.)

 

 어떻게 reference counting의 동기화 문제를 해결할까 고민하던 귀도반로섬은 문제 자체를 뿌리뽑았다. 바로 Race Condition이 일어날 환경자체를 막았다. 파이썬 전체적으로 적용될 뮤텍스를 하나 만들었고 이름은 GIL이라 붙였다! 물론 "아 머리 아프다. 그냥 막아 ㄱㄱ." 이런 판단은 아니다. 동기화를 위해 파이썬 코드의 수 많은 객체와 객체들 간 연산에서 일일히 Lock을 걸어주는것보다 GIL을 사용하는 방법이 훨씬 적은 비용으로 처리할 수 있다. 만약 각각의 객체가 Lock을 갖고 thread-safe하게 만든다면 그 어떤 천재가 와서 코딩해도 DeadLock을 마주하고 "이게 왜 안돼" 하다가 GIL을 만들었을거다. 

 

 

그럼 파이썬을 쓰면 멀티 스레드 환경을 고려 안해도 될까?

 물론 아니다. 기본적으로 GIL은 CPU-bound 작업에 한해 Lock을 건다. Sleep, I/O-bound(입출력, File, DB, Network)에서는  GIL이 적용되지 않는다. 따라서 I/O작업( 네트워크 요청 DB액세스 등)을 수행할때는 멀티 스레딩이 성능적으로 이점을 얻을 수 있다. 그러나 일반적인 I/O 이벤트를 보면 이벤트를 처리하는 비용보다는 이벤트가 발생하기까지 기다리는 비용이 크기때문에 멀티 스레드 보다는 비동기 프로그래밍을 사용하는게 성능적으로 이점을 얻는 경우가 많다. 

 

 CPU-bound의 작업을 수행하는 경우에는 멀티스레드 보다는 멀티프로세스 혹은 다른 병렬처리 방식을 고려해야 한다. 이때 파이썬은 multiprocessing이나, 비동기 작업을 처리하기위한 asyncio와 같은 라이브러리를 제공한다. 싱글 스레드에서 비동기로 작업하기 는 다음에 포스팅하겠다 !.

 

Java는 GIL이 없는데..

Java는 GIL없이 어떻게 메모리를 자동으로 관리하고 있을까? 

파이썬은 Reference Counting을 사용하는 것과 달리 자바에서는 기본적으로 Garbage Collection을 Mark and Sweep 방식으로 관리한다. (JDK 8기준)

 간단하게 설명하면 jvm의 heap 메모리에는 Eden, Survive0, Survive1, Old 영역이 존재한다. 가장 처음 객체가 할당되면 Eden으로, Eden 영역에 메모리가 다 차면 모든 스레드를 일시 정지(Stop-the-world)한 뒤 root객체로 부터 접근 가능한 객체들을 마킹(Mark)하고, 마킹되지 않은 객체는 전부 메모리를 해제(Sweep)한다. 이게 Mark-and-sweep이다. 여기서 마킹된 객체는 살아 남아 survive0 영역으로 이동되며 해당영역까지 가득 찰 경우 다시 한 번 Mark-and-sweep을 진행하고 살아남은 객체는 다른 survive영역으로 이동한다.  일정 횟수의 mark-and-sweep에서 살아남으면 Old 영역으로 넘어가게 된다. 

 Reference Counting 방식은 메모리를 해제하기 위해 객체의 참조가 변경 될 때마다 Critical Section에 진입을 막으며 Atomic한 연산을 수행해야하지만, Mark-and-sweep은 메모리가 가득 찼을때 메모리를 해제하기 때문에 Lock이 필요가 없다. 내비뒀다가 한 번에 해제하기 때문이다. 하지만 마킹을 위해 모든 스레드를 일시정지(Stop-The-world)하는 과정이 존재하고, 객체 소멸 시점을 예측하기도 어렵다는 단점이 있다.(모든 스레드가 일시정지 되는 시점을 예측하기 힘들다.)

 

 

 

 

 


 

 

실험하다가 만난 문제가 많다..

pypy가 실제 동작하는거에 비해 연산시간이 짧게 나오는가 하면 Python은 그냥 하염없이 기다려야했다. 

 

- python의 time.time은 sec를 반환하고 java는 milisec을 반환하기때문에 단위를 맞췄다. "파이썬 빠르네 !" 하고 좋아했다가 아차 싶었다. ㅎㅎ;

 

 

Integer Interning과 Integer Caching

 

x,y에 대해 주소값이 달라질때까지 더하고, 빼봤다.

 

"파이썬은 모든게 객체다." 라고 말하면서 -5~256의 값을 미리 할당해놓고 쓴다는걸 보여주고, 자바와 차이를 보여주려했다. 자바는 미리 할당하지 않는다고 생각했기 때문이다.

그런데 이게 왠걸 자바도 -128~127의 값은 같은 주소값을 바라보고 있었다. 

 

얘넨 이런짓을 왜할까 - 정수 인터닝과 정수 캐싱

 

파이썬의 경우에는 "정수 인터닝 (Integer Interning)" 이라는 최적화 기법이며 작은 정수값은 자주 사용된다는 가정하에 정수객체를 미리 할당하고 지속적으로 재사용하는 방식을 채택했다. 미리 생성하여 캐시에 저장해두고, 동일한 정수 값에 대해 해당 정수 객체를 가리키도록 하며 재사용한다. 이는 정수 객체를 생성하고 해제하는 비용을 줄이고 메모리를 절약할 수 있다. 

 

자바의 경우에는 정수 인터닝과는 조금 다른 "정수 캐싱 (Integer Caching)"을 수행한다. 자세히 말하자면 자바는 Byte, Short, Integer, Long 등의 정수 래퍼 클래스에서, 범위가 -128~127까지인 정수 값에 대해 "정수 캐싱"을 수행한다.  이또한 작은 정수값에 대해 객체를 재사용함으로써 메모리를 절약하고 성능을 향상시키는 것을 목적으로 한다. 

 

 

 

 

 

문제

제한사항

입출력 예 

ret : 3

풀이

import java.util.*;
class Solution {
	/*
    * 시간을 모두 분단위로 바꿈 
    * 종료시간은 청소해야되니까 +10분 
    */
    public int[] HtoM(String[] hour){
        int[] ret = new int[2]; 
        StringTokenizer stk ;
        for(int i =0 ; i <2; i ++){
            stk = new StringTokenizer(hour[i],":");
            ret[i] = 60*Integer.parseInt(stk.nextToken()); 
            ret[i] += Integer.parseInt(stk.nextToken()) + i*10; 
        }
        return ret;
    }
    
    public int solution(String[][] book_time) {
        int answer = 0;
               
        int[][] timeLine = new int[book_time.length][2];
        int i = 0; 
        for(String[] book : book_time){
            timeLine[i++] = HtoM(book);
        }
        
        // 입실 시간을 기준으로 정렬 
        Arrays.sort(timeLine, new Comparator<int[]>(){
            @Override
            public int compare(int[] a1, int[] a2){
                return a1[0] == a2[0] ? a1[1]-a2[1] : a1[0]-a2[0];
            }
        });
        
        // 퇴실시간이 빠른것부터 퇴실
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1,o2)->{
            return o1[1] == o2[1] ? o1[0]-o2[0] : o1[1] - o2[1];
        });
        
        int cnt = 0; // 현재 방 갯수 
        int maxCnt = 0; // 최대였던 방 갯수 
        
        for(int[] time : timeLine){
            int[] last;
            while(!pq.isEmpty()){
                last = pq.poll(); 
                cnt -=1;
                if(time[0] < last[1]){
                // 가장 빠른 퇴실시간이 지금 손님 입실시간보다 느리면 방 추가.
                    pq.add(last);
                    cnt+=1;
                    break; 
                }
            }
            pq.add(time); 
            cnt+=1;
            maxCnt = Math.max(cnt,maxCnt);
        }
        return maxCnt;
    }
    
    /**
    * 디버깅용 print function
    */
    public void sysout(int[] a){
        System.out.println(""+a[0] + " "+ a[1]);
    }
}

 

 

느낀점 - "파이썬 VS 자바"

자바로 알고리즘을 안푼지 오래돼서 세미콜론때매 자꾸 오류가 났다;

프로그래머스는 자동완성도 안되니까 죽을맛이다.

 

알고리즘 문제를 풀 때 파이썬이 훨씬 편하다. 

 

예를들어 위 문제에서 2차원 배열을 0번 index기준으로 정렬할때

자바는 아래와 같이 정렬할 수 있다.

Arrays.sort(arr, new Comparator<int[]>({

    @Override
    public int compare(int[] o1, int[] o2){
    	return o1[0]==o2[0] ? o1[1]-o2[1] : o1[0]-o2[1];
    }
    
}));

파이썬은 아래와 같이 정렬할 수 있다. 

arr.sort(key= lambda x : x[0])

 

 

자바에서 저거보다 쉽게 2차원 배열을 정렬하는 법 있으면 알려주세여,.

 

 

우선순위큐도 역시 파이썬이 간단하다

위에 문제처럼 timeline 배열의 1번인덱스 변수를 기준으로 최소힙을 사용하고 싶다면

자바는 아래와 같이 pq를 만든 후 add를 진행하면 된다. 

PriorityQueue<int[]> pq = new PriorityQueue<>((o1,o2)->{
	return o1[1] == o2[1] ? o1[0] - o2[0] : o1[1] - o2[1]; 
});

pq.add(new int[]{1,2});
pq.add(new int[]{2,3});

 

 

 

 

파이썬은 heapq 모듈을 아래와 같이 사용하면 된다. 

arr =[] 
heapq.heappush(arr, (a[1],a))
heapq.heappush(arr, (b[1],b))

_, first = heapq.heappop(arr)
_, second = heapq.heappop(arr)

 

 

번외로 최소힙이 아닌 최대힙을 만들고 싶다면 자바는 lambda 식의 파라미터로 들어온 o1, o2중 뒤의 값에서 앞의 값을 빼면 된다. 

PriorityQueue<int[]> pq = new PriorityQueue<>((o1,o2)->{
	return o1[1] == o2[1] ? o2[0] - o1[0] : o2[1] - o1[1]; 
});

 

파이썬은 최대힙을 만들기 위해 음수를 한 번 곱해준다. 

arr =[] 
heapq.heappush(arr, (-a[1],a))
heapq.heappush(arr, (-b[1],b))

_, first = heapq.heappop(arr)
_, second = heapq.heappop(arr)

 

 


 

다음은 자바 익명객체하고 무중단배포 포스팅해야쥐ㅣㅇ

 

 

JPA (Java Persistence API)

JPA는 JAVA의 객체지향과 관계형 DB를 매핑해주는 ORM(Object Relational Mapping) 기술이다.

 

장점으로는 

1. 특정 데이터베이스에 종속되지 않는다.

2. 객체지향적이다.

 

단점으로는

1. 복잡한 쿼리는 처리하기 힘들다.

2. 자동으로 생성되는 쿼리로 인해 개발자가 의도하지 않은 쿼리를 날리면서 성능저하가 있을 수 있다. 

3. 쓰긴 쉬운데 잘쓰긴 어렵다.

++ 특정 데이터베이스에 종속되지 않는다.

#Oracle의 현재 시간 
SYSDATE

#MySQL의 현재 시간
NOW()

위처럼 DB마다 쿼리가 조금씩 다르다. 하지만 JPA는 추상화된 데이터 접근 계층을 제공하고 이를통해 같은 함수라면 같은 결과를 기대할 수 있다. 

 

++ 객체지향적이다. 

@Entity
@Table(name = "A")
public class A{
	. . . 
    @NToM(~~)
    @JoinColumn(name="b_id")
    private B b;
}

테이블 하나와 매핑되는 클래스가 뚝딱 만들어지는데 이는 몹시 객체지향적이다. 내부에서 다양한 annotation을 활용하여 연관관계(N:M, 1:N, N:1, 1:1)를 매핑할 수 있다.  


Entity

JPA Entity Manager

Entity

entity란 DB의 테이블에 대응하는 클래스이다. 

@Entity annotation을 통해 Entity Manager가 관리한다.

 

Persistence Context (영속성 컨텍스트)

entity를 영구 저장하도록 지원하는 환경으로 Entity manager를 통해 접근한다. 

Application과 DB 중간사이의 계층의 영속성 컨텍스트는 버퍼링과 캐싱 등에서 이점을 갖는다. 

Entity Manager

Persistence Context 에 접근하여 DB작업을 제공하는 객체이다.

따라서 Entity Manager를 통해 DB Connection을 이용하고 DB에 접근한다.

Transaction단위를 수행할때마다 생성된다. 즉 사용자의 요청이 올때마다 생성되고 끝나면 닫는다.

Transaction 수행후에는 반드시 Entity Manager를 닫으면 내부적으로 DB Connection을 반환한다.

thread간 공유하지 않는다. 

Entity Manager Factory 

entity manager instance를 관리하는 주체이다.

Application 실행 시 한개의 Entity Manager Factory가 생성된다.

사용자로부터 요청이 오면 Entity manager를 생성한다. 

 


Entity Lifecycle 


new : 비영속 상태로 new 키워드를 통해 생성된 상태이다. 아직 영속성 컨텍스트에 저장되지 않았다.

Managed : 영속 상태로 엔티티가 영속성 컨텍스트에 저장되어 관리되는 상태이다. Application과 DB사이 계층에 존재하여 DB의 값과 차이가 있을 수 있다. 트랜잭션 commit 후 DB와 값을 일치시킨다.

Detached : 준영속 상태로 엔티티가 저장되었다가 분리된 상태이다. commit 전에 detached됐다면 엔티티가 변경되어도 쿼리가 날아가지 않는다. 

Removed : 삭제 상태로 영속성 컨텍스트에서 삭제되고 flush()혹은 commit()이 실행되면 DB에서도 삭제된다. 

 

 

 

 

* commit()과 flush()

flush는 jpa의 영속성 컨텍스트와 DB를 동기화하는 작업을 말한다. flush()가 실행되면 DB에 쿼리를 날린다.

이 때 변경감지(Dirty Checking)를 통해 수정된 entity를 update하는 쿼리를 날린다. 또한 *쓰기 지연된 쿼리들을 한 번에 보낸다. 

 

commit은 트랜잭션을 begin()으로 실행한 뒤 commit()하여 트랜잭션을 종료한다. 이때 commit()은 내부적으로 flush()를 호출하며 트랜잭션을 끝내는 역할을 한다. 

 

가장 큰 차이는 flush를 통해 전송된 쿼리는 rollback될 수 있지만 commit은 트랜잭션을 종료하므로 rollback될 수 없다. 

 


Persistence Context 이점

Persistence Context는 Application과 DB사이의 계층에서 여러가지 작업을 수행할 수 있다. 

1차 캐시

영속성 컨텍스트 내에서 저장되는 캐시로 Map<Key, Value>로 저장된다.

entityManager.find() 메소드 호출 시 1차 캐시에서 조회하고, 없으면 DB에서 조회 후 1차 캐시에 저장하고 반환한다.

즉, 처음 find 호출 시 1차 캐시에 존재하지 않는다면 SELECT query가 날아가고, 다음 같은 key값을 find할 경우 SELECT query는 날아가지 않는다.

이를 통해 DB에 접근하는 횟수를 줄이며 성능을 향상시킬 수 있다. 

 

동일성 보장

하나의 트랜잭션에서 같은 Key값은 같은 Entity 조회를 보장받을 수 있다. 즉, == 연산에서 결과값이 true이다. 

반대로 MyBatis는 조회 결과를 다시 인스턴스화하여 return하기때문에 == 연산에서 결과값이 false이다. 

 

변경감지 (Dirty Checking)

1차 캐시에 DB에서 처음 불러온 entity의 스냅샷을 저장한다. 

이후 스냅샷과 비교하여 다른점이 있다면 UPDATE쿼리를 사용한다. 

find 메소드를 호출하여 entity를 불러오고, 값을 변경한 뒤 save메소드를 호출하면 insert가 아닌 update 쿼리가 날아가도록 해준다. 

반대로 entity의 ID와 1차 캐시에 같은 ID를 찾을 수 없다면 insert 쿼리가 날아가도록 해준다. 

*쓰기 지연

영속성 컨텍스트는 트랜잭션 처리를 도와주는 쓰기지연을 지원한다.

 한 트랜잭션안에서 이뤄지는 UPDATE나 SAVE의 쿼리는 쓰기지연 저장소에 저장되었다가 트랜잭션이 commit(내부적으로는 flush)되는 순간 DB에 날린다. 이를통해 DB Connection 시간을 줄일 수 있고 트랜잭션이 테이블에 접근하는 시간을 줄여 교착상태 등을 방지할 수 있다. 

 

** 하지만 특정 INSERT 쿼리는 즉시 날아간다. 

Entity가 영속(Managed)상태가 되려면 식별자(PK)가 필요하다. 이때 Entity의 ID 생성전략을  IDENTITY로 사용한다면, 이는 데이터베이스에 실제로 저장한 뒤 다른 식별자들과 구분한다. 따라서 Insert쿼리는 즉시 날아가고 이런 경우에는 쓰기지연을 통한 성능 최적화를 얻기 힘들다. 

생성전략을 Sequence로 사용한다면, entityManager가 entity를 영속화하기전에 DB Sequence를 먼저 조회하고 조회한 식별자를 통해 Entity를 생성한뒤 영속화한다. 그 후 flush를 통해 Entity를 DB에 저장한다.

 

 


이전에 JPA를 사용해보기전에도 공부를 목적으로 포스팅을 한 번 했었다. 지금 그 글을 보니 너무 뜬구름 잡는 내용이고 개념도 정확하게 작성되지 않은 게시글을 포스팅했다. 지금은 삭제했다. 수정하는거보다 이렇게 새로 적는게 빠를 정도였으니까 :-)

+ Recent posts