포스트

성능을 좌우하는 DB 설계와 쿼리 [주니어 백엔드 실무지식]

성능을 좌우하는 DB 설계와 쿼리 [주니어 백엔드 실무지식]

성능을 좌우하는 DB 설계와 쿼리 🚀

서버 성능 문제의 가장 큰 주범은 역시 데이터베이스(DB)인 경우가 많다. 특히 사용자가 늘어나고 데이터가 엄청나게 쌓이면서 DB의 CPU 사용률이 90%에 육박하고 쿼리 실행 시간이 하염없이 길어지는 현상이 나타난다면, 우리 서비스의 DB 성능이 심각하게 저하되었다는 신호로 봐야 한다.

성능과 DB

이런 문제는 주로 비효율적인 쿼리나 데이터 증가로 인한 불필요한 풀 스캔에서 발생하며, 결국 전체 서비스 응답 시간을 느리게 만들 뿐만 아니라, 심지어 DB에 부하가 생겨 DB 연결이 느려지면 모든 서버의 응답 시간까지 덩달아 느려져 클라이언트에도 악영향을 미치게 된다.

근데 DB 자체의 문제보다는 DB를 잘 못 사용해서 벌어지는 일이 대부분이다

조회 트래픽을 고려한 인덱스 설계

느린 조회 속도는 성능 문제의 핵심 원인 중 하나다. 이 문제를 해결하기 위한 가장 강력한 무기 중 하나가 바로 인덱스 설계다.

일반적인 시스템에서는 조회 기능의 실행 비율이 가장 높고, 이 조회 성능을 증가시켜줄 수 있는게 바로 인덱스이다.

Article 테이블 구조

위와 같이 Article 테이블이 있고, 우리가 카테고리 별로 게시글을 보여주기 위해 category 칼럼을 정의했다.

카테고리별로 분류하는 기능을 구현한 쿼리는 아래와 같이 조건절을 써서 비교해야 한다.

카테고리별 조회 쿼리

여기서 이제 인덱스가 없다면? 만약 Article이 엄청 적고 보는 사람도 적으면 크게 문제될건 없다. (풀스캔을 해도 얼마 안걸리니까)

근데 만약 등록된 Article이 1000만개고 많은 사용자가 동시에 조회를 한다? → 당연히 엄청 느릴거고 심하면 DB 서버의 CPU 사용률이 터지면서 DB가 맛탱이가 갈거다.

인덱스 사용시 유의사항

잘못된 인덱스 사용

이딴식으로 쿼리 날리면 인덱스 효과를 못 누린다. 어떤 바보들이 인덱스로 성능 올렸다고 하고 %를 양쪽에 붙여서 검색하던데 이러면 되겠냐?

만약 이런 식으로 쿼리 날리고 인덱스로 성능개선 했다는게 들키는 순간 “아 얘는 인덱스 구조(B+Tree 등)를 아예 모르는구나” 한다.

인덱스의 필요성 정리

  • 풀 스캔 방지: 데이터 양이 많아지면 WHERE 절 조건에 맞는 데이터를 찾기 위해 DB가 전체 데이터를 싹 다 스캔하는 ‘풀 스캔(Full Scan)‘이 발생한다. 풀 스캔은 데이터가 많아질수록 쿼리 실행 시간을 기하급수적으로 늘려 DB 부하를 키우고 서비스 응답 시간을 망가뜨린다. 인덱스는 이 풀 스캔을 막아 조회 성능을 비약적으로 향상시켜 준다.

  • 예시: 게시판에서 특정 category의 게시글 목록을 조회할 때, category 컬럼에 인덱스가 없다면 수많은 게시글 데이터를 모두 뒤져야 하므로 조회 시간이 끔찍하게 길어진다. 또한, 본인이 작성한 글 목록을 조회할 때 writerId 컬럼에 인덱스가 없다면 역시 풀 스캔이 발생한다.

단일 인덱스와 복합 인덱스

  • 단일 인덱스: 하나의 컬럼에만 인덱스를 생성하는 방식이다. 예를 들어, userId와 같이 특정 컬럼만으로 검색하는 경우에 효과적이다.

  • 복합 인덱스: 여러 컬럼을 조합하여 인덱스를 생성하는 방식이다. 예를 들어, userIdactivityDate를 함께 검색해야 하는 경우 (userId, activityDate)와 같이 복합 인덱스를 사용하면 조회 성능을 크게 높일 수 있다.

    • 주의사항: 복합 인덱스에 어떤 컬럼을 포함할지는 해당 쿼리의 빈도실행 시간을 충분히 고려하여 결정해야 한다. 만약 10만 명의 회원이 하루 평균 50건의 활동을 기록하여 1,500만 건의 데이터가 쌓여도, userId 단일 인덱스만으로 성능 문제가 발생하지 않을 수도 있다.

복합 인덱스 예시

위와 같은 쿼리를 보면 지금 풀 스캔하지 않으려면 activityDate 칼럼을 인덱스로 사용 해야한다. 여기서 이제 고민해야하는게 activityType 칼럼을 인덱스에 포함시키냐 마냐다.

activityDate 만 인덱스가 존재하면 해당 일자에 해당 하는 모든 데이터를 검색해야한다. 근데 이제 하루 평균 데이터가 50만개씩 쌓이면 이것만 해도 양이 많아서 실행시간이 꽤 걸릴 수 있다.

근데 이제 이 쿼리를 하루에 한번만 실행해서 그 결과를 별도의 테이블에 저장한다? → 그러면 쿼리 실행 시간이 길게 걸려도 별 문제가 안된다. 어차피 하루에 한번만 하니까

어쨌든 서비스가 급격하게 성장해서 데이터가 하루에 데이터가 수백만 개씩 쌓이면 DB 성능에 따라 쿼리 실행 시간이 급격하게 증가할 수 있기 때문에 이럴 때는 activityDate, activityType 을 포함한 복합 인덱스 적용이 필요하고, 이렇게 하면 인덱스에만 있는 걸로 조회가 되니까(인덱스 설정한거랑 select에 포함된 컬럼이 일치) 커버링 인덱스를 통해 실제 데이터를 읽지 않아서 성능이 되게 좋아진다.

선택도(Selectivity)를 고려한 인덱스 선택

  • 선택도: 인덱스에 특정 컬럼의 고유한 값의 비율을 나타내는 지표다.
    • 선택도 = 카디널리티 / 전체 레코드 수
    • 즉 선택도가 높을 수록? 인덱스 성능이 좋다.
    • 당연히 카디널리티가 높아야? 선택도가 높다
    • 즉 고유값이 많아야 인덱스 성능이 좋아지는거고, 인덱스가 어쨌든 정렬이 된걸로 logN으로 찾는거기 때문에 고유해야 그걸로 정렬해서 빨리 찾을 수 있다.
  • 예시: gender 컬럼처럼 고유한 값이 적은(M, F, N) 컬럼은 선택도가 낮아 인덱스 효율이 떨어질 수 있다. 하지만 jobqueue 테이블의 status 컬럼처럼 고유한 값이 적더라도(W, P, C), 특정 값(W)을 기준으로 조회하는 쿼리가 압도적으로 많고 이로 인해 풀 스캔이 자주 발생한다면, 인덱스를 통해 처리해야 할 데이터 수를 크게 줄여 성능 개선에 도움이 될 수 있다.

커버링 인덱스(Covering Index) 활용하기

  • 개념: 쿼리에 필요한 모든 컬럼을 포함하는 인덱스를 의미한다.

  • 장점: 커버링 인덱스를 사용하면 DB가 실제 데이터가 저장된 테이블에 접근하여 데이터를 읽지 않고, 인덱스만으로 쿼리 결과를 반환할 수 있다. 이로 인해 쿼리 실행 시간이 획기적으로 단축된다.

  • 예시: activityLog 테이블에서 activityDateactivityType을 조회하는데, 이 두 컬럼이 모두 인덱스에 포함되어 있다면 실제 데이터를 읽는 과정이 생략되어 쿼리가 훨씬 빨라진다.

인덱스는 필요한 만큼만 만들기

인덱스 과다 생성 예시

이제 첫번째 쿼리는 userld 칼럼과 activityDate 칼럼을 이용해서 검색하고, 두번째 쿼리는 여기에 activityType 칼럼을 추가로 이용해 검색한다. 이 두 쿼리를 빠르게 실행하기 위해 아래와 같이 인덱스 설정을 했다고 가정하자.

  • (userId, activityDate)
  • (userid, activityDate, activityType)

이게 이제 오 그러면 인덱스 설정이 이렇게 되어있으면 두번째 쿼리까지 인덱스로 다 되니까 좋은데? 이렇게 생각할 수 있다. 근데 이건 한 사용자가 하루에 만들어 내는 데이터 개수가 조회 성능에 영향을 줄 만큼 많아야 한다.

하루에 수백개 만들어내는 그런건 인덱스 효과를 체감하기 힘들고, 오히려 추가, 변경, 삭제 시에는 인덱스가 있으면 그거까지 업데이트를 하느라 추가적인 시간이 더 들어가서 안 좋아질 수 있다.

정리 : 과도한 인덱스 생성 지양

인덱스는 만능이 아니다. 데이터 추가, 변경, 삭제 시 인덱스도 함께 업데이트되어야 하므로 추가적인 관리 비용(시간, 자원)이 발생한다. 인덱스가 많아질수록 메모리와 디스크 사용량도 증가한다. 따라서 불필요하게 많은 인덱스를 만드는 것은 오히려 성능을 저하시킬 수 있다.

그래서 새로 추가하는 쿼리가 기존 인덱스를 사용하지 않을 때는 요구사항을 일부 변경해서 해결 가능한지 검토해보는게 좋다. 작은 변경으로도 인덱스를 활용할 수 있으니까.

예를 들어, 아래 예약 테이블을 보자

예약 테이블

예약자 이름으로 조회를 하는 경우에는 아래와 같이 name 칼럼을 비교할 수 있다.

예약자 이름 조회

근데 이제 이 테이블이 name 칼럼을 인덱스로 갖고 있지 않아 위 쿼리로는 풀 스캔이 발생한다. 그럼 이제 풀 스캔 방지를 위해 name 칼럼을 인덱스로 추가할 수도 있지만, 요구사항 변경으로 인덱스 추가를 방지할 수 있다.

특정일자에 예약한 예약자 이름으로 조회

이렇게 요구사항을 바꾸고 바꾼 것에 대한 쿼리를 보자

개선된 예약자 이름 조회

이 쿼리는 reserveDate 칼럼을 포함하고 있는 idxRate 인덱스를 사용을 한다. 그래서 특정 일자에 속한 데이터만 비교를 하니까 인덱스 추가 없이도 풀 스캔 없이 예약자 이름으로 데이터 조회가 가능하다

정리 : 기존 인덱스 활용: 새로운 인덱스를 무작정 추가하기 전에, 기존 인덱스를 재활용하거나 변경하여 사용할 수 있는지 충분히 검토해야 한다. 예를 들어, reservation 테이블에서 name으로 예약 정보를 조회하는 기능이 필요할 때, 기존에 reserveDate에 인덱스가 있다면 reserveDatename으로 함께 조회하도록 쿼리를 변경하는 방안을 고려해볼 수 있다.

몇 가지 조회 성능 개선 방법

효율적인 DB 설계를 넘어, 쿼리 자체를 최적화하고 인프라를 활용해 조회 성능을 높이는 실무적인 방법들이 있다.

미리 집계하기

다음 기능을 제공하는 간단한 설문조사 기능을 만든다고하자.

  • 각 설문은 질문이 4개로 고정되어있다.
  • 회원은 각 설문 조사마다 좋아요를 할 수 있다.
  • 설문 조사 목록을 보여줄 때 답변 수와 좋아요 수를 표시한다.

설문조사 테이블 구조

  • 문제점: 게시글 목록에서 답변 수나 ‘좋아요’ 수를 실시간으로 COUNT(*)와 같은 복잡한 쿼리로 계산하면 심각한 성능 저하를 유발한다. 데이터가 많아질수록 쿼리 실행 시간은 기하급수적으로 늘어난다.

    실시간 집계 쿼리

    위와 같은 쿼리를 날린다고 했을 때, 실제로는 논리적으로 몇번의 쿼리가 실행될까?

    • 목록조회1번
    • 답변자 수를 세는 쿼리 30번: select 쿼리가 30개의 설문 데이터를 조회하므로 각 설문마다 답변자 수를구하기 위한 서브쿼리가 30번 실행된다. 각 쿼리는 10만개를 센다.
    • 좋아요 수를 세는 쿼리 30번: 각 설문마다 좋아요를 한 회원수를 구하기 위한 서브쿼리가30번실행된다. 각쿼리는1만개를센다.

    쿼리시간 0.01(게시글 목록 조회 시간)

    • 0.1(답변자 수를 세는 쿼리 시간) * 30
    • 0.05(좋아요 수를 세는 쿼리 시간) * 30 = 4.51

    이게 사용자가 많은 온라인 서비스에서는 느린 시간이고 트래픽이 몰리면 조회 성능이 급격히 느려지기 때문에, 이따구로 쿼리가 날라가면 아주 클난다.

  • 해결책: answerCnt, likedCnt와 같은 집계 값을 테이블에 컬럼으로 추가하고, 데이터 변경 시(예: 답변 추가/삭제, 좋아요 누름/취소)마다 해당 값을 업데이트하는 방식으로 미리 계산해둔다. 이는 쿼리를 단순화하고 조회 속도를 크게 높여준다.

  • 참고: 이 방식은 데이터를 중복해서 저장하는 비정규화에 해당하며, 데이터 일관성 유지를 위해 트랜잭션 처리에 각별히 주의해야 한다.

미리 집계된 테이블

페이지 기록 목록 조회 시 ID 방식 활용

  • 문제점: 대용량 테이블에서 LIMIT offset, count 방식의 페이지네이션은 offset 값이 커질수록 DB가 앞선 데이터를 모두 스캔해야 하므로 성능이 급격히 저하된다. 10만 건의 게시글에서 1만 번째 페이지를 조회하려면 99,991번째 ID부터 10개의 데이터를 찾기 위해 DB가 99,990개의 데이터를 건너뛰고 10개를 조회해야 하는 비효율이 발생한다.

  • 해결책: 마지막으로 조회된 데이터의 ID를 기준으로 다음 페이지를 조회하는 방식(where id < [마지막 ID] order by id desc limit [갯수])을 사용한다. ID는 기본 키(PK)이므로 DB가 빠르게 해당 ID를 찾아 효율적으로 다음 데이터를 조회할 수 있다. 이는 “스크롤하여 더보기”와 같은 모바일 환경에서 특히 효율적이다.

  • 프런트엔드 연동: 프런트엔드 개발자에게 다음 데이터 존재 여부(hasMore)를 함께 응답하여 불필요한 조회 요청을 줄일 수 있다.

조회 범위를 시간 기준으로 제한하기

  • 원칙: 대부분의 서비스는 최신 데이터를 주로 조회하므로, 쿼리 범위를 최근 일정 기간(예: 3개월, 6개월)으로 제한하면 데이터 양을 줄여 조회 성능을 개선할 수 있다. 쿼리의 범위를 좁히면 인덱스를 효율적으로 사용하거나, 아예 인덱스 없이도 성능을 유지할 수 있다.

시간 범위 제한

  • 예시: 10년 치의 과거 데이터를 한 번에 조회하는 대신, 3개월 치 조회만 가능하도록 제한하거나, 파일을 쪼개서 다운로드하도록 유도하는 방식이다.

서비스 정책 제한

그래서 이런건 서비스 정책적인 제한사항을 두면 강제적으로 쿼리 범위를 제한할 수 있어 많이 사용한다.

전체 개수 세지 않기 (COUNT(*))

  • 문제점: COUNT(*) 쿼리는 조건에 맞는 모든 데이터를 탐색해야 하므로 데이터가 많아질수록 실행 시간이 기하급수적으로 길어진다. 커버링 인덱스를 사용하더라도 인덱스 내 모든 데이터를 스캔해야 하는 경우가 많아 느려질 수 있다.

  • 해결책: 정확한 전체 개수가 반드시 필요한 경우가 아니라면, 미리 집계된 값을 활용하거나, 페이지별로 일부만 표시하는 등 COUNT(*) 사용을 가급적 지양해야 한다.

  • Tip: 페이지네이션을 구현할 때 Page → Slice를 써야한다. Spring Data JPA - Page vs Slice

오래된 데이터 삭제 및 분리 보관하기

  • 문제점: 데이터가 계속 쌓이면 쌓일수록 쿼리 실행 시간은 증가하고, 서비스 품질은 저하된다.

  • 해결책: 일정 기간이 지난 오래된 데이터(예: 180일이 지난 로그인 기록)는 삭제하거나 별도의 아카이브 시스템으로 분리하여 보관함으로써 전체 데이터 수를 일정하게 유지한다. 이는 서비스 품질을 일관되게 유지하는 데 큰 도움이 된다.

  • 참고: DELETE 쿼리만으로는 디스크 용량이 줄어들지 않으므로 주기적인 단편화 제거(최적화) 작업(OPTIMIZE TABLE 등)이 필요하다. 단편화된 데이터는 디스크 I/O를 증가시켜 쿼리 성능을 저하시키고, 실제 데이터보다 더 많은 디스크 공간을 사용하게 된다.

DB 장비 확장

  • 수직적 확장: 급한 성능 문제를 해결하기 위해 CPU, 메모리, SSD 등 서버 자원을 증설하는 방법이다. 단기적으로는 효과적이지만, 근본적인 해결책이 아니며 장기적으로 비용 부담이 매우 크다.

  • 수평적 확장: 조회 트래픽이 많은 서비스의 경우, 주(Primary) DB와 복제(Replica) DB를 두어 조회 트래픽을 분산하는 방식으로 처리량을 늘릴 수 있다. 쓰기 작업은 주 DB로, 조회 작업은 복제 DB로 분리하여 DB 부하를 효과적으로 줄일 수 있다.

    DB 수평적 확장

서버 캐시 구성

  • DB 서버를 직접적으로 확장하지 않고도 응답 시간과 처리량을 개선하는 좋은 대안이 바로 캐시다.

  • 캐시는 Key-Value 형태로 데이터를 메모리에 저장하는 방식이다. 캐시에 데이터가 존재하면 DB 조회를 생략하고 캐시에서 바로 데이터를 읽어와 DB 부하를 줄이고 응답 속도를 크게 향상시킬 수 있다. DB뿐만 아니라 복잡한 계산 결과나 외부 API 연동 결과도 캐시에 저장하여 성능 개선이 가능하다.

알아두면 좋을 몇 가지 주의 사항

DB 성능 개선 시 놓치기 쉬운 실무적인 주의사항들이다.

쿼리 타임아웃(Query Timeout)

  • 설정 필요성: 장시간 실행되는 쿼리가 DB 자원을 점유하여 다른 요청까지 느려지게 하는 현상(병목)을 방지하기 위해 쿼리 타임아웃을 적절히 설정해야 한다. 쿼리 실행 시간이 15초 이상 걸리는 요청이 늘어난다면 이미 서비스에 문제가 발생하고 있다는 뜻이다.

  • 이점: 타임아웃 설정으로 DB 부하가 커지기 전에 빠르게 에러 응답을 반환하여 사용자의 불필요한 대기 시간을 줄이고 서버 부하를 완화할 수 있다. 블로그 게시글 조회처럼 단순 조회 기능은 타임아웃을 1초 정도로 짧게 설정해도 무방하지만, 결제 처리와 같이 데이터 정합성이 중요한 기능은 더 긴 타임아웃과 복잡한 처리 방식이 필요하다.

상태 변경 기능은 복제 DB에서 조회하지 않기

  • 문제점
    1. 주 DB와 복제 DB는 순간적으로 데이터가 일치하지 않을 수 있다.

    네트워크를 통해 전달되는 시간, 복제 DB가 자체 데이터에 변경 내용을 반영하는 시간에 의해 지연이 발생하고 이러한 지연으로 인해 일시적으로 서로 다른 값을 갖게 된다.

    DB 복제 지연

    1.1에서 주 DB의 데이터를 변경하고, 1.2에서 변경된 데이터를 조회하는 상황이다. 복제 과정에서 지연이 발생할 수 있기 때문에, 주 DB에서 변경한 데이터가 복제 DB에 반영되기 전에 복제 DB에서 SELECT 쿼리가 실행될 수 있다. 이 경우 잘못된 데이터를 조회하게 되어 사용자의 요청을 제대로 처리할 수 없게 된다.

    1. 트랜잭션 문제가 발생할 수 있다. 주 DB와 복제 DB간 데이터 복제는 트랜잭션 커밋 시점에 이뤄진다. 주DB의 트랜잭션 범위 내에서 데이터를 변경하고, 복제 DB에서 변경대상이 될 수 있는 데이터를 조회하면데이터 불일치로 인해 문제가 생긴다.
  • 해결책: 상태를 변경하는 기능에 대한 SELECT 쿼리(예: 회원 가입 직후 회원 정보 조회)는 반드시 주 DB에서 실행해야 한다. 트랜잭션 커밋 후 복제 지연으로 인한 데이터 불일치 문제를 방지할 수 있다.

배치 쿼리 실행 시간

  • 모니터링: 대량의 데이터를 집계하거나 처리하는 배치 프로그램의 쿼리 실행 시간을 지속적으로 모니터링해야 한다. 데이터 증가로 인해 실행 시간이 예측 불가능하게 길어지는 경우가 많다. 처음에는 30분 만에 끝났던 쿼리가 몇 분 만에 끝나지 않고 예상치 못하게 길어질 수 있다.

배치 쿼리 비교

배치 쿼리 개선

  • 개선 방법: 커버링 인덱스를 활용하거나, 데이터를 시간 단위로 쪼개서 처리하는 등의 방법을 고려해야 한다.

타입이 다른 조인 주의

  • 문제점: JOIN 조건에 사용되는 컬럼들의 데이터 타입이 다르면, DB가 암묵적으로 타입 변환을 시도할 수 있다. 이 경우 DB는 인덱스를 활용하지 못하고 풀 스캔으로 이어져 쿼리 성능이 저하된다. 예를 들어, user 테이블의 userId(integer)와 push 테이블의 receiverId(varchar)를 조인하는 경우, receiverIdinteger 타입으로 변환하려 하면서 인덱스가 사용되지 않을 수 있다.

  • 해결책: 조인하는 컬럼들의 데이터 타입을 일치시켜 불필요한 타입 변환을 방지해야 한다.

테이블 변경은 신중하게

  • 위험성: 데이터가 많은 테이블에 새로운 컬럼을 추가하거나 기존 컬럼을 변경하는 작업은 서비스 중단이 발생할 수 있을 정도로 민감한 작업이다. MySQL과 같은 DB는 테이블 변경 시 새 테이블을 생성하고 데이터를 복사한 후 교체하는 과정을 거치기 때문에, 이 과정에서 DML(Data Manipulation Language) 작업이 중단되거나 지연될 수 있다.

  • 계획: 반드시 서비스 트래픽이 적은 시간을 활용하고, DBA와 협의하여 신중하게 진행해야 한다.

테이블 변경 주의사항

DB 최대 연결 개수

  • 문제점: API 서버를 늘리면 DB 연결 요청도 늘어나는데, DB의 max_connections 설정 값을 초과하면 새로운 연결이 실패하여 서비스에 장애가 발생할 수 있다.

  • 해결책: DB CPU 사용률이 70% 이상인 상황에서 무턱대고 연결 개수를 늘리지 않도록 주의해야 한다. 먼저 캐시 도입이나 쿼리 최적화로 DB 부하를 줄인 후, 필요한 경우에만 연결 개수를 늘리는 것이 좋다.

실패와 트랜잭션 고려하기

안정적인 백엔드 서비스를 위해서는 정상적인 상황뿐만 아니라 비정상 상황에서의 트랜잭션 처리를 반드시 고려해야 한다. 모든 코드가 항상 정상적으로 동작하는 것은 아니기 때문에, 트랜잭션을 고려하지 않고 코드를 작성하면 데이터 일관성에 문제가 생길 수 있다.

트랜잭션 처리 예시

요런식으로 쿼리 작업이 여러 개 있는데 이걸 트랜잭션으로 묶지 않고 그냥 처리했다? 그러다가 뻑나면 처리가 굉장히 힘들어진다.

  • 트랜잭션의 중요성: 트랜잭션은 여러 개의 DB 작업을 하나의 논리적인 단위로 묶어, 모든 작업이 성공하거나(커밋) 하나라도 실패하면 모두 되돌려(롤백) 데이터의 일관성을 유지한다. 개발자는 DB 관련 코드를 작성할 때 트랜잭션의 시작과 종료 경계를 명확히 설정해야 한다.

  • 예시: 회원 가입 시 member 테이블에 데이터를 추가하고 mailClient.sendMail()로 환영 이메일을 발송하는 과정에서, 이메일 발송 중 RuntimeException과 같은 예외가 발생하면 member 테이블에 추가된 회원 데이터까지 모두 롤백되어야 데이터 일관성이 깨지지 않는다. 만약 메일 발송이 실패하더라도 회원 가입 자체는 성공 처리해야 한다면, 메일 발송 오류를 별도로 처리하는 등 상황에 맞는 전략이 필요하다.

외부 API 연동과 트랜잭션

  • 외부 API 연동과의 결합: 외부 API 연동과 DB 트랜잭션이 섞이면 처리 로직이 훨씬 복잡해진다. 외부 API 호출은 성공했지만 DB 작업이 실패하는 경우 등 다양한 상황에 대한 대응이 필요하며, 이에 대한 자세한 내용은 4장에서 다룬다.

핵심 정리

  1. 인덱스 설계: 조회 성능 개선의 핵심, 단일/복합 인덱스 구분과 선택도 고려
  2. 쿼리 최적화: 미리 집계, ID 기반 페이지네이션, 시간 범위 제한
  3. 인프라 활용: 캐시 도입, DB 수평/수직 확장
  4. 주의사항: 쿼리 타임아웃, 복제 DB 사용 주의, 타입 일치
  5. 트랜잭션: 데이터 일관성 유지를 위한 필수 요소

실무 적용 포인트

  • 성능 모니터링: 쿼리 실행 시간과 DB CPU 사용률 지속적 관찰
  • 점진적 개선: 급한 불은 인덱스 추가로, 근본적 해결은 설계 개선으로
  • 예방적 조치: 데이터 정책 수립, 배치 작업 최적화
  • 안정성 확보: 트랜잭션 경계 명확화, 예외 상황 대응

다음 챕터 미리보기

다음 챕터에서는 외부 API 연동과 트랜잭션 처리에 대해 자세히 알아보겠습니다.


이 스터디는 매주 일요일 진행되며, 실무에서 바로 적용할 수 있는 백엔드 지식을 다룹니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.