728x90

데이터베이스 트랜잭션의 핵심, ACID 완벽 정리

백엔드 개발을 하다 보면 반드시 마주하게 되는 개념이 바로 트랜잭션(Transaction)입니다. 그리고 이 트랜잭션의 안정성을 보장하는 핵심 개념이 바로 ACID입니다.

ACID는 데이터베이스가 안전하게 동작하기 위한 최소한의 약속이라고 볼 수 있습니다.

 

 

ACID란 무엇인가?

ACID는 다음 네 가지 속성의 앞 글자를 따온 용어입니다.

  • Atomicity (원자성)
  • Consistency (일관성)
  • Isolation (격리성)
  • Durability (지속성)

즉, "트랜잭션이 안전하게 수행되도록 보장하는 4가지 성질"을 의미합니다.

 

 

1. Atomicity (원자성)

원자성은 트랜잭션이 모두 성공하거나, 모두 실패해야 한다는 성질입니다.

예를 들어 계좌 이체를 생각해보겠습니다.

  1. A 계좌에서 돈을 출금
  2. B 계좌에 돈을 입금

만약 2번에서 오류가 발생하면, 1번도 반드시 취소되어야 합니다.
즉, 중간 상태는 존재하면 안 됩니다.

 

 

2. Consistency (일관성)

일관성은 트랜잭션 수행 전과 후의 데이터가 항상 유효한 상태를 유지해야 한다는 것을 의미합니다.

  • 계좌 잔액은 0 이상이어야 한다
  • 외래키 제약조건 유지
  • 유니크 값 중복 금지

이러한 규칙을 위반하면 트랜잭션은 자동으로 롤백됩니다.

 

 

3. Isolation (격리성)

격리성은 동시에 실행되는 트랜잭션이 서로에게 영향을 주지 않도록 하는 성질입니다.

즉, 각각의 트랜잭션은 독립적으로 실행되는 것처럼 보여야 합니다.

다른 트랜잭션은 항상 완료된 결과만 볼 수 있어야 하며, 중간 상태를 읽을 수 없어야 합니다.

 

 

4. Durability (지속성)

지속성은 트랜잭션이 성공적으로 완료되면 그 결과가 영구적으로 저장되는 것을 의미합니다.

시스템 장애가 발생하더라도 데이터는 반드시 유지되어야 합니다.

이를 위해 데이터베이스는 보통 로그(WAL, Write-Ahead Log)를 활용합니다.

 

 

 

ACID 한눈에 정리

속성 설명
Atomicity All or Nothing
Consistency 데이터 규칙 유지
Isolation 트랜잭션 간 간섭 없음
Durability 영구 저장

 

 

ACID가 중요한 이유

ACID는 단순한 이론이 아니라 실제 서비스의 안정성을 보장하는 핵심 개념입니다.

  • 금융 시스템
  • 결제 처리
  • 주문/재고 관리
  • 데이터 정합성이 중요한 서비스

특히 데이터가 틀리면 서비스 전체가 무너질 수 있는 상황에서 반드시 필요합니다.

 

 

마무리

ACID는 데이터베이스를 사용할 때 반드시 이해해야 하는 핵심 개념입니다.

👉 한 줄 정리
ACID는 "데이터를 안전하게 다루기 위한 최소한의 규칙"입니다.

728x90
728x90
본 문서는 Discord Migrates Trillions of Messages from Cassandra to ScyllaDB 아티클을 정리한 글입니다.

디스코드에서 트래픽과 메세지가 증가하면서 기존 메세지 스토리지인 Cassandra에서 ScyllaDB로 마이그레이션한 내용을 정리합니다.

 

Discord 시작: MongoDB

MongoDB → Cassandra: What we were doing

 

2015년 개발 당시 Discord는 MongoDB 를 사용

  • a single MongoDB replica set

2015년 11월, 사용이 증가함에 따라 저장된 메세지가 1억 건에 달했고 이 때 성능 문제가 발생

  • 데이터/인덱스가 메모리(RAM) 크기를 넘어섬
  • latency가 예측 불가능해짐

메세지의 수는 지속적으로 증가

  • 2016년 7월: 일 4천만 개
  • 2016년 12월: 일 1억 개

 

MongoDB 를 사용했을 때 발생했던 문제점과 새로운 DB 선택 요구사항

MongoDB → Cassandra: Choosing the Right Database

 

MongoDB 를 사용했을 때 발생했던 문제점

매우 랜덤하게 읽기 작업이 발생

  • 읽기/쓰기 비율이 5:5

음성 채팅이 많은 서버(디스코드 내 organization/workspace): 연간 1000개 메세지 발생

  • 소수 메세지 조회 → 디스크에 많은 랜덤 검색 발생 → 캐시 효율 감소

비공개 채팅이 많은 서버(디스코드 내 organization/workspace): 연간 10~100만개의 메세지 발생

  • 요청은 최근 메세지만 조회, 회원 수는 100명 미만 → 요청 비율이 낮고, 캐시 적중률(히트)이 낮음

대규모 공개 Discord 서버(디스코드 내 organization/workspace): 연간 수백만 개의 메세지 발생

  • 최근 한 시간 내에 발생하는 메세지(데이터)를 조회하는 경우가 많음 → 캐시 히트가 높음

또한 앞으로 랜덤 검색을 유발하는 기능들이 추가될 예정


DB 선택 요구사항

  • 선형 수평 확장: 솔루션 재검토하거나 데이터를 수동으로 재배치(re-shard)하고 싶지 않음
  • 자동 장애 조치: 스스로 복구될 수 있는 시스템
  • 낮은 유지보수 비용: 데이터가 증가하면 노드만 추가
  • 검증된 기술: 새 기술을 좋아하지만 너무 새롭지 않은 기술
  • 예측 가능한 성능: p95의 응답 시간이 80ms 이하, Redis 또는 Memcached에 메세지를 캐시하고 싶지 않음
  • blob 저장소 아닌 저장소: 초당 수천 개의 메세지가 작성되기 때문에 blob 직렬화/역직렬화는 비효율적임
  • 오픈 소스: 타사에 의존하고 싶지 않음

 

Cassandra 선택

Cassandra → ScyllaDB: Our Cassandra Troubles


2017년: 12개 노드로 시작, 수 십억 개의 메세지를 저장

 

적용 후 좋았으나 GC가 10초동안 발생하는 문제가 발생하기도 함

  • 툼스톤(데이터 삭제 작업)때문에 발생한 문제
    • 툼스톤 기간: 10일에서 2일로 축소
    • 빈 버킷을 조회하지 않도록 함

 

2022년: 177개 노드, 메세지는 수 조개에 도달

 

많은 동시 읽기 → 핫 파티션 → 성능 문제 발생: 대기 시간 예측 불가

  • 카산드라: 읽기 비용 > 쓰기 비용
  • 읽기는 Memtable(메모리)에 데이터가 없으면 SSTable(파일)을 조회

유지보수 비용 증가

  • SSTable 압축에 따른 성능 문제 → gossip dance 운영 작업
  • gossip dance: 클러스터 내 노드 중 한 대를 가져와서 트래픽을 받지 않고, 파일을 압축하고, 다시 클러스터에 돌려보내는 작업을 반복

 

아키텍처 변경

Cassandra → ScyllaDB: Changing Our Architecture


Cassandra → ScyllaDB

  • GC로 인해 발생하는 지연 시간 문제 감소

데이터 서비스 API(레이어) 추가

  • 동일 데이터에 대한 여러 요청을 한 번에 DB로 보냄 → DB로 갈 쿼리 수를 줄여서 DB 부하를 감소
  • 일관성 해시(consistence hashing) 사용으로 동일 데이터에 대한 요청은 동일 데이터 서비스로 보냄

 

결과

2022년 5월 기준

  • Cassandra 177개 노드 → ScyllaDB 72개 노드
    • 노드 별 평균 4TB 디스크 → 9TB 디스크
  • 메세지 히스토리 읽기 p99: 40-125ms → 15ms
  • 메시지 작성 p99: 5-70ms → 5ms
  • 온콜 대응이 줄어듦

 

 

Reference

728x90

'Storage' 카테고리의 다른 글

[데이터베이스] ACID 개념 쉽게 이해하기  (0) 2026.05.05
728x90

상황

  • MySQL에서 IN 절 안에 조회하고 싶은 컬럼의 리스트를 넣은 후 조건으로 설정한 리스트의 순서대로 반환받고 싶을 때
  • ex.
조회해야 하는 Member들의 id와 반환되어야 할 순서로 정렬된 리스트를 입력받는다.
Member를 조회하고, 입력받은 순서에 맞게 정렬해서 Member List를 리턴한다.

 

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    // Example 1
    public List<Member> getMembers1(List<Long> memberIds) {

        List<Member> members = memberRepository.findAllById(memberIds); // ①

        List<Member> sortedMembers = new ArrayList<>(); 
        for (Long memeberId: memberIds) {
            Optional<Member> optionalMember = members.stream()  // ②
                    .filter(m -> m.getId().equals(id))
                    .findAny();

            optionalMember.ifPresent(sortedMembers::add);       // ③
        }

        return sortedMembers;
    }

    // Example 2
    public List<Member> getMembers2(List<Long> memberIds) {
        
        List<Member> members = memberRepository.findAllById(memberIds); // ①

        Map<Long, Member> memberMap = new HashMap<>();
        members.forEach(member -> memberMap.put(member.getId(), user)); // ②

        List<Member> sortedMembers = new ArrayList<>();
        for (Long memeberId: memberIds) {
            Member member = memberMap.get(memeberId);                   // ③

            if (member != null)
                sortedMembers.add(member);
        }

        return sortedMembers;
    }
}
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

    ...

    @Override
    List<T> findAllById(Iterable<ID> ids);

    ...
}
  • Example 1
    • ① : id list를 조회
    • ② : 조회한 멤버 리스트에서 알맞는 id의 회원을 조회
    • ③ : 해당 회원을 리스트에 추가
  • Example 2
    • ① : id list를 조회
    • ② : 조회한 멤버 리스트를 Map 형태로 보관
    • ③ : Map에서 아이디를 조회해서 리스트에 추가

해결

  • 지금의 나는 코드를 작성할 때 Example1이나 Example2를 생각했을 거고, Example2를 활용해서 값을 반환했을 것 같다.
  • 하지만 이 방법은 매우 비효율적이다. DB에서 값을 조회하고, 그다음 다시 반복문을 실행시켜야 한다는 점과 굳이 불필요한 Map 객체를 생성해서 처리해야 한다.
  • 따라서 이 부분을 애초에 쿼리를 실행할 때 정렬 순서를 조건으로 설정해서 값을 반환받을 수 있다면 불필요한 반복문 실행이나 객체 생성이 필요 없을 수 있다.

기존 쿼리

SELECT * FROM member WHERE id IN (2, 1, 3);
  • 조회할 아이디 값과 정렬된 리스트가 2,1,3 일 때

해결

SELECT * FROM member WHERE id IN (2, 1, 3) ORDER BY FIELD(id, 2, 1, 3);
  • ORDER BY FEILD를 설정한다.
    • ORDER BY FELID('컬럼명', '정렬 순서 1', '정렬 순서 2', '정렬 순서 3', ...)

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "SELECT * FROM member WHERE id IN (?1) ORDER BY FIELD(id, ?1);", nativeQuery = true)
    List<Member> findMembersByIdOrderByFeild(String ids); // 리스트를 문자열로 변환
}

 

728x90

+ Recent posts