728x90

제가 일하고 있는 분야에서 RDBMS 에 보단 NoSql(Elasticsearch) 를 주로 쓰다 보니 공부하는 겸 공유를 하게 되었습니다.

예전부터 궁금했던것들중에 하나가 선착순으로 지급해주는 이벤트성은 DB에서 어떻게 처리할까? 였습니다. 

DB lock에서 어느정도 궁금증을 해소하게 되었습니다. 혹시나 저처럼 이러한 생각을 한 분들은 이미 DB lock에 대해서 공부를 하셨다고 생각이 들지만 그럼에도 글을 공유하게 되었습니다.


DataBase는 데이터를 영속적으로 저장하는 시스템입니다. 즉 같은 자원에 대해서 동시에 접근하는 경우가 생길 수밖에 없습니다.

 

Lock 이란 트랜잭션 처리의 순차성을 보장하는 방법이라고 했습니다. 트랜잭션이란 DB의 나누어지지 않는 최소한의 처리 단위입니다. 

2가지의 lock 종류가 존재하는데 Shared LockExclusive Lock이 있습니다. Shared Lock은 다른 말로 Read Lock이라고 불리며 Exclusive Lock 은 Write Lock이라고 불립니다. 

 

Shared Lock( 공유락) 공유 Lock 은 데이터를 읽을 때 사용되어지는 lock 이다. 
공유 Lock 끼리는 동시에 접근이 가능합니다.
즉, 하나의 데이터를 읽는 것은 여러 사용자가 동시에 할수 있다는 뜻입니다. 하지만 공유 Lock이 설정된 데이터에 베타 Lock을 사용할 수 없습니다.
Exclusive Lock(베타락) 데이터를 변경하고 할때 사용됩니다. 트랜잭션이 완료될 때까지 유지됩니다. 베타락은 Lock이 해제될 때까지 다른 트랜잭션(읽기 포함)은 해당 리소스에 접근할 수 없습니다. 또한 해당 LocK은 다른 트랜잭션이 수행되고 있는 데이터에 대해서는 접근 하여 함께 Lock을 설정할 수 없습니다.

 

간단한 예를 들어, 서비스를 제공하는 있는 회사에서 이벤트 성으로 선착순 100명에게 커피 쿠폰을 뿌린다고 가정했을 때를 생각해 봅시다.

 

동시다발적으로 100명 이상의 사람들이 접속을 하였지만 누군가에겐 당첨 / 꽝이 나올 겁니다. 이러한 이유는 바로 데이터베이스 잠금을 통해서입니다. 즉 DB lock은 동시에 여러 사용자가 데이터를 접근하려고 할 때 데이터 일관성을 유지하기 위해 사용된다. 

 

흐름을 한번 가정해 봅시다.

 - 이벤트 준비 단계

  •   데이터베이스 테이블 설계 :  이벤트 참여자 정보를 저장할 테이블을 설계한다. 예를 들어) 테이블에는 참여자의 이름, 이메일, 발급된 쿠폰 여부 등의 필드가 포함될 수 있습니다.

 - 쿠폰 발급 과정 : 

  • 사전 준비 : 이벤트 시작 전에 DB Lock을 설정하고, 쿠폰 발급을 준비해야 합니다.
  • 참여자의 요청 처리 : 참여자가 이벤트 페이지에 접속하여 쿠폰을 신청하면, DB lock을 걸어 다른 사용자가 동시에 같은 자원에 접근하지 못하도록 합니다.
  • 조건 확인 : DB에서 현재까지 발급된 쿠폰 수를 확인하여, 발급 가능한 쿠폰의 잔여 수가 100개 이내인지 확인합니다.

 - 쿠폰 발급 및 DB lock 해제

  • 쿠폰 발급 : 잔여 쿠폰 수가 100개 이내일 경우, 새로운 참여자에게 쿠폰을 발급하고, DB lock을 해제합니다.
  • 쿠폰 발급 실패 시 처리 : 잔여 쿠폰 수가 100개 이상이라면, 쿠폰 발급을 실패로 처리하고, 참여자에게 해당 사실을 통보한다.

- 동시 접근 관리 

  • 여러 사용자가 동시에 쿠폰을 신청할 때 DB loc을 통해 데이터 일관성을 유지하고, 데드락(deadlock)이나 경합 상태(race condition)를 방지합니다. 

 - 로그와 모니터링 

  • 쿠폰 발급 로그를 기록하고, 이벤트 참여 현황을 모니터링하여, 문제 발생 시 빠르게 대응할 수 있는 시스템을 유지합니다.

   

위 흐름에서 쿠폰 발급 과정 2단계, 즉 이용자가 이벤트 참여를 클릭하고 서버에 요청을 보낼 때 잠금을 수행하는 것이 가장 좋다고 생각을 할 수 있습니다.

 

예제 코드

-- MySQL syntax

-- 트랜잭션 시작
START TRANSACTION;

-- 사용 가능한 쿠폰이 있는지 확인합니다(쿠폰이라는 테이블이 있다고 가정).
SELECT COUNT(*) INTO @remaining_coupons FROM coupons WHERE used = 0;

-- 사용 가능한 쿠폰이 남아 있는지 확인
IF @remaining_coupons < 100 THEN
    -- Rollback the transaction if there are not enough coupons
    ROLLBACK;
    SELECT 'No more coupons available' AS message;
ELSE
    -- 현재 사용자에 대해 사용 가능한 첫 번째 쿠폰을 업데이트합니다(쿠폰_배정이라는 테이블이 있다고 가정).
    UPDATE coupons 
    SET used = 1, user_id = 'user_id_here' -- Replace '해당 이용자 ID' 실제 ID
    WHERE coupon_id = (
        SELECT coupon_id 
        FROM coupons 
        WHERE used = 0 
        ORDER BY coupon_id ASC 
        LIMIT 1
    );

    -- 쿠폰 할당이 성공하면 거래를 커밋합니다.
    COMMIT;
    SELECT 'Coupon assigned successfully' AS message;
END IF;

 

좀 더 쉽게 설명을 해드리자면,

  1. 트랜잭션이 시작 : 데이터 일관성을 유지하기 위해 트랜잭션을 시작한다.
  2. 남은 쿠폰 수 조회 : 사용하지 않은 쿠폰의 수를 조회하여 '@remaining_coupons' 변수에 저장합니다.
  3. 쿠폰 수 체크 : '@remaining_coupons' 변수를 이용하여 남은 쿠폰 수가 100개 미만인지 확인합니다.
  4. 쿠폰 발급 : 남은 쿠폰 수가 100개 이상일 경우, 사용하지 않은 첫 번째 쿠폰을 해당 사용자에게 할당합니다.  'UPDATE' 문을 사용하여 'coupons' 테이블에서 'used' 상태를 '1'로 업데이트하고, 사용자 ID를 저장합니다. 
  5. 트랜잭션 완료('COMMIT;') : 쿠폰 할당이 성공적으로 이루어졌을 때, 트랜잭션을 커밋하여 변경 사항을 영구적으로 반영합니다.
  6. 오류 처리 : 남은 쿠폰이 100개 미만이면 롤백을 수행하여 이전 상태로 복구하고, 쿠폰이 모두 소진되었음을 사용자에게 알린다.

정말 간단한 예시이며, 실제 환경에서는 보안 및 성능을 고려해서 추가적인 작업들이 필요하다고 생각이 듭니다. 

(사용자 인증, 인덱스 설정 등...) DB 종류에 따라 문법이 다를 수 있다는 점 유의 하시면 좋겠습니다.

 

 

728x90
728x90

오늘은 JPA N+1의 문제 해결에 대해서 글을 공유해드리려고 합니다. 

* Java 백엔드 개발자를 위한 데이터베이스 쿼리 최적화에 적합한 내용입니다.

개발자가 직면하는 가장 일반적인 성능 병목 현상 중 하나가 N+1입니다.
애플리케이션이 단 한 번의 쿼리로 동일한 결과를 얻을 수 있는데도 N+1번의 데이터베이스 쿼리를 수행할 때 발생합니다.
과도한 데이터베이스 Hit는 느린 응답 시간, 높은 서버 부하, 열약한 사용자 경험으로 이어질 수 있습니다. 
 
원인을 함께 파악해 보고 개발자가 이러한 문제를 어떻게 완화할지에 대해서 다양한 전략과 기법에 대해서 적어보겠습니다.

앞서, N+1 문제가 무엇인지 알아보도록 하겠습니다.

N+1 문제란 무엇인가?

애플리케이션이 개체목록(예시: 제품, 사용자 또는 게시물 목록)을 가져온 다음 목록의 각 객체애 대해 추가 데이터베이스 쿼리를 수행하여 관련 데이터를 검색할 때 발생을 하게 됩니다.
예를 들어 작성자 정보와 함께 블로그 게시물 목록을 표시하는 Java 애플리케이션을 생각해 보자.
애플리케이션이 글 목록을 가져온 다음 각 글의 작성자 정보를 검색하기 위해 별도의 쿼리를 수행하면 N+1 쿼리가 발생하며, 여기서 N은 글의 수입니다. 이러한 비효율적인 쿼리 패턴은 많은 수의 데이터베이스 HIT로 빠르게 이어질 수 있습니다.
 

N+1 문제 원인

1. 지연 로딩(Lazy Loading) : 
수많은 Java ORM 프레임워크들, 가령 Hibernate에서 기본적으로 지연 로딩을 사용합니다.
지연 로딩이란 관련된 데이터를 가져올때만  데이터베이스에 가져오는 것을 의미합니다. 개발자가  컬렉션의 각 항목에 대한 관련 데이터에 접근할 때 N+1 쿼리를 발생할 수 있습니다.  
 
2.비효율적인 쿼리(Inefficient Queries)
개발자는 관련 데이터를 반복해서 가져오는 코드를 작성하여 최적화된 단일 쿼리로 충분할 수 있는 데이터베이스 쿼리를 여러 번 수행하게 될 수 있습니다.
 
3. 일괄 가져오기 부족 (Lack of Batch Fetching)
일부 ORM 프레임워크에서 제공하는 기술인 일괄 가져오기를 사용하면 단일 쿼리에서 여러 개체에 대한 관련 데이터를 검색할 수 있습니다.
개발자들은 이 기능을 간과하거나 오용하는 경우가 많습니다.
 

N+1 문제를 완화하기 위한 전략들

N+1 문제를 해결하고 과도한 데이터베이스 히트를 최소화하기 위해 Java 백엔드 엔지니어는 다양한 전략과 모범 사례를 활용할 수 있습니다

 

1. 에저 로딩 

에지 로딩을 사용하면 관련 데이터를 미리 불러와 추가 쿼리의 필요성을 줄일 수 있습니다. 대부분의 ORM 프레임워크는 관련 데이터를 로드할 시기와 방법을 지정하는 메커니즘을 제공하므로 데이터베이스 쿼리를 최적화할 수 있습니다.

2. 일괄 가져오기

ORM 프레임워크에서 제공하는 일괄 가져오기 기능을 사용하여 관련 데이터를 하나씩 가져오지 않고 일괄적으로 검색하세요. 이렇게 하면 데이터베이스 쿼리 횟수를 크게 줄일 수 있습니다. 

3. DTO 투영

데이터베이스에서 필요한 데이터만 가져오려면 DTO(데이터 전송 객체) 투영을 사용하는 것이 좋습니다. 이 접근 방식을 사용하면 검색되는 데이터의 양을 최소화하여 쿼리 속도를 높일 수 있습니다.

4. 캐싱

캐싱 메커니즘을 구현하여 자주 액세스하는 데이터를 메모리에 저장하세요. 캐싱은 반복적인 데이터베이스 쿼리의 필요성을 줄여 응답 시간을 개선하고 데이터베이스 부하를 줄이는 데 도움이 될 수 있습니다.

5. 페이지 매김 및 필터링

페이지 매김 및 필터링을 구현하여 단일 쿼리에서 검색되는 레코드 수를 제한하세요. 이는 대규모 데이터 세트를 다룰 때 특히 유용할 수 있습니다.

6. 쿼리 최적화

데이터베이스 쿼리를 정기적으로 검토하고 최적화하세요. 쿼리 실행 계획을 분석하고 데이터베이스 프로파일링 도구를 사용해 성능 병목 현상을 파악하고 해결하세요.
 

마치며

N+1 문제와 과도한 데이터베이스 히트는 Java 백엔드 엔지니어가 직면하는 일반적인 성능 문제입니다. 개발자는 N+1 문제의 원인을 이해하고 일괄 로딩, 배치 가져오기, DTO 예측, 캐싱, 페이지 매김 및 쿼리 최적화와 같은 효과적인 전략을 채택함으로써 애플리케이션의 성능을 크게 향상해 원활한 사용자 경험을 보장하고 서버 부하를 줄일 수 있습니다. N+1 문제를 해결한다는 것은 단순히 데이터베이스 쿼리를 최적화하는 것만이 아니라 사용자에게 효율적이고 확장 가능한 백엔드 솔루션을 제공하는 것입니다.
 

728x90

'DB' 카테고리의 다른 글

Database Lock 이란?  (1) 2024.06.18
PL/SQL 이란  (0) 2022.01.10
데이터베이스 모델링 -1  (0) 2022.01.07
SQL 사용자 권한  (0) 2022.01.05
SQUENCE INDEX(순차 적으로 증가하는 값)  (0) 2022.01.05

+ Recent posts