반응형

개요

 InnoDB 스토리지 엔진은 MySQL 엔진에서 제공하는 잠금과는 별개로 레코드 기반의 잠금 방식을 탑재하고 있다. 테이블 기반의 잠금 방식을 채택하고 있는 MyISAM 엔진보다 동시성 처리 측면에서 뛰어나다.  레코드 락 뿐 아니라 레코드와 레코드 사이의 간격을 잠그는 갭 락(GAP LOCK), 이 두 락을 합쳐놓은 형태의 넥스트 키 락(NEXT KEY LOCK)도 지원한다. 이번 게시글에서는 이 세가지 락에 대해 이해하고, 팬텀 리드와 연관지어 생각해보도록 하자.

 

 아마 트랜잭션 격리 수준에 대해 공부했던 경험이 있다면 MySQL에서는 팬텀리드를 방지할수 있다는 얘기를 들어봤을것이다. 이를 넥스트 키락과 연계시켜 이해해볼것이다.

* 참고로 갭 락은 타겟 레코드를 잠그지 않는다. 레코드와 레코드 사이의 빈 공간(GAP)만을 잠근다.

 

 

InnoDB 엔진 락

 


레코드 락 (Record Lock)

 말 그대로 레코드에 대한 락이다. 특정 레코드를 수정하거나 잠금과 함께 조회할 때, 그 레코드만을 잠그고, 나머지 레코드에는 자유롭게 접근할 수 있다. 단, 일반적인 레코드 락과 다른점이 하나 있는데, 레코드 자체가 아닌 인덱스의 레코드를 잠근다는 것이다. 인덱스가 없는 테이블이더라도 자동 생성된 클러스터드 인덱스를 이용해 잠금을 설정한다. 아래 쿼리는 id = 1 인 프라이머리 키(인덱스)를 통해 레코드를 찾음과 동시에 인덱스의 레코드에 락을 거는 것이다.

 

SELECT * FROM [table] WHERE id = 1 FOR UPDATE;

 

클러스터드 인덱스의 레코드 락

 

 

인덱스 레코드를 잠근다? 🤔

MySQL의 데이터들은 B+트리 구조로 관리된다. 즉, 인덱스의 레코드는 리프 노드를 의미한다. 클러스터드 인덱스의 경우 리프 노드에 실제 레코드 데이터들이 들어가있다. 즉, 리프노드에 잠금을 건다는 건 실제 레코드에 락을 획득해 접근할 수 없도록 잠그는것과 같다.

 

논 클러스터드 인덱스라면? 🤔

마찬가지로 논 클러스터드 인덱스의 리프 노드를 잠근다. 단, 실제 데이터 레코드에 접근하기 위해 클러스터드 인덱스를 사용해야 하는 상황이 발생하면, 클러스터드 인덱스의 리프 노드 또한 잠근다. 아래와 같이 users 테이블이 있고, email 컬럼에 대해 논 클러스터드 인덱스를 설정한 상태에서 실제 레코드 인덱스가 잠기는지 테스트해보자.

CREATE TABLE users (
  id INT PRIMARY KEY, -- 클러스터드 인덱스
  name VARCHAR(100),
  email VARCHAR(100)
);

CREATE INDEX idx_email ON users(email); -- 논 클러스터드 인덱스

-- 데이터 추가
insert into users values (1,'andrew','andrew@tistory.com');
insert into users values (2,'test','test@tistory.com');
insert into users values (3,'maple','maple@tistory.com');
insert into users values (4,'andrew2','andrew@tistory.com');

 

start transaction; -- 트랜잭션 1 시작

SELECT name FROM users WHERE email = 'andrew@tistory.com' FOR UPDATE; -- 논클러스터드 인덱스를 조건으로 검색



start transaction; -- 트랜잭션 2 시작

update users set name = 'andrew.sim' where email = 'andrew@tistory.com'; -- 논 클러스터드 인덱스 잠금으로 인한 대기
update users set name = 'andrew.sim' where id = 1; -- 클러스터드 인덱스 잠금으로 인한 대기
update users set name = 'andrew.sim' where id = 2; -- 클러스터드 인덱스가 잠기지 않았으므로 실행 완료

 

그 이후 email 컬럼을 잠금과 함께 조회한다면, 논 클러스터드 인덱스의 리프노드가 잠길것이다. 그런데 조회하는 컬럼 name이므로 이를 조회하기 위해서는 클러스터드 인덱스를 사용하여 데이터 레코드에 접근해야한다. 이때 클러스터드 인덱스의 리프노드도 함께 잠기게 된다.

 

더보기

B Tree와 B+Tree 시뮬레이션 해보기

 

B Tree와 B+Tree 가 실제로 어떻게 동작하는지 확인할 수 있는 시뮬레이션 사이트이다. 

 

https://www.cs.usfca.edu/~galles/visualization/BTree.html

 

B-Tree Visualization

 

www.cs.usfca.edu

 

https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html

 

B+ Tree Visualization

 

www.cs.usfca.edu

 


갭 락 (GAP LOCK)

레코드와 레코드 사이를 잠그는 락이다. 갭 락의 역할은 레코드와 레코드 사이에 새로운 레코드가 생성(INSERT)되는 것을 제어하는 것이다. 갭 락은 단독으로 거의 사용되지 않고 넥스트 키 락의 일부로 사용된다.

 

레코드 락은 인덱스 레코드로 잠그는데... 갭락은 뭘로 잠그죠?  🤔

 레코드 락은 인덱스 레코드를 잠그는 것이라고 했다. 하지만 갭 락은 실제로 존재하는 뭔가가 없다. 레코드와 레코드 사이는 텅 빈 값인데, 도대체 어떻게 이 범위에 잠금을 거는걸까? 바로 메타데이터를 활용해 잠금을 건다. 인덱스 레코드에 존재하는 메타데이터에는 자신과 인접한 인덱스 레코드의 정보도 포함되어 있다. 이 정보를 활용해 갭 락을 거는 것이다.

 

 만약 책장에 5번 책과 10책이 꽂혀있다고 해보자. 5번 책과와 10번 책 사이에 무언가를 끼우는 행위를 막으려면 10번 책에에 포스트잇을 붙여 '이전 책은 5번 책입니다.' 또는 5번 책에 '다음 책은 10번 책입니다' 라는 정보를 기재해주면 된다. 그럼 5번 책을 보면 다음 책은 10번 책이니 5 ~ 10 사이에는 갭이 있다는 것을 알 수 있다.

 


넥스트 키 락 (NEXT KEY LOCK)

 레코드 락과 갭 락을 합쳐 놓은 형태의 잠금으로 락 기반의 조회 쿼리(SELECT ... FOR UPDATE)범위 조건(<, >, BETWEEN)을 걸었을 때 설정되며, 해당 레코드 뿐 아니라 갭(GAP)에도 락이 걸리게 된다. 레코드 락과 갭 락이 함께 걸리는 이유를 이해하기 위해서는 InnoDB 엔진의 B+트리 구조를 이해해야한다. B+트리 구조에 대해서는 따로 설명하지 않겠다. 여기서는 B+트리 구조와 넥스크 키 락의 관계와 팬텀리드를 방지할 수 있는 이유를 알아볼것이다.

 

넥스트 키 락이 팬텀 리드를 방지할 수 있는 이유? 🤔

 MySQL에서는 넥스트 키 락에 의해 팬텀리드 현상을 방지한다. 이 넥스트 키락이 존재할 수 있게 하는 것이 B+트리이다. B+트리의 특성을 활용해 넥스트 키 락을 걸고, 이 락에 의해 팬텀 리드 현상을 막게 되는 것이다. B+트리의 가장 큰 특성 중 하나는 리프노드가 오른쪽 리프노드의 포인터를 갖는 연결 구조로 되어있다는 것이다. 이를 활용한다면 특정 리프 노드는 다음으로 큰 리프노드에 빠르게 접근할 수 있다.

 그럼 아래와 같이 10 단위의 ID 가 있는 B+ 트리 구조에서 특정 범위에 대한 SELECT ... FOR UPDATE 쿼리를 날리면 어떻게 될까?

 

B+트리 구조

 

SELECT * FROM USERS WHERE ID >= 10 AND ID <= 55 FOR UPDATE;

 

 먼저 ID가 10 이상인 인덱스 레코드(리프노드)를 찾고, 인덱스 레코드를 잠근다. 그 후 포인터를 통해 오른쪽 리프노드인 20으로 이동한다.

 그 후 현재 인덱스 레코드의 ID가 55 이하인지를 체크한다. 55 이하일 경우 마찬가지로 인덱스 레코드를 잠그고 오른쪽 리프노드로 이동하기를 반복해나간다. 그러다 ID가 60인 값을 만났을 때 55 이하라는 조건에 만족하지 않아 멈추게 된다.

 최종적으로 10, 20, 30, 40, 50 인덱스 레코드가 잠기고(레코드 락), 각 인덱스 레코드의 메타데이터를 통해 갭도 잠기게 된다.(갭 락) 레코드 락과 갭 락이 동시에 걸렸다. B+트리의 특성을 활용해 넥스트 키 락을 건 것이다.

 

이제 넥스트 키 락이 걸렸을 때, 팬텀 리드를 방지할 수 있을지를 생각해보자. 넥스트 키락은 레코드와 갭을 잠근다. 해당 범위에 인덱스 레코드를 삽입할 수 없다는 뜻이다. 그럼 너무나 당연하게도 팬텀리드 현상을 방지할 수 있게된다.

 

넥스트 키 락이 팬텀 리드를 방지할 수 있는 이유! 🤩

 정리하면, MySQL은 InnoDB 엔진을 사용하기에 B+트리 구조로 데이터가 저장되고, B+트리 구조에서 넥스트 키 락을 걸 수 있으며, 넥스트 키 락에 의해 팬텀 리드 현상이 방지되는 것이다. 

 

MySQL에서는 팬텀 리드가 발생하지 않는다고도 하던데... 🤔

이건 잘못된 말이다. MySQL에서도 팬텀 리드 현상이 발생할 수 있다. 다만, 잠금을 사용하여 조회한다면 B+트리를 사용하는 InnoDB엔진 특성 상 넥스트 키 락이 걸릴거고, 넥스트 키 락에 의해 레코드와 갭이 잠기면서 팬텀리드 현상을 방지하게 되는것이다. 잠금을 사용하여 조회하지 않는다면?? 팬텀리드 현상은 당연히 발생한다.

 


자동 증가 락 (AUTO INCREMENT LOCK)

 자동 증가하는 숫자 값을 채번하기 위한 잠금으로 테이블 락과 함께 동작한다. AUTO_INCREMENT 속성이 사용된 컬럼의 테이블에 레코드가 INSERT 될때 잠기며, UPDATE나 DELETE 쿼리에서는 잠금이 걸리지 않는다. AUTO_INCREMENT 락을 명시적으로 획득하는 방법은 없으며, 아주 짧은 시간동안 걸리는 잠금이기에 대부분의 경우 문제가 되지 않는다.

 MYSQL 5.1 이상부터는 innodb_autoinc_lock_mode 라는 시스템 변수를 사용해 자동 증가 락의 작동 방식을 변경할 수 있다.

 

1. innodb_autoinc_lock_mode = 0

테이블 락 기반의 기본적인 자동 증가 락을 사용한다.

 

2. innodb_autoinc_lock_mode = 1

여러 건의 레코드를 INSERT 하는 중, MySQL 서버가 INSERT 되는 레코드의 건수를 정확히 예측할 수 있을 때 자동 증가 락을 사용하지 않고, 훨씬 가볍고 빠른 래치(Latch)를 이용해 처리한다. 자동 증가 락보다 훨씬 짧은 시간동안만 잠금을 걸고, 필요한 자동 증가 값을 한번에 가져온다. 다만, INSERT ... SELECT 와 같이 MySQL 서버가 건수를 예측할 수 없을 때는 자동 증가 락이 사용된다.

 

3. innodb_autoinc_lock_mode = 2 (MySQL 8.0 기본 값)

자동 증가 락은 사용하지 않고 래치만을 사용한다. 이 방식은 연속된 자동 증가 값을 보장하지 않지만, INSERT ... SELECT와 같이 대량 INSERT 문이 설정되는 중에도 다른 커넥션에서 INSERT를 수행할 수 있다.

 

래치(Latch)가 뭔가요? 🤔

InnoDB 내부에서 데이터 구조를 보호하기 위해 사용하는 경량의 잠금이다. 자동 증가 락은 락의 범위가 테이블로, 자동 증가 값인 AUTO_INC 값을 가져올때INSERT가 완료될때까지 잠금을 유지한다. 여러 트랜잭션이 INSERT 시 잠금이 끝날때까지 대기하며 순차적으로 처리되는 구조이다.
 이에 반해 래치(Latch)는 AUTO_INC 값을 가져올때만 잠금을 유지한다. 여러 트랜잭션이 INSERT 시 병렬 INSERT 처리가 가능한 것이다.

 

반응형
반응형

개요

 MySQL의 잠금은 크게 두가지로 MySQL 엔진, InnoDB 스토리지 엔진 잠금이 있다. 이번 포스팅에서는 먼저 MySQL 엔진에 대한 잠금을 알아보자.

 


MySQL 엔진과 InnoDB 스토리지 엔진이 뭔가요?

MySQL 엔진과 InnoDB 스토리지 엔진은 MySQL 서버의 구성요소이다. MySQL 엔진은 쿼리 파싱, 최적화, 실행 뿐 아니라 캐시, 커넥션과 같은 부가 기능들을 관리하고, InnoDB 스토리지 엔진은 실제 데이터의 저장, 디스크 I/O, 인덱스 관리와 같은 하드웨어 처리를 담당한다. 즉, MySQL 엔진이 쿼리를 실행하면, InnoDB 스토리지 엔진은 데이터를 읽고, 쓰는 역할을 하는것이다.

 


MySQL 엔진 잠금

 

1.  글로벌 락

 글로벌 락은 SELECT를 제외한 DDL이나 DML 문장에 대한 락으로, MySQL 서버에 존재하는 모든 테이블과 DB에 영향을 미치는 락이다. FLUSH TABLES WITH READ LOCK 명령으로 락을 획득한다.

 다만, 이미 UPDATE와 같은 DML 트랜잭션이 진행중이라면 끝날때까지 대기해야하는데, 읽기 잠금을 획득해야하기 때문이다. 또한 다른 트랜잭션에서 장시간 SELECT 쿼리를 실행하는 상황에서도 대기해야한다.

 글로벌 락은 mysqldump와 같이 일관된 백업을 받아야할 때 사용하곤 한다.

 

DDL (Data Definition Language)
데이터 구조(스키마)를 정의/변경하는 명령어이다.

 

DML (Data Manipulation Language)
데이터 자체 조작하는 명령어로 SELECT, INSERT, UPDATE, DELETE 가 있다.

 

 

SELECT 쿼리를 기다려야 하는 이유?

FLUSH TABLES WITH READ LOCK을 끊어 읽어보면 FLUSH, READ LOCK 이라는 단어가 눈에 띈다. FLUSH는 버퍼 풀의 레코드를 디스크에 저장하는 작업을, READ LOCK은 읽기 잠금을 뜻한다. 읽기 잠금인데 다른 트랜잭션에서 발생하는 SELECT 쿼리에 영향을 받는 이유는 뭘까? 글로벌 락은 당연하게도 READ LOCK 뿐 아니라 METADATA LOCK을 획득해야 하고, 더 나아가 MySQL의 테이블 핸들이 닫혀있어야 한다. SELECT 쿼리는 테이블 핸들을 필요(열어야하는)로 하는 작업이다.

 

테이블 핸들 (Table Handle)
접근하는 테이블을 가리키는 내부 객체이다. 쿼리를 통해 테이블을 액세스 하면 테이블 핸들이 열리고, 트랜잭션이 끝나면 핸들이 닫힌다.

 

 

글로벌 락 테스트해보기

총 두가지를 테스트해볼 것이다. 첫번째는 글로벌 락이 걸렸을 때 읽기 잠금이 걸리는지, 두번째는 테이블 핸들이 열렸을 때 글로벌 락을 획득하지 못하고 대기하는지이다.

 

1) 글로벌 락이 걸렸을 때 읽기 잠금 

 

1번 트랜잭션에서 글로벌 락 획득

# transaction 1

start transaction; // 트랜잭션 시작

FLUSH TABLES WITH READ LOCK; // 글로벌 락

 

 

2번 트랜잭션에서 읽기 잠금 테스트

# transaction 2

start transaction;

SELECT * FROM TBL_USER; //성공 : 테이블 데이터 조회됨

UPDATE TBL_USER SET NAME = '승경' WHERE USER_ID = 1; // 실패 : 글로벌 락으로 인함

 

 

 

2) 테이블 핸들이 열렸을 때 글로벌 락 획득 대기

 

1번 트랜잭션에서 select sleep from [table] 쿼리를 통해 테이블 핸들 오픈

# transaction 1 

start transaction;

select sleep(10) from tbl_user; // tbl_user 에 로우 개수만큼 10초씩 sleep (테이블 핸들을 열기 위함)

 

2번 트랜잭션에서 글로벌 락 획득 시도

# transaction 2

start transaction;

FLUSH TABLES WITH READ LOCK; // 실패 또는 SELECT 작업이 끝난 후 성공

 

 

 

2.  테이블 락

 

테이블 락은 테이블 단위로 설정되는 잠금으로 명시적, 묵시적으로 락을 획득할 수 있다. 명시적 테이블 락은 'LOCK TABLES [TABLE NAME] [ READ or WRITE ]' 명령으로 획득할 수 있다. 단, 테이블 락은 어플리케이션에 상당한 영향을 끼치므로 실무에서는 거의 사용되지 않는다.

 묵시적 테이블 락은 DDL 쿼리 실행 시 발생하며, 읽기/쓰기를 모두 차단한다. DML 쿼리에 대해서는 스토리지 엔진에 따라 달리 동작하는데, MyISAM 또는 MEMORY 스토리지 엔진의 경우 자동으로 테이블 락이 발생한다. 데이터가 변경되는 테이블에 잠금을 설정하고 데이터를 변경한 후, 즉시 잠금이 해제되는 프로세스이다. InnoDB 스토리지 엔진은 엔진 차원에서 레코드 기반의 잠금을 제공하기 때문에 단순 DML 쿼리로 테이블 락이 설정되지 않는다.

 

DDL 쿼리도 트랜잭션처럼 Commit 해야 반영되나요?

맞다. Commit을 해야 실제로 반영된다. 다만, DDL 쿼리는 Auto Commit 으로 처리된다.

 

테이블 락  테스트해보기

테이블에 쓰기 잠금을 걸었을 때 테이블 데이터를 조회, 수정하는 쿼리를 날려보자.

 

1) 1번 트랜잭션에서 WRITE LOCK 실행

start transaction;

LOCK TABLES tbl_user WRITE; // tbl_user에 대한 Write Lock

 

 

2) 2번 트랜잭션에서 SELECT 및 UPDATE 쿼리 실행

start transaction;

select * from tbl_user; // 실패 : 테이블 Write Lock 으로 인함
update tbl_user set name = '승경' where user_id = 1; // 실패 : 테이블 Write Lock 으로 인함

 

 

 

3.  네임드 락

 네임드락은 임의의 문자열에 대한 잠금으로, GET_LOCK() 함수를 사용한다. 이 잠금은 테이블이나 레코드, 데이터 베이스 객체가 아닌 단순히 사용자가 지정한 문자열에 대한 잠금을 획득하고 반납하는 잠금이다. 반환은 RELEASE_LOCK() 함수를 사용한다.

 

 네임드 락을 사용하는 예는 다중 어플리케이션에서의 배치 실행이다. 다중 어플리케이션에서 한 번의 배치만 돌게 하기 위해 네임드 락을 사용하기도 한다.

SELECT GET_LOCK('batch_lock', 0); // batch_lock 획득 시도, 획득 대기시간 없음(0초)

 

 여러 어플리케이션에서 동시에 batch_lock 이라는 네임드 락 획득을 시도하면 최초 획득한 트랜잭션의 어플리케이션만 배치가 돌고, 나머지는 대기 시간이 없어 실패와 함께 배치 실행을 하지 않게 된다.

 

 네임드 락을 사용하는 또 다른 예는 비지니스 로직에 대한 잠금 범위를 지정하고 싶을때이다. 특정 로직이 실행되기 전 네임드 락을 획득하고, 로직이 끝난 후 네임드 락을 반환한다면, 멀티 스레드 환경에서 해당 로직이 순차적으로 처리될것이다.

// 비지니스 로직 시작
...
SELECT GET_LOCK('service_1', 100); // 네임드 락 획득
...
// 비지니스 로직 종료
...
SELECT RELEASE_LOCK('service_1'); // 네임드 락 반환

 

단, 락을 획득한 후 해제해주지 않으면 시스템이 멈추는 위험을 초래할 수 있고, 락 이름 충돌 가능성이 있으므로 구분할 수 있는 prefix를 붙여주는 것이 좋다.

 

 

4.  메타 데이터 락

 메타 데이터 락은 테이블이나 뷰 등 DB 객체의 이름이나 구조를 변경하는 경우에 획득하는 잠금이다. 이 락은 자동으로 획득/반환되는 묵시적 잠금이다. 앞서 설명한 글로벌 락, 테이블 락이 발생할 경우 메타 데이터 락도 함께 획득한다.

 

 

 

 

반응형
반응형

개요

슬로우 쿼리 개선을 위해 스프링 부트 환경에서 AOP 기반으로 슬로우 쿼리 로그를 남기는 인터셉터를 구현했다. 다만, JPA 환경이냐 Mybatis 환경이냐에 따라 다른 방식으로의 구현이 필요했고, Interceptor 타겟을 설정하는 부분에 따라 로깅을 남기는 구조였기 때문에 '정말 모든 쿼리를 다 체크하고 있을까?' 라는 의문이 들었다.

 

 그러던 중 Real MySQL의 슬로우 쿼리 관련 챕터를 읽게 되었고, MySQL 자체에 슬로우 쿼리 관련 설정이 있다는 것을 알게 되었다. 이 설정들을 적용해보고, 어떤 형태로 슬로우 쿼리가 추출되는지 확인해보았다.

 

MySQL 슬로우 쿼리 로깅 활성화

 

먼저 MySQL에 접속 후 여러 옵션값들을 확인하고 설정해야한다.

 

1) slow_query_log

슬로우 쿼리 로깅 활성화 여부를 나타내는 설정값이다. 기본값은 OFF이므로 ON 또는 1로 설정한다.

show variables like '%slow_query_log%'; // 슬로우쿼리 활성화 여부 확인

set global slow_query_log = 'ON'; // 슬로우쿼리 활성화

 

2) long_query_time

슬로우 쿼리에 대한 기준 시간을 나타내는 설정값이다. 기본값은 10초로 되어있어 1초로 수정하였다.

show variables like '%long_query_time%'; // 슬로우 쿼리 기준 시간 확인

set global long_query_time = 1; // 슬로우 쿼리 기준 시간을 1초로 변경

 

 

set global 명령어를 통해 변수들을 설정할 경우 MySQL이 재시작될 때 설정값들이 초기화된다. 영구 설정을 하고자하면 my.ini나 my.cnf 파일을 다음과 같이 수정하면 된다.

// my.ini 파일
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1

 

 

3) log_output

슬로우 쿼리를 남길 수 있는 방법은 두 가지로, 테이블 혹은 디스크이다. 온프레미스 환경에서는 디스크에 저장하는 것을 선호한다. 테이블로 설정할 경우 슬로우 쿼리가 발생할때마다 쓰기 작업이 발생하며, 데이터가 많아질 경우 부하가 발생할 수 있기 때문인데, 이를 방지하기 위해 슬로우 쿼리에 적재된 데이터를 주기적으로 삭제해주는 작업이 필요하다.

 AWS RDS 환경일 경우 파일 시스템에 직접 접근하지 못하므로 테이블에 저장하기도 한다. 참고로, 디스크로 설정한 후 RDS 설정에 SlowQuery Logging 을 활성화하면 Cloud Watch 의 로그 그룹에 저장된다. 필자의 경우 테이블에 적재되도록 설정하였다.

 

show variables like '%log_output%'; // 슬로우 쿼리 저장 타입 확인 (TABLE or FILE)

set global log_output = 'TABLE'; // 슬로우 쿼리를 테이블에 저장하도록 설정

 

 

슬로우 쿼리 테이블 확인

슬로우 쿼리 테이블은 mysql.slow_log 이다. 아래 명령어를 사용해 데이터가 쌓였는지 확인해보자. 아마 비어있을건데, 다음은 슬로우 쿼리를 직접 발생시켜 적재되는지를 확인해볼 것이다.

select * from mysql.slow_log order by start_time desc;

 

 

컬럼 설명
query_time 쿼리 실행 시간 (실질적 시간)
lock_time 테이블 락 시간
rows_sent 클라이언트에 전송된 행 수
rows_examined 스캔한 전체 행 수

* rows_examined 값이 매우 크면 인덱스가 제대로 안 쓰이고 있을 가능성이 높다.

 

슬로우 쿼리 발생시키기

 

슬로우 쿼리가 발생했을 때, mysql.slow_log 테이블에 적재되는지 확인하기 위해 select sleep(2) 쿼리를 날려보자.

 만약 슬로우 쿼리에 로그가 남지 않는다면, 아까 설정했던 global 설정 값들이 현재 세션에는 적용되지 않은 상태일 가능성이 있다. 이 경우 재접속하면 된다.

select sleep(2);
select * from mysql.slow_log order by start_time

mysql.slow_log 조회 결과

 

sql_text 값이 BLOB 형태로 출력된다면 아래 쿼리를 통해 조회하면 된다.

SELECT 
  start_time,
  user_host,
  CAST(sql_text AS CHAR) AS sql_text,
  query_time,
  lock_time,
  rows_sent,
  rows_examined
FROM mysql.slow_log;

 

 

 

 

 

 

반응형
반응형

개요

트랜잭션 격리수준을 공부하면 꼭 나오는 MVCC 에 대해 확실히 이해해보자.

 

MVCC란? 

Multi Version Concurrency Control 의 약자로, 직역하면 멀티 버전 동시 제어를 말한다. 단순히 MySQL에만 한정된 기술이 아닌, DB에서 데이터를 관리하는 방법론 중 하나이다. 그럼 DB 입장에서 여러 버전의 데이터를 관리한다는 건 무슨 말일까? 그리고 여러 데이터를 관리해야하는 이유는 뭘까? 그건 바로 여러 트랜잭션들! 동시에! 일관된 읽기를 제공해야하기 때문이다.

 

일관된 읽기? 📕

일관된 읽기란 뭘 말하는 걸까? 수준이 가장 낮은 트랜잭션 격리수준인 READ UNCOMMITED를 생각해보자. 트랜잭션 격리 수준 중 하나로 트랜잭션이 어떤 레코드의 값을 수정하면 굳이 커밋하지 않아도 다른 트랜잭션에서 수정된 값을 볼 수 있다. 당연하게도 해당 트랜잭션이 롤백될 경우 데이터 정합성 문제가 발생하는 매우 낮은 수준의, 일반적으로는 사용하지 않는 격리수준임을 알것이다.

 

 이 정합성 문제를 해결하기 위해서는 레코드를 수정한 트랜잭션이 끝날때까지 다른 트랜잭션이 이 레코드의 데이터에 접근하지 못하도록 잠궈 (Exclusive Lock) 버리는 것이다. 읽기든, 쓰기든말이다. 그럼 데이터 정합성 이슈는 해결되지만, 동시에 접근하지 못하는 이슈가 발생한다.

 

여러 트랜잭션들이? 동시에 ? 🙌

 방금 말했던 동시성 이슈는 여러 트랜잭션이 동시에 수정/조회를 할때 발생한다. 잠금을 갖는 트랜잭션이 끝나야 비로소 접근 가능하기 때문이다. 이러한 동시성 이슈로 인해 트랜잭션의 처리속도는 전체적으로 저하된다. 결국 잠금 대신 다른 방법을 사용해야했다. 원본 데이터와 별개로 다른 버전의 데이터를 관리하는, 즉, 멀티 버전의 데이터를 제어할 수 있는 방법을 사용해 동시성 이슈를 해결하는 것이다.

 데이터들이 변경될때마다 스냅샷처럼 버전을 관리한다면, 상황에 따라 수정 전 데이터를 줄 수 있을것이다. 예를 들어 레코드 데이터를 수정중인 트랜잭션이 Commit 되지 않았을 때, 조회 트랜잭션이 발생한다면, "수정중인 데이터가 롤백될 수 있으니 끝날때까지 기다리라고 하자" 가 아닌 "수정 전 데이터를 주자"와 같이 컨트롤이 가능해진다.

 

바로 이것이 Multi Version Concurrency Control. MVCC이다. 그리고 MVCC와 함께 나오는 용어가 하나 있다. 바로 언두 로그이다. 언두 로그에 대해 먼저 이해하고, MVCC와 어떤 관계를 갖는지도 연결해보자.

 

MVCC 공부하면 언두 로그도 꼭 나오던데...🤔

 MVCC는 멀티 버전의 데이터를 관리하는 방식의 동시성 제어 전략이자 트랜잭션 격리 방법론 중 하나이다. 그럼 MySQL에서는 무엇을 통해 데이터를 다중 버전으로 관리하는 걸까? 그 무엇이 바로 언두 로그이다. MySQL에서는 언두 로그를 활용해 다중 버전의 데이터를 관리하는 것이다.

 

 

언두 로그 개념 이해하기

 

먼저 Undo 라는 단어의 사전적 의미는 다음과 같다. 

Undo
어떤 행위를 되돌리거나 취소하는 것을 의미

 

 언두 로그는 트랜잭션이 삽입, 수정, 삭제한 행위를 되돌리기 위해 사용하는 로그를 말한다. 트랜잭션으로부터 삽입, 수정, 삭제 행위가 발생하면, 이에 대한 로그를 언두 로그에 저장해놓고, 트랜잭션이 실패했을 때, 이 로그를 참조하여 원래의 데이터로 되돌려 놓는것이다. 예제를 통해 이해해보자.

 

먼저 다음과 같은 테이블에 한 건의 레코드를 INSERT 한 후 UPDATE 했을 때 언두 로그가 어떻게 활용되는지를 알아보자.

CREATE TABLE member{
	m_id INT NOT NULL,
    m_name VARCHAR(20) NOT NULL,
    m_area VARCHAR(100) NOT NULL,
    PRIMARY KEY (m_id),
    INDEX ix_area (m_area)
};

 

1) INSERT 문 실행 및 커밋

INSERT INTO member (m_id, m_name, m_area) VALUES (12, '홍길동', '서울');
COMMIT;

 

INSERT 문 실행후 커밋되면 데이터베이스의 상태는 아래와 같이 바뀔것이다. InnoDB 버퍼 풀에 데이터를 선 적재한 후, 내부 알고리즘에 의해 디스크에도 데이터가 저장될것이다. 참고로 커밋을 하고 언두 로그의 데이터도 삭제된 상황을 가정한거라 언두 로그에 데이터가 없는 것이다.

 

 

* 혹시 Insert 쿼리는 커밋을 안해도 언두 로그에 데이터가 없는걸까?

 언두 로그는 트랜잭션이 데이터를 변경하기 전 상태를 저장해둔다고 했다. INSERT 하기 전에는 해당 데이터가 없는 상태이므로, 언두 로그에도 데이터가 없을 것이라 생각할 수 있지만, 실제로는 INSERT 로그가 들어간다. INSERT 한 데이터가 기록되지 않는다면 트랜잭션을 롤백했을 때, "어떤 데이터를 INSERT 했었는지"를 찾을 수 없기 때문이다. 

 

2) UPDATE 문 실행 후 미 커밋상태

UPDATE member SET m_area = '영암' WHERE m_id = 12;

 

새로운 트랜잭션에서 UPDATE 쿼리가 실행되면 커밋 실행 여부와 관계 없이 InnoDB의 버퍼 풀은 새로운 값인 '경기'로 업데이트된다. 마찬가지로 디스크의 데이터 파일은 InnoDB 내부 알고리즘에 의해 추후 저장될것이다. 디스크에 저장된 m_area는 '경기'가 될수도, '서울'이 될수도 있으므로 '?' 로 기재하였다. 마지막으로 언두 로그에는 수정 전 데이터에 대한 로그가 들어갈것이다.

 

3) Commit 전 다른 트랜잭션의 SELECT

그럼 이렇게 언두 로그에 데이터가 있을 때 다른, 혹은 여러 트랜잭션에서 해당 로우에 대한 SELECT 쿼리를 날리면 어떻게될까?

 

SELECT * FROM member WHERE m_id = 12;

 

이 질문의 답은 MySQL 서버의 격리 수준에 따라 다르다.

 격리 수준이 READ_UNCOMMITTED인 경우 InnoDB 버퍼 풀이 가진 데이터를 읽으므로 '경기' 로 조회될것이다.

 READ_COMMITTED나 그 이상의 격리 수준인 REPEATABLE_READ, SERIALIZABLE 인 경우에는 아직 커밋되지 않았기 때문에 언두 영역의 데이터를 참조해 조회한다. 

 

* 갑자기 언두 영역 데이터를 왜 조회해?

커밋되지 않는 데이터를 다른 트랜잭션에서 조회해버린다면 처음 말했던 정합성 문제가 발생하게 된다. 이 문제를 해결하려면 수정 전 데이터가 조회되도록 하면 된다. 그럼 여기서 지금까지의 내용을 다시 짚어보자. 정합성 문제를 해결하기 위해 Lock 을 도입했다가, Lock의 동시 접근 이슈가 있어 MVCC를 통해 멀티 버전의 데이터를 관리하여 조회되는 데이터를 제어할 수 있다고 했다. 

 

 언두 로그를 보자. Lock을 걸지 않으니 여러 트랜잭션에서 언두 로그에 동시 접근 가능하다. 수정 트랜잭션이 끝나지 않은 상황에서 해당 레코드에 조회 트랜잭션이 발생할 때 언두 로그를 통해 수정 전 데이터를 보여지도록 하고있다. 즉, MySQL은 언두 로그 기반의 MVCC 방식을 채택하고 있는 것이다. MVCC와 언두 로그는 뗄레야 뗄수 없는 관계인 것이다.

 

 언두 로그를 통해 하나의 레코드(회원 번호가 12인 레코드)에 대해 2개의 버전을 유지하고, 필요에 따라 보여지는 데이터를 제어하고 있다. 위 예제는 한 개의 데이터만 가지고 설명했지만 관리해야 하는 예전 버전의 데이터는 무한히 많아질 수 있다. 즉, 트랜잭션이 길어지면 언두 로그에서 관리하는 예전 데이터가 오랫동안 관리되어야 하며, 그만큼 메모리를 많이 차지하게 될것이다.

 

4) Commit

 UPDATE 트랜잭션이 COMMIT 되면 InnoDB는 이상의 변경 작업 없이 지금의 상태를 영구적인 데이터로 만든다. 하지만 롤백될 경우 언두 로그에 있는 데이터를 InnoDB 버퍼 풀로 다시 복구하고, 언두 로그의 내용을 삭제해버린다.

 참고로 트랜잭션이 끝난다고 언두 영역의 백업 데이터가 항상 바로 삭제되는 것은 아니다. 이 언두 로그을 필요로 하는 트랜잭션이 더는 없을 때 비로소 삭제된다.

 

언두 로그가 삭제될 수 있는 조건?

다음 두 조건을 모두 만족하면 백그라운드에서 실행되는 Purge Thread에 의해 언두 로그가 제거된다.

 

1) 변경을 발생시킨 트랜잭션이 끝났을 때

 해당 언두 로그를 생성한 트랜잭션이 커밋 또는 롤백이 되어야한다. 너무 당연한 얘기이다. 트랜잭션이 끝나지도 않았는데 언두 로그에 데이터가 날아간다면, 롤백이 불가능해진다. 또한 트랜잭션이 끝나지 않은 상태에서 다른 트랜잭션이 조회 쿼리를 날린다면 언두 로그를 참조하여 정합성을 지켜줘야한다.

 

2) 해당 언두 로그를 참조하는 모든 트랜잭션이 종료되었을 때

 변경을 발생시킨 트랜잭션이 끝났다고 하더라도, 이미 다른 트랜잭션이 언두 로그를 참조하고 있었다면, 그리고 언두 로그를 삭제할 수 없다. Repeatable Read 격리 수준을 위해서도 삭제시키면 안된다. 언두 로그를 어떻게, 얼마나 활용하냐에 따라 Read Commited와 Repeatable Read가 나뉠정도로 매우 중요한 로그이다.

Read Commited 와 MVCC

 

Read Commited 단계에서의 핵심은 커밋된 데이터만을 조회하는 것이다. 커밋된 데이터를 조회하는 방법은 "최신의 커밋 데이터"를 읽어오는 것이다. 그런데 언두 로그 기반의 MVCC 생각하면 수정 전 데이터는 관리하지만 최신의 커밋 데이터는 관리하지 않는다. 그럼 최신의 커밋 데이터는 뭘 말하는걸까? 이건 바로 버퍼풀 + MVCC 를 함께 활용해 얻는 최신의 커밋 데이터를 말한다. 만약 MVCC 만을 사용한다면 가장 마지막으로 커밋된, 즉 언두 로그에 존재하는 수정 전 데이터를 로드할 수도 있지만, 로드하지 못할 수도 있다.

 

 

 

아까 m_id가 12인 레코드의 m_area의 값을 '서울' 에서 '경기'로 변경했던 걸 다시보자. 커밋하지 않은 상태라면 위처럼 언두 로그에는 '서울'이, 버퍼 풀에는 '경기'가 들어갈 것이다. 이때 다른 트랜잭션이 m_id = 12 인 데이터를 조회한다면 Read Commited 레벨에서는 언두 로그를 참조하여 '서울'이라는 데이터가 조회될것이다.

 

여기서 중요한 점은 언두 로그를 참조하기 전에, '조회된 데이터가 마지막으로 커밋된 데이터가 맞는지'를 체크해야한다는 것이다. 언두 로그는 커밋과 동시에 삭제되지 않는다고 했다. 즉, '경기'로 수정시킨 트랜잭션이 커밋됐다 하더라도, 언두 로그에 '서울'이라는 데이터가 남아있을 수 있다. Read Commited 격리 수준이 아무런 체크 과정 없이 언두로그의 데이터를 읽는 것이었다면 '서울' 로 조회될것이고, 커밋된 데이터가 조회되는 Read Commited 의 원칙에 어긋난다.

 

마지막으로 커밋된 데이터가 맞는지?

이를 이해하기 위해서는 레코드에 저장된 몇가지 메타 데이터를 알아야 한다. 레코드에는 단순 데이터 뿐 아니라, 트랜잭션 정보를 포함한 여러 메타 데이터들이 저장되어 있다.

 

* DB_TRX_ID

 이 레코드를 최종적으로 수정한 트랜잭션의 ID

 trx_sys라는 시스템 정보엔 트랜잭션 ID에 대한 커밋 여부를 확인할 수 있다.

 

* DB_ROLL_PTR

 이 레코드가 수정되기 전의 Undo Log를 가리키는 포인터

 

이제 최근 마지막으로 커밋된 데이터를 체크하는 방법을 알아보자.

 

1) 현재 레코드 확인 (버퍼 풀)

 트랜잭션이 읽으려는 레코드를 버퍼 풀에서 먼저 확인한 후, DB_TRX_ID가 커밋된 트랜잭션인지 확인한다.

 

2) 트랜잭션 커밋 여부 확인

 trx_sys를 참고하여 해당 트랜잭션의 커밋 여부를 확인한다.

 

3) 커밋 여부에 따른 읽기

 커밋된 트랜잭션이라면 버퍼 풀에 저장된 값을 그대로 읽고, 그렇지 않다면 DB_ROLL_PTR 를 통해 Undo Log로 접근 후 가장 마지막으로 커밋된 데이터인 수정 전 데이터를 읽어온다.

 

Repeatable Read 와 MVCC

 

그럼 Repeatable Read 에서는 어떨까? 가장 마지막으로 커밋된 데이터를 읽어오는게 아니라, 현재 트랜잭션이 시작되기 이전 시점을 기준으로 가장 마지막으로 커밋된 데이터를 읽어와야한다. 즉, 아래와 같은 상황에서 수정된 트랜잭션이 커밋됐다 하더라도, 조회 트랜잭션 이후에 발생한 커밋이므로, 이를 무시하고, 언두로그를 참조해 이전 커밋된 버전을 찾아나가게 된다.

 

정리

MVCC란 동시성을 보장하는 트랜잭션 격리 방법론중 하나로, MySQL에서는 언두 로그 기반의 MVCC를 사용한다.

단순히 트랜잭션이 끝난다고 해서 언두 로그가 삭제되지 않는다.

언두 로그를 어떻게 활용하느냐에 따라 트랜잭션의 격리 수준이 달라진다.

 

반응형
반응형

개요

 최근 사내망에서의 SSL Certification 오류가 발생하거나, SSL 인증서를 갱신하는 작업을 하면서 SSL 인증서가 도대체 어떤 녀석이길래 '신뢰할 수 있는 사이트'라는 타이틀을 쥐어주는지, 암호화는 어떻게 가능한지에 대한 의구심이 들었다. SSL 인증서의 메커니즘을 알아보기 전 HTTPS와 암호화 방식에 대해 알아보고, 왜 굳이 CA 기관을 통해 SSL 인증서를 발급받아야하는지도 분석해보았다.

 

HTTPS 란?

HTTP는 Hypertext Transfer Protocol의 약자로 웹서비스에 데이터를 전송하는 통신 규약을 말한다.

HTTPS는 HTTP에 보안을 더한 프로토콜로 암호화 기반의 프로토콜인 SSL(Secure Socket Layer) 이 적용된 프로토콜을 말한다. 즉, HTTPS는 HTTP에 대한 암호화 기반의 데이터 통신규약을 말한다.

 

암호화? 우리가 아는 암호화??

맞다. 네트웤 구간에 떠다닐 우리의 요청, 응답 데이터들을 공격자가 알아볼 수 없는 형태로 암호화하는 것이다. 그럼 잠깐 암호화에 대해 알아보자.

 

대칭키?! 비대칭키?! 공개키?! 개인키??!! 😱

 암호화는 데이터를 특정한 키 기준으로 연산하는 것을 말한다. 또 키를 이용해 복호화한다. 동일한 키로 암호화와 복호화를 둘다 할 수 있는 방식을키가 대칭된다 하여 대칭키 방식이라한다. 반대로 암호화하는 키와 복호화하는 키가 다른 방식을 비대칭키 방식이라 한다.

 

암호화 키와 복호화 키 중 어느 키가 더 중요할까? 

당연히 복호화 키가 더 중요하다! 암호화 키가 유출되어 모든 사람들이 갖게된다고 하여도, 복호화 키가 없다면 그 누구도 암호화된 데이터를 볼 수 없기 때문이다.

 

대칭키는 암호화 키와 복호화 키가 똑같으니 모두 공개되어서는 안된다. 그럼 비대칭키는 어떨까? 공개되어도 되는 암호화 키와, 공개되어선 안되는 복호화키로 구성된다고 했다. 여기서 공개되어도 되는 키를 '공개키', 공개되어선 안되는 키를 '개인키'라고 한다. 그럼 HTTPS에 사용되는 암호화 방식은 대칭키일까? 비대칭키일까?

 

HTTPS에 사용되는 암호화 키 방식은 대칭키 인가요...? 🧐

 

대칭키 방식이라고 가정하고 생각해보자. 요청을 하는 클라이언트에서는 요청 데이터를 암호화해야한다. 즉, 키가 필요하다. 이 키는 서버로부터 전달받아야 한다. 그럼 최초 통신(핸드 쉐이크) 단계에서 서버가 클라이언트에게 키를 전달해야한다.

 

그런데 만약 이 과정에서 공격자가 요청을 가로챈다면, 그리고 이 대칭키를 탈취한다면 어떨까? 클라이언트와 서버간 대칭키 암호화를 통해 데이터를 주고 받더라도 공격자는 탈취한 키를 이용해 복호화를 할 것이고, 데이터를 감청할 수 있게 된다. 즉, 단순 비대칭키 방식을 사용하는 것은 네트웤 통신 관점에서는 매우 위험한 방식이다.

 

최초 통신 시 전달되는 대칭키를 가로채는 공격자

 

 

아! 그럼 비대칭키 방식이군요?! 😏

 

비대칭키를 사용하면 클라이언트로 전달되는 키, 즉 공개키가 공격자에게 탈취당해도 괜찮다. 복호화를 하지 못하기 때문이다. 하지만 여기서도 큰 문제가 있다. 바로 데이터를 응답할때 암호화가 불가능한 점이다. 예를들어 로그인에 성공 시 클라이언트에게 액세스 토큰을 전달하는 상황을 가정해보자. 액세스 토큰도 마찬가지로 암호화를 해야하는데, 현재 서버는 복호화를 할수있는 개인키밖에 없는 상황이다. 암호화를 위한 키가 없는 상황인 것이다.

 

비대칭키 상황에서의 이슈

 

 

서버도 자신이 사용할 공개키를 발급한 후 암호화하면 되잖아? 😏

 

그럼 서버의 응답 데이터는 암호화 되겠지만 클라이언트는 서버에서 준 데이터를 복호화 하지 못한다. 클라이언트는 복호화를 할 수 있는 개인키가 없기 때문이다

 

클라이언트에게 개인키를 주면 되잖아? 😏

그럼 대칭키를 사용했을 때의 문제 상황과 마찬가지로 공격자가 개인키를 가로챌 수 있는 상황에 처한다. 또한 비대칭키 방식은 대칭키보다 암/복호화 복잡도가 높고 속도도 수 천배 느리다. 즉, 속도와 시스템 자원 낭비 문제를 안게 된다.

 

 

키를 대체할 수 있는 값을 활용하면 되지 않아? 😏

클라이언트에서 키를 대체할 수 있는 랜덤 값을 생성하고, 이 값을 서버의 공개키로 암호화하는것이다. 그럼 서버에서 개인키로 이를 복호화하여 랜덤 값을 얻고, 서버와 클라이언트가 이 값을 대칭키로 삼아 암/복호화 하는것이다. 대칭키 방식이니 클라이언트, 서버 모두 암/복호화 할 수 있고, 키가 되는 랜덤 값을 전달할 때에는 서버의 공개키로 암호화해서 전달할 것이기 때문에 공격자에게 탈취되도 문제되지 않는다.

 

비대칭키 + 대칭키 방식 사용

 

 

 

위에서 말한대로 위 구조에서는 공격자는 클라이언트에서 생성한 대칭키(랜덤 값)를 획득하지 못한다. 서버의 공개키로 암호화됐기에 서버의 개인키로만 복호화되기 때문이다. 

 이제 문제가 없어보일 수 있으나, 이 구조에서도 보안 이슈가 존재한다. 바로 서버가 클라이언트에게 전달하는 공개키를 공격자의 공개키로 바꿔치는 것이다. 이를 중간자 공격이라 한다.

 

중간자 공격 (Man-in-the-middel attack)
공격자가 공개키를 가로채어 변경하고 상대방에게 전달하여 신뢰를 잃게 만드는 방식.

중간자 공격 상황

 

 

서버가 공개키를 전달할 때를 캐치하여 서버의 공개키 대신 공격자의 공개키를 주면, 클라이언트는 공격자의 공개키로 생성한 대칭키를 암호화할것이다.

 그 후 클라이언트가 전달한 암호화된 대칭키를 공격자가 가로채 공격자의 개인키로 복호화한다. 대칭키를 탈취한 것이다. 탈취한 대칭키를 서버가 사용하도록 만들어야하므로 4번과정에서 획득한 서버의 공개기로 대칭키를 암호화 한 후 서버로 전달한다. 서버는 아무것도 모르고 이를 복호화할것이고 대칭키로 사용할 것이다. 이제 데이터가 암호화되더라도 공격자가 복호화할 수 있게 되었다.

 

신뢰할 수 있는 기관을 끼자! 🏢

 위처럼 중간에 공개키가 위변조되는 것을 막는 방법은 위변조를 못하도록 보안을 강화하면 된다. 보안을 위한 여러 프로세스들과 보안 장비를 붙이고, 취약점이 발생할때마다 수시로 업데이트해주며, 모니터링도 철저히 하는 것이다.

 

그렇다... 배보다 배꼽이 더 커진다.

 

 서비스 운용 비용보다 보안을 위한 비용과 기술력이 훨씬 많이 필요할것이다. 그럼 서비스 운용자가 아닌, 믿을 수 있는, 매우 높은 보안성을 갖는 기관에게 서버의 공개키를 전달받으면 어떨까?

 브라우저가 신뢰하는 기관, 신뢰할 수 있는 공개키를 제공하는 기관, 이 기관이 바로 CA이다.

CA가 도입된 구조 (실제로는 공개키를 그대로 주진않아요~)

 

CA (Certificate Authority)
인증기관이라 부르며, 다른 곳에서 사용하기 위한 디지털 인증서를 발급하는 곳.

 

 

CA도 털린적...이 있답니다 :)

실제로 2011년 DigiNotar 이라는 CA 가 해킹당해 Google과 Facebook 등 수많은 사이트가 피해를 입었다. 때문에 브라우저가 신뢰할 수 있는 CA 는 매우 깐깐한 조건으로 선별된다고 한다.

이제 본론으로 들어가 실제 CA와 서버, 클라이언트(브라우저)가 어떻게 상호작용하며 데이터를 암/복호화하는지 알아보자.

 

SSL 인증서 발급부터 통신까지의 과정을 알아보자! ✏️

SSL인증서 발급부터 통신까지의 과정

 

 

1. SSL 인증서 발급 요청

SSL 프로토콜을 적용하고 싶다면 먼저 CA 에게 SSL 인증서 발급을 요청해야한다. 중요한 점은 서버의 공개키를 함께 전달하는 것이다.

 

2. 발급된 SSL 인증서 전달 및 SSL 프로토콜 적용

CA 로부터 SSL 인증서를 전달받으면 사용하는 웹 서비스(웹서버 또는 AWS ALB 등) 에 맞게 SSL 인증서를 적용시킨다.

 

3. Client Hello (Handshake-1)

클라이언트로부터 요청이 들어온다면 바로 요청/응답 데이터를 주고받지 않는다. 핸드쉐이크 과정을 거치는데, 첫번째 과정을 Client Hello 라고 하며, 클라이언트는 서버에게 '클라이언트 랜덤 값(편의를 위해 C Random이라 칭함)'과 클라이언트가 지원 가능한 암호화 메서드 리스트 등을 전달한다. 

 

* 암호화 메서드 리스트들은 왜 전달하나요? 🤔

핸드쉐이크 과정에서 주고받는 데이터들에 대한 암호화 방식을 설정하기 위함이다. 핸드쉐이크 과정에서도 키 교환, 대칭키 암호화, PRF 알고리즘 적용과 같은 프로세스가 발생하는데, 이 때 서버와 클라이언트 모두 지원하는 암호화 방식을 사용해야하기 때문이다.

 

4. Server Hello (Handshake-1)

두번째 과정을 Server Hello 라 하며, '서버 랜덤 값(편의를 위해 S Random이라 칭함)'과 클라이언트에서 전달한 암호화 메서드 리스트 중 채택한 암호화 메서드, SSL 인증서를 전달한다.

 

5-6-7. SSL 인증서 확인 및 서버 공개키 획득

클라이언트는 SSL 인증서를 전달받으면 인증서를 발급한 CA 기관을 확인한다. 그리고 이 기관이 브라우저에 저장되어 있는지를 확인한다. 저장되어 있지 않다면 '신뢰할 수 없는 인증서'라는 경고가 발생할것이고, 저장되어 있다면 함께 저장된 해당 CA 의 공개키를 가져온다.

 공개키를 통해 복호화(SSL 인증서 전자서명 검증)에 성공한다면 공인하는 CA로부터 발급된 위변조 없는 SSL 인증서라는 것을 알게 된다.

그리고 SSL 인증서에 포함되어있던 서버의 공개키를 획득한다.

 

브라우저에 저장된 CA는 브라우저가 신뢰하는 CA
위에서 말했듯이 CA 는 매우 깐깐한 기준으로 선별된다. 브라우저 내에 저장되어 관리되며, CA 리스트와 CA 공개키를 함께 관리한다.
만약 CA 리스트에 대한 업데이트가 발생한다면, 브라우저 업데이트를 통해 진행된다.

 

8. C + S 값 생성

C Random 값과 S Random 값을 조합한 C + S 값(제가 편의를 위해 칭한 이름입니다.) 을 생성한다.

 

9-10. Pre Master Secret 생성 및 전달

C + S 값을 서버에게 전달하기 위해 서버의 공개키로 암호화한다. 암호화된 C + S 값을 Pre Master Secret 이라한다.

이 값을 서버로 전달한다.

 

11. 서버측 C + S 값 획득

Pre Master Secret은 서버의 공개키로 암호화됐기에 서버는 자신의 개인키로 복호화하여 내장된 C + S 값을 획득한다.

 

12. 서버측 Master Secret 생성

획득한 C + S 값과 Pre Master Secret, PRF 알고리즘을 조합하여 Master Secret 을 생성한다.

 

PRF(Pesudo-Random Function)
특정 입력에 대해 항상 동일한 출력을 주는 랜덤 함수.

 

13. 대칭키 발급

Master Secret 을 기반으로 데이터를 암/복호화 하기 위한 대칭키를 발급한다. 여러 대칭키를 발급하는데, 실제 암/복호화에 사용하는 대표적인 키는 Session Key와 Mac Key이다.

 

Session Key
실 데이터를 암/복호화할때 사용하는 키.

 

Mac Key
데이터에 대한 HMAC 값을 구할 때 사용하는 키.
송신자 측에서 Mac Key를 사용해 데이터의 HMAC 값을 계산하여 암호화된 데이터와 함께 전달하면, 수신자측에서는 데이터를 전달받아 Session Key로 복호화한 후, Mac Key를 통해 데이터의 HMAC 값을 구한다. 송신자가 전달한 HMAC 값과 수신자가 연산한 HMAC 값이 동일하면 메시지가 변조되지 않았다는 것을 검증하게 되는 것이다.

 

HMAC
해시 함수와 공유 비밀 키를 사용하여 해시화하여 메시지의 무결성을 확인하는 메시지 인증 코드(MAC)의 한 종류.

 

 

14. 클라이언트측 Master Secret 생성

클라이언트에서도 12 번 단계에서 사용했던 것과 동일한 C+S 값과 Pre Master Secret, PRF 값을 조합하여 Master Secret을 생성한다. 여기서 중요한 점은 위 세개 값들을 서버로부터 다시 전달받는게 아닌, 자신이 이미 갖고 있던 값들을 사용한다는 것이다.

 

15. 대칭키 발급 

13번 과정과 마찬가지로 Master Secret 을 기반으로하여 데이터를 암/복호화 하기 위한 대칭키를 발급한다. 서버와 마찬가지로 Session Key, Mac Key 등을 생성한다.

 

16. 대칭키 암호화 기반 통신 시작!

이 후 서버와 클라이언트간 통신 시 각자 갖고있는 Session Key와 Mac Key를 사용해 데이터를 암/복호화하여 통신한다!

서버와 클라이언트가 갖고있는 Session Key와 Mac Key 동일하므로 대칭키 암호화 기반으로 통신이 된다!



회고

 이 글을 보는 독자들이 SSL 프로토콜의 내부 동작과 CA 의 SSL 인증서를 연결시켜 이해할 수 있길 바란다. 이 글에 적진 않았지만, 중간자 공격의 메커니즘을 이해한다면 사내망에서 SSL Certification 이슈가 발생하는 이유와 조치 방안도 이해할 수 있을 것이다!

반응형
반응형

개요

개발 시 무료 버전의 ChatGPT를 사용하곤 한다. 다만 보안 이슈로 인해 실제 프로젝트의 코드 일부분을 발췌하여 질문하긴 힘들다. 우회책으로 코드에 대한 설명을 하거나, 비지니스와 관련된 코드들을 모두 제거한 후 껍데기 형태의 코드를 제공하는 형태로 질의를 했었다.

 그러던 와중, ollama를 사용해 LLM 모델을 로컬 환경에 다운로드받고, 로컬 환경에서 질의하는 방법을 접하게 되었다. 최대의 장점은 보안. 로컬에서 동작하는 방식이다보니 프라이빗하게 사용할 수 있었다. 또한 이러한 방식을 Intellij IDE Plugin 에서 제공한다는 사실을 알게되었다. Plugin을 일주일정도 사용해보고 느낀점들을 리뷰해보도록 하겠다. 2025년 2월 12일 기준, 플러그인 중 Continue와 CodeGPT에 대한 리뷰를 하려했으나, 갑자기 CodeGPT에 대한 플러그인이 삭제되어 Continue만 리뷰한다.

 

먼저 Continue 뭔지에 대해 한번 알아보자.

 

Continue 란 무엇인가요? 

오픈소스 기반의 AI 코드 어시스턴트이다. 다양한 LLM 모델을 지원하며, 자동 완성, 코드 수정, LLM과의 채팅 기능을 제공한다. Intellij Continue Plugin의 공식문서 내용 기반으로 기능들을 테스트 한 후 현업에서 사용해보았다.

 

https://plugins.jetbrains.com/plugin/22707-continue

 

Continue - IntelliJ IDEs Plugin | Marketplace

Continue Continue is the leading open-source AI code assistant. You can connect any models and any context to build custom autocomplete and chat experiences inside VS...

plugins.jetbrains.com

 

 

Chat 기능

Continue Chat UI

 

 IDE 를 벗어나지 않고 LLM 에게 질의할 수 있는 가장 기본적인 기능이다. Plugin 설치 시 우측에 Continue 아이콘이 생기는데, 클릭하면 위와 같은 채팅 UI가 나타난다. 단순 채팅 뿐 아니라, IDE에 있는 코드나 파일, 폴더를 단축키 기반으로 손쉽게 참조시킬 수 있다.

 

 Continue 설정 통해 API 기반 LLM 서비스로 질의를 보내게 할수도 있지만, 로컬 환경에서 실행되는 ollama 서버의 LLM 모델과 연동도 가능하다. 필자의 경우 로컬 환경의 ollama 서버와 연동하여 채팅 기능을 사용하고 있다. 모델 선택도 간단하다. 로컬 ollama 서버와 연동만 되면 모델들이 자동으로 조회된다. 원하는 모델을 선택하기만 하면 된다.

 

ollama 연동 및 model 선택

 

 

특정 코드블럭을 LLM 질의 내용에 참조시키고싶다면 IDE 상에서 블럭씌운 후 [ Command + J ] 단축키를 통해 추가할 수 있다.

코드 블럭 참조

 

 

 참조된 코드는 프롬프트와 함께 질의 가능하며, 응답받은 코드를 실제 코드에 적용하고 싶다면, 우측 상단의 'insert at cursor' 버튼을 누르면 된다. IDE 상에 위치한 커서로 코드가 복사된다.

질의 결과

 

 파일 참조도 가능하다. 프롬프트 입력창에 '@' 입력 후 베이스가 되는 파일 명을 입력하면 된다. 코드에 대한 리뷰 및 리팩토링이 필요할 경우 좋은 옵션으로 사용될 수 있을 것으로 기대된다.

파일 참조

 

Auto Complete

출처 : continue 공식문서

 

 자동완성 기능을 제공한다. 활성화를 위해 IDE 설정 > Continue > Enable Tab Autocomplete 옵션을 활성화해야한다는데, 필자의 경우 해당 옵션을 활성화해도 자동완성 기능이 활성화되지 않았다. Intellij 의 자체 자동완성 기능을 비활성화해도 마찬가지였다.

 기능을 직접 경험해보면 좋았겠지만, 사실 지원하지 않아도 크게 아쉽진 않았다. IDE 자체에서도 준수한 수준의 자동완성을 제공해주기때문이다.

 

Code Edit

출처 : continue 공식문서

 

 LLM 질의 결과로 코드를 리턴받고, 이를 UI 기반으로 편하게 수정 할 수 있는 기능이다. LLM 에서 제안한 코드는 diff 형식으로 IDE 상에 표시되며 개발자가 판단 후 적용할 수 있다. 이 기능은 별도의 설명 없이 수정할 '코드'만 출력되기 때문에 몇번의 대화가 오가는 수준의 질의가 아닌 단순한 메서드를 만들거나, 가벼운 리펙토링, 로깅처리, 예외처리 등 정형화되고 표준화된 부분에 효과적으로 사용 가능할 것 같다.

 

 사용법은 대상 코드 블럭 지정 후 [ Command + i ] 단축키를 누르면 코드 바로 위에 프롬프트 입력 창이 뜬다. 원하는 내용을 입력하면 코드 수정 제안이 diff 형식으로 나타난다. 기존 코드는 빨간색, 제안 코드는 파란색으로 표시되며, 제안 코드로 변경을 원할경우 우측 Y를, 거절할 경우 우측 N 클릭을 통해 가능하다. Rejcet All, Accept All 버튼을 통해 일괄 처리도 가능하다.

 

아래는 메서드 명을 리팩토링해달라는 요청이었는데, 메서드 명명 규칙에 맞게 동사, 카멜케이스 형태로 자동 리팩토링 한 것을 확인할 수 있다.

메서드명 리팩토링 명령

 

 

일주일간의 사용 후기

 당연한 얘기긴 하지만 일단 답변 품질에서의 차이는 기존 API 기반 LLM 서비스와 차이가 없었다. 개발 전용 IDE에 연동되었기에 품질에서도 뭔가 다르지 않을까라는 조그만 기대를 했던것같다. 답변받은 코드들에 대해 개발자의 자체 검증 과정은 기존 LLM 서비스를 사용했을때와 같이 필요했다. 해서 LLM 품질이 아닌 다른 영역에서 느꼈던 부분들을 정리해보았다.

 

* 코드 유출, 외부 학습의 위험이 없다.

API 기반 LLM 서비스를 사용할 경우 질의한 코드를 모델이 학습할 우려인터넷망을 거친다는 점에서 코드 유출 위험을 우려했는데, 로컬 ollama 를 연동할 경우 인터넷 연결 없이 로컬 환경에서 사용 가능하다는 점에서 위 우려가 해소되었다. 코드에 대해 질의가 필요할 경우 설명으로 대체하는 케이스가 많았는데, 코드를 그대로 질의할 수 있다는 점이 좋았다.

 또한 IDE와 연동이 되어있어 빠르고 편리하게 리소스를 참조시킬 수 있었다. 보다 쉽고 빠른 질의가 가능했다.

 

* 자유롭고 빠른 모델선택

 모델마다 특화된 역량이 있다. 때문에 모델을 바꿔가며 질의하는 경우가 있었는데 모델 변경 방식도 매우 간단했다. 필자는 ollama 3.1, qwen2.5-coder, deepseek-r1 모델을 주로 사용했는데 이중 qwen2.5-coder 모델은 코드 기반의 답변을, deepseek는 도출 과정 기반의 답변을 받을 수 있었다. (참고로 qwen2.5-coder 모델은 코딩과 수학에 특화된 모델이다.)

 

qwen2.5-coder Description 일부 발췌

이 분야의 전문화된 전문가 모델 덕분에 코딩 과 수학 에 대한 지식이 상당히 많아 지고 역량이 크게 향상되었습니다 .

 

qwen2.5-coder 모델과 deepseek-r1 모델에게 메서드 명에 대한 리팩토링 질의를 해본 결과,

질의내용

 

qwen2.5-coder 모델은 아래와 같이 리팩토링 코드간단한 설명만 존재했고, (코드적으로 설명이 필요하거나 새로운 개념이 등장할 경우에는 친절하게 설명해준다.)

qwen2.5-coder:7b 모델의 응답

 

 

deepseek-r1 모델은 최종 결과 코드에 대한 도출 과정이 자초지종 설명되어 있다. 아래는 답변에 대해 부분 번역 것인데, 구구절절한 느낌이 없지않아 있다. 이 외에도 코드 리뷰를 요청했는데, 마찬가지로 코드에 대한 도출 과정들을 매우 길게 설명했다. 실제로 모든 응답을 받기까지 5분 이상 걸렸었다. 개발을 시작한지 얼마 되지 않았거나, 과정을 이해하고싶은 개발자들에게는 좋은 옵션이 될 수 있을것같다.

 

enterRoom 내부의 각 줄을 통과하겠습니다. 먼저 ValidAteChattingroom (룸메이트)이 있습니다. 그것은 간단 해 보이지만 어쩌면 더 설명 적으로 만들 수 있습니다. 그것이 검증되기 때문에 아마도 그대로 유지하거나 가능하면 더 나은 동사를 찾으십시오.
...(중략)
어쩌면 "검증"이 여기에서 최고 일 것입니다.

다음은 KickoutDuplicationUserInsameroom (룸메이트, 이메일)입니다. 복제라는 단어는 철자가 잘못 될 수 있습니다. 중복되어야합니다.
...(중략)
어쩌면 "제거"가 더 나을 것입니다. 따라서 삭제 된 UplyserInsameroom 또는 그와 비슷한 것으로 변경하십시오.

그런 다음 SessionUser.of (SessionId, 이메일, 룸메이트)가 있습니다. ()의 메소드 이름은 공장 방법에 맞으므로 다른 곳에 다른 이름 지정 규칙이 없다면이를 변경할 필요가 없다고 생각합니다.

다음 줄은 CachingUsersessionTosessionStore (SessionUser)입니다. 흠, 캐싱은 여기서 올바른 동사가 아닐 수도 있습니다. 저장 또는 저장에 대한 것입니다.
...(중략)
아마도 SavessessionUserInsessionStore가 더 나을 것입니다.

...(중략)
So putting it all together, I'll go through each method call and adjust the names accordingly:
validateChattingRoom(roomId) → keep as is since "validate" is a verb.kickOutDuplicationUserInSameRoom(...) → change to removeDuplicateUserFromRoom or something similar, but maybe better phrased as ensureSingleSessionPerRoom to make it more proactive.cachingUserSessionToSessionStore(sessionUser) → saveSessionUserToSessionStorecachingUserSessionToRoomStore(sessionUser) → saveSessionUserToRoomStoresendRoomStatusToRedis(roomId) → updateRoomStatusOnRedis or sendRoomStatusUpdateToRedissendRoomEnterAlarmToRedis(email, roomId) → notifyRoomEntryToRedis

따라서 최종 리팩토링된 메서드 호출은 다음과 같을 것이라고 생각합니다.

validateChattingRoom(roomId);
ensureSingleSessionPerRoom(roomId, email);
sessionUser = SessionUser.of(...);
saveSessionUserToSessionStore(sessionUser);
saveSessionUserToRoomStore(sessionUser);
sendRoomStatusUpdateToRedis(roomId);
notifyRoomEntryToRedis(email, roomId);

이것은 각 메소드 이름을 Camelcase의 동사로 만들고 각각의 행동을 명확하게 설명합니다.
  public void enterRoom(EnterRoomDto.Request request, String email, String sessionId) {
        Long roomId = request.getRoomId();

        validateChattingRoom(roomId);
        ensureSingleSessionPerRoom(roomId, email);

        SessionUser sessionUser = SessionUser.of(sessionId, email, roomId);
        saveSessionUserToSessionStore(sessionUser);
        saveSessionUserToRoomStore(sessionUser);
        sendRoomStatusUpdateToRedis(roomId);
        notifyRoomEntryToRedis(email, roomId);
    }

 

* 리팩토링, 코드리뷰 활용

 리팩토링과 코드리뷰에 잘 활용될 수 있을것이라 생각한다. 필자의 경우 비지니스 클래스에 대한 코드 리뷰를 요청했더니, 실제로 문제가 될 수 있는 부분에 대한 개선을 제안받았다. 병렬 처리가 필요한 부분에서 SingleThread 를 계속 생성해나가는 방식을 채택했었는데, Thead Pool 방식으로의 개선을 제안받았고, 제안받은 코드도 정상적으로 돌아갔다. 참고로 모델은 qwen2.5-coder 모델을 사용했다.

 

* 다국어 대화 지원 여부 파악 필요

 필자는 영어와 친하지 않아 주로 한글로 질의했다. qwen2.5-coder나 ollama 3.1 모델은 한글로 질의할 경우 한글 답변을 줬는데, deepseek-r1 모델의 경우 영어 답변을 줘서 번역기를 돌려야하는 불편함이 있었다. ollama 공식 사이트에 접속하여 모델 정보를 읽어보니 qwen2.5-coder나 ollama 3.x의 경우 모델 설명 부분에 '다국어를 지원한다', '다국어 대화가 가능하다' 등의 내용이 있는데 반해, deepseek-r1 모델은 다국어 관련 내용이 존재하지 않았다. 영어를 잘 못하는 사람들은 모델에 명시된 다국어 지원 여부를 잘 살펴보는것이 좋을것같다.
(deepseek도 프롬프트에 한국어로 답변해달라고 추가하면 한글 답변이 나오긴 하지만, 추가하는걸 자꾸 까먹...)

 

ollama 3.1 Description 일부 발췌

8B 및 70B 모델의 업그레이드된 버전은 다국어이며 128K의 상당히 긴 컨텍스트 길이, 최첨단 도구 사용 및 전반적으로 더 강력한 추론 기능을 갖추고 있습니다. 이를 통해 Meta의 최신 모델은 장문 텍스트 요약, 다국어 대화 에이전트코딩 어시스턴트와 같은 고급 사용 사례를 지원할 수 있습니다.

 

qwen2.5-coder Description 일부 발췌

중국어, 영어, 프랑스어, 스페인어, 포르투갈어, 독일어, 이탈리아어, 러시아어, 일본어, 한국어, 베트남어, 태국어, 아랍어 등 29개 이상의 언어에 대한
 다국어 지원 제공합니다 .

 

참고로 잠깐 사용했던 CodeGPT는 모델과 별개로 시스템 프롬프트 설정이 가능했는데, 한글로 답변해달라는 내용을 추가하니 deepseek-r1 모델 또한 한글 답변을 주긴 했었다. continue에도 이러한 설정이 있다면 좋을것같다.

  

* 시스템 리소스에 따른 모델 규모 선택

 아무래도 로컬 환경에 설치된 LLM 모델이 동작하는 거라 시스템 리소스를 많이 먹는다. 필자의 컴퓨터 메모리가 18GB인데, 14b 규모의 모델을 사용할 경우 컴퓨터가 심하게 버벅거린다. 7b 규모로 낮추니 버벅거림이 많이 사라졌다. 

 규모가 클수록 더 좋은 답변을 받을 수 있겠지만 본인의 PC 사양을 고려하여 모델 규모를 설정하는 것이 필요해보인다. 모델 크기는 GB 기준 메모리의 절반을 넘지 않는 것이 바람직해보인다. 메모리가 18GB라면 모델 크기는 9B를 넘지 않는것이 정신건강에 좋았다.

 

회고

 일주일동안 현업에 적용하려고 시도해보았으나, LLM의 답변 내용을 검증하기 위해, 또는 믿지못해서 구글링을 하는 자신을 볼 수 있었다. 사용 초기엔 무지성 채팅 기능만 사용했는데, 공식문서를 읽어보니 꽤나 유용한 기능이 있음에 놀랐다. 특히 Edit 기능이 매우 매력적이라 생각됐다.

 사용 기간이 길지 않았고, 기능 경험, 사용 부분에 초점이 맞춰져서인지, 다양하고 구체적인 현업 적용 아이디어는 도출해내지 못했다. 계속해서 현업에 활용하면서 유의미한 포인트를 찾아봐야겠다.

 

 

 

반응형
반응형

 

1. 개요

 리두로그와 언두로그를 공부하려했더니 Buffer Pool 이라는 개념이 등장했다. 테이블을 메모리에 캐싱해두는 개념인데, 단순 개념만 알고있기에는 중요한 내용이기에 정리하였다.

 

2. InnoDB Buffer Pool이 뭐에요?

말 그대로 InnoDB 엔진에서 제공하는 버퍼 풀인데, 이녀석의 역할은 바로 캐싱이다.

테이블이나 인덱스를 캐싱한다고 한다. 그럼 정확히 어떤 형태로 캐싱을 하고, 실제 환경에서 어떻게 사용되는지 알아보자.

 

3. Buffer Pool 은 '페이지'들이 캐싱되어 있어요

 버퍼 풀에는 '페이지'들이 저장되어 있다. InnoDB 엔진에서의 페이지란 데이터베이스에서 데이터를 저장 단위이다. I/O를 통해 디스크로부터 페이지 단위로 데이터를 조회하면, Buffer Pool에 이를 그대로 저장한다. 페이지 하나의 크기는 16KB이다. (수정 가능하다)

 페이지들은 버퍼 풀에 차곡차곡 쌓이는게 아니다. 버퍼 풀은 데이터를 효율적으로 캐싱하기 위해 LRU 리스트, Flush 리스트, Free 리스트 라는 3개의 리스트로 구성되어 있으며, 이 중 LRU 리스트에 어떤 규칙에 따라 저장되게 된다.

 

3.1. Free 리스트

비어있는 페이지들의 목록이다. 사용자의 쿼리가 디스크의 데이터 페이지를 읽어와야 하는 경우 사용된다. 

 

3.2. Flush 리스트

디스크로 동기화되지 않은 데이터를 가진 데이터 페이지(== 더티 페이지)들의 목록이다. 즉, 디스크에서 데이터 페이지를 읽은 후 변경이 가해진 데이터 페이지들의 목록이다. 데이터가 변경되면 해당 페이지는 Flush 리스트에서 관리됨과 동시에 변경 내용을 리두 로그에 기록하고, 버퍼 풀에도 변경 내용을 반영한다. 그 후 특정 시점에 리두 로그가 디스크로 기록된다.

 

3.3. LRU (+MRU) 리스트

MySQL InnoDB 에서는 변형된 LRU 방식을 채택하고 있다. 바로 LRU와 MRU(Most Recently Used)가 결합된 하이브리드 방식이다. 참고로 LRU와 MRU는 캐시 알고리즘이다.

 

* LRU (Least Recently Used)
가장 최근에 사용되지 않은 페이지를 교체하는 알고리즘. 가장 오래된 캐시를 제거한다.

 

 

* MRU (Most Recently Used)
가장 최근에 사용된 페이지를 교체하는 알고리즘. 가장 최근에 사용된 캐시를 제거한다.

 

 

4. 가장 오래된 페이지와 가장 최근의 페이지를 제거한다고??

위 두 캐시 알고리즘이 버퍼 풀에 적용되어 있는 구조이다. 이해가 잘 되지 않는다면 어떻게 동작하는지에 대한 메커니즘을 그려나가보면 이해할 수 있을것이다.

 

아래는 변형된 LRU, 즉 LRU와 MRU가 결합된 형태의 리스트이다.

LRU+MRU 리스트

 

InnoDB에서의 페이지 캐시 메커니즘에 의해 New 서브리스트 자주 사용되는 페이지가 저장되는 영역, Old 서브리스트는 신규 혹은 자주 사용되지 않는 페이지가 저장되는 영역으로 사용될것이다. 편의상 New 서브 리스트가 존재하는 영역을 MRU 영역, Old 서브 리스트가 존재하는 영역을 LRU 영역이라 표현하겠다.

긴말 필요없이 바로 동작 메커니즘을 살펴보자

 

1) 필요한 레코드가 저장된 데이터 페이지가 버퍼 풀에 있는지 검사한다. [InnoDB 어댑티브 해시 인덱스 > 테이블 인덱스] 를 통해 페이지를 검색한다. 

* 어댑티브 해시 인덱스
B-Tree 의 성능 개선을 위해 InnoDB 에서 자동으로 추가하는 인덱스이다. 자주 읽히는 데이터 페이지의 키값을 이용해 해시 인덱스를 만들고, 필요할 때마다 해당 데이터 페이지로 즉시 찾아갈 수 있게 한다.
- LRU/MRU를 설명하는 글이므로 자세히 다루지 않겠다.

 

2) 버퍼 풀에 데이터 페이지가 있다면 MRU 영역 방향으로 승급한다. (MRU 리스트로 들어가는게 아닌 방향으로 승급이다.)

 

3) 버퍼 풀에 데이터가 없다면 디스크로부터 필요한 데이터 페이지를 버퍼 풀에 적재와 동시에 LRU 영역 Head 부분에 추가한다.

 

4) LRU 영역에 추가한 데이터 페이지가 조회되면 MRU 영역 방향으로 승급하고, 버퍼풀에 없다면 LRU Head 부분에 추가하는 과정을 반복한다.

 

5) 이때, 승급하는 데이터 페이지가 LRU 영역의 Head에 위치했을 경우, 바로 위인 MRU 영역의 Tail이 아닌 MRU 영역의 Head로 이동한다. (이부분이 핵심이다!)

 

6) 1,2,3,4,5 과정이 반복되면서 자주 사용되지 않는 데이터 페이지들은 자연스럽게 LRU 영역의 Tail 쪽으로 밀려난다. 끝까지 밀려난 데이터 페이지들은 버퍼 풀에서 제거된다.

 

7) 자주 사용되는 데이터 페이지의 경우 성능 향상을 위해 페이지의 인덱스 키를 '어댑티브 해시 인덱스'에 추가한다.

 

5. LRU, 오래된 데이터가 제거되는 것은 이해가는데.. MRU는?

LRU 알고리즘이 적용됐다는 것은 이해가 갈것이다. 그럼 최근에 사용된 데이터가 먼저 삭제된다는 MRU는 대체 어디에 적용된 것일까? 데이터 레코드를 버퍼 풀로 읽어오는 과정을 직접 그려보며 '최근 사용된 데이터가 먼저 삭제'되는 MRU 부분을 이해해보자.

 

1) 1시 30분, 디스크로부터 A 데이터 페이지를 읽어 버퍼 풀에 적재한다. 최초 버퍼 풀 적재시에는 LRU 영역 Head 부분에 저장된다.

A 데이터 레코드 조회

 

2) 1시 32분, 디스크로부터 B 데이터 페이지를 읽어 버퍼 풀에 적재한다. LRU 영역 Head 부분에 저장됨과 동시에 기존 A 데이터 페이지는 Tail 쪽으로 밀려난다.

 

B 데이터 레코드 조회

 

 

3) 2시 0분, 디스크로부터 C 데이터 페이지를 읽어 버퍼 풀에 적재한다. LRU 영역 Head 부분에 저장됨과 동시에 A, B 데이터 페이지는 Tail 쪽으로 밀려난다.

 

 

 

4) 2시 10분, 드디어 C 데이터 페이지를 찾는 쿼리가 실행됐고, C 데이터 페이지를 버퍼 풀에서 로드하게 된다. 이때 C 데이터 페이지는 LRU 영역의 Head에 위치했기 때문에 MRU 영역의 Head로 이동하게 된다.

 

 

5) 2시 20분, 디스크로부터 D 데이터 페이지를 읽어 버퍼 풀에 적재한다. LRU 영역 Head 부분에 저장됨과 동시에 A, B 데이터 페이지는 Tail 쪽으로 밀려난다.

 

 

6) 계속해서 신규 쿼리가 들어옴에 따라 디스크로부터 데이터 페이지를 읽어오게 되었다. E, F, G... X, Y, Z 까지, 다양한 데이터 페이지를 디스크로부터 로드하다보니 5시 30분이 되었다.

 

6. 최신의 데이터가 먼저 제거된다!

2시 10분에 저장한 C 데이터 페이지보다 최근에 사용했었던 E, F, G, H... 들의 데이터 페이지가 버퍼 풀에서 먼저 제거되었다. MRU 영역에서도 자주 사용되지 않은 데이터 페이지들은 Tail 쪽으로 밀려난다. 단, MRU 영역에 있는 C 데이터 페이지가 삭제되지 않은 이유는, 다른 데이터 페이지들이 여러번 재사용되지 않았고, 이로 인해 MRU 영역에서의 '경쟁'이 발생하지 않았기 때문이다. 

 

 '최근 사용된 데이터인 E,F,G,H 등의 데이터 페이지들이 먼저 삭제'된다는 점에서 MRU 알고리즘이 적용되었음을 알 수 있다.

 

Z 레코드까지 LRU 리스트에 저장됨

 

 

7. 정리하면... InnoDB의 LRU + MRU 버퍼 풀

 InnoDB 버퍼 풀은 하이브리드 방식을 사용함으로써 단순 오래된 데이터 페이지 뿐 아니라, 자주 사용되지 않은 최신의 데이터 페이지들도 제거하고 있다. 만약 MRU 알고리즘이 적용되지 않았다면 어떨까? 최근 액세스한 데이터 페이지 들 중 자주 사용되지 않아 사실상 필요없는 데이터 페이지가 메모리에서 공간만 차지할것이고, 버퍼 풀이 가득 차서야 제거될것이다. 캐시 메모리를 최대한 활용하기 위해서는 자주 사용하는 데이터를 많이 저장하고, 최신의 데이터라할지라도 자주 사용하지 않은 데이터는 제거하는 것이 좋지 않겠는가?!

 

오늘은 InnoDB Buffer Pool에 대한 기본 구조와 InnoDB의 LRU + MRU 리스트에 대해 알아보았다. 다음은 리두 로그와 언두 로그에 대해 공부해보도록 하겠다!

 

 

 

 

반응형
반응형

1. 개요

 도커 클라이언트, 도커 호스트, 도커 레지스트리의 개념을 이해하고, 도커 기본 명령어를 알아보자.

 


2. 도커 전체 구조

앞서 말한 3개 개념은 도커 전체 구조에 대한 개념이다. 도커는 클라이언트, 호스트, 레지스트리라는 기본 구조를 갖는다.

도커 상세 구조 (출처 : https://velog.io/@koo8624)

 

2.1. 도커 클라이언트

 도커에 명령을 내릴 수 있는 CLI 도구이다. 도커 이미지 다운로드, 컨테이너 실행, 중지, 조회 등 도커를 관리할 수 있는 다양한 명령어를 제공한다.

 

2.2. 도커 호스트

도커를 설치한 서버 혹은 가상 머신을 말한다. 현재 내 PC에 도커를 설치했다면, 도커 호스트는 내 PC이다.

 

2.3. 도커 레지스트리

 도커 이미지를 저장하거나 배포하는 시스템이다. 만약 이미지가 필요하다면 도커 레지스트리로부터 다운받게 된다. 대표적으로 DockerHub(http://hub.docker.com)가 있다. 도커 이미지계의 깃허브라 할 수 있다.

 

위 홈페이지에 들어간 후 검색창에 원하는 도커 이미지를 검색하고,

DockerHub

 

 

상세 정보를 통해 도커 이미지를 다운받을 수 있는 명령어(ex. docker pull redis)를 확인할 수 있다. 이 명령어를 도커 클라이언트를 통해 입력하면 도커 레지스트리를 통해 이미지를 다운받게 된다. 명령어는 도커 호스트에 위치한 도커 데몬이 처리하게 된다.

도커 이미지 다운 명령어 확인

 

 

 

* 도커 데몬

 도커와 관련된 리소스를 관리하는 백그라운드 프로세스이다. 이미지 다운로드 명령어가 입력될 경우 도커 호스트(도커가 설치된 서버)에  도커 이미지 존재 여부를 체크한 후, 이미지가 없을 경우 도커 레지스트리로부터 이미지를 다운받게 된다.

 

* 도커 이미지

 컨테이너 형태로 소프트웨어를 배포하기 위해 필요한 모든 요소(코드, 라이브러리, 설정 등)를 실행할 수 있는 빌드된 패키지이다. 도커 이미지는 독립적이기때문에 의존성을 고려할 필요가 없다.

 

* 도커 컨테이너

 도커 이미지를 실행할 수 있는 인스턴스를 말한다. 도커 컨테이너는 도커 이미지로부터 생성된다. 도커 컨테이너는 자체 파일 시스템을 갖으며 외부와 시스템 리소스를 공유하지 않고 독립적으로 실행된다. 단, OS는 공유한다!

 

3. 도커 기본 명령어

 

도커 이미지 다운로드

docker image pull {이미지 이름:태그 이름}

 

 

도커 이미지 목록 확인

 docker image ls

 

컬럼명 내용
REPOSITORY 이미지 이름
TAG 이미지 태그
IMAGE ID 이미지 ID (로컬 PC에서 할당해주는 ID)
CREATED 이미지 생성 시간
SIZE 이미지 크기

 

 

도커 컨테이너 실행

docker container run {이미지명}

 

 

실행중인 도커 컨테이너 리스트 조회

docker container ls

 

 

 

** 컨테이너를 실행했는데 컨테이너 리스트에서 조회가 되지 않아요

 

docker container run 명령어를 통해 컨테이너를 실행했을때, 해당 컨테이너 내에서 실행되는 프로세스나 서비스가 없을 경우, 컨테이너가 자동으로 종료되기 때문이다. 대표적으로 ubuntu 컨테이너가 이에 해당하며, 컨테이너 내에서 프로세스 실행을 유지시키기 위해 /bin/bash 와 같은 명령어를 함께 입력한다.

 

이에반에 Redis나 MySQL의 경우 컨테이너 실행 시 백그라운드 서비스가 돌아가기 때문에 자동으로 종료되지 않는다.

 

 

모든(정지 상태 포함) 도커 컨테이너 리스트

docker container ls -a

 

 

컨테이너 종료

docker container stop {컨테이너 ID}

 

 

컨테이너 강제 종료

docker container kill {컨테이너 ID}

 

 

컨테이너 삭제

docker container rm {컨테이너 ID}

 

 

이미지 삭제

docker image rm {이미지 ID}

 

반응형
반응형

개요

InnoDB의 잠금에 대해 알아보기 전 비관적 잠금과 낙관적 잠금에 대해 알아보았다.

 

낙관적 잠금

각 트랜잭션이 같은 레코드를 변경할 가능성이 낮다고 낙관적으로 가정하는 상황에서의 잠금이다. 리소스를 잠그지 않고 데이터를 업데이트 한 후, 마지막 커밋 시 충돌을 확인하는 방식이다.

충돌이 감지되면(트랜잭션이 시작되고 끝나는 사이, 다른 트랜잭션이 데이터를 수정) 트랜잭션이 롤백되고 오류를 반환한다.

 

충돌을 어떻게 확인할까?

테이블에 수정 시점을 추적할 수 있는 버전 필드 또는 타임스탬프를 추가하는 것이다.

버전 필드 (Version Field)
낙관적 잠금에서 각 데이터 레코드에 추가되는 필드로, 해당 레코드가 마지막으로 수정된 시점을 추적하는 용도로 사용된다. 레코드가 업데이트 될 때마다 이 버전 번호가 증가하는 형태이다.

 

트랜잭션이 데이터를 읽을 때 버전 필드를 함께 읽고, 트랜잭션이 커밋할 때 버전 번호를 확인하여 일치하는지를 확인한다. 만약 버전 필드 값이 달라졌다면, 다른 트랜잭션에서 데이터를 수정한 것이라 판단하고 트랜잭션을 롤백 또는 재시작한다.

 타임스탬프를 사용할 경우 해당 레코드의 수정 시간을 기재한다. 마찬가지로 트랜잭션이 시작, 종료될 때 레코드의 수정 시간이 달라졌다면 롤백을, 같다면 커밋을 하는 방식이다.

 

-- 1. 데이터 읽을 때 버전 정보도 함께 읽음
SELECT product_id, product_name, price, version
FROM products
WHERE product_id = 1;

-- 2. 수정 시 버전 확인
UPDATE products
SET price = 200.00, version = version + 1
WHERE product_id = 1 AND version = 2;  -- '2'는 읽은 버전 번호

 

낙관적 잠금 장단점

자원에 대한 잠금이 없기때문에, 대기 시간이 짧고, 처리 속도가 향상될 수 있다. 특히 데이터 충돌이 드문 경우에는 트랜잭션이 빠르게 처리된다. 또한 여러 트랜잭션이 동시에 데이터를 읽고 수정할 수 있으므로, 데이터 충돌이 발생하는 상황이 적다면 그만큼 높은 동시성을 유지하게된다.

 

하지만 데이터 충돌이 발생할 경우 트랜잭션을 다시 시도해야 한다.  충돌이 자주 발생하거나 처리 비용이 높은 트랜잭션이라면, 재시도 비용도 증가하게 된다. 또한 충돌에 대한 예외 로직이 필요하다. 변경을 다시 시도하거나, 사용자에게 알림을 주는 등의 별도 처리가 필요하다. 이러한 예외 코드를 잘못 구현하기라도 한다면 데이터 무결성이 손상될수 있으므로 주의해야한다.

 

 

비관적 잠금

각 트랜잭션이 같은 레코드를 변경할 가능성이 높다고 비관적으로 가정하는 상황에서의 잠금이다.즉, 하나의 트랜잭션이 데이터를 조회하는 시점에 락을 걸고, 트랜잭션이 끝날때까지 유지한다.

조회 시점에 잠금을 획득하는 대표적인 방법은 SELECT FOR UPDATE 이다.

 

비관적 잠금의 장단점

트랜잭션의 동시 접근을 확실하게 방지할 수 있기에 데이터의 무결성이 보장된다.

 

하지만 동시성이 떨어져 처리 속도가 느리다. 트랜잭션이 각각의 자원을 점유하는 상태에서 서로의 자원을 요청한다면, 데드락이 발생하게 된다.

 

비관적 잠금 데드락 재현

users 테이블

 

1) 1번 트랜잭션이 users 테이블의 id = 1인 레코드에 대해 비관적 잠금을 설정한다. 트랜잭션은 끝나지 않고, 다른 작업들을 처리하고 있다.

 

select * from users where id = 1 FOR UPDATE;

 

2) 도중 2번 트랜잭션이 시작되었고, users 테이블의 id = 10인 레코드에 대해 비관적 잠금을 설정한다. 마찬가지로 트랜잭션은 끝나지 않고, 다른 작업들을 처리하고 있다.

 

select * from users where id = 10 FOR UPDATE;

 

3) 1번 트랜잭션의 작업 중 유저 정보 일괄 수정 위해 업데이트 쿼리가 나갔지만, 2번 트랜잭션에서 id = 10인 레코드에 대한 잠금으로 인해 '대기상태'에 들어간다.

 

update users set name = '이름';

 

4) 2번 트랜잭션에서도 마찬가지로 유저 정보 일괄 수정 위해 업데이트 쿼리가 나갔지만, 1번 트랜잭션에서 id = 1인 레코드에 대한 잠금으로 인해 '대기상태'에 들어간다.

 

5) 트랜잭션 1, 2번은 데드락 상태에 빠지게 된다.

 

하지만 실제 테스트를 해보니 무기한 데드락이 걸리지 않고 한쪽 트랜잭션을 롤백시키는 것을 볼 수 있는데, 이는 InnoDB에서 제공하는 자동 데드락 탐지 기능에 의해 한쪽 트랜잭션이 롤백되는 것이다.

데드락 상황을 인지하고, 트랜잭션 재시작

 

또한 락에 대한 타임아웃도 설정되어있기에, 락이 걸린 자원을 획득하지 못한다면 무한정 대기상태에 들어가지 않고, 에러 및 롤백 후 트랜잭션 재시작을 시도한다.

Lock wait timeout 초과 시 트랜잭션 재시작

 

잠금에 대한 대기 시간 변수는 innodb_lock_wait_timeout이며 , 기본 50초로 설정되어 있다.

innodb_lock_wait_timeout

 

 

반응형
반응형

개요

  도커를 이해하기 위해서 필요한 사전지식인 운영체제, 프로그램, 스레드, 네임스페이스의 개념을 알아보자. 

Docker

운영체제

 컴퓨터는 하드웨어와 소프트웨어로 구성된다. 좁은 의미에서 하드웨어는 기계, 소프트웨어는 프로그램들을 말하는데 소프트웨어에는 '운영체제'라 부르는 시스템 소프트웨어가 존재한다. 운영체제는 하드웨어와 소프트웨어 자원을 관리하고, 프로그램이 실행될 수 있는 환경을 제공하는 역할을 한다.

 

프로그램

 실행 가능한 명령어의 집합을 의미한다. 디스크에 저장되어 있으나, 메모리에는 올라가지 않은 정적인 상태로 존재한다. 컴퓨터에 설치된 게임, 파워포인트, 우리가 개발하여 jar로 빌드한 파일 모두 프로그램이다.

 

프로세스

 실행중인 프로그램을 의미한다. 프로그램이 실행되면 메모리에 올라간다. 즉, 프로세스는 메모리에 올라간 상태로 존재한다.

 

스레드

 프로세스 내에 실행되는 여러 흐름의 단위를 의미한다. 예를들어 워드 프로그램을 실행시킨다면, 하나의 스레드는 화면상에 글자를 보여주는 일을, 다른 스레드는 사용자의 키 입력에 대응하는 일을, 또 다른 스레드는 오탈자와 문법을 확인하는 일을 한다.

 

 

정리하면, 컴퓨터를 구성하는 소프트웨어 중 '프로그램을 실행할 수 있는 환경을 제공'하는게 운영체제이고, '프로그램을 실행하면 프로세스'가 되며, '프로세스 내부에서 여러 스레드들이 동작' 하며 기능들을 수행한다.

 


네임스페이스

 프로세스를 실행할 때 시스템 리소스를 분리해서 실행할 수 있도록 도와주는 기능을 의미한다. 예를들어 여러 프로세스를 실행했을때, 각각의 프로세스가 모든 파일 시스템에 접근할 수 있는게 아닌, 첫번째 프로세스는 A 디렉토리에만, 두번째 프로세스는 B 디렉토리에만 접근할 수 있도록 하면 어떨까? 각각의 프로세스 입장에서 보면 하나는 A 디렉토리, 다른 하나는 B 디렉토리만을 할당받은 체로 동작하게 된다. 즉, 네임 스페이스를 잘 활용하면 같은 운영체제 내에 실행되는 프로세스들이 논리적으로 분리된 환경에서 실행 될 수 있는 것이다.

 

개념적으로는 이해되나, 네임스페이스들이 어떻게 상호작용하는지는 따로 공부가 필요할 것 같다. 잘 알려진 네임스페이스는 다음과 같다.

네임 스페이스 의미 역할
pid PID: ProcessID 리눅스 커널의 프로세스 ID를 분리합니다.
net NET: Networking 네트워크 인터페이스를 관리합니다.
ipc IPC: Inter Process Communication 프로세스 간 통신(IPC) 접근을 관리합니다.
mnt MNT: Mount 파일 시스템의 마운트를 관리합니다.
uts UTS: Unix Timesharing System 커널과 버전 식별자를 관리합니다.

 


도커란?

 운영체제 수준의 가상화 방식을 통해 소프트웨어를 배포하는 가상환경을 제공하는 소프트웨어이다. 사용자 입장에서는 OS 까지의 가상 환경을 제공받아 어플리케이션만 관리/배포하면 되니 도커를 PaaS 제품이라고도 부른다.

출처 - https://www.redhat.com/

 

 운영체제 수준의 가상화 방식은 뭘까? 운영체제가 가상화 즉, '논리적으로 분리된 환경'을 제공하는 방식을 말한다. 운영체제가 논리적으로 분리된 환경을 제공할 수 있을까? 있다! 시스템 리소스들을 분리한 후 환경별로 다른 시스템 리소스들을 제공하면 된다. 네임스페이스들을 통해서 말이다. 

 

도커를 활용한 어플리케이션 실행

 

위와 같이 운영체제에서 네임 스페이스를 통해 시스템 리소스들을 분리한 후, 서로 다른 네임 스페이스를 도커가 띄운 프로세스에게 제공하는 것이다. 그럼 이 프로세스는 운영체제 위에서 다른 프로세스들과는 격리된 환경으로 구성된 하나의 '패키지' 역할을 하게 되는데, 바로 이를 컨테이너라 한다.

 

컨테이너란?

 컨테이너는 소프트웨어를 배포할 때 필요한 코드, 라이브러리, 환경 설정 파일들을 한데 모아 격리시킨 후, 실행 가능한 패키지로 만든 것을 의미한다. 

반응형

+ Recent posts