요약
- 12장. 데이터 시스템의 미래의 나머지 부분에 대해 이해함.
- 데이터베이스 언번들링
- 파생 상태 관찰하기
- 구체화 뷰와 캐싱
- 오프라인 대응 가능한 상태 저장 클라이언트
- 상태 변경을 클라이언트에게 푸시하기
- 종단 간 이벤트 스트림
- 읽기도 이벤트다
- 다중 파티션 데이터 처리
- 정확성을 목표로
- 데이터베이스에 관한 종단 간 논증
- 연산자의 정확히 한 번 실행
- 중복 억제
- 연산 식별자
- 종단 간 논증
- 종단 간 사고를 데이터 시스템에 적용하기
메모
파생 상태 관찰하기
- 데이터플로 시스템은 검색 색인, 구체화 뷰, 예측 모델 등의 파생 데이터셋을 생성하고 최신 상태로 유지하는 과정에 사용할 수 있음.
- 이 과정은 '쓰기 경로'로, 시스템에 정보를 기록할 때마다 일괄 처리와 스트림 처리의 여러 단계를 거친 후, 결과적으로 기록된 데이터를 모든 파생 데이터셋에 통합해 갱신함.
- 파생 데이터셋은 이후에 다시 질의할 가능성이 크기 때문에 생성됨.
- 이것이 '읽기 경로'로, 사용자 요청을 처리할 때 먼저 파생 데이터셋을 읽고 그 결과를 가공해 사용자 응답을 만듦.
- '쓰기 경로'는 데이터의 여정 중 미리 계산된 부분이며, 데이터가 들어오는 대로 바로 계산함.
- '읽기 경로'는 데이터의 여정 중에서 누군가 요청했을 때만 발생하는 부분이다. 이 둘은 각각 조급한 평가(eager evaluation)와 느긋한 평가(lazy evaluation)와 비슷함.
- 파생 데이터셋은 쓰기 경로와 읽기 경로가 만나는 장소로, 쓰기 시간에 필요한 작업의 양과 읽기 시간에 필요한 작업의 양 간에 트레이드오프를 나타냄.
구체화 뷰와 캐싱
- 전문 검색 색인은 '쓰기 경로'와 '읽기 경로'의 좋은 예시임.
- 쓰기 경로는 색인을 갱신하며, 이는 문서에 출현한 모든 용어의 색인 항목을 갱신하는 작업을 포함함.
- 반면에 읽기 경로는 색인을 사용해 키워드를 찾는 작업이며, 이는 질의에 포함된 각 단어를 검색한 후, 불논리(Boolean logic)를 적용해 질의에 포함된 모든 단어를 포함하는 문서를 찾는 것을 포함함.
- 색인이 없을 경우, 검색 질의는 모든 문서를 스캔해야 하며, 이는 상당한 비용이 드는 방법임.
- 반면에 모든 가능한 질의의 검색 결과를 미리 계산해 놓는다면 읽기 경로에서 처리하는 작업량이 줄어들지만, 쓰기 경로에 많은 비용이 발생함.
- 그런데 고정된 가장 공통적인 질의 집합의 검색 결과를 미리 계산해 두는 방법이 있음.
- 이를 구체화 뷰라고 하며, 이는 공통 질의 중 하나의 결과에 포함해야 하는 새 문서가 나타날 때 갱신되어야 하기 때문임.
- 이것은 일반적으로 공통 질의 캐시라고 부름.
- 읽기 경로와 쓰기 경로 사이에 가능한 경계에는 색인뿐만 아니라 흔한 검색 결과를 캐시하는 것이나 적은 문서에서는 색인 없이 스캔하는 것 등도 가능함.
- 이런 관점에서 보면 캐시와 색인 그리고 구체화 뷰는 읽기 경로와 쓰기 경로 사이의 경계를 옮기는 단순한 역할을 함.
- 이런 파생 데이터셋을 사용하면 쓰기 경로에서 더 많은 일을 수행해 미리 결과를 계산할 수 있으므로 읽기 경로의 작업이 줄어듦.
오프라인 대응 가능한 상태 저장 클라이언트
- 읽기 경로와 쓰기 경로 사이 경계 개념은 흥미로움.
- 웹 애플리케이션에 대한 몇 가지 가정이 생겨났음.
- 클라이언트는 대체로 상태 비저장이고 서버가 데이터를 관리한다는 것임.
- 그러나, "단일 페이지" 자바스크립트 웹 앱은 클라이언트 측 사용자 인터페이스 상호작용과 웹 브라우저 내 영속적인 로컬 저장소를 포함해서 상태 저장 능력을 얻었음.
- 이와 같이 모바일 앱도 많은 상태를 모바일 장치에 저장할 수 있고 사용자 상호작용 대부분을 처리할 때 서버까지 왕복할 필요가 없음.
- 이런 변화는 오프라인 우선(offline-first) 애플리케이션에 관심을 불러일으켰음.
- 이는 인터넷 연결 없이 로컬 데이터베이스를 이용하고, 네트워크 연결 가능 시 원격 서버와 동기화함.
- 사용자 인터페이스가 동기식 네트워크 요청을 기다리지 않아도 되고 앱이 대부분 오프라인으로 작동한다면 사용자에게 이점임.
- 상태 비저장 클라이언트가 항상 중앙 서버와 통신한다는 가정에서 벗어나 최종 사용자 장치에서 상태를 유지하는 쪽으로 나아가면 새로운 기회가 있음.
- 장치 상태를 서버 상태의 캐시로 생각할 수 있음.
- 클라이언트 앱의 모델 객체는 원격 데이터센터의 상태를 로컬에 복제한 것이고, 화면의 화소는 클라이언트 앱의 모델 객체를 보여주는 구체화 뷰임.
상태 변경을 클라이언트에게 푸시하기
- 웹 페이지가 로드된 후 서버에서 데이터 변경이 일어나도 브라우저는 이를 알 수 없음.
- 이를 해결하기 위해 서버 전송 이벤트(EventSource API)와 웹소켓(WebSocket)이 사용됨.
- 이들은 웹 브라우저가 서버와 TCP 접속을 유지하면서 서버가 주도적으로 메시지를 브라우저에 보내는 통신 채널을 제공함.
- 상태 변화를 클라이언트 장치에 푸시함으로써, 쓰기 경로가 최종 사용자까지 확장됨.
- 초기 상태를 읽기 위해 읽기 경로를 사용한 후에는, 서버가 보내주는 상태 변경 스트림만 따르면 됨.
- 장치가 오프라인 상태일 때는 서버에서 상태 변경 알림을 받지 못함.
- 이 문제는 로그 기반 메시지 브로커를 사용하는 소비자가 접속이 끊긴 이후에도 메시지를 빠짐없이 받을 수 있게 하는 방법으로 해결할 수 있음.
- 이런 방식은 각 장치가 작은 이벤트 스트림을 구독하는 구독자로 작동하게 함.
종단 간 이벤트 스트림
- 엘름(Elm) 언어와 리액트(React), 플럭스(Flux), 리덕스(Redux) 같은 도구는 사용자 입력을 표현하는 이벤트 스트림이나 서버 응답 스트림을 구독하는 방식을 사용해 클라이언트 측 상태를 관리한다.
- 상태 변경 이벤트를 클라이언트 측 이벤트 파이프라인으로 푸시하는 프로그래밍 모델 확장이 자연스러움.
- 이렇게 하면 상태 변경이 종단 간 쓰기 경로를 따라 흐르게 되어, 상호작용에서 발생한 상태 변경이 이벤트 로그, 파생 데이터 시스템, 스트림 처리자를 거쳐 다른 장치의 사용자 인터페이스까지 이어질 수 있음.
- 이런 상태 변경이 전파되는 지연 시간은 대개 1초 이하로 매우 낮음.
- 상태 비저장 클라이언트와 요청/응답 방식의 상호작용이 데이터베이스, 라이브러리, 프레임워크, 프로토콜에 깊게 뿌리박혀 있어서 모든 애플리케이션을 위와 같이 구축하기 어려움.
- 또한, 변경 사항 구독 기능을 지원하는 데이터스토어는 드뭄.
- 최종 사용자까지 쓰기 경로를 확장하려면 요청/응답 상호작용 방식에서 발행/구독 데이터플로 방식으로 변경해야 함.
- 이런 변경은 더 반응성 있는 사용자 인터페이스와 더 나은 오프라인 지원을 위해 필요하며, 데이터 시스템을 설계할 때 현재 상태를 단지 질의하는 것이 아니라 변경 사항을 구독하는 방식을 고려해야 함.
읽기도 이벤트다
- 읽기도 이벤트다고 주장하며, 데이터 저장소와 스트림 처리자 사이의 경계를 설명함.
- 스트림 처리자 내부에 숨겨진 상태를 고려하면, 스트림 처리자가 데이터베이스 역할을 할 수 있음.
- 읽기 요청을 이벤트 스트림으로 표현하는 것도 가능하며, 이는 쓰기와 읽기 모두 이벤트로 처리하게 됨.
- 이 방식은 요청 서빙과 조인 수행을 동일하게 볼 수 있으며, 인과적 의존성 추적에 이점이 있음.
- 읽기 이벤트 로그를 기록하면 사용자가 결정을 내릴 때 참고한 정보를 재구성할 수 있음.
- 하지만 이 방법은 추가적인 저장소와 I/O 비용이 발생함.
- 아직 최적화 문제는 해결되지 않았지만, 요청 처리의 부수 효과로 로깅할 경우, 로그를 요청의 출처로 바꾸는 것이 어렵지 않음.
다중 파티션 데이터 처리
- 다중 파티션 데이터 처리를 고려하면, 스트림 처리자가 제공하는 메시지 라우팅, 파티셔닝, 조인용 인프라를 이용해 복잡한 질의를 분산 실행할 수 있음.
- 스톰의 분산 RPC 기능은 이런 사용 패턴을 지원하며, 트위터에서 특정 URL을 본 사람 수를 계산하는 데 사용 가능함.
- 사기 방지를 위해 구입 이벤트의 사기성 위험도를 평가하기 위해 여러 파티션의 결과를 결합해야 할 때도 이 패턴이 유용함.
- MPP 데이터베이스의 내부 질의 실행 그래프도 비슷한 특성을 갖고 있어 다중 파티션 조인을 수행할 필요가 있다면, 스트림 처리자보다 이 기능을 제공하는 데이터베이스를 사용하는 것이 더 간단함.
- 하지만 질의를 스트림으로 간주하면 기성 솔루션의 한계를 넘어서는 대규모 애플리케이션 구현이 가능함.
정확성을 목표로
- 데이터를 읽기만 하는 상태 비저장 서비스는 문제가 생겨도 재시작하면 해결되지만, 상태 저장 시스템은 잘못되면 그 효과가 영원히 지속될 가능성이 있음.
- 애플리케이션은 신뢰성 있고 정확해야 하며, 이를 위한 트랜잭션의 속성은 원자성, 격리성, 지속성임.
- 하지만 이러한 토대는 약할 수 있으며, 완화된 격리 수준을 사용할 때 생기는 혼란이 그 예임.
- 일부 영역에서는 트랜잭션을 포기하고 성능과 확장성을 제공하는 모델로 대체했지만, 이 경우 일관성이 잘 정의되지 않을 수 있음.
- 특정 트랜잭션 격리 수준이나 복제 설정으로 애플리케이션을 실행하는 것이 안전한지 결정하기는 어려움.
- 데이터베이스 같은 인프라 제품에 문제가 없더라도 제품에서 제공하는 기능을 애플리케이션 코드에서 정확하게 사용할 필요가 있음.
- 데이터가 가끔 예측하지 못한 방식으로 깨지거나 누락되는 것을 허용할 수 있다면 단순한 솔루션을 선택할 수 있지만, 더 강력한 정확성 보장이 필요하다면 직렬성과 원자적 커밋을 선택해야 하지만 비용이 따름.
- 전통적인 트랜잭션 접근법이 사라지고 있지는 않지만, 이것이 애플리케이션을 정확하게 만들고 결함에 견딜 수 있게 하는 유일한 방법은 아니라는 생각을 제시함.
데이터베이스에 관한 종단 간 논증
- 애플리케이션이 직렬성 트랜잭션과 같은 강력한 안전성 속성을 지원하는 데이터 시스템을 사용한다 해도, 데이터 유실과 손상을 완전히 방지할 수는 없음.
- 애플리케이션에 버그가 있어 데이터베이스에 정확하지 않은 데이터를 기록하거나 데이터를 지우게 되면, 직렬성 트랜잭션만으로는 이를 해결할 수 없음.
- 애플리케이션 버그와 사람의 실수는 피할 수 없는 일이며, 이를 통해 불변성과 추가 전용 데이터의 중요성을 강조함.
- 결함 있는 코드로 인해 유용한 데이터를 파괴하는 가능성을 제거하면, 이러한 실수로부터 복구하기가 용이해짐.
- 하지만 불변성만으로는 모든 문제를 해결할 수 없으며, 더욱 미묘한 데이터 손상 사례도 발생할 수 있음.
연산자의 정확히 한 번 실행
- '정확히 한 번' 시맨틱은 메시지 처리 중 문제가 발생하면 포기하거나 재시도하는 것을 의미함.
- 재시도는 데이터 손상의 위험을 수반함.
- 두 번 처리되는 것은 데이터 손실의 한 형태이며, 이는 고객에게 과다 청구하거나 통계를 과장하는 결과를 초래할 수 있음.
- '정확히 한 번'은 연산이 어떤 결함 때문에 재시도했더라도 결함이 없었던 것과 동일한 결과를 최종적으로 얻기 위해 계산을 조정하는 것을 의미함.
- 이를 달성하기 위한 한 방법은 연산을 멱등으로 만드는 것임.
- 멱등이란 연산을 한 번 실행하든 여러 번 실행하든 같은 효과가 보장되는 것을 의미함.
- 그러나 본질적으로 멱등이 아닌 연산을 멱등으로 만드는 데는 노력과 신중함이 필요함.
- 메타데이터를 추가로 유지하거나 장애 복구 시 펜싱을 보장할 필요가 있음.
중복 억제
- TCP는 패킷의 일련번호를 사용해 패킷을 올바른 순서로 전달하며, 네트워크 상에서 패킷을 잃어버렸는지 중복됐는지 확인함.
- 그러나 이러한 중복 억제는 단일 TCP 연결 문맥 내에서만 작동함.
- TCP 연결이 데이터베이스에 접속하고 트랜잭션을 실행하면, 트랜잭션은 클라이언트 연결과 묶임.
- 데이터베이스 서버로부터 응답을 받지 못하면 해당 트랜잭션이 커밋을 성공했는지 실패했는지 알 수 없음.
- 클라이언트는 데이터베이스에 재연결해 트랜잭션을 재시도할 수 있지만 그때는 TCP 중복 억제 범위를 벗어나 버림.
- 2단계 커밋 프로토콜은 TCP 연결과 트랜잭션 간 일대일 대응 관계를 깸.
- 네트워크 장애 이후 트랜잭션 코디네이터가 데이터베이스에 재접속해 의심스러운 트랜잭션이 커밋됐는지 어보트됐는지 파악해야 하기 때문임.
- 그러나 이것만으로는 트랜잭션이 한 번만 실행되도록 보장하기에는 충분하지 않음.
- 데이터베이스 클라이언트와 서버 사이에서 중복 트랜잭션을 억제할 수 있더라도 최종 사용자 장치와 애플리케이션 서버 간 네트워크 상황도 고려해야 함.
- 사용자가 웹 브라우저로 서버에 명령을 제출할 때, 연결 문제로 인해 서버에서 응답을 받지 못하면 사용자는 에러 메시지를 받고 수동으로 재시도함.
- 이런 상황에서 일반적인 중복제거 메커니즘은 도움이 되지 않음.
연산 식별자
- 네트워크 통신 홉을 통과하는 연산을 멱등적으로 만들기 위해서는 데이터베이스의 트랜잭션 메커니즘에 의존하는 것만으로는 부족하다. 종단 간 흐름을 고려해야 한다.
- 이를 위해 연산의 고유 식별자를 생성해 클라이언트 애플리케이션 내 숨은 폼 필드에 포함하거나, 모든 폼 필드의 해시값을 계산하여 연산 ID를 만들 수 있다.
- 웹 브라우저가 POST 요청을 두 번 제출해도, 두 요청은 같은 연산 ID를 갖기 때문에, 해당 연산 ID를 데이터베이스까지 전달하여 특정 ID에 대해 딱 한 번만 연산을 실행할 수 있다.
- p517 예제 12-2 참고
- 예제 12-2는 request_id를 사용하는 유일성 제약 조건에 의존한다. 이미 존재하는 ID를 삽입하려 하는 경우, 트랜잭션이 중단된다. 따라서 같은 트랜잭션을 두 번 수행하는 것을 막을 수 있다.
- 또한 예제 12-2의 requests 테이블은 이벤트 로그처럼 동작하며 이벤트 소싱을 나타낸다. 계좌 잔고 갱신은 이벤트 삽입 트랜잭션과 같은 트랜잭션에서 실행될 필요는 없다. 이벤트가 정확히 한 번 처리되기만 하면 되는데, 이것은 다시 요청 ID를 사용해 보장할 수 있다.
종단 간 논증
- 중복 트랜잭션 억제는 종단 간 논증의 한 예임.
- 이는 문제 기능이 통신 시스템의 종단점에 위치한 애플리케이션의 지식과 도움이 있어야만 완벽하게 구현될 수 있다는 원리를 말함.
- 이 원리에 따르면, 통신 시스템 자체 기능으로 문제 기능을 제공하는 것은 불가능함.
- TCP는 TCP 연결 수준에서 중복 패킷을 억제하고, 스트림 처리자는 메시지 처리 수준에서 정확히 한 번의 시맨틱을 제공하지만, 이는 사용자가 중복 요청을 제출하는 것을 막지 못함.
- 이러한 중복 문제를 해결하기 위해서는 종단 간 해결책이 필요함.
- 이는 최종 사용자 클라이언트로부터 데이터베이스에 이르는 모든 경로에 트랜잭션 식별자를 포함하는 방법을 의미함.
- 종단 간 논증은 데이터 무결성 검사에도 적용될 수 있음.
- 이더넷, TCP, TLS 내부의 체크섬을 사용하면 네트워크 상에서 패킷이 손상되었는지 감지할 수 있지만, 소프트웨어의 버그나 디스크의 손상으로 인한 데이터 손상은 감지하지 못함.
- 이를 위해 종단 간 체크섬이 필요함.
- 암호화에도 비슷한 논증을 적용할 수 있음.
- 와이파이 네트워크의 암호는 와이파이 트래픽을 스누핑하는 사람으로부터 보호하지만, 인터넷의 공격자나 서버 침해로부터는 보호하지 못함.
- 이를 위해 종단 간 암호화와 인증이 필요함.
- TCP 중복 억제, 이더넷 체크섬, 와이파이 암호화와 같은 저수준 기능이 종단 간 기능을 제공하지 않아도, 이들은 여전히 유용함.
- 이는 더 높은 수준에서 문제가 발생할 확률을 낮추기 때문임.
- 하지만 이들 저수준 신뢰성 기능이 종단 간 정확성을 보장하기에는 충분하지 않다는 사실을 기억해야 함.
종단 간 사고를 데이터 시스템에 적용하기
- 중복 트랜잭션 억제 시나리오는 종단 간 논증의 한 예로, 이는 문제 기능이 완벽하게 구현될 수 있도록 애플리케이션에 있어야 함을 주장하는 원리임.
- TCP, 데이터베이스 트랜잭션, 스트림 처리자 자체로는 중복 문제를 해결할 수 없으며, 이 문제는 종단 간 해결책, 즉 최종 사용자 클라이언트로부터 데이터베이스에 이르는 모든 경로에 트랜잭션 식별자를 포함하는 방법이 필요함.
- 종단 간 논증은 데이터 무결성 검사, 암호화에도 적용될 수 있으며, 종단 간 체크섬, 암호화, 인증이 필요함.
- 저수준 기능은 그 자체로 필요한 종단 간 기능을 제공하지 않지만, 이러한 기능들은 문제 발생 확률을 낮추는데 유용함.
댓글