[백엔드 개발자 면접] DB

diense_kk·2024년 12월 19일
0

Interview

목록 보기
7/8
post-thumbnail

데이터베이스

데이터베이스를 한 마디로 정의하면 "데이터의 집합"이라고 할 수 있다.

데이터베이스의 특징

1) 실시간 접근성 - 실시간 처리에 의한 응답이 가능해야 된다.
2) 지속적인 변화 - 데이터베이스의 상태는 동적이다. 즉, 새로운 데이터의 삽입, 삭제, 갱신으로 항상 최신의 데이터를 유지해야 된다.
3) 동시 공용 - 다수의 사용자가 동시에 같은 내용의 데이터를 이용할 수 있어야 된다.
4) 내용에 의한 참조 - 데이터를 참조할 때 데이터 레코드의 주소나 위치에 의해서가 아닌 사용자가 요구하는 데이터 내용으로 찾는다.

데이터베이스 언어

1) DDL (Data Definition Language) - 데이터베이스 구조를 정의, 수정, 삭제하는 언어 (alter, create, drop)
2) DML (Data Manipulation Language) - 데이터베이스 내의 자료 검색, 삽입, 갱신, 삭제를 위한 언어 (select, insert, update, delete)
3) DCL (Data Control Language) - 데이터에 대해 무결성 유지, 병행 수행 제어, 보호와 관리를 위한 언어 (commit, rollback, grant, revoke)

DBMS

데이터베이스를 데이터의 집합이라고 정의한다면, 데이터베이스를 관리하고 운영하는 소프트웨어를 DBMS라고 한다. 다양한 데이터가 저장되어 있는 데이터베이스는 여러 명의 사용자나 응용 프로그램과 공유하고 동시에 접근이 가능해야 된다.
응용 프로그램들이 데이터베이스에 접근할 수 있는 인터페이스를 제공하고 복구기능과 보안성 기능을 제공한다.

DBMS의 기능

1) 정의 - 데이터에 대한 형식, 구조, 조건들을 정의하는 기능이다. 정의 및 설명은 카탈로그나 사전형태로 저장된다.
2) 저장 - 기억장치에 데이터를 저장하는 기능이다.
3) 보안 - 하드웨어나 소프트웨어의 오류 또는 권한이 없는 접근으로부터 시스템을 보호한다.
4) 공유 - 여러 사용자와 프로그램이 데이터베이스에 접근할 수 있도록 공유한다.
5) 기능 - 데이터의 검색을 위한 질의나 데이터베이스의 갱신, 생성기능을 포함한다.
6) 유지 - 요구사항의 변화에 따라 반영할 수 있도록 하는 기능이다.

DBMS의 종류

Oracle - MySQL, MSSQL보다 대량의 데이터를 처리하기 용이하다, 대기업에서 주로 사용하며, DB 시장 점유율 1위이다.
MySQL - 오픈소스로 무료 프로그램이다(상업적 사용시 비용 발생), 5천만건 이하의 데이터를 다루는데 적합하다.
MariaDB - MySQL과 동일한 소스 코드 기반이다. MySQL과 비교하여 애플리케이션 부분속도가 약 4~5천배 빠르다.
PostgreSQL - 다른 DBMS에 비해 복잡한 쿼리에 탁월하다. 대용량 데이터 관리에 적합하다. NoSQL 및 다양한 데이터 형식을 지원한다.

RDBMS의 특징

1) 데이터의 일관성(Consistency) 보장
RDB는 트랜잭션 ACID원칙을 준수하여 데이터의 일관성을 보장할 수 있다. 이러한 주요 특징은 데이터의 무결성과 안정성을 보장해 준다는 특징을 가지고 있다.

2) 정형화된 데이터 구조를 갖는다.
데이터들이 테이블의 형태로 구조화되어 있기 때문에 데이터의 형식과 구조가 명확하게 정의된다. 이러한 특징을 통해서도 데이터의 일관성을 보장받을 수 있다.

3) 데이터의 보안, 권한 제어
관계형 데이터베이스는 사용자 인증, 엑세스 제어 등 다양한 보안 기능을 제공하고 있으며 이를 통해 관리되고 있는 데이터들의 보안을 유지할 수 있다.

옵티마이저(Optimizer)

SQL을 가장 빠르고 효율적으로 수행할 최적의 처리 경로를 생성해주는 DBMS 내부의 핵심 엔진이다.
컴퓨터의 두뇌가 CPU인 것처럼 DBMS의 두뇌는 옵티마이저라고 할 수 있다.
개발자가 SQL을 실행하면 바로 실행되는 것이 아닌 옵티마이저라는 곳에서 "이 쿼리문을 어떻게 실행시키겠다" 라는 여러가지 실행 계획을 세우고, 최고 효율을 갖는 실행계획을 판별할 후 그 실행계획에 따라 쿼리를 수행하게 되는 것이다.

무결성과 일관성의 차이

무결성은 데이터가 정확하고 유효한 상태를 유지해야 하는 것이고 이에 따라 데이터의 형식, 제약조건(Constraint) 등이 준수되어야 한다.
일관성은 데이터베이스에서 관련된 데이터 간의 상호 연관성을 의미하고 데이터들을 대상으로 이루어지는 모든 작업들에 대해 항상 일관된 상태를 유지해야 된다. 따라서 여러 테이블 간의 관계가 정확해야 된다.

NoSQL

Not Only SQL의 약자로 비관계형 데이ㅐ터베이스를 지칭한다.
기존의 RDBMS와 같은 관계형 데이터 모델을 지양하며 대량의 분산된 비정형 데이터를 저장하고 조회하는데 특화된 데이터베이스로 스키마 없이 사용하거나 느슨한 스키마를 제공하는 저장소이다.

주로 빅데이터, 분산 시스템 환경에서 대용량의 데이터를 처리하는데 적합하다.
즉, 기존의 RDBMS는 일관성(Consistency)와 가용성(Availability)에 중점을 두었다면 NoSQL은 확장성(Scalability)과 가용성(Availability)에 중점을 두고 있는 것이다.

NoSQL의 특징

1) RDBMS와 달리 데이터 간의 관계를 정의하지 않는다.
RDBMS는 데이터 간의 관계를 Foreign Key로 정의하고 Join 연산을 수행할 수 있지만 NoSQL은 Key-Value 형태로 저장되기 때문에 Join 연산이 불가능하다.

2) RDBMS에 비해 대용량의 데이터를 저장할 수 있다.

3) 분산형 구조로 설계되어 있다.
여러 곳의 서버에 데이터를 분산 저장하여 특정 서버에 장애가 발생했을 때도 데이터 유실 혹은 서비스 중지가 발생하지 않도록 한다.

4) 데이터 중복이 발생할 수 있다.
중복된 데이터가 변경될 경우 수정을 모든 컬렉션에서 수행해야된다는 단점이 있다.

RDBMS, NoSQL 선택

RDBMS는 데이터 구조가 명확하고, 변경 될 여지가 없으며 스키마가 중요한 경우 사용하는 것이 좋다. 또한, 중복된 데이터가 없어 변경이 용이하기 때문에 관계를 맺고 있는 데이터가 자주 변경이 이루어지는 시스템에 적합하다.
NoSQL은 정확한 데이터 구조를 알 수 없고 데이터가 변경/확장 될 수 있는 경우 사용하는 것이 좋다. 중복이 허용되어 모든 컬렉션에서 수정해야 되기 때문에 Update가 많이 발생하지 않는 시스템에 좋다.
대용량의 데이터를 저장해야 돼서 DB를 Scale-out 해야 되는 시스템에 적합하다.

MySQL

MySQL은 데이터를 저장하고 관리하는 데 널리 사용되는 오픈 소스 관계형 데이터베이스 관리 시스템(RDBMS)이다.

MySQL은 SQL을 사용하여 데이터를 관리하고 조작한다. 트랜잭션, 보기, 저장 프로시저 및 트리거를 비롯한 다양한 기능을 지원한다.

장단점

장점

1) 무료로 사용할 수 있고 널리 지원되는 오픈 소스 데이터베이스이다.
2) 다른 데이터베이스보다 빠르고 저렴하며 안정적인 고유한 스토리지 엔진 아키텍처가 있다.
3) 뷰, 트리거 및 저장 프로시저를 사용하여 개발자에게 더 높은 생산성을 제공한다.

단점

1) MySQL은 복잡하고 강력할 수 있으므로 소규모 애플리케이션에 적합하지 않을 수 있다.
2) Oracle이 MySQL을 인수한 이후 MySQL의 운명에 대한 우려가 있다.
3) PostgreSQL처럼 기능이 풍부하지 않다.

트리거 (Trigger)

트리거는 특정 테이블에 대한 이벤트에 반응해 Insert, Delete, Update 같은 DML 문이 수행되었을 때, 실행시키고자 하는 추가 쿼리 작업들을 자동으로 수행할 수 있게끔 트리거를 미리 설정해 두는 것이다.

사용자가 직접 호출하는 것이 아닌, 데이터베이스에서 자동적으로 호출한다는 것이 가장 큰 특징이다.

데이터베이스 트리거는 테이블에 대한 이벤트에 반응해 자동으로 실행되는 작업을 의미한다.

Join

Food 테이블

Buy 테이블

Inner Join (내부 조인)

MySQL에서는 Join, Inner Join, Cross Join이 모두 같은 의미로 사용된다.

Inner Join을 하면 조인 관계에 부합되는 레코드를 모두 반환한다.

select * from food f join buy b on f.name = b.name;


위 결과와 같이 두 테이블에서 이름이 같은 2개의 결과만이 조회된다.

Outer Join (외부 조인)

Left Outer Join

A LEFT OUTER JOIN B일 경우, LEFT OUTER JOIN은 왼쪽 테이블(이 경우 Food 테이블)의 모든 데이터를 반환하며, 오른쪽 테이블(이 경우 Buy 테이블)과 일치하는 데이터를 반환합니다. 만약 오른쪽 테이블에 일치하는 데이터가 없다면, 그 오른쪽 테이블의 필드는 NULL로 채워집니다.

select * from food f left outer join buy b on f.name = b.name;

Food의 데이터는 모두 출력이 되었지만 Join 조건을 만족하지 못한 Food 데이터들은 Buy와 조인되지 않은채로 null이 입력된 모습을 볼 수 있다.

Right Outer Join

Left Outer Join과 실행 방식은 비슷하다. 다만 Left가 아닌 Right가 기준이 된다.

Full Outer Join

MySQL에서는 Full Outer Join을 사용하기 위해 Left Join과 Right Join 을 Union하는 형태로 사용한다.

클러스터링과 리플리케이션

클러스터링

여러 개의 DB를 수평적인 구조로 구축하는 방식이다. 동기 방식으로 사용된다.

클러스터링 장점

1) DB간의 데이터를 동기화하여 항상 일관성 있는 데이터를 얻을 수 있다.
2) 1개의 DB가 죽어도 다른 DB가 살아있어 시스템을 장애없이 운영할 수 있다.(높은 가용성)
3) 기존에 하나의 DB서버에 몰리던 부하를 여러 곳으로 분산시킬 수 있다.(로드밸런싱)

클러스터링 단점

1) 저장소 하나를 공유하면 병목현상이 발생할 수 있다.
2) 서버를 동시에 운영하기 위한 비용이 많이 든다.

리플리케이션

두 개 이상의 DBMS를 이용하여 Master/Slave (수직적) 구조를 활용하여 DB의 부하를 분산시키는 기술이다.

리플리케이션은 Master DB에는 Insert, Update, Delete 작업을 수행하도록 하고 Select 작업을 Slave DB에서 하도록 구성을 한다. Master에서 발생한 데이터 변경 작업이 자동으로 Slave로 동기화된다.
Select 작업을 따로 뺴는 이유는 Select 작업이 시간이 많이 걸리기 때문이다.

리플리케이션 장점

1) DB 요청의 60%~80% 정도가 읽기 작업이기 때문에 리플리케이션만으로도 충분히 성능을 높일 수 있다.
2) 비동기 방식으로 운영되어 지연시간이 거의 없다.

리플리케이션 단점

1) 노드들 간 데이터 동기화가 보장되지 않아 일관성 있는 데이터를 얻지 못할 수 있다.
2) Master DB가 다운되면 복구 및 대처가 까다롭다.

데이터베이스 트랜잭션

트랜잭션의 정의

트랜잭션이란 데이터베이스 내부에서 수행되는 일련의 작업들을 하나의 논리적인 단위로 묶은 것을 의미한다.

돈을 송금하는 상황이라고 가정해보자. 이때 송금이라는 작업에 대한 트랜잭션이 시작된다. A 사용자의 계좌에서 100원을 뺴서 B 사용자 계좌에 100원을 송금해야 하는 상황이다. 이러한 전체적인 작업의 논리적 단위를 트랜잭션이라고 한다.

이 과정에서 내부 작업이 하나라도 실패하게 되면 롤백이 발생되어 트랜잭션 작업 전으로 돌아갈 수 있어야 되고, 트랜잭션이 성공하면 커밋이 수행되어야 할 것이다.
트랜잭션은 데이터베이스의 일관성을 유지하는 데 큰 역할을 한다.

트랜잭션의 4대 원칙 ACID

RDBMS는 시스템이 안정적이고 신뢰할 수 있는 트랜잭션 처리를 보장해 주어 데이터의 무결성과 일관성을 유지할 수 있게 도와준다.

1) 트랜잭션의 원자성 (Atomicity of Transaction)
트랜잭션 내부에서 수행된 모든 연산은 성공적으로 완료되거나 중간에 문제가 있어서 실패한다면 어떠한 연산도 수행되지 않은 상태로 되돌아 갈 수 있어야 된다.

2) 트랜잭션의 일관성 (Consistency of Transaction)
트랜잭션이 수행된 이후에도 데이터베이스는 항상 일관된 상태를 유지해야 하는 것. 즉, 트랜잭션이 시작하기 전과 끝난 이후에도 데이터베이스는 유효한 규칙을 따라야 된다.

3) 트랜잭션의 격리성 (Isolation of Transaction)
여러 트랜잭션이 동시에 수행될 때 각각의 트랜잭션이 서로에게 영향을 주지 않고 독립적으로 실행되어야 함을 의미한다. 트랜잭션은 다른 트랜잭션의 수행에 있어 간섭하지 않아야 된다.

4) 트랜잭션의 지속성(Durability of Transaction)
트랜잭션이 성공적으로 수행된 후 결과값이 영구적으로 저장되는 것을 보장하는 성질. 시스템에 문제가 발생하더라도 트랜잭션의 결과는 손실되지 않아야 된다.

정규화

정규화의 기본 목표는 테이블 간에 중복된 데이터를 허용하지 않는 것이다.
중복된 데이터를 허용하지 않음으로써 무결성을 유지할 수 있으며, DB의 저장 용량 역시 줄일 수 있다.

이상 현상

이상 현상은 테이블을 설계할 때 잘못 설계하여 데이터를 삽입, 삭제, 수정할 때 생기는 논리적 오류를 말한다.

1) 삽입 이상 - 자료를 삽입할 때 특정 속성에 해당하는 값이 없어 Null을 입력해야 하는 현상
2) 갱신 이상 - 중복된 데이터 중 일부만 수정되어 데이터 모순이 일어나는 현상
3) 삭제 이상 - 어떤 정보를 삭제하면, 의도하지 않은 다른 정보까지 삭제되어버리는 현상

이러한 이상 현상을 예방하고 효과적인 연산을 하기 위해 데이터 정규화를 한다.

제1 정규화

제1 정규화란 테이블의 컬럼이 원자값(하나의 값)을 갖도록 테이블을 분해하는 것이다.

위 테이블에서는 추신수와 박세리가 여러 개의 취미를 가지고 있기 때문에 1정규형을 만족하지 못하고 있다.


제1 정규화를 진행한 테이블은 위와 같다

제2 정규화

제1 정규화를 진행한 테이블에 대해 완전 함수 종속을 만족하도록 테이블을 분해하는 것이다.
완전 함수 종속이란 기본키의 부분집합이 결정자가 되어선 안된다는 것을 의미한다.


위 테이블에서 기본기는 학생번호, 강좌이름으로 복합키이다. 그리고 학생번호, 강좌이름인 기본키는 성적을 결정하고 있다. (학생번호, 강좌이름) -> (성적)
하지만 여기서 강의실이라는 컬럼은 기본키의 부분집합인 강좌이름에 의해 결정될 수 있다. (강좌이름) -> (강의실)


제2 정규화를 진행하여 위와 같이 기존의 테이블에서 강의실을 분해하여 별도의 테이블로 관리한다.

제3 정규화

제2 정규화를 진행한 테이블에 대해 이행적 종속을 없애도록 테이블을 분해하는 것이다.
이행적 종속이란 A -> B, B -> C가 성립될 때 A -> C가 성립되는 것을 의미한다.

위 테이블에서 학생번호는 강좌이름을 결정하고 있고, 강좌이름은 수강료를 결정하고 있다. 그렇기 때문에 위 테이블을 (학생번호, 강좌이름) 테이블과 (강좌이름, 수강료) 테이블로 분해해야 된다.

만약 위 테이블에서 501번 학생이 수강하는 강좌이름을 스포츠경영학으로 수정할 경우 수강료 또한 같이 변경해줘야 하는 번거로움이 생긴다.


위와 같이 테이블을 분해하면 강좌이름만 수정하면 된다.

BCNF 정규화

제3 정규화를 진행한 테이블에 대해 모든 결정자가 후보키가 되도록 테이블을 분해하는 것이다.

위 테이블에서 기본키는 (학생번호, 특강이름)이다. 그리고 기본키는 교수를 결정하고 있다. 또한 여기서 교수는 특강이름을 결정하고 있다.
여기에서 문제는 교수가 특강이름을 결정하는 결정자이지만, 후보키가 아니라는 것이다.


위와 같이 테이블을 분해하면 된다.

정규화의 장점과 단점

장점

1) 데이터베이스 변경 시 이상현상이 발생하는 문제점을 해결할 수 있다.
2) 데이터베이스 구조 확장 시 정규화된 데이터베이스는 그 구조를 변경하지 않아도 되거나 일부만 변경해도 된다.

단점

1) 릴레이션의 분해로 인해 릴레이션 간의 연산(Join)이 많아진다. 이로인해 질의에 대한 응답 시간이 느려질 수 있다.

역정규화란?

정규화를 거치면 릴레이션 간의 연산(Join)이 많아지는데, 이로인해 성능이 저하될 우려가 있다.
역정규화를 하는 가장 큰 이유는 성능 문제가 있는(읽기작업이 많이 필요한) DB의 전반적인 성능을 향상시키기 위함이다.

인덱스

인덱스란 추가적인 쓰기 작업과 저장 공간을 활용하여 데이터베이스 테이블의 검색 속도를 향상시키기 위한 자료구조이다.
인덱스는 특정 조건을 만족하는 데이터를 빠르게 조회하기 위해, 빠르게 정렬하거나 그룹핑하기 위해 사용한다.

데이터베이스에서 테이블의 모든 데이터를 검색하면 시간이 오래 걸리기 때문에 데이터와 데이터의 위치를 포함한 자료구조를 생성하여 빠르게 조회할 수 있도록 돕고있다.

인덱스를 활용함변, 데이터를 조회하는 SELECT 외에도 UPDATE, DELETE의 성능이 함께 향산된다.
이유는 UPDATE, DELETE를 실행하기 위해서는 그 대상을 조회해야만 작업을 할 수 있기 때문이다.

만약 인덱스를 사용하지 않은 컬럼을 조회해야 하는 상황이라면 전체를 탐색하는 Full Scan을 수행해야 된다. Full Scan은 전체를 비교하여 탐색하기 때문에 처리 속도가 떨어진다.

인덱스 관리

DBMS는 인덱스를 항상 최신의 정렬된 상태로 유지해야 원하는 값을 빠르게 탐색할 수 있다.
그렇기 때문에 SELECT를 제외한 INSERT, UPDATE, DELETE가 수행된다면 각각 다음과 같은 연산을 추가적으로 해주어야 하며 그에 따른 오버헤드가 발생한다.
1) INSERT - 새로운 데이터에 대한 인덱스를 추가함
2) DELETE - 삭제하는 데이터의 인덱스를 사용하지 않는다는 작업을 진행함
3) UPDATE - 기존의 인덱스를 사용하지 않음 처리하고, 갱신된 데이터에 대한 인덱스를 추가함

B+Tree 인덱스 자료구조

자식 노드가 2개 이상인 B-Tree를 개선시킨 자료구조이다.
해시 테이블보다 나쁜 O(log2N)의 시간복잡도를 갖지만 일반적으로 사용되는 자료구조이다.

해시 테이블

컬럼의 값으로 생성된 해시를 기반으로 인덱스를 구현한다.
시간 복잡도가 O(1)이라 검색이 매우 빠르다.
부등호와 같은 연속적인 데이터를 위한 순차 검색이 불가능하기 때문에 사용에 적합하지 않다.

장점과 단점

장점

1) 테이블을 조회하는 속도와 그에 따른 성능을 향상시킬 수 있다.
2) 전반적인 시스템의 부하를 줄일 수 있다.

단점

1) 인덱스를 관리하기 위해 DB의 약 10%에 해당하는 저장공간이 필요하다.
2) 인덱스를 관리하기 위해 추가 작업이 필요하다.
3) 인덱스를 잘못 사용할 경우 오히려 성능이 저하되는 역효과가 발생할 수 있다.

만약 INSERT, DELETE, UPDATE가 빈번한 속성에 인덱스를 걸게 되면 인덱스의 크기가 비대해져서 성능이 오히려 저하되는 역효과가 발생할 수 있다. 그 이유는 UPDATE, DELETE 연산은 기존의 인덱스를 삭제하는 것이 아닌, 사용하지 않음 처리를 하는 것이기 때문이다.

인덱스를 사용하면 좋은 경우

1) 규모가 작지 않은 테이블
2) INSERT, UPDATE, DELETE가 자주 발생하지 않는 컬럼
3) JOIN이나 WHERE 또는 ORDER BY에 자주 사용되는 컬럼
4) 데이터의 중복도가 낮은 컬럼 (인덱스는 내부적으로 Key Value의 트리 형태로 데이터를 저장하는데, 데이터(Key)가 중복되어 여러개 존재하면 검색되는 대상이 증가하기 떄문이다.)

인덱스를 사용할 떄 주의할 점

1) 데이터 변경 작업이 얼마나 자주 일어나는지 고려
2) 단일 테이블에 인덱스가 많으면 속도가 느려질 수 있다.(테이블당 4~5개 권장)
3) 검색할 데이터가 전체 데이터의 20% 이상이라면, MySQL에서 인덱스를 사용하지 않음. (강제로 사용할 시 성능 저하를 초래할 수 있음)
전체 페이지의 대부분을 읽어야 하고, 인덱스 관련 페이지도 읽어야 해서 작업량이 크기 때문이다.
검색할 데이터가 전체 데이터의 20% 이상이라는 말은 테이블에 100개의 레코드가 있고, 특정 조건으로 쿼리를 실행했을 때 결과가 20개가 넘는 것을 말하는 것이다.
4) 사용하지 않는 인덱스는 제거하는 것이 바람직함

동시성

동시성 이슈는 멀티슬레드 환경에서 발생하는 문제로, 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 말한다. 이는 데이터의 일관성을 해칠 수 있다.

특정 테이블의 속성 값에 +1 하는 로직을 멀티 스레드를 사용하여 100번 실행시키면 값이 몇이 될까?

	@Transactional
    public void plusOne() {
        Stock stock = stockRepository.findById(1L).get();
        stock.plusOne();
    }

JMeter를 사용하여 5000번의 요청을 보내보았다. 이 결과로 stock의 num은 5000이 되었을까?

예상과는 다르게 num 값은 1758로 5000이 나오지 않았다.

해결 방법

1) synchronized

	public synchronized void plusOne(){
        this.num++;
    }

자바에서의 동시성 해결 방법이다.
결론부터 말하자면 synchronized는 정답이 아니다.

plusOne 메서드에 synchronized를 붙여도 결과는 이전과 비슷하다.
그 이유는 synchronized 키워드는 메서드 수준에서만 동기화를 보장하며, 데이터베이스 레벨에서의 동시성 제어를 하지 않는다.
데이터에 동시에 하나의 스레드만 접근이 가능하다는 조건은 하나의 프로세스에서만 보장된다.

2) 비관적 락(Pessimistic Lock)

	@Lock(LockModeType.PESSIMISTIC_WRITE) // 읽기 쓰기 잠금
    @Query("SELECT s FROM Stock s WHERE s.id = :stockId")
    Optional<Stock> findByIdForUpdate(@Param("stockId") Long stockId);

Lock 어노테이션을 사용하여 비관적 락을 설정했다.
위와 같이 설정하면, Select ... FOR UPDATE 쿼리가 실행되어 다른 트랜잭션이 해당 데이터를 수정하거나 읽을 수 없도록 읽기 쓰기 잠금을 설정한다.

FOR UPDATE 키워드가 사용되면, 트랜잭션이 해당 데이터를 읽는 순간, 데이터에 잠금이 걸려서 읽거나 수정할 수 없게 된다.
다른 트랜잭션이 동일한 데이터를 읽으려고 시도하면 잠금을 해제할 때까지 대기한다.
잠금은 트랜잭션 범위 내에서만 유효하며, 트랜잭션이 커밋되거나 롤백되면 해제된다.

단점으로는, 트랜잭션을 완전히 기다리기 때문에 대기 시간이 길어지고 높은 트래픽 환경에서는 성능 저하를 초래할 가능성이 높은 편이다.

비관적 락은 데이터 정합성(여러 데이터 간에 일관성이 유지되는 상태)이 매우 중요하거나, 충돌 가능성이 높은 경우에 적합하다.

3) 낙관적 락(Optimistic Lock)

낙관적 락은 동시성 충돌을 허용하지만, 충돌이 발생하면 이를 감지하고 처리하는 방식이다.
일반적으로 @Version 어노테이션을 사용해 구현하며, 데이터를 수정할 때 버전 정보를 기반으로 변경 충돌을 감지한다.

	@Version
    private Long version;

낙관적 락을 위한 버전 필드를 추가한다.

	@Lock(LockModeType.OPTIMISTIC)
    @Query("SELECT s FROM Stock s WHERE s.id = :stockId")
    Optional<Stock> findByIdForUpdate(@Param("stockId") Long stockId);

이후 Lock 어노테이션의 LockModeType을 OPTIMISTIC으로 수정한다.

Hibernate: 
	/* SELECT s FROM Stock s WHERE s.id = :stockId */ 
    select s1_0.id,s1_0.num,s1_0.version from stock s1_0 where s1_0.id=?
Hibernate: 
	/* update for com.example.testserver.Domain.Stock */
    update stock set num=?,version=? where id=? and version=?

낙관적 락은 트랜잭션이 시작될 때 잠금을 걸지 않고, 트랜잭션이 커밋될 때 버전 정보를 비교하여 충돌 여부를 확인한다. 만약 버정 정보가 일치한다면 버전 값 증가 후 커밋하고, 그렇지 않은 경우 충돌이 발생한 것으로 간주하고 예외가 발생한다.

@Version 어노테이션을 추가한 필드인 version은 트랜잭션이 커밋될 때 자동으로 검증되고, 충돌이 발생하면 ObjectOptimisticLockingFailureException이 발생한다.

	@Transactional
    public void plusOne() {
        int retry = 0;
        while (retry < 3) {
            try {
                Stock stock = stockRepository.findByIdForUpdate(1L).orElseThrow(() -> new RuntimeException("Stock not found"));
                stock.plusOne();
                return; // 성공 시 종료
            } catch (ObjectOptimisticLockingFailureException e) {
                retry++;
                if (retry >= 3) {
                    log.warn("실패");
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

ObjectOptimisticLockingFailureException가 발생할 경우, 그냥 예외를 던지지 않고 재시도 횟수를 정해두고 재시도 해보는 로직을 구현할 수 있다.
여기에서 재시도 횟수를 과도하게 설정하면 좋지 않을 것같다.

낙관적 락이 비관적 락보다 잠금 시간이 짧기 떄문에 성능이 더 나을 수 있다. 하지만, 재시도 로직으로 인해 더 많은 시간이 걸릴 수 도 있을 것이다.

낙관적 락은 데이터 충돌이 자주 일어나지 않을 것으로 예상되고, 조회 성능이 중요한 경우에는 괜찮은 방법일 것이라 생각한다.

4) 수정쿼리 작성

	//Repository
	@Modifying
    @Query("UPDATE Stock s SET s.num = s.num + 1 WHERE s.id = :stockId")
    void incrementNum(@Param("stockId") Long stockId);
    
    //Service
    @Transactional
    public void plusForQuery(){
        stockRepository.findById(1L).orElseThrow(() -> new RuntimeException("Stock not found"));

        stockRepository.incrementNum(1L);
    }

기존 코드는 애플리케이션 수준에서 값을 읽고 증가시키는 것이였다. 사용자 두명이 동시에 ID가 1인 Stock을 조회하여 +1을 하는 메서드를 실행했다면 그 값은 1이였을 것이다.
하지만, 이 방법은 데이터베이스 수준에서 값을 증가시킨다. 두명의 사용자가 동시에 ID가 1인 Stock에 +1을 하면 값이 2가 된다. 그 이유는 데이터베이스는 트랜잭션 직렬화 매커니즘을 활용하여 동시 접근 시 순차적 처리를 보장하기 때문이다.

  • 트랜잭션 직렬화 매커니즘 - 여러 트랜잭션이 동시에 실행될 때 서로 간섭이나 충돌을 방지하는 방법이다.

SQL Injection이란

공격자가 악의적인 의도를 갖는 SQL 구문을 삽입하여 데이터베이스를 비정상적으로 조작하는 코드 인젝션 공격 기법이다.

' OR '1' = '1 같은 형태로 값을 넣는 것이다.

SELECT user FROM user_table WHERE id='admin' AND password=' ' OR '1' = '1';

0개의 댓글