티스토리 뷰

반응형

오늘날 많은 애플리케이션은 계산 중심(compute-intensive)과는 다르게 데이터 중심(data-intensive)적이다. 이러한 애플리케이션의 경우 CPU 성능은 애플리케이션을 제한하는 요소가 아니며, 더 큰 문제는 보통 데이터의 양, 데이터의 복잡도, 데이터의 변화속도다.

일반적으로 데이터 중심 애플리케이션은 공통으로 필요로 하는 기능을 제공하는 표준 구성 요소(standard building block)으로 만든다. 예를 들어, 많은 애플리케이션은 다음을 필요로 한다.

- 구동 애플리케이션이나 다른 애플리케이션에서 나중에 다시 데이터를 찾을 수 있게 데이터를 저장(데이터베이스)
- 읽기 속도 향상을 위해 값비싼 수행 결과를 기억(캐시)
- 사용자가 키워드로 데이터를 검색하거나 다양한 방법으로 필터링 할 수 있게 제공(검색 색인(search index))
- 비동기 처리를 위해 다른 프로세스로 메시지 보내기(스트림 처리(stream processing))
- 주기적으로 대량의 누적된 데이터를 분석(일괄 처리(batch processing))

너무나 뻔한 말처럼 들린다면 이는 언제나 많은 생각 없이 사용할 수 있게 데이터 시스템이 성공적으로 추상화 되었기 때문이다. 애플리케이션을 만들 때 대부분의 엔지니어들은 처음부터 새로운 데이터 저장소 엔진을 작성하는 모습을 상상하지 않는다. 왜냐하면 데이터베이스가 데이터 저장 작업을 위해 더할 나위 없이 좋은 도구이기 때문이다.

하지만 현실은 그리 간단하지 않다. 애플리케이션마다 요구사항이 다르기 때문에 데이터베이스 시스템 또한 저마다 다양한 특성을 가지고 있다. 캐싱을 위한 다양한 접근방시과 검색 색인을 구축하는 여러가지 방법드잉 있다. 애플리케이션을 만들 때 어떤 도구와 어떤 접근 방식이 수행중인 작업에 가장 적합한지 생각해야 한다. 단 하나의 도구만으로 할 수 없는 것을 해야하는 경우 도구들을 결합하기 어려울 수 있다.

이 책은 데이터 시스템의 원칙(principle)과 실용적(practicality), 그리고 이를 활용한 데이터 중심 애플리케이션을 개발하는 방법을 모두 담고 있다. 이 책에서 소개된 다양한 도구가 공통으로 지닌 것은 무엇이고 서로 구별되는 것은 무엇인지, 그리고 어떻게 그러한 특성을 구현해냈는지 알아본다.

신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 데이터 시스템을 구축하기 위한 기초적인 노력을 살펴보는 것으로 이번 장을 시작한다. 신뢰성, 확장성, 유지보수성의 의미를 명확히 하고 이를 고려하는 몇 가지 방법을 개략적으로 설명하고 이후 장에서 필요한 기본 사항을 거듭 검토한다. 다음장에서는 데이터 중심 애플리케이션을 개발할 때 고려해야 할 다양한 설계 결정 사항을 계층별로 계속 살펴본다.

데이터 시스템에 대한 생각

일반적으로 데이터베이스, 큐, 캐시 등을 매우 다른 범주에 속하는 도구로 생각한다. 데이터베이스와 메시지 큐는 표면적으로 비슷하더라도(둘 다 얼마 동안 데이터를 저장) 매우 다른 접근 패턴을 갖고 있어 서로 다른 성능 특성이 있기 때문에 구현 방식이 매우 다르다.

그러면 모든 것을 왜 데이터 시스템이라는 포괄적 용어로 묶어야 할까?

데이터 저장과 처리를 위한 여러 새로운 도구는 최근에 만들어졌다. 새로운 도구들은 다양한 사용사례(use case)에 최적화됐기 때문에 더 이상 전통적인 분류에 딱 들어맞지 않는다. 예를 들어 메시지 큐로 사용하는 데이터스토어(datastore)인 레디스(Redis)가 있고, 데이터베이스 처럼 지속성(durability)을 보장하는 메시지 큐인 아파치 카프카(Apache Kafka)도 있다. 분류 간 경계가 흐려지고 있다.

두 번째로 점점 더 많은 애플리케이션이 단일 도구로는 더 이상 데이터 처리와 저장 모두를 만족시킬 수 없는 과도하고 광범위한 요구사항을 갖고 있다. 대신 작업(work)은 단일 도구에서 효율적으로 수행할 수 있는 태스크(task)로 나누고 다양한 도구들은 애플리케이션 코드를 이용해 서로 연결 한다.

예를 들어 메인 데이터베이스와 분리된 애플리케이션 관리 캐시 계층(멤캐시디(Memcached)나 유사한 도구를 사용)이나 엘리스틱서치(Elasticsearch)나 솔라(Solr) 같은 전문(full-text) 검색 서버의 경우 메인 데이터베이스와 동기화된 캐시나 색인을 유지하는 것은 보통 애플리케이션 코드의 책임이다. 다음 그림은 이 같은 모습을 살짝 보여준다.

서비스 제공을 위해 각 도구를 결합할 때 서비스 인터페이스나 애플리케이션 프로그래밍 인터페이스(API)는 보통 클라이언트가 모르게 구현 세부 사항을 숨긴다. 기본적으로 좀 더 작은 범용 구성 요소들로 새롭고 특수한 목적의 데이터 시스템을 만든다. 복합 데이터 시스템(composite data system)은 외부 클라이언트가 일관된 결과를 볼 수 있게끔 캐시를 오바르게 무효화하거나 업데이트하는 등의 특정 보장 기능을 제공할 수 있다. 이제부터 개발자는 애플리케이션 개발자 뿐만 아니라 데이터 시스템 설계자 이기도 하다.

데이터 시스템이나 서비스를 설계할 때 까다로운 문제가 많이 생긴다. 내부적으로 문제가 있어도 데이터를 정확하고 완전하게 유지하려면 어떻게 해야할까? 시스템의 일부 성능이 저하되더라도 클라이언트에 일관되게 좋은 성능을 어떻게 제공할 수 있을까? 부하 증가를 다루기 위해 어떻게 규모를 확장할까? 서비스를 위해 좋은 API는 어떤 모습일까?

관련자의 기술 숙련도, 기존 시스템의 의존성, 전달 시간 척도, 다양한 종류의 위험에 대한 조직의 내성, 규제 제약 등은 시스템 설계에 영향을 줄 수 있는 많은 요소다. 이런 요소는 상황에 크게 좌우 된다.

이 책에서는 대부분의 소프트웨어 시스템에서 중요하게 여기는 세 가지 관심사에 중점을 둔다.

신뢰성(Reliability) 하드웨어나 소프트웨어 결함, 심지어 인적 오류(human error) 같은 역경에 직면하더라도 시스템은 지속적으로 올바르게 동작(원하는 성능 수준에서 정확한 기능을 수행)해야 한다.

확장성(Scalabiltiy) 시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다.

유지보수성(Maintainability) 시간이 지남에 따라 여러 다양한 사람들이 시스템 상에서 작업(현재 작업을 유지보수하고 새로운 사용 사례를 시스템에 적용하는 엔지니어링과 운영)할 것이기 때문에 모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야한다.

이 용어들은 종종 그 의미를 명확하게 이해하지 못한 채 사용되곤 한다. 이번 장의 나머지 부분에서는 엔지니어링 관점에서 신중하게 신뢰성, 확장성, 유지보수성을 생각하는 방법을 살펴본다. 다음 장에서는 이러한 목표를 달성하기 위한 다양한 기술과 아키텍처, 알고리즘을 설명한다.

 신뢰성

누구나 어떤 것을 신뢰하거나 신뢰하지 않는다는 의미가 무엇인지에 대한 직관적인 개념을 가지고 있다. 소프트웨어의 경우 일반적인 기대치는 다음과 같다.

- 애플리케이션은 사용자가 기대한 기능을 수행한다.
- 시스템은 사용자가 범한 실수나 예상치 못한 소프트웨어 사용법을 허용할 수 있다.
-시스템 성능은 예상된 부하와 데이터 양에서 필수적인 사용 사례를 충분히 만족한다.
- 시스템은  허가되지 않은 접근과 오남용을 방지한다.

이 모든 것이 '올바르게 동작함'을 의미하는 경우, 대략 '무언가 잘못되더라도 지속저으로 올바르게 동작함'을 신뢰성의 의미로 이해할 수 있다.

잘못될 수 있는 일을 결함(fault)이라 부른다. 그리고 결함을 예측하고 대처할 수 있는 시스템을 내결함성(fault-tolerant) 또는 탄력성(resilient)을 지녔다고 말한다. 내결함성이라는 용어는 약간 오해의 소지가 있다. 모든 종류의 결함을 견딜 수 있는 시스템을 만들 수 있음을 시사하지만 실제로는 실현 가능하지 않다. 예를 들어 블랙홀이 지구와 지구 상의 모든 서버를 삼켜버려도 웹 호스팅이 가능한 내결함성을 지닐 순 없다. 따라서 특정 유형의 결함 내성에 대해서만 이야기하는 것이 타당하다.

결함은 장애(failure)와 동일하지 않다. 일반적으로 결함은 사양에서 벗어난 시스템의 한 구성 요소로 정의되지만, 장애는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우다. 결함 확률을 0으로 줄이는 것은 불가능하다. 따라서 대개 결함으로 인해 장애가 발생하지 않게끔 내결함성 구조를 설계하는 것이 가장 좋다. 이 책에서는 신뢰할 수 없는 여러 부품으로 신뢰할 수 있는 시스템을 구축하는 다양한 기법을 다룬다.

반직관적이지만 이러한 내결함성 시스템에서 경고 없이 개별 프로세스를 무작위로 죽이는 것과 같이 고의적으로 결함을 일으켜 결함률을 증가시키는 방법은 납득할 만하다. 실제로 많은 중대한 버그는 미흡한 오류 처리에 기인한다. 고의적으로 결함을 유도함으로써 내결함성 시스템을 지속적으로 훈련하고 테스트해서 결함이 자연적으로 발생했을 때 올바르게 처리할 수 있다는 자신감을 높인다. 넷플릭스(Netflix)의 카오스 몽키(Chaos Monkey)가 이런 접근방식의 한 예다.

일반적으로 결함 예방을 넘어 내결함성을 갖기를 선호하지만 예방책이 해결책보다 더 좋은 경우(예를 들어 해결책이 없기 때문에)가 있다. 바로 보안 문제다. 예를 들어 공격자가 시스템을 손상시키고 민감한 데이터에 대한 접근 권한을 얻는다면 이를 되돌릴 수 없다. 이 책은 다음 절에서 설명하는 것처럼 해결책이 있는 결함 유형을 다룬다.

하드웨어 결함

시스템 장애의 원인을 생각할 때 하드웨어 결함이 바로 떠오른다. 하드디스크가 고장 나고, 램에 결함이 생기고, 대규모 정전 사태가 발생하고, 누군가가 네트워크 케이블을 잘못 뽑는 것과 같은 결함을 말한다. 규모가 큰 데이터센터에서 일하는 사람은 많은 장비를 다룰 경우 이같은 일이 늘상 일어난다고 말한다.

하드디스크의 평균 장애 시간(mean time to failure, MTTF)은 약 10 ~ 50년으로 보고됐다. 따라서 10,000개의 디스크로 구성된 저장 클러스터는 평균적으로 하루에 한 개의 디스크가 죽는다고 예상해야 한다. (MTTF = 가동 시간/ 장애횟수 이므로 장애 횟수는 가동시간을 MTTF로 나누면된다. 따라서 하루 장애 횟수는 10,000 (장비수) * 1 / (30 * 365) = 1 이 된다.)

시스템 장애율을 줄이기 위한 첫 번쨰 대응으로 각 하드웨어 구성 요소에 중복(redundancy)을 추가하는 방법이 일반적이다. 디스크는 RAID 구성으로 설치할 수 있고 서버는 이중 전원 디바이스와 핫 스왑(hot-swap) 가능한 CPU를, 데이터센터는 건전지와 예비 전원용 디젤 발전기를 갖출 수 있다. 구성 요소 하나가 죽으면 고장 난 구성 요소가 교체되는 동안 중복된 구성 요소를 대신 사용할 수 있다. 이런 접근 방식은 하드웨어 문제로 장애가 발생하는 것을 완전히 막을 수는 없지만 이해하기 쉽고 보통 수년간 장비가 중단되지 않고 계속 동작할 수 있게 한다.

최근까지 단일 장비의 전체 장애는 매우 드물기 때문에 대부분의 애플리케이션은 하드웨어 구성 요소의 중복으로 충분했다. 새 장비에 백업을 매우 빠르게 복원할 수 있는 한, 장애 발생 시 중단 시간(downtime)은 대부분의 애플리케이션에 치명적이지 않다. 따라서 다중 장비 중복은 고가용성 (high availability)이 절대적으로 필수적인 소수의 애플리케이션에서만 필요했따.

하지만 데이터 양과 애플리케이션의 계산 요구가 늘어나면서 더 많은 애플리케이션이 많은 수의 장비를 사용하게 됐고 이와 비례해 하드웨어 결함율도 증가했다. 또한 아마존 웹 서비스(Amazon Web Service, AWS) 같은 일부 클라우드 플랫폼은 가상 장비 인스턴스가 별도의 경고없이 사용할 수 없게 되는 상황이 상당히 일반적이다. 이런 플랫폼은 단일 장비 신뢰성보다 유연성(flexibility)과 탄력성(elasticity)을 우선적으로 처리하게끔 설계됐기 때문이다.

따라서 소프트웨어 내결함성 기술을 사용하거나 하드웨어 중복성을 추가해 전체 장비의 손실을 견딜 수 있는 시스템으로 점점 옮겨가고 있다. 게다가 이런 시스템에는 운영상 장점이 있다. 장비를 재부팅해야하는 경우 (예를 들어 운영체제 보안 패치를 적용) 단일 서버 시스템은 계획된 중단시간이 필요하지만 장비 장애를 견딜 수 있는 시스템은 전체 시스템의 중단시간 없이 한 번에 한 노드씩 패치 할 수 있다.

소프트웨어 오류

보통 하드웨어 결함을 무작위적이고 서로 독립적이라고 생각한다. 즉 한 장비의 디스크에 장애가 있다고 해서 다른 장비의 디스크에 장애가 발생하지 않는다는 뜻이다. 약하게 상관관계(예를 들어 서버 랙의 온도 같은 공통 원인)가 있을 수도 있다. 그렇지 않다면 다수의 하드웨어 구성 요소에 동시에 장애가 발생하지는 않는다

또 다른 부류의 결함으로 시스템 내 체계적 오류(systematic error)가 있다. 이 결함은 예상하기가 더 어렵고 노드 간 상관관계 때문에 상관관계 없는 하드웨어 결함보다 오히려 시스템 오류를 더욱 많이 유발하는 경향이 있다. 예를 들면 다음과 같다.

- 잘못된 특정 입력이 있을 때 모든 애플리케이션 서버 인스턴스가 죽는 소프트웨어 버그. 예를 들어 리눅스 커널의 버그로 인해 많은 애플리케이션이 일제히 멈춰버린 원인이 된 2012년 6월 30일 윤초를 생각해보자.
- CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭처럼 공유 자원을 과도하게 사용하는 일부 프로세스 
- 시스템의 속도가 느려져 반응이 없거나 잘못된 응답을 반환하는 서비스
- 한 구성 요소의 작은 결함이 다른 구성 요소의 결함을 야기하고 차례차례 더 많은 결함이 발생하는 연쇄 장애(cascading failure)

이 같은 소프트웨어 결함을 유발하는 벅느느 특정 상황에 의해 발생하기 전까지 오랫동안 나타나지 않는다. 이런 상황으로 볼 때 소프트웨어에는 환경에 대한 일종의 가정이 있다는 사실을 알 수 있다. 이 가정은 대개 사실이지만 어떤 이유로 최종적으로는 사실이 아니게 된다.

소프트웨어의 체계적 오류 문제는 신속한 해결책이 없다. 시스템의 가정과 상호작용에 대해 주의 깊게 생각하기, 빈틈없는 테스트, 프로세스 격리(process isolation). 죽은 프로세스의 재시작 허용. 프로덕션 환경에서 시스템 동작의 측정. 모니터링. 분석하기와 같은 여러 작은 일들이 문제 해결에 도움을 줄 수 있다. 시스템이 뭔가 보장하길 기대한다면(예를 들어 메시지 큐에 수신된 메시지 수와 송신된 메시지 수가 같다) 수행 중에 이를 지속적으로 확인해 차이가 생긱는 경우 경고를 발생시킬 수 있다.

인적 오류

사람은 소프트웨어 시스템을 설계하고 구축하며, 운영자로서 시스템을 계속 운영한다. 이들이 최선의 의도를 갖고 있어도 사람은 미덥지 않다고 알려져 있다. 예를 들어 대규모 인터넷 서비스에 대한 연구에 따르면 운영자의 설정 오류가 중단의 주요 원인인 반면 하드웨어(서버나 네트워크) 결함은 중단 원인의 10~25% 정도에 그친다.

사람이 미덥지 않음에도 시스템을 어떻게 신뢰성 있게 만들까? 최고의 시스템은 다양한 접근 방식을 결합한다.

- 오류의 가능성을 최소화하는 방향으로 시스템을 설계하라. 예를 들어 잘 설계된 추상화, API, 관리 인터페이스를 사용하면 "옳은 일"은 쉽게 하고, "잘못된 일"은 막을 수 있다. 하지만 인터페이스가 지나치게 제한적이면 사람들은 좋은 점을 잊은 채 제한된 인터페이스를 피해 작업한다. 따라서 이런 시스템 설계는 올바르게 작동하게끔 균형을 맞추기가 어렵다.
- 사람이 가장 많이 실수하는 장소(부분)에서 사람의 실수로 장애가 발생할 수 있는 부분을 분리하라. 특히실제데이터를 사용해 안전하게 살펴보고 실험할 수 있지만 실제 사용자에게는 영향이 없는 비 프로덕션 샌드박스(sandbox)를 제공하라.
- 단위 테스트부터 전체 시스템 통합 테스트와 수동 테스트까지 모든 수준에서 철저하게 테스트하라. 자동 테스트는 널리 사용되며 잘 알려져 있다. 특히 정상적인 동작에서는 거의 발생하지 않는 코너케이스를 다루는데 유용하다.
- 장애 발생의 영향을 최소화 하기 위해 인적 오류를 빠르고 쉽게 복구할 수 있게하라. 예를 들어 설정 변경 내역을 빠르게 롤백하고 새로운 코드를 서서히 롤아웃하게 만들고(예상치 못한 버그가 일부 사용자에게만 영향이 미치게 함) 이전 계산이 잘못된 경우를 대비해 데이터 재계산 도구를 제공하라.
- 성능 지표와 오류율 같은 상세하고 명확한 모니터링 대책을 마련하라. 모니터링을 다른 엔지니어링 분야에서는 원격 측정(telemetry)이라고 부른다. 일단 로켓이 발사되면 원격 측정은 일어나는 일을 추적하고 장애 해석을 위해 필수적이다. 모니터링은 조기에 경고 신호를 보내줄 수 있고 특정 가정이나 제한을 벗어나는지 확인할 수 있게한다. 문제가 발생했을 때 지표(metrics)는 문제를 분석하는데 매우 중요하다.
- 조작 교육과 실습을 시행하라. 까다롭지만 매우 중요한 측면이다. 하지만 책의 범위를 벗어나므로 여기서 다루지는 않는다.

신뢰성은 얼마나 중요할까?

신뢰성은 원자력 발전소나 항공 교통 관제 소프트웨어만을 위한 것이 아니다. 더 많은 수의 일상적인 애플리케이션도 안정적으로 작동해야 한다. 비즈니스 애플리케이션에서 버그는 생산성 저하의 원인이고(공식 자료 수치를 잘못 전달하면 법적으로 위험하다) 전자 상거래 사이트의 중단은 매출에 손실이 발생하고 명성에 타격을 준다는 면에서 많은 비용이 든다.

'중요하지 않은' 애플리케이션도 사용자에 대한 책임(responsibility)이 있다. 사진 애플리케이션에 아이들의 사진과 동여상을 모두 보관한 부모를 생각해보자. 사진과 동영상이 보관된 데이터베이스에 갑자기 오류가 생기면 부모는 어떻게느낄까? 그들은 백업을 복원하는 방법을 알고 있을까?

증명되지 않은 시장을 위해 시제품을 개발하는 비용이나 매우 작은 이익률의 서비슬르 운영하는 비용을 줄이려 신뢰성을 희생해야 하는 상황이 있다. 하지만 이 겨웅에는 비용을 줄여야 하는 시점을 매우 잘 알고 있어야 한다.

확장성

시스템이 현재 안정적으로 동작한다고 해서 미래에도 안정적으로 동작한다는 보장은 없다. 성능 저하를 유발하는 흔한 이유 중 하나는 부하 증가다. 어쩌면 시스템의 동시 사용자 수가 1만 명에서 10만 명 또는 100만 명에서, 1000만 명으로 증가했을 수도 있다. 시스템은 전에 처리했던 양보다 더 많은 데이털르 처리하고 있을지도 모른다.

확장성은 증가한 부하에 대처하는 시스템 능력을 설명하는 데 사용하는 용어지만 시스템에 부여하는 일차적인 표식이 아님을 주의하자. 'X는 확장 가능하다' 또는 'Y는 확장성이 없다' 같은 말은 의미가 없다. 오히려 확장성을 논한다는 것은 '시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇인가?'와 '추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?' 같은 질문을 고려한다는 의미다.

부하 기술하기

무엇보다 시스템의 현재 부하를 간결하게 기술해야한다. 그래야 부하 성장 질문(부하가 두 배로 되면 어떻게 될까?)을 논의할 수 있다. 부하는 부하 매개변수(load parameter)라 부르는 몇 개의 숫자로 나타낼 수 있다. 가장 적합한 부하 매개변수 선택은 시스템 설계에 따라 달라진다. 부하 매개변수로 웹 서버의 초당 요청 수, 데이터베이스의 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자(active user), 캐시 적중률 등이 될 수 있다. 평균적인 경우가 중요할 수도 있고 소수의 극단적인 경우가 병목 현상의 원인이 될 수도 있다. 

이를 조금 더 구체적으로 설명하기 위해 트위터(Twitter)를 예로 살펴보자. 주요 두 가지 동작은 다음과 같다.

트윗 작성: 사용자는 팔로워에게 새로운 메시지를 게시할 수 있다.(평균 초당 4.6k 요청, 피크일 때 초당 12k 이상).
홈 타임라인: 사용자는 팔로우 한 사람이 작성한 트윗을 볼 수 있다.(초당 300k 요청)

단순히 초당 12,000건의 쓰기(피크일 때의 트윗 작성 속도) 처리는 상당히 쉽다. 하지만 트위터의 확장성 문제는 주로 트윗 양이 아닌 팬 아웃(fan-out) 때문이다. 개별 사용자는 많은 사람을 팔로우하고 많은 사람이 개별 사용자를 팔로우 한다. 이 두 가지 동작을 구현하는 방법은 크게 두 가지다.

1. 트윗 작성은 간단히 새로운 트윗 전역 컬렉션에 삽입한다. 사용자가 자신의 홈 타임라인을 요청하면 팔로우하는 모든 사람을 찾고, 이 사람들의 모든 트윗을 찾아 시간순으로 정렬해서 합친다. 다음 그림과 같은 관계형 데이터베이스에서는 다음과 같이 질의한다.

SELECT tweets.*, users.* FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user

2. 각 수신 사용자용 트윗 우편함처럼 개별 사용자의 홈 타임라인 캐시를 유지한다. 사용자가 트윗을 작성하면 해당 사용자를 팔로우하는 사람을 모두 찾고 팔로워 각자의 홈 타임라인에 캐시에 새로운ㅌ느윗을 삽입한다. 그러면 홈 타임라인의 읽기 요청은 요청 결과를 미리 게싼했기 때문에 비용이 저렴하다.

트위터의 첫 번째 버전은 접근 방식 1을 사용했는데, 시스템이 홈 타임알인 질의 부하를 버텨내기 위해 고군분투했고 그 결과 접근 방식 2로 전환했다. 평균적으로 트윗 게시 요청량이 홈 타임라인 읽기 요청량에 비해 수백 배 적기 때문에 접근 방식 2가 훨씬 잘 동작한다. 그래서 이 경우에는 쓰기 시점에서 더 많은 일을 하고, 읽기 시점에 적은 일을 하는 것이 바람직하다.

하지만 접근 방식 2의 불리한 점은 이제 트윗 작성이 많은 부가 작업을 필요로 한다는 점이다. 평균적으로 트윗이 약 75명의 팔로워에게 전달되므로 초당 4.7k 트윗은 홈 타임라인 캐시에 초당 345k 건의 쓰기가 된다. 그러나 평균값은 사용자마다 팔로워 수가 매우 다르다는 사실을 가린다. 즉 일부 사용자는 팔로워가 3천만 명이 넘는다. 이것은 단윌 트윗이 홈 타임라인에 3천만 건 이상의 쓰기 요청이 될지도 모른다는 의미다! 적시에 트윗을 전송하는 작업이(트위터는 5초 이내에 팔로워에게 트윗을 전송하려고 노력한다.) 중요한 도전과제다.

트위터 사례에서 사용자당 팔로워의 분포(해당 사용자의 트윗 빈도에 따라 편중도리 수도 있음) 는 팬 아웃 부하를 결정하기 때문에 확장성을 논의할 때 핵심 부하 매개변수가 된다. 애플리케이션마다 특성은 매우 다르지만 부하에 대한 추론에 비슷한 원리를 적용할 수 있다.

트위터 일화의 최종 전개는 다음과 같다. 접근 방식 2가 견고하게 구현돼 트위터는 두 접근 방식의 혼합형(hybrid)으로 바꾸고 있다. 대부분 사용자의 트윗은 계속해서 사람들이 작성할 때 홈 타임라인에 펼쳐지지만 팔로워 수가 매우 많은 소수 사용자(예를 들어 유명인)는 팬 아웃에서 제외된다. 사용자가 팔로우한 유명인의 트윗은 별도로 가져와 접근 방식 1처럼 읽는 시점에 사용자의 홈 타임라인에 합친다. 이 혼합형 접근 방식은 좋은 성능으로 지속적인 전송이 가능하다. 조금 더 기술적인 근거를 다룬 후에 12장에서 트위터 사례를 다시 설명한다.

성능 기술하기

일단 시스템 부하를 기술하면 부하가 증가할 때어떤 일이 일어나는지 조사할 수 있다. 다음 두 가지 방법으로 살펴볼 수 있다.

- 부하 매개변수를 증가시키고 시스템 자원(CPU, 메모리, 네트워크 대역폭 등)은 변경하지 않고 유지하면 시스템 성능은 어떻게 영향을 받을까?
- 부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되길 원한다면 자원을 얼마나 많이 늘려야 할까?

두 질문 모두 성능 수치가 필요하다. 따라서 시스템 성능에 대해 간단히 살펴보자.

하둡 같은 일괄 처리 시스템은 보통 처리량(throughput)(초당 처리할 수 있는 레코드 수나 일정 크기의 데이터 집합으로 작업을 수행할 때 걸리는 전체 시간)에 관심을 가진다. 온라인 시스템에서 더 중요한 사항은 서비스 응답 시간(response time), 즉 클라이언트가 요청을 보내고 응답을 받는 사이의 시간이다.

지연 시간(latency)과 응답 시간(response time)
지연 시간과 응답 시간은 종종 같은 뜻으로 사용하지만 동일하지는 않다. 응답 시간은 클라이언트 관점에서 본 시간으로, 요청을 처리하는 실제 시간(서비스 시간) 외에도 네트워크 지연과 큐 지연도 포함한다. 지연 시간은 요청이 처리되길 기다리는 시간으로 ,서비스를 기다리며 휴지(latent) 상태인 시간을 말한다.

클라이언트가 몇 번이고 반복해서 동일한 요청을 하더라도 매번 응답 시간이 다르다. 실제로 다양한 요청을 다루는 시스템에서 응답 시간은 많이 변한다. 그러므로 응답 시간은 단일 숫자가 아니라 측정 가능한 값의 분포로 생각해야 한다.

대부분의 요청은 꽤 빠르지만 가끔 오래 걸리는 특이 값(outlier)이 있다. 아마도 느린 요청은 더 많은 데이터를 처리하기 때문에 본질적으로 더 비쌀지도 모른다. 하지만 모든 요청에 동일한 시간이 걸려야 한다고 생각하는 상황에도 다양한 값을 얻게 된다. 백그라운드 프로세스의 컨텍스트 스위치(context switch), 네트워크 패킷 손실과 TCP 재전송, 가비지 컬렉션 휴지(garbage collection pause), 디스크에서 읽기를 강제하는 페이지 폴트(page fault), 서버 랙의 기계적인 진동 이나 다른 여러 원인으로 추가 지연이 생길 수 있다.

보고된 서비스 평균 응답 시간을 살피는 일은 일반적이다(엄밀히 말하면 '평균'이란 용어가 특정 공식을 의미하지는 않지만 실제로는 대개 산술 평균(arithmetic mean, n개의 값이 주어지면 모든 값을 더하고 n으로 나눔)으로 이해한다.) 하지만 '전형적인' 응답 시간을 알고 싶다면 평균은 그다지 좋은 지표가 아니다. 얼마나 많은 사용자가 실제로 지연을 경험했는지 알려주지 않기 떄문이다.

일반적으로 평균보다는 백분위(percentile)를 사용하는 편이 더 좋다. 응답 시간 목록을 가지고 가장 빠른 시간부터 제일 느린 시간까지 정렬하면 중간 지점이 중앙값(median)이 된다 .예를 들어, 중간 응답 시간이 200밀리초면 요청의 반은 200밀리초 미만으로 반환되고 나머지 반은 그보다 오래 걸린다는 뜻이다.

사용자가 보통 얼마나 오랫동안 기다려야 하는지 알고 싶다면 중앙값이 좋은 지표다. 사용자 요청의 절반은 중앙값 응답 시간 미만으로 제공되고, 나머지 반은 중앙값보다 오래 걸린다. 중앙값은 50분위로서 p50으로 축약할 수 있다. 중앙값은 단일 요청을 참고한다는 점을 주의하자. 사용자가 여러개의 요청을 보내면(세션이 많거나 한 페이지에 여러 자원이 포함돼 있어서) 최소한 하나의 요청이 중앙값보다 느릴 확률이 50%보다 훨씬 높다.

특이 값이 얼마나 좋지 않은지 알아보려면 상위 백분위를 살펴보는 것도 좋다. 이때 사용하는 백분위는 95분위, 99분위, 99.9분위가 일반적이다. (축약해서 p95, p99, p999) 요청의 95%, 99%, 99.9%가 특정 기준치보다 더 빠르면 해당 특정 기준치가 각 백분위의 응답 시간 기준치가 된다. 예를 들어, 95분위 응답 시간이 1.5초라면 100개의 요청중 95개는 1.5초 미만이고, 100개의 요청 중 5개는 1.5초보다 더 걸린다. 

꼬리 지연 시간(tail latency)으로 알려진 상위 백분위 응답 시간은 서비스의 사용자 경험에 직접 영향을 주기 때문에 중요하다. 예를 들어 아마존은 내부 서비스의 응답 시간 요구사항을 99.9분위로 기술한다. 99.9분위는 요청 1000개 중 1개만 영향이 있음에도 말이다. 보통 응답 시간이 가장 느린 요청을 경험한 고객들은 많은 구매를 해서 고객 중에서 계정에 가장 많은 데이터를 갖고 있어서다.(즉, 이 고객들은 가장 소중한 고객이다.) 웹 사이트를 빠르게 제공 가능하게끔 보장해 고객을 계속 행복하게 만드는 게 중요하다. 아마존은 응답 시간이 100밀리초 증가하면 판매량이 1% 줄어들고 1초가 느려지면 고객의 만족도 지표는 16% 줄어드는 현상을 관찰했다.

반면 99.99분위(10,000건의 요청 중 가장 느린 1건)를 최적화하는 작업에는 비용이 너무 많이 들어서 아마존이 추구하는 목표에 충분히 이익을 가져다주지 못한다고 여겨진다. 최상위 백분위는 통제할 수 없는 임의 이벤트에 쉽게 영향을 받기 때문에 응답 시간을 줄이기가 매우 어려워 이점은 더욱 줄어든다.

예를 들어 백분위는 서비스 수준 목표(service level objective, SLO)서비스 수준 협약서(service level agreement, SLA)에 자주 사용하고 기대 성능과 서비스 가용성을 정의하는 계약서에도 자주 등장한다. 다음 문장은 서비스 수준 협약서의 한 예다. "응답 시간 중앙값이 200밀리초 미만이고 99분위가 1초 미만인 경우(응답 시간이 길면 서비스가 종료될 수도 있다.) 정상 서비스 상태로 간주하며 서비스 제공 시간은 99.9%이상이어야 한다." 이런 지푠느 서비스 클라이언트의 기대치를 설정해 서비스 수준 협약서를 지키지 못하면 고객이 환불을 요구할 수 있게 한다.

큐 대기 지연(queueing delay)은 높은 백분위에서 응답 시간의 상당 부분을 차지한다. 서버는 병렬로 소수의 작업만 처리할 수 있기 때문에(예를 들어 CPU 코어 수에 제한됨) 소수의 느린 요청 처리만으로도 후석 요청 처리가 지체된다. 이 현상을 선두차단(head-of-line blocking)이라 한다. 서버에서 후속 요청이 빠르게 처리되더라도 이전 요청이 완료되길 기다리는 시간 떄문에 클라이언트는 전체적으로 응답 시간이 느리다고 생각할 것이다. 이런 문제 때문에 클라이언트 쪽 응답 시간 측정이 중요하다.

시스템의 확장성을 테스트하려고 인위적으로 부하를 생성하는 경우 부하 생성 클라이언트는 응답시간과 독립적으로 요청을 지속적으로 보내야한다. 만약 클라이언트가 다음 요청을 보내기 전에 이전 요청이 완료되길 기다리면 테스트에서 인위적으로 대기 시간을 실제보다 더 짧게 만들어 평가를 왜곡한다.

실전 백분위

상위 백분위는 단일 최종 사용자 요청의 일부로서 여러 번 호출되는 백엔드 서비스에서 특히 중요하다. 병렬로 호출해도 최종 사용자 요청은 여전히 병렬 호출 중 가장 느린 호출이 완료되길 기다려야 한다. 그림에서 볼 수 있듯이 하나의 호출만으로도 전체 최종 사용자의 요청을 느리게 할 수 있다. 작은 비율의 백엔드 호출만 느려도 최종 사용자 요청이 여러 밴 백엔드를 호출하면 느린 호출이 발생할 가능성이 증가한다. 그래서 최종사용자 요청 중 많은 비율의 응답 시간이 결국 느려진다. 이 효과를 꼬리 지연 증폭(tail latency amplification)이라 한다.

서비스의 모니터링 대시보드에 응답 시간 백분위를 추가하려면 지속적으로 백분위를 효율적으로 계산할 필요가 있다. 예를 들어, 지난 10분간 요청의 응답 시간을 롤링 윈도(rolling window)로 유지하고 싶다면 1분마다 구간 내 중앙값과 다양한 백분위를 계산해 각 지표를 그래프에 그리면된다.

단순한 구현으로 시간 구간 내 모든 요청의 응답 시간 목록을 유지하고 1분마다 목록을 정렬하는 방법이 있다. 이 구현이 너무 비효율적이라면 상황에 따라 포워드 디케이(forward decay), T 다이제스트(t-digest), Hdr히스토그램(Hdr-histogram) 같은, CPU와 메모리 비용을 최소로 하면서 좋은 백분위 근사치를 계산할 수 있는 알고리즘이 있다. 백분위 평균(예를 들어, 시간 해상도를 줄이거나 여러 장비의 데이터 를 결합하기)은 수학적으로 의미가 없으니 주의하자. 응답 시간 데이터를 집계하는 올바른 방법은 히스토그램을 추가하는 것이다.

부하 대응 접근 방식

성능 측정을 위한 부하와 지표를 기술하는 매개변수에 대해 설명했으니 본격적으로 확장성 논의를 시작한다. 부하 매개변수가 어느 정도 증가하더라도 좋은 성능을 유지하려면 어떻게 해야할까?

부하 수준 1단계에 적합한 아키텍처로는 10배의 부하를 대응할 수 없다. 급성장하는 서비스를 맡고있다면 부하 규모의 자릿수가 바뀔 때마다 혹은 그보다 자주 아키텍처를 재검토해야 할지 모른다.

사람들은 확장성과 관련해 용량 확장(scaling up)(수직 확장(vertical scaling), 좀 더 강력한 장비로 이동)과 규모 확장(scaling out(수평 확장(horizontal scaling), 다수의 낮은 사양장비에 부하를 분산)으로 구분해서 말하곤 한다. 다수의 장비에 부하를 분산하는 아키텍처를 비공유(shared-nothing) 아키텍처라 부른다. 단일 장비에서 수행될 수 있는 시스템은 보통 간단하지만 고사양 장비는 매우 비싸기 때문에 상당히 집약된 작업 부하는 대개 규모 확장을 피하지 못한다. 현실적으로 좋은 아키텍처는 실용적인 접근 방식의 조합이 필요하다. 예를 들어 적절한 사양의 장비 몇 대가 다량의 낮은 사양 가상 장비보다 여전히 훨씬 간단하고 저렴하다.

일부 시스템은 탄력적이다(elastic). 즉 부하 증가를 감지하면 컴퓨팅 자원을 자동으로 추가할 수 있다. 반면 그렇지 않은 시스템은 수동으로 확장(사람이 용량을 분석하고 시스템에 더 많은 장비 추가를 결정)해야 한다. 탄력적인 시스템은 부하를 에측할 수 없을 만큼 높은 경우 유용하지만 수동으로 확장하는 시스템이 더 간단하고 운영상 예상치 못한 일이 더 적다.

다수의 장비에 상태 비저장(stateless) 서비스를 배포하는 일은 상당히 간단하다. 하지만 단일 노드에 상태 유지(stateful) 데이터 시스템을 분산 설치하는 일은 아주 많은 복잡도가 추가적으로 발생한다. 이런 이유로 확장 비용이나 데이터베이스를 분산으로 만들어야 하는 고가용성 요구가 있을 때 까지 단일 노드에 데이터베이스를 유지하는 것(용량 확장)이 최근까지의 통념이다.

분산 시스템을 위한 도구와 추상화가 좋아지면서 이 통념이 적어도 일부 애플리케이션에서는 바뀌었다. 대용량 데이터와 트래픽을 다루지 않는 사용 사례에도 분산 데이터 시스템이 향후 기본 아키텍처로 자리잡을 가능성이 있다. 이 책의 나머지 부분에서는 많은 종류의 분산 데이터 시스템을 다루고 확장성 뿐ㅁ만 아니라 손쉬운 사용과 유지보수를 어떻게 달성하는지 설명한다.

대개 대규모로 동작하는 시스템의 아키텍처는 해당 시스템을 사용하는 애플리케이션에 특화돼 있다. 범용적이고 모든 상황에 맞는(one-size-fits-all) 확장 아키텍처(비공식적으로 마법의 확장 소스(magic scaling source)라 부른다)는 없다. 아키텍처를 결정하는 요소는 읽기의 양, 쓰기의 양, 저장할 데이터의 양 ,데이터의 복잡도, 응답 시간 요구사항, 접근 패턴 등이 있다. 혹은 (대개) 이 요소 중 일부 조합에 더 많은 문제가 추가된 경우도 있다.

예를 들어 각 크기가 1kB인 초당 100,000건의 요청을 처리하도록 설계한 시스템과 각 크기가 2GB인 분당 3건의 요청을 처리하기 위해 설계한 시스템은 서로 같은 데이터 처리량이라 해도 매우 다르다.

특정 애플리케이션에 적합한 확장성을 갖춘 아키텍처는 주요 동작이 무엇이고 잘 하지 않는 동작이 무엇인지에 대한가정을 바탕으로 구축한다. 이 가정은 곧 부하 매개변수가 된다. 이 가정이 잘못되면 확장에 대한 엔지니어링 노력은 헛수고가 되고 최악의 경우 역효과를 낳는다. 스타트업 초기 단계나 검증되지 않은 제품의 경우에 미래를 가정한 부하에 대비해 확장하기 보다는 빠르게 반복해서 제품 기능을 개선하는 작업이 좀 더 중요하다.

확장성을 갖춘 아키텍처가 특정 애플리케이션에 특화됐을지라도 이런 아키텍처는 보통 익숙한 패턴으로 나열된 범용적인 구성 요소로 구축한다. 이 책에서는 이러한 구성요소와 패턴에 대해 설명한다.

유지보수성

소프트웨어 비용의 대부분은 초기 개발이 아니라 지속해서 이어지는 유지보수에 들어간다는 사실은 잘 알려져있다. 이런 유지보수에는 버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적응, 새 사용 사례를 위한 변경, 기술 채무(techincal debt) 상환, 새로운 기능 추가 등이 있다.

유감스럽게도 여전히 소프트웨어 시스템상에서 일하는 많은 사람은 소위 레거시 시스템 유지보수 작업을 좋아하지 않는다. 어쩌면 다른 사람의 실수를 고쳐야 하거나 한물 간 플랫폼에서 작업해야하거나 정말 하기 싫은 일을 해야 하는 시스템에 관여해야 하기 때문이다. 모든 레거시 시스템은 각자 나름대로의 불편함이 있다. 그래서 이런 레거시 시스템을 다루기 위해 일반적으로 추천할 만한 방법을 제시하는 일은 매우 어렵다.

하지만 희망적인 점은 유지보수 중 고통을 최소화하고 레거시 소프트웨어를 직접 만들지 않게끔 소프트웨어를 설계할 수 있다는 것이다. 아니 꼭 그래야한다. 그러기 위해 주의를 기울여야할 소프트웨어 시스템 설계 원칙은 다음 세가지다.

운용성(operability): 운영팀이 시스템을 원활하게 운영할 수 있게 쉽게만들어라
단순성(simplicity): 시스템에서 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라(사용자 인터페이스의 단순성과는 다르다는 점에 유의하라)
발전성(evolvability): 엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하라. 그래야 요구사항 변경 같은 예기치 않은 사용 사례를 적용하기가 쉽다. 이 속성은 유연성(extensibility), 수정 가능성(modifiability), 적응성(plasticity)으로 알려져있다.

운용성: 운영의 편리함 만들기

"좋은 운영은 종종 나쁜(또는 불완전한) 소프트웨어의 제약을 피하는 대안이 될 수 있다. 하지만 좋은 소프트웨어라도 나쁘게 운영할 경우 작동을 신뢰할 수 없다"는 말이 있다. 운영 중 일부 측면은 자동화할 수 있고 또 자동화해야 한다. 그러나 자동화를 처음 설정하고 제대로 동작하는지 확인하는 일은 여전히 사람의 몫이다.

시스템이 지속해서 원하랗게 작동하려면 운영팀이 필수다. 좋은 운영팀은 일반적으로 다음과 같은 작업 등을 책임지낟.

- 시스템 상태를 모니터링하고 상태가 좋지 않다면 빠르게 서비스를 복원
- 시스템 장애, 성능 저하 등의 문제의 원인을 추적
- 보안 패치를 포함해 소프트웨어와 플랫폼을 최신 상태로 유지 
- 다른 시스템이 서로 어떻게 영향을 주는지 확인해 문제가 생길 수 있는 변경 사항을 손상을 입히기 전에 차단
- 미래에 발생 가능한 문제를 예측에 문제가 발생하기 전에 해결(예를 들어 용량 계획)
- 배포, 설정 관리 등을 위한 모범 사례와 도구를 마련
- 애플리케이션을 특정 플랫폼에서 다른 플랫폼으로 이동하는 등 복잡한 유지보수 태스크를 수행
- 설정 변경으로 생기는 시스템 보안 유지보수
- 예측 가능한 운영과 안정적인 서비스 환경을 유지하기 위한 절차 강의
- 개인 인사 이동에도 시스템에 대한 조직의 지식을 보존함

좋은 운영성이란 동일하게 반복되는 태스크를 쉽게 수행하게끔 만들어 운영팀이 고부가가치 활동에 노력을 집중한다는 의미다 .데이터 시스템은 동일 반복태스크를 쉽게하기 위해 아래 항목등을 포함해 다양한 일을 할 수 있다.

- 좋은 모니터링으로 런타임(runtime) 동작과 시스템의 내부에 대한 가시성 제공
- 표준 도구를 이용해 자동화와 통합을 위한 우수한 자원을 제공
- 개별 장비 의존성을 회피. 유지보수를 위해 장비를 내리더라도 시스템 전체에 영향을 주지 않고 계속해서 운영 가능해야 함.
- 만족할 만한 기본 동작을 제공하고, 필요할 때 기본값을 다시 정의할 수 있는 자유를 관리자에게 부여
- 적절하게 자기 회복(self-healing)이 가능할 뿐 아니라 필요에 따라 관리자가 시스템 상태를 수동으로 제어할 수 있게 함
- 예측 가능하게 동작하고 예기치 않은 상황을 최소화함

단순성: 복잡도 관리

소규모 소프트웨어 프로젝트에서는 간단하고 표현이 풍부한 코드로 말끔하게 시스템을 작성할 수 있지만 프로젝트가 커짐에 따라 시스템은 매우 복잡하고 이해하기 어려워진다. 복잡도는 같은 시스템에서 작업해야 하는 모든 사람의 진행을 느리게 하고 나아가 유지보수 비용이 증가한다. 복잡도 수렁에 빠진 소프트웨어 프로젝트를 때론 커다란 진흙 덩어리(big ball of mud)로 묘사한다.

복잡돈느 다양한 증상으로 나타난다. 상태 공간의 급증, 모듈 간 강한 커플링(tight coupling), 복잡한 의존성, 일관성 없는 명명(naming)과 용어, 성능 문제 해결을 목표로 한 해킹, 임시방편으로 문제를 해결한 특수 사례(special-casing)등이 이런 증상이다. 이 주제는 이미 많이 회자되고 있다.

복잡도 때문에 시스템 유지보수가 어려울 때 예산과 일정이 초과되곤 한다. 복잡한 소프트웨어에서는 변경이 이 있을 떄 버그가 생길 위험이 더 크다. 개발자가 시스템을 이해하고 추론하기 어려워지면 시스템에 숨겨진 가정과 의도치 않은 결과 및 예기치 않은 상호작용을 간과하기 쉽다. 반대로 복잡도를 줄이면 소프트웨어 유지보수성이 크게 향상된다. 따라서 단순성이 구축하려는 시스템의 핵심 목표여야한다.

시스템을 단순하게 만드는 일이 반드시 기능을 줄인다는 의미는 아니다. 우발적 복잡도(accidental complexity)를 줄인다는 뜻일 수 도 있다. 모슬리(moseley)와 마크스(marks)는 우발적 복잡도를 소프트웨어가 풀어야 할 (사용자에게 보이는) 문제에 내재하고 있지 않고 구현에서만 발생하는 것으로 정의했다.

우발적 복잡도를 제거하기 위한 최상의 도구는 추상화다. 좋은 추상화는 깔끔하고 직관적인 외관 아래로 많은 세부 구현을 숨길 수 있다. 또한 좋은 추상화는 다른 다양한 애플리케이션에서도 사용 가능하다. 이러한 재 사용은 비슷한 기능을 여러번 재 구현하는 것 보다 더 효율적일 뿐만 아니라 고품질 소프트웨어로 이어진다. 추상화된 구성 요소의 품질 향상이 이를 사용하는 모든 애플리케이션에 도움이 되기 때문이다.

예를 들어, 고수준 프로그래밍 언어는 기계 언어, CPU 레지스터, 시스템 호출을 숨긴 추상화다. SQL은 디스크에 기록하고 메모리에 저장한 복잡한 데이터 구조와 다른 클라이언트의 동시 요청과 고장 후 불일치를 숨긴 추상화다. 물론 고수준 언어로 프로그래밍해도 여전히 기계어를 사용한다. 단지 직접 사용하지 않을 뿐이다. 프로그래밍 언어의 추상화 덕분에 기계어를 생각할 필요가 없기 때문이다.

하지만 좋은 추상화를 찾기는 매우 어렵다. 분산 시스템 분야에는 여러 좋은 알고리즘ㅈ이 있다. 하지만 관리 가능한 수준에서 시스템 복잡도를 유지하는데 도움이 되는 추상화로 이런 알고리즘을 묶는 방법은 명확하지 않다.

책 전반에 걸쳐 좋은 추상화를 눈여겨볼 것이다. 이런 추상화는 큰 시스템의 일부를, 잘 정의되고 재사용 가능한 구성 요소로 추출할 수 있게 한다.

발전성: 변화를 쉽게 만들기

시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다. 시스템의 요구사항이 끊임없이 변할 가능성이 훨씬 크다. 새로운 사실을 배우고 미처 예기치 않은 사용 사례가 나타나고 비즈니스 우선순위가 바뀌고 사용자가 새로운 기능을 요청하고 새로운 플랫폼이 기존 플랫폼을 대체하고 법적 또는 규제 요구사항이 변경되고 시스템의 성장으로 인한 아키텍처 변화 등이 이런 요구사항에 해당한다.

조직 프로세스 측면에서 애자일(agile) 작업 패턴은 변화에 적응하기 위한 프레임워크를 제공한다. 또한 애자일 커뮤니티는 테스트 주도 개발(test-driven development(TDD)) 과 리팩토링(refactoring) 같이 자주 변화하는 환경에서 소프트웨어를 개발할 때 도움이 되는 기술 도구와 패턴을 개발하고 있다.

이런 애자일 기법에 대한 설명은 대부분 매우 작고, 로컬 규모(동일 애플리케이션 내 소스코드 파일이 몇 개만 있음)에 초점을 맞추고 있다. 이 책에서는 다양한 애플리케이션이나 다른 특성을 가진 서비스로 구성된 대규모 데이터 시스템 수준에서 민첩성을 높이는 방법을 찾는다. 예를 들면 홈 타임라인을 구성하기 위한 트위터의 아키텍처(11쪽의 '부하 기술하기')를 접근 방식 1에서 접근 방식 2로 '리팩토링'하는 방법을 찾는다.

데이터 시스템 변경을 쉽게하고 변화된 요구사항에 시스템을 맞추는 방법은 시스템의 간단함과 추상화와 밀접한 관련이 있다. 간단하고 이해하기 쉬운 시스템은 대개 복잡한 시스템보다 수정하기 쉽다. 하지만 이것은 매우 중요한 개념이기 때문에 데이터 시스템 수준에서 민첩성을 언급할 때는 민첩성 대신 다른 단어로 발전성을 사용하겠다.

정리

이번 장에서는 데이터 중심 애플리케이션을 생각하는 기본적인 방법 몇 가지를 살펴봤다. 이 원칙들은 이 책의 나머지 부분에서 기술적인 세부 사항을 좀 더 깊게 살펴보는 데 도움될 것이다.

애플리케이션이 유용하려면 다양한 요구사항을 충족시켜야 한다. 다양한 요구사항에는 기능적 요구사항(여러 방법으로 데이터를 저장하고 조회하고 검색하고 처리하게끔 허용하는 작업과 같이 해야 하는 일)과 비기능적 요구사항(보안, 신뢰성, 법규 준수, 확장성, 호환성, 유지보수성과 같은 일반속성)이 있다. 이번 장에서는 신뢰성, 확장성, 유지보수성을 자세히 살펴보았다.

신뢰성은 결함이 발생해도 시스템이 올바르게 동작하게 만든다는 의미다. 결함은 (일반적으로 무작위 적이고 비상관 관계의) 하드웨어와 (보통 체계적이고 다루기 어려운) 소프트웨어 버그와 (가끔 불가피하게 실수를 하는) 사람에게 있을 수 있다. 내결함성 기술은 사용자에게 특정 유형의 결함을 숨길 수 있게 해준다.

확장성은 부하가 증가해도 좋은 성능을 유지하기 위한 전략을 의미한다. 확장성을 설명하려면 먼저 양적으로 부하와 성능을 설명하는 방법이 필요하다. 부하를 설명하는 예로 트위터의 홈 타임라인을, 성능 측정 방법으로 응답 시간 백분위를 간단히 살펴봤다. 확장 가능한 시스템에서는 부하가 높은 상태에서 신뢰성을 유지하기 위해 처리 용량을 추가할 수 있다.

유지보수성에는 많은 측면이 잇지만 유지보수성의 본질은 시스템에서 작업하는 엔지니어와 운영 팀의 삶을 개선하는 데 있다. 좋은 추상화는 복잡도를 줄이고 쉽게 시스템을 변경할 수 있게 하며 새로운 사용 사례에 적용하는 데 도움이 된다. 좋은 운영성이란 시스템의 건강 상태를 잘 관찰하고 시스템을 효율적으로 관리하는 방법을 보유한다는 의미다.

안타깝게도 애플리케이션을 신뢰할 수 있고 확장 가능하며 유지보수하기 쉽게 만들어주는 간단한 해결책은 없다. 하지만 여러 애플리케이션에서 계속 재현되는 특정 패턴과 기술이 있다. 이후 몇 개의 장에서는 데이터 시스템 몇 가지를 예제로 살펴보고 이런 목표를 향해 데이터 시스템이 어떻게 작동하는지 분석한다.

 

반응형