김대용
Book

DDIA 파트1. 데이터 시스템의 토대

--------------------

애플리케이션의 신뢰성, 확장성, 유지보수 용이성

사람들은 인터넷이 너무 잘 만들어진 나머지 사람이 만들었다는 사실을 잊어버리고 태평양처럼 천연자원인 마냥 주어진 것이라고 여긴다. 이렇게 규모 있는 기술이 오류로부터 자유로웠던 적이 있었던가?

앨런 케이, 돕 박사와의 인터뷰 중 (2012)

현대의 애플리케이션은 데이터 집중적이다. CPU 성능은 충분히 좋아져 애플리케이션의 앞길을 가로막진 않는다. 그러나, 데이터의 크기, 복잡성, 변경빈도가 문제다.

데이터 집중적인 애플리케이션은 데이터를 처리(CRUD, 캐시, 스트림, 배치 등)하는 일을 한다. 데이터 시스템은 이런 일을 할 수 있게 도와주고, 개발자는 레고처럼 쌓아 애플리케이션을 만든다. 예를 들어 이미 있는 데이터베이스(MySQL, Oracle 등)를 쓰지 처음부터 만들진 않는다.

데이터를 처리하는데 단 하나의 도구만 쓸 수는 없을까? 그러나 흔히들 '은총알은 없다'고 한다. 한 도구로 모든 데이터를 처리할 수 없기에 각자의 분야에서 뛰어난 여러 도구를 조합해 사용해야 한다. 예를 들어 데이터를 저장하는 메인 데이터베이스(RDB, NoSQL 등), 텍스트 검색을 위한 서버(ElasticSearch, Solr 등), 캐싱을 위한 데이터베이스(Memcached, Redis 등)를 조합할 수 있다.

데이터시스템은 이런 도구들을 부르는 추상적인 말이다. 그래서 작은 데이터시스템을 조합해 만든 API도 데이터시스템이라고 부를 수 있다. 즉, 우리는 애플리케이션 개발자이면서 데이터시스템 디자이너라고 부를 수 있다.

어떻게 데이터시스템을 만들 때 고려해야 하는 점이 뭘까? 이를 판단하는 기준은 여러 개겠지만 아래 세 가지를 집중적으로 살펴보자.

  • 신뢰성 : 어떤 고난과 역경에 맞서도 계속해서 올바르게 동작해야 한다.
  • 확장성 : 시스템이 성장할 방법이 있어야 한다.
  • 유지보수 용이성 : 결국 시스템을 관리하는 건 사람이다. 사람들이 쉽고 효율적으로 관리할 수 있는 구조인가.

신뢰성

일이 틀어져도 견딜 수 있는 성질을 말한다. 이를 보고 결함 내성(fault-tolerant)또는 결함 회복탄성(fault-resilient)이라고 부른다.

결함(fault)과 실패(failure)는 다른 말이다. 결함이란 시스템의 한 부분이 정해진 규약을 벗어난 행동을 할 때를 말하고, 실패란 시스템 전체가 멈춰버려 먹통을 겪는 상태를 말한다.

결함의 종류
  • 하드웨어 결함, Hardware Fault
    • 예시 : 하드디스크 배드섹터, 정전, 부품 노후화 등
    • 예방 : 서버 부품 이중화, 서버 머신 이중화 등
  • 소프트웨어 오류, Software Error
    • 예시 : OS 커널 오류, 갑작스러운 서버 자원(CPU, MEM 등) 고갈, 계단식 결함 발생 등
    • 예방 : 테스팅, 모니터링, 자동 재시작 등 여러 방법이 있다. 확실하고 간단한 방법은 없다.
  • 인적 오류, Human Error
    • 예시 : 코딩 실수, 데이터베이스 조작 실수, MD의 실수로 100% 할인 행사 등등
    • 예방 : 소프트웨어 테스팅, 접근 권한 설정, 사용자 입력 검증, 사용자 행동을 검증/강제하는 인터페이스 등

하드웨어 결함
가장 먼저 생각나는 결함을 꼽자면 하드웨어 결함을 생각할 수 있다. 하드디스크가 죽었다던가, 네트워크 선이 잘못 연결됐다거나 등 여러 경우가 있다. 이는 흔히 중복(Redundancy)으로 예방한다.

최근까지만 해도 서버의 부품을 다중화하는 것만으로 충분했다. 서버 자체가 완전히 죽는 일은 드물었기 때문이다. 디스크(RAID), 전원(UPS, 디젤발전기), CPU(hot-swappable) 등을 다중화해서 한 부품이 죽으면 다른 부품으로 빠르게 대체되도록 했다.

그러나 데이터양과 애플리케이션이 처리해야 하는 요청은 더 많아졌다. 이를 대응할 서버대수는 더 많아지고, 자연스레 부품이 망가질 가능성도 커진다. 특히, AWS 같은 클라우드는 단일 서버가 신뢰성 있게 동작하는 걸 보장하지 않는다. 이는 다중 서버를 사용한 유연함과 탄력적 처리를 중요시한 설계에서 비롯됐다. 따라서 서버 자체를 다중화하는 방향으로 변화해 갔다.

소프트웨어 오류
하드웨어가 사용하는 소프트웨어(OS, 커널 프로세스, 사용자 프로세스 등)에서 발생한 문제를 말한다. 이런 문제들은 깊게 동면해 있다가 트리거되면서 갑작스럽게 나타나는 경우가 꽤 많다. 그래서 예상하기 어렵고, 한번 일어나면 시스템에 크게 오류가 생길 수 있다. 심하게는 시스템 전체가 먹통이 될 수도 있다. 예를 들어, 2012년 6월 30일에 일어난 윤초 버그로 인해 많은 애플리케이션이 동시에 멈췄던 적이 있다.

이를 쉽게 해결할 수 있는 명료한 방법은 따로 없다. 그러나, 테스팅, 프로세스 격리, 모니터링, 데이터 흐름 검증 등 여러 작은 것들이 도움이 될 수 있다.

인적 오류
인간은 믿음직스럽지 못하다. 그런데 시스템을 만들고 운영하는 주체는 인간이다. 당연하게도 인간의 실수들이 시스템의 결함을 초래한다.

  • 실수를 최소화하는 시스템을 설계한다 : 추상화, API, 관리자 인터페이스 등을 활용해 올바른 행동을 하도록 돕는다.
  • 실수가 일어나도 상관없는 샌드박스를 제공한다 : 운영 서버와 동일하지만, 격리된 공간을 제공해 사용자가 학습할 수 있는 환경을 마련한다.
  • 모든 계층에서의 테스트를 진행한다 : 유닛테스트, 통합테스트 등 자동화 테스팅과 수동 테스팅으로 실수를 잡아낸다.
  • 빠르고 쉬운 복구 방법을 마련한다 : 롤백 및 롤링 배포 등 실패를 최소화할 방법을 마련한다.
  • 상세한 모니터링을 설정한다 : 성능 및 오류 발생률 같은 수치를 원격측정(Telemetry)한다. 뭔가 잘못됨을 알아채고 원인을 찾는 데 도움이 된다.

우리는 고객에 대한 책임이 있다.
핵 발전소, 비행기, 로켓 발사체, 수술 로봇 등 사람의 생명과 안전에 직접적으로 영향이 있는 거대한 규모의 애플리케이션만 신뢰성이 중요한 게 아니다.

일상적인 애플리케이션도 중요하다. 기업용 애플리케이션의 장애는 큰 생산성 하락 및 법적 문제까지 야기할 수 있다. 또 쇼핑몰같이 사용자에게 서비스를 제공하는 애플리케이션은 매출과 명성이 크게 줄어들 수도 있다. 클라우드 사진 저장소가 문제가 생겨 가족사진이 다 없어진 부모의 마음은 어떻겠는가?

우리는 고객에 대한 책임이 있다.

확장성

서비스가 성장함에 따라 시스템 부하(load)가 늘어났을 때 대응할 수 있는 능력이 있는 지를 말한다. 단순히 이 시스템은 확장성이 있어, 저 시스템은 확장성이 없어라고 구분할 수 없다. 확장성은 "시스템의 특정한 부하가 늘어날 때, 대응할 방법은 무엇인가?", "부하가 늘어날 때 컴퓨팅 자원을 어떻게 추가할 수 있을까?"와 같은 질문에 대답하는 것이다.

부하를 정의하기
그래서 현재 시스템의 부하가 뭔지 알아야 대응할 방법을 논의할 수 있다. 부하는 특정한 숫자들로 표현될 수 있는데, 이를 부하 인자(load paramters)라고 한다. 시스템마다 다르지만, 예를 들어보면 웹 서버의 초당 요청 수, 데이터베이스 읽기/쓰기의 비율, 병목이 되는 특정 API의 동시 처리량 등 여러 경우가 있을 수 있다.

트위터를 생각해보자. 트위터는 내가 쓴 트윗이 날 팔로우하는 사람들의 홈에 보여야한다. 트위터는 이를 처리하기위해 두가지 방법을 생각했다.

  1. 거대한 트윗 보관함 : 트윗을 쓰면 tweets 테이블에 모두 저장되고, 홈 타임라인 조회 시 join으로 팔로우한 사람의 트윗을 가져온다.
  2. 개인별 트윗 보관함 : 트윗을 쓰면 메일함처럼 각자의 보관함 테이블에 트윗을 넣고, 홈 타임라인 조회 시 보관함을 조회해 가져온다.

1번은 쓰기 단계를 최소화했고, 2번은 읽기 단계를 최소화했다. 트위터는 트윗을 쓰는 것보다, 읽는 게 평균 2배 정도 요청이 많다. 그래서 이럴 때는 2번 방법이 효율적이다.

그러나, 팔로워의 수는 굉장히 넓게 분포되어있다. 십만, 백만 단위 팔로워를 가진 사람도 꽤 있다. 이런 사람들이 트윗을 쓰면 1초당 몇백, 몇천만개의 데이터를 동시에 써야한다. 이를 수초 내에 처리하긴 어렵다. 그래서 이럴 때는 1번 방법이 효율적이다.

이렇게 팔로워 수 분포에 따라 확장성을 어떻게 고려해야하는지 논의할 수 있기 때문에, 이를 주요 부하 인자라고 볼 수 있다.

성능을 표현하기
이제 부하를 알고 있으니, 부하 인자를 늘렸을 때 성능이 어떻게 변하는지 확인해볼 수 있다. 그렇다면 성능은 어떻게 표현할 수 있을까?

성능은 시스템의 종류에 따라 다르게 표현할 수 있다.

  • 일괄 처리 시스템 : 처리량(throughput) - 한꺼번에 처리할 수 있는 데이터의 크기
  • 온라인 시스템 : 응답시간(response time) - 사용자 요청이 처리되는데 걸리는 시간

성능 수치는 고정적이지 않다. 예를 들어 응답시간은 애플리케이션의 쓰레드 풀 상태, GC Pause time, TCP 패킷 유실, 디스크 캐시 폴트, 하드웨어 발열 진동 등 여러 변수로 인해 유동적이다.

응답시간을 분석할 때 평균을 사용하기도한다. 그러나 평균은 극단적인 수치에 영향을 받고, 평균값이 대략 몇명의 사용자가 겪는지 알 수 없다.

그래서 백분위수(percentile)을 사용한다. 응답시간의 중간값(median)은 대체로 사용자 절반이 해당 수치 이하로 응답을 받는다고 말할 수 있다. 중간값은 곧 50번째 백분위수이며, p50p50라고 표현한다. 극단적인 응답시간을 겪는 경우를 알아보려면 흔히 p95p95, p99p99, p99.9p99.9를 본다. 이렇게 큰 백분위수를 꼬리 지연(tail latencies)이라 한다.

응답시간이 길어지면 서비스에 나쁜 영향이 갈 수 있다. 실제로 아마존은 응답시간이 100ms 늘 경우, 매출의 1%가 줄어든다고 분석했다. 아마존은 특히 꼬리 지연 시간을 신경쓰는데, 응답시간이 느렸던 사용자는 구매이력이 많은, 곧 데이터가 많은 핵심 사용자일 수 있기 때문이다. 그러나 일반적으론 꼬리 지연 시간을 줄이는 노력에 비해 효과는 미미하다.

그리고 응답시간은 클라이언트 쪽에서 측정해야한다. 서버는 동시에 처리가능한 요청의 양이 정해져 있다. 유독 처리가 늦는 요청이 있다면 뒤이은 요청을 빠르게 처리하더라도 클라이언트가 받는 응답이 전반적으로 느려지기 때문이다. 이를 HOL 블로킹(head-of-line)이라고 한다.

부하를 극복하는 방법

  • 수직적 / 수평적 확장
    • 서버의 스펙을 늘리는 수직적 확장(vertical scaling, scaling up)
    • 서버의 대수를 늘리는 수평적 확장(horizontal scaling, scaling out)
    • 실무에선 어느 하나를 선택하는게 아닌, 두 방법을 동시에 사용한다.
  • 탄력적(elastic) / 수동 스케일링
    • 로드가 증가하면 자동으로 스케일링한다. 로드가 예측하기 어려울때 유용하다.
    • 그렇지만 관리 요소가 많아지므로 사람이 수동으로 판단하는게 더 나을 때도 있다.

데이터베이스 같이 하나의 상태(데이터)를 서로 공유해야하는 시스템을 유상태(stateful) 데이터 시스템이라한다. 상태를 공유하는 만큼 부하를 분산시키기는 어렵다. 그래서 최후의 수단으로 분산 환경을 구축한다. 반면, 무상태(stateless, share-nothing) 데이터 시스템은 그런 걱정이 없어 분산 환경을 구축하기 쉽다.

큰 규모의 연산을 처리하는 시스템의 아키텍쳐는 그 애플리케이션에 크게 좌지우지된다. 마찬가지로 모든 경우를 커버해주는 은총알은 없다. 그래도 확장성 있는 시스템의 아키텍처는 일반적으로 패턴이 있다. 입/출력의 용량, 데이터의 용량 및 복잡도 등 여러 사항을 고려하고 적절한 패턴을 조합해 문제를 해결할 수 있다. 앞으로 이 책에서는 이런 패턴에 대해서 알아볼 것이다.

확장성을 고려한 시스템 설계에선 어떤 연산이 흔하고, 드문지에 대한 추정을 기반한다. 즉, 부하 인자를 예상해야 하는데, 잘못 예상하면 여태 작업한게 말짱도루묵이 되어버린다. 생산성에 크게 악영향을 줄 수 있기때문에, 초기 스타트업이나 신규런칭 서비스의 경우 미래를 예상해 대비하기보다 기능 출시에 맞춰 빠르게 대응할 수 있어야 한다.

유지보수 용이성

가장 많은 비중을 차지하는 소프트웨어의 비용(돈, 시간)은 초기개발이 아니라 유지보수다. 특히 레거시(legacy) 시스템를 유지보수하는 건 정말 끔찍하다. 모든 이들이 기피하고 싶어하는 영역이다. 그러나 이런 시스템도 누군가가 만들었기에 레거시 시스템이 된 것이다.

그렇기에 우리가 만든 시스템이 레거시가 되어 또 다른 이들의 발목을 잡지 않도록 노력해야한다. (물론 완벽할 순 없겠지만) 유지보수가 용이한 시스템을 만들기 위해서 신경써야할 부분 세 가지를 보자.

  • 운영 용이성 : 운영팀이 시스템을 관리하기 쉽게 만든다.
    • 운영팀의 책임 : 모니터링, 디버깅, 플랫폼 최신화, 배포 시스템 구축, 시스템 관련 사내 지식 보존화 작업 등
    • 관리팀이 생산성이 높은 일에 집중할 수 있게끔, 반복적인 일을 자동화하거나 쉽게 만들어줘야한다. 예를 들어
      1. 잘 구축된 모니터링으로 시스템 내부에서 어떤 일이 일어나는지 가시성을 제공해야한다.
      2. 자동화 및 일반적인 툴을 쉽게 사용할 수 있도록 지원해야한다.
      3. 각 서버 머신들이 서로 의존하지 않도록 만든다. (점검시 하나를 죽여도 정상 동작하게끔.)
      4. 잘 작성된 문서를 제공하고, 동작 방식을 이해하기 쉽게 만든다. (X를 하면 Y가 일어날 것이다.)
      5. 기본 설정만으로 충분히 좋은 시스템을 제공하면서, 관리자가 필요하면 직접 설정할 수 있게 제공한다.
      6. 예외를 최소화해 예측하기 쉽게 만든다.
  • 간결성 : 새로 들어온 엔지니어도 이해하기 쉬운 시스템을 만든다.
    • 코드가 커지면 커질 수록 복잡성은 커지기 마련이다. 복잡성은 유지보수를 힘들게할 뿐더러 비용을 더 발생시킨다.
    • 복잡성의 수렁에 빠진 코드를 겁나 큰 진흙공이라고 부른다.
    • 복잡한 시스템을 간결화한다는건 꼭 기능 제거를 말하지 않는다. 개발자가 소프트웨어를 만들면서 생긴 의도하지않은 복잡성(accidental complexity)을 없애는 것이다.
    • 의도하지않은 복잡성을 제거하는 대표적인 방법은 추상화다. 그러나 올바른 추상화를 찾기란 어렵다.
      • 좋은 추상화란 세부적인 구현을 숨겨 깔끔하고 이해하기 쉬운 사용 방법을 제공한다.
      • 덕분에 어느 하나에 귀속되지않고 여러 군데서 재사용될 수 있다.
      • 예를 들어
        1. SQL : 데이터베이스의 구현을 몰라도 CRUD를 가능하게 해준다.
        2. 고수준 프로그래밍 언어 : 기계어나 CPU의 동작 방식을 몰라도 프로그램을 만들 수 있다.
        3. 운영체제 : 사용자나 프로세스가 파일 관리나 명령어 스케줄링을 직접하지 않아도 된다.
  • 진화 가능성 : 미래의 기능 변경사항을 대응하기 쉽게 만든다.
    • 시스템 요구사항이 안바뀔일은 없다. 사용자 피드백을 받았건, 기능이 추가되던, 비즈니스 우선순위가 바뀌던.
    • 조직 관점으로 보자면, 애자일(agile;민첩한) 업무 방식은 빠르게 변화하는 환경의 대처법을 제안한다.
    • 애자일 커뮤니티는 이를 위한 기술 패턴이나 도구를 만들었는데, TDD나 리팩토링 같은 기법이 있다.
      • 그러나 이런 기술은 소스코드 몇개처럼 작은 규모에 집중하기에, 이 책에서는 데이터 시스템 단에서 어떻게 애자일(민첩)하게 대응할 수 있을 지를 고민한다.
    • 데이터 시스템을 변경하거나 새로운 요구사항에 쉽게 변화할 수 있는 지는 간결성과 추상화에 크게 좌우된다.

데이터 모델과 쿼리 언어

언어의 한계는 곧 세상의 한계이다.

루드비히 비트겐슈타인, 논리-철학 논고 (1922)

데이터 모델이 중요한 이유는 소프트웨어의 작성 방식을 가를 뿐만 아니라 우리가 문제를 접근하는 방식도 가르기 때문이다. 대다수의 애플리케이션은 데이터 모델을 여러 레이어로 나눈다. (ex: 실제세계 <-> 애플리케이션 <-> 데이터베이스) 이는 하위 레이어의 복잡함을 가려 여러 그룹의 사람들이 서로 효율적으로 일 할 수 있도록 만든다. 데이터 모델은 소프트웨어가 할 수 있는 역량의 범위를 결정한다. 때문에 어떤 데이터 모델을 써야할지 아는게 중요하다.

관계형 모델 vs 도큐먼트 모델

관계형 모델은 1970년에 에드가 코드가 제안되어 80년대 중반에 관계형 데이터베이스로 구현됐다. 관계형 모델은 관계(SQL: 테이블)와 튜플(SQL: ROW)로 이루어졌다. 관계형 데이터베이스는 깔끔한 인터페이스를 바탕으로 애플리케이션 개발자가 세부 구현을 알 필요가 없었다.

그동안 관계형 모델의 경쟁자로 여러 모델이 있었다. 1970년대와 1980년대 초엔 네트워크 모델과 계층형 모델, 1980년대와 1990년대에는 객체 데이터베이스, 2000년대에는 XML 데이터베이스가 나타났으나 많이 사용되진 않았다.

그러다 2010년대에 NoSQL이 등장했다. NoSQL은 Not Only SQL(SQL 말고)의 약어다. 관계형 데이터베이스의 한계나 불만에서 비롯되어 만들어진 개념이다.

객체-관계 패러다임의 불일치
현대의 애플리케이션 개발은 대부분 OOP 언어로 만들어진다. 이는 관계형 테이블과의 조화가 좋지 않다. 이를 패러다임의 불일치(impedance mismatch)라고 한다. 이와 관련한 내용은 여기서 더 알아볼 수 있다.

ActiveRecord, Hibernate 같은 객체-관계 매핑(ORM) 프레임워크가 두 모델 사이의 차이를 극복할 수 있도록 도와주지만, 차이를 완전히 숨길 수는 없다.

일대다 관계
페이스북 프로필 예로 들어보자. 프로필에는 여러 정보가 들어갈 수 있다. 학력, 경력, 친구 목록, 팔로우 목록 등이 있을 수 있다. 이 중 사용자-학력 관계를 한번 표현해보자.

한 사용자는 여러 학력을 가질 수 있다. 즉 일대다 관계를 가질 수 있는데 이는 여러 방식으로 표현될 수 있다.

  • 정규화 표현 :
  • XML/JSON 튜플 :
  • XML/JSON 컬럼 :

스토리지와 탐색

작성중...

직렬화와 호환성

작성중...

--------------------