본문 바로가기

[Kotlin&Spring] 5기 내일배움캠프

[Kotlin&Spring] 5기 운영체제에서 출발한 동기화 문제와 DB 로딩 전략

여러 개의 프로세스가 동시에 공유된 자원 CPU, 데이터에 동시에 접근하기 될 때 문제가 발생할 수 있다

경쟁상태(Race Condition)는 무작위로 접근하는 Thread 들이 하나의 자원에 경쟁하듯 접근하는 현상을 말한다
경쟁상태는 데이터 불일치(inconsistent) 문제를 야기할 수 있다
따라서 동기화가 필요하며 공유 데이터에 한 번에 하나의 프로세스만 접근할 수 있도록 제한을 두어야한다

동기화란, 여러 프로세스나 스레드가 동시에 공유자원에 접근하려고 할 때, 데이터의 일관성을 보장하기 위해 그들 사이의 실행 순서를 제어하는 기법이다
즉, 여러 작업들이 충돌이 일어나지 않도록 조정하는 것이다

 



교착 상태(Dead Lock)은 lock이 걸린 상태를 함수(메서드)가 끝나도 계속 유지하는 것이다
둘 이상의 작업이 서로 상대방의 작업이 끝나기를 기다리는 상태를 말한다

교착상태가 발생할 수 있는 조건들은 아래와 같다

 

교착 상태를 일으키는 조건
1. 상호배제(Mutual Exclusion)
프로세스들이 필요로 하는 자원에 대한 배타적인 통제권을 요구하는 것을 말한다

하나의 프로세스가 공유 자원 사용할 떄 다른 프로세스가 공유자원에 접근하는 것을 통제한다
2. 점유 대기
자원을 가진 프로세스가 다른 자원을 기다릴 떄, 보유하고 있는 자원을 놓지 않고 계속 가지고 있는 상태이다
3. 비선점
프로세스는 OS에 의해 강제로 자원을 뱨앗기지 않는다
즉,  자원을 강제로 빼앗는 게 아니라 자원을 점유하고 있는 프로세스가 해당 자원을 해제해야 한다
4. 순환 대기
자원을 기다리는 프로세스 간에 사이클이 형성되어야 한다

임계영역(critical section)
다중 프로그래밍 운영체제에서 여러 프로세스가 데이터를 공유하면서 수행될 때 각 프로세스에서 공유 데이터를 접근(Access)하는 프로그램 코드 부분이다
한 작업이 자원을 점유하면 다른 작업들은 작업이 종료될 때까지 기다려야 한다

Context는 사용자와 다른 사용자, 사용자와 시스템 또는 디바이스 간의 상호작용에 영향을 미치는 사람, 장소, 개체 등 현재 상황을 규정하는 정보를 말한다
OS에서는 CPU가 해당 프로세스를 실행하기 위한 해당 프로세스의 정보를 말하며 프로세스의 PCB에 저장된다
PCB(Process Control Block)는 중량 프로세스(heavy weight Process) 관리하는 자료구조이다

 


Context Switching은 멀티프로세스 환경에서 CPU가 어떤 하나의 프로세스를 실행하고 있는 상태에서, 인터럽트 요청에 의해 다음 우선 순위의 프로세스가 실행되어야 할 때 기존의 프로세스의 상태 또는 레지스터 값(context)을 저장하고 CPU가 다음 프로세스를 수행하도록 새로운 프로세스의 상태 또는 레지스터 값(context)를 교체하는 작업이다

OS의 커널(Kernel)은 운영체제의 핵심부로 리소스(System Resource: CPU, 파일, 네트워크 등)를 관리, 감독하는 역할을 한다
Context Switching은 커널에 의해 수행된다
메모리의 커널영역에 커널이 위치한다

이 영역은 사용자(유저모드)가 직접 접근할 수 없다

접근 시 system call을 통한 커널모드로의 전환이 필요하다


한 프로세스가 실행되다가 하드웨어와 밀접한 일들 혹은 컴퓨터에 있는 여러 리소스들을 다뤄야하는 상황이 발생하면 프로세스가 직접 컴퓨터의 리소스에 접근하는 것이 아니라 운영체제를 통해 접근하게 되는데, 특히 운영체제에서도 커널을 통해 접근한다.

커널모드는 이때 이 프로세스에서 커널로 통제권이 넘어가서 커널에 의해 실행되는 것을 말한다
레지스터는 CPU에서 명령어들을 수행하기 위해 필요한 여러 데이터들을 저장하는 부품인데, 레지스터의 상태를 교체한다
한 프로세스가 CPU에서 실행되는 동안 레지스터 값은 계속 바뀌면서 실행된다

다른 프로세스가 실행되면 기존 프로세스의 레지스터 상태들을 저장하고 다시 이 프로세스가 실행되었을 때 이어서 실행한다

Context Switching이 필요한 이유
1. 여러 프로세스와 스레드들을 동시에 실행시키는 것처럼 보이는 정도의 실행 성능을 이끌어내기 위해
2. 여러 프로세스와 스레드들이 공정하게 CPU 시간을 나눠 갖기 위해
3. 높은 우선순위의 작업이 빠르게 처리될 수 있기 위해
PCB의 정보를 읽어서 적재하고 CPU가 전 프로세스가 하던 일에 이어서 수행할 수 있다

 

Context Switching시 캐시 오염(Cache Pollution)이 발생한다
또 사용할 것이라고 예상하고 이전 프로세스의 데이터나 값을 캐시에 저장하는데 의미가 없어진다

내가 필요한 정보가 아니며 이전 프로세스에서의 정보는 다음 프로세스에서 필요없다
이는 캐시 메모리가 작고, 한정되어있기 때문에 성능적으로 손해를 일으킨다
CPU는 한 번에 한가지 일밖에 할 수 없다

한가지 일에만 CPU를 독점하는 것이 잦아지면 오버헤드가 발생해 성능이 떨어진다.

동기화 매커니즘
1. Mutex(동기화 대상이 1개)
여러 Thread를 실행하는 환경에서 자원에 대한 접근 제한을 위한 동기화 매커니즘이다
Locking 매커니즘으로 락을 걸은 스레드만 임계 영역을 나갈 때 락(Lock)을 해제할 수 있다
대기 큐를 생성하고, 임계영역에 스레드가 있을 경우 다른 스레드가 공유 자원을 사용하려고 한다면 스레드를 Blocking하고 대기큐에 Sleep시킨다
2. Spin Lock
임계 구역에 진입이 불가능할 떄 진입이 가능할 떄까지 루프를 돌면서 재시도 하는 방식으로 구현된 락(Lock)이다
임계영역에 진입이 불가능할 때 컨텍스트 스위칭을 하지 않으면서 재시도한다
임계 구역 진입 전까진 루프를 계속 돌아 busy waiting이 발생한다

*busy waiting: 점유한 프로세스가 lock을 오래 유지하면 CPU 시간 소모하게 되는 것

하나의 CPU나 하나의 코어만 있는 경우에는 유용하지 않음
3. Semaphore(동기화 대상이 1개 이상)/커널 객체, 알고리즘
음수가 아닌 정수 값을 가지고 스레드 간에 공유되는 변수이며 리소스의 상태를 나타내는 카운터이다
이 변수로 임계 구역 문제를 해결하고, 동기화를 구현한다
signaling 매커니즘으로, 락을 걸지 않은 스레드도 signal을 사용해 락을 해제 할 수 있다
자원의 개수를 의미하기도 한다
Semaphore(n)은 전역 변수 RS를 n으로 초기화한다

RS 에는 현재 사용가능한 자원의 수가 저장된다
P(): 잠금을 수행하는 코드이다

RS가 크면(사용 가능한 자원이 있으면) -1 하고 임계구역에 진입한다

만약 RS가 0보다 작으면 0보다 커질 때까지 기다린다
V(): 잠금 해제와 동기화를 같이 수행하는 코드이다

RS 값 1 증가 시키며, 세마포어에서 기다리는 프로세스에게 임계 구역에 진입해도 좋다는 wake up 신호를 준다
Binary Semaphore는 0 또는 1의 값만 가진다

임계 구역 문제를 해결하는 데 사용하며 자원이 하나기 때문에 뮤텍스로도 사용이 가능하다
Counting Semaphore는 도메인이 0이상인 임의의 정수값인 세마포어이다

여러 개의 자원을 가질 수 있으며 제한된 자원을 가지고 엑세스 작업할 떄 사용한다

락의 종류

1. 공유 락 (Shared Lock, S-Lock)
다른 트랜잭션이 읽기(Read)는 가능하지만 쓰기(Write)는 불가능한 락이다
2. 배타 락 (Exclusive Lock, X-Lock)
해당 자원을 읽기, 쓰기 모두 다른 트랜잭션이 접근하지 못하도록 차단하는 락이다
3. 의도적 락 (Intent Lock)
다중 트랜잭션 환경에서 상위 테이블이나 페이지 단위 락을 보호하기 위한 목적으로 사용된다

 


4. 낙관적 락 (Optimistic Lock)
트랜잭션 간 충돌이 적을 것으로 가정하고, 데이터 변경 시 버전 정보를 확인하는 방식이다
버전 번호, 타임스탬프 등을 활용하여 충돌이 발생하면 재시도합니다.
5. 비관적 락 (Pessimistic Lock)
트랜잭션이 자원을 확보하는 동안 다른 트랜잭션이 접근하지 못하도록 락을 거는 방식이다
JPA의 @Transactional 로 비관적 락을 구현할 수 있다

스레드 간 간섭에 의한 동기화 문제에 안전함(Thread Safe)
ACID 속성을 띄는 쿼리의 메소드에서 사용한다
Atomacity(원자성): 트랜잭션이 실행되거나 아예 실행되지 않음
Consistency(일관성): 트랜잭션 후에도 데이터 무결성 유지
Isolation(격리성): 트랜잭션 간 서로 간섭하지 않음
Durability(내구성): 트랜잭션 완료 후 데이터 영구 저장

데이터베이스의 데이터와 연관된 데이터 로딩 전략

1. Cascade (영속성 전이)

Cascade는 부모 엔티티의 변경(삽입, 수정, 삭제 등)이 자식 엔티티에도 영속성(Persistence)을 전이한다
JPA 등 ORM에서 사용되며, 특정 연관 관계에서 부모 엔티티가 변경될 때 자식 엔티티에도 같은 작업을 수행하도록 설정할 수 있다

ex) @OneToMany(cascade = CascadeType.ALL)
CascadeType enum의 값들
ALL: 모든 변경 전파
PERSIST: 부모 저장 시 자식도 저장
REMOVE: 부모 삭제 시 자식도 삭제
MERGE: 부모 병합 시 자식도 병합
REFRESH: 부모 갱신 시 자식도 갱신
DETACH: 부모가 detach되면 자식도 detach됨

2. Eager Loading (즉시 로딩)

즉시 로딩 방식으로, JOIN을 통해 엔티티를 조회할 때 연관된 엔티티도 함께 로딩된다
JOIN FETCH나 EAGER 설정을 통해 사용할 수 있다
장점은 필요한 데이터를 한 번의 쿼리로 가져올 수 있어 성능이 향상될 수 있다는 것읻
단점은 불필요한 데이터까지 가져올 가능성이 있으며, 복잡한 관계에서는 N+1 문제를 초래할 수 있다는 것이다

 

Lazy Loading

 

3. Lazy Loading (지연 로딩)

지연 로딩은 필요할 때 엔티티를 조회하고, 프록시 객체로 대체하는 것을 말한다
연관된 엔티티를 실제로 사용할 때까지 로딩을 지연하는 방식이다
JPA에서는 기본적으로 Lazy 전략을 사용하며, 프록시 객체를 활용하여 필요한 순간에 데이터를 조회한다
장점은 초기 로딩 속도가 빠르고, 필요할 때만 데이터를 가져와 성능을 최적화할 수 있다
단점은 예상치 못한 추가 쿼리 발생(N+1 문제) 가능성이 있다