2025.12.24: 오랜 시간이 지난 후에 다시 이 글을 보니, 책의 내용은 문제가 없지만 내가 쓴 후기는 부끄럽게 느껴진다. 민망하지만 그 세월의 시간 동안 성장했다고 생각하고 글은 특별히 수정하지는 않았다

후기

이 책은 사람들이 좋다는 이야기가 있어서 읽기 시작했는데, 정리하면서 보니 번역의 질이 크게 좋지는 않은 거 같다. 비문도 여러 차례 확인했고, 번역이 아예 잘못 된 부분도 있는 거 같다. 추가적으로 읽으면서 생각난 부분을 여기에 정리하려고 한다.

  1. 최근 데이터 중심 애플리케이션은 거의 대부분 샤딩을 지원하는 거 같다.
  • state 한 데이터를 저장하기 위해서 데이터를 샤딩하여 각기 서버에 저장하고, 이를 복재하여 저장함으로써 데이터의 고가용성을 보장하는 정책을 취하고 있다. elasticSearch 는 데이터 노드를 따로 구성하고 요청 처리는 마스터에서 수행하는 구조이고, 카프카 또한 topic 을 파티션 단위로 분할하고 각각의 파티션을 샤딩한다.
  • 다만 이렇게 저장하면 단점이 있는데, 데이터를 insert 하는 즉시 읽을 수 없다는 단점이 있다. 따라서 NoSQL 로 데이터를 저장할 때는 저장 후 곧바로 읽으면 데이터가 있을 수도 있고 없을 수도 있다는 점을 주의해야 한다.
  1. 성능 최적화는 p99 단위의 요청을 주로 최적화하는 거 같다. 다만 애플리케이션 자체가 느린 경우도 있는데, 이는 보통 WAS 의 문제가 아닌 경우가 많다. 보통 다음 프로세스를 타곤 한다.
  • 모니터링 도구를 활용하여 느린 요청을 체크한다.
  • 어디가 느린 지 본다
    • DB 가 느리다면, 데이터베이스의 쿼리를 최적화하고 인덱스를 튜닝한다. 인덱스가 타지 않는 경우라면 인덱스를 추가하고, 그렇지 않다면 쿼리를 수정하여 인덱스를 타게 한다.
    • WAS 가 느리다면 쓰레드를 확인해 본다.
    • 외부의 API 가 느린 경우도 있다. 이런 경우엔 요청을 비동기로 전환하거나 필수적인 데이터가 아니라면 타임아웃 시간을 줄이는 등의 튜닝을 한다.

신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션

이 장에서는 다음과 같은 이론을 논한다.

신뢰성, 확장성, 유지보수성이란 무엇인가?

오늘날 많은 애플리케이션은 데이터 중심이다. 이 경우 데이터의 양, 복잡도, 변화 속도가 애플리케이션을 설계하는 데 있어서 중요한 요소가 된다. 일반적으로 애플리케이션은 다음 기능을 제공하여야 한다.

  • 데이터를 저장한다. (데이터베이스)
  • 값비싼 수행 결과를 기억한다 (캐시)
  • 사용자가 키워드로 데이터를 검색할 수 있다. (검색 색인)
  • 비동기 처리를 위해 다른 프로세스로 메시지를 보낸다 (스트림 처리)
  • 주기적으로 대량의 누적된 데이터를 분석한다 (일괄 처리)

이는 너무나 뻔한 이야기처럼 보이지만, 이는 언제나 많은 생각 없이 사용할 수 있게 데이터 시스템이 성공적으로 추상화되었기 때문이다. 대부분 엔지니어들은 잘 구성된 도구를 가져와, 위의 기능들을 구현한다.

그러나 이는 말처럼 쉬운 것은 아니다. 애플리케이션마다 요구사항이 다르기 때문에, 우리는 어떤 도구와 어떤 접근 방식이 가장 적합할지 고민하여야 한다. 또한 여러 개의 도구를 가져와 잘 결합하여 사용해야 할 수도 있다.

이 책은 위의 도구에 대해 다룬다. 데이터를 다루는 도구가 공통으로 지닌 것은 무엇이고 서로 구분되는 것은 무엇인지, 그리고 어떻게 그러한 특성을 구현해냈는지 알아본다.

데이터 시스템에 대한 생각

데이터 저장과 처리를 위한 여러 새로운 도구들은 최근에 만들어졌다. 새로운 도구들은 다양한 사용사례에 최적화되었기 때문에, 더 이상 전통적인 분류에 딱 들어맞지 않는다. 또한 단일 도구로는 더 이상 데이터 처리와 저장을 모두 만족시킬 수 없다. 대신에 작업을 여러 테스크로 쪼개고, 각 테스크를 처리할 수 있는 여러 도구들을 사용하는 추세이다.

여러 도구들은 각기 API 를 활용해 통신하고, 개발자는 클라이언트가 일관된 결과를 볼 수 있께 캐시를 무효화하거나 업데이트 하는 등의 동작을 수행한다.

이런 시스템을 설계하는 데는 여러가지를 고려하여야 한다.

  • 내부적으로 문제가 있더라도 데이터를 정확하게 유지하려면?
  • 시스템의 일부 성능이 저하되더라도 클라이언트에게 일관된 성능을 제공하려면?
  • 어떻게 규모를 확장할 수 있는지?
  • 좋은 API 의 모습이란?

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

  1. 신뢰성

하드웨어나 소프트웨어 결함, 심지어 인적 오류와 같은 역경에 직면하더라도 시스템은 지속적으로 올바르게 동작해야 한다.

  1. 확장성

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

  1. 유지보수성

시간이 지남에 따라 여러 다양한 사람들이 시스템 상에서 작업할 것이기 때문에 모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야 한다.

신뢰성

소프트웨어의 경우 일반적인 기대치는 다음과 같다.

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

위의 기대치가 “올바르게 동작함” 을 의미한다면, 신뢰성이란 대략 “무언가 잘못되더라도 지속적으로 올바르게 동작함” 이라고 이해할 수 있다.

잘못될 수 있는 일을 결함이라 부른다. 그리고 결함을 예측하고 대처할 수 있는 시스템을 내결함성을 지녔다고 한다. 또한 결함은 장애와 동일하지 않는데, 결함은 시스템의 한 구성 요소가 잘못된 것이지만 장애는 시스템 전체가 멈춰 사용자에게 필요한 서비스를 제공하지 못하는 것이기 때문이다. 결함 확률을 0 으로 하는 건 불가능하기 때문에, 결함으로 인해 장애가 발생하지 않게금 내결함성 구조를 설계하는것이 좋다.

일반적으로 결함 예방을 넘어 내결함성을 갖기를 원하지만, 결함이 한번 발생했을 때 복구할 수 없는 문제도 있다. 대표적으로 보안이 그렇다.

하드웨어 결함

다음과 같은 문제로 하드웨어 결함이 발생할 수 있다.

  • 인간의 실수
  • 하드디스크 평균 장애 확률 : 평균 장애 시간은 10 ~ 50년이므로, 10,000개의 디스크로 구성된 저장 클러스터는 하루에 한 개의 디스크가 사망한다.

장애율을 줄이기 위해 다음과 같은 방법을 사용할 수 있다.

  • 하드웨어를 중복으로 사용한다. RAID 를 구성하고, 서버는 핫스왑이 가능한 CPU를 사용한다.

데이터 양과 애플리케이션의 계산 요구가 늘어나면서 더 많은 장비를 사용하게 되었고, 그에 따라 결함율도 증가했다. 따라서 하드웨어 중복성을 추가하는 방향으로 점점 시스템이 움직이고 있다.

소프트웨어 오류

다음과 같은 오류들이 있다.

  • 잘못된 특정 입력이 있을 때 모든 애플리케이션 서버 인스턴스가 죽는 소프트웨어 버그
  • CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭처럼 공유 자원을 과도하게 사용하는 일부 프로세스
  • 시스템의 속도가 느려져 반응이 없거나 잘못된 응답을 반환하는 서비스
  • 한 구성 요소의 작은 결함이 다른 구성 요소의 결함을 야기하고 차례차례 더 많은 결함이 발생하는 연쇄 장애

소프트웨어의 체계적 오류 문제는 신속한 해결책이 없다. 주의 깊게 생각하고, 테스트를 빈틈없이 하고, 프로세스를 격리하고, 철저한 모니터링을 할 수 밖에 없다.

인적 오류

사람은 미덥지 않다. 사람이 미덥지 않음에도 시스템을 어떻게 신뢰성 있게 만들까?

  • 오류의 가능성을 최소화하는 방향으로 시스템을 설계하라. 잘 설계된 추상화, API, 관리 인터페이스 등으로 잘못된 일을 저지르는 것을 막을 수 있다.
  • 사람의 실수로 장애가 발생할 수 있는 부분을 분리하라. 실제 사용자에게 영향이 없는 샌드박스를 제공하라
  • 단위 테스트부터 인티그레이션 테스트, 수동 테스트까지 모든 수준에서 철저하게 테스트하라. 특히 코너 케이스를 주의 깊게 확인하라
  • 인적 오류를 빠르게 쉽게 복구할 수 있게 하라. 예를 들면 설정 변경 내역을 빠르게 롤백하고, 새로운 코드를 서서히 일부 유저에게만 적용되게 하라
  • 성능 지표와 오류율 같은 명확한 모니터링 기준을 마련하라
  • 조작 교육과 실습을 시행하라

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

비즈니스 애플리케이션에서 버그는 생산성 저하의 원인이고, 사이트의 중단은 매출에 손실이 발생하고 명성에 타격을 준다. 중요하지 않은 애플리케이션이라고 해도, 각각의 사용자에 대해 책임을 져야 한다. 예를 들면, 사진 애플리케이션에 아이들의 사진과 동영상을 모두 보관한 부모를 생각해 보자.

가끔은 프로토타이핑을 위해, 혹은 이익률이 작은 서비스를 위해 신뢰성을 희생해야 하는 경우가 있다. 하지만 이 때는 비용을 줄여야 하는 시점을 매우 잘 알고 있어야 한다.

확장성

확장성은 증가한 부하에 대처하는 시스템 능력을 설명하는 데 사용하는 용어이다. 그러나, 보통은 “시스템이 특정 방향으로 커지면 이에 대처하기 위한 선택은 무엇인가?” 와 “추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?” 와 같은 질문을 고려한다는 의미로 사용한다.

부하 기술하기

애플리케이션이 얼마나 요청을 받는지 설명할 수 있는 지표를 결정하여야 한다. 이는 부하 매개변수(load parameter) 라고 부르는데, 시스템 설계에 따라 어떤 부하 매개변수를 선택할지 결정하여야 한다. 다음과 같은 예제가 있다.

  • 웹 서버의 초당 요청 수
  • 데이터베이스의 읽기 대 쓰기 비율
  • 대화방의 동시 활성 사용자
  • 캐시 적중률

등이 있다. 또한 소수의 극단적인 케이스를 고려하여야 한다. 트위터의 예를 들어보자. 트위터는 원래는 다음과 같은 방식으로 홈 타임라인 피드를 구성하였다.

  1. 새로운 트윗이 작성되면 전체 트윗을 모아놓는 테이블에 삽입한다. 사용자가 자신의 홈 타임라인을 요청하면, 팔로우하는 모든 사람을 찾아서 그 사람의 트윗을 시간순으로 결합한다.
SELECT tweets.*, users.* from tweets
JOIN users ON tweets.sender_id = users.id
JOIN floowers ON followers.followee_id = users.id
WEHERE follows.follower_id = current_user
  1. 새로운 트윗이 작성되면 사용자를 팔로우하는 모든 사람을 찾아서, 팔로워 각자의 홈 타임라인 캐시에 새로운 트윗을 insert 한다.

트위터는 원래는 1번 방식을 사용하였는데, 시스템이 홈 타임라인의 부하를 줄이기 위해 고군분투하여아 했고, 그리하여 2번 방식으로 전환하였다. 게시된 트윗의 평균 속도가 읽기 속도보다 100배 정도 낮기 때문에, 트윗을 쓸 때 많은 일을 하는 것이 바람직하다. 하지만 팔로워가 많은 사람들(일부 사용자는 팔로워가 3천만명이 넘는다) 때문에, 트윗이 많은 사람들은 별도로 가져와 읽기 시점에 1번 방식처럼 타임라인에 결합한다.

부하를 측정할 때는 트위터처럼 소수의 극단적인 예를 고려하여야 한다.

성능 기술하기

일단 부하 매개변수를 결정하면(cpu 사용량을 보겠다, 초당 TPS 를 확인하겠다 등), 다음 두 가지 방법으로 성능을 측정하여 지표화 할 수 있다.

  • 부하 매개변수를 증가시키고 시스템 자원을 변경하지 않고 유지하면 성능은 어떻게 영향을 받을까?
  • 부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되기를 원한다면 얼마나 많이 자원을 늘려야 할까?

두 질문 모두 성능 수치가 필요하다. 따라서 시스템 성능을 기술할 수 있는 지표를 확인해 보자

다음과 같은 두 지표가 있다.

  • 처리량(throughput) : 초당 처리할 수 있는 레코드 수나 일정 크기의 데이터 집합을 처리할 때 걸리는 전체 시간
    • 하둡과 같은 일괄 처리 시스템에서 사용하는 지표이다.
  • 응답 시간(response time) : 클라이언트가 요청을 보내고 응답을 받는 시간

응답 시간은 매번 다르기 때문에, 값이 아니라 분포로 생각하여야 한다. 대게 중앙값을 사용하면 좋다. 중앙값은 50분위로 p50 으로 기술하곤 하는데, 이는 사용자의 절반은 중앙값보다 빠르고, 절반은 느리다는 의미이다. 아마존은 내부 서비스의 응답 시간 요구사항을 p999 로 나타내곤 한다. 이는 요청 1000 개 중에서 가장 느린 한 개의 응답속도를 나타내는 숫자이다.

보통 응답 시간이 가장 느린 고객들이 웹 계정에 가장 많은 데이터를 가지고 있는, 가장 소중한 고객일 확률이 높기 때문이다. 반면 p9999, 99.99% 를 최적화하는 작업에는 비용이 너무 많이 들어가기 때문에, 아마존에게 충분한 이익을 주지 못한다고 여겨진다.

p999 를 최적화해야 하는 다른 이유가 있을까? 큐 대기 지연(queueing delay) 또한 그 이유 중 하나가 될 수 있다. 서버는 병렬로 소수의 작업만 처리할 수 있기 때문에, 소수의 느린 요청 처리만으로도 후속 요청 처리가 지체된다. 이 현상을 선두 차단(head-of-line blocking) 이라 한다.

부하 대응 접근 방식

부하 매개변수가 어느 정도 증가하더라도 좋은 성능을 유지하려면 어떻게 해야 할까? 사람들은 확장성을 두 가지로 분리하여 이야기하곤 한다.

  1. scaling up
  • 좀 더 강력한 장비로 이동한다.
  1. scaling out
  • 다수의 낮은 장비에 부하를 분산한다.

고사양 장비는 보통 매우 비싸기 때문에, 대게 규모 확장이 효율적이다.

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

다수의 장비에 stateless 한 서비스를 배포하는 일은 상당히 간단하다. 하지만 stateful 한 테이터 시스템을 분산하는 일은 매부 족잡하기 때문에, 확장 비용이나 데이터베이스를 분산으로 만들어야 하는 요구가 있기 전까지는 단일 노드에 데이터베이스를 유지하는 것이 최근까지의 통념이다.

다만 최근에는 분산 시스템을 위한 도구와 추상화가 좋아지면서, 이 통념이 적어도 일부 애플리케이션에서는 바뀌었다. 대용량 데이터와 트래픽을 다루지 않는 경우에도 분산 데이터 시스템이 향후 기본 아키텍처로 자리 잡을 가능성이 있다.

대게 대규모로 동작하는 시스템의 아키텍처는 해당 시스템을 사용하는 애플리케이션에 특화되어 있다. 범용적이고 모든 상황에 맞는 확장 아키텍처는 없다. 읽기의 양, 쓰기의 양, 저장할 데이터의 양, 데이터의 복잡도, 응답 시간 요구사항, 접근 패턴 등에 맞추어 아키텍처를 설계하여야 한다.

특정 애플리케이션에 적합한 확장성을 갖춘 아키텍처는 주요 동작이 무엇이고, 잘 하지 않는 동작이 무엇인지에 대한 가정을 바탕으로 구축된다. 이 가정은 곧 부하 매개변수가 된다.

유지보수성

소프트웨어의 비용은 대부분 유지보수에 들어간다.

버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적응, 새 사용 사례를 위한 변경, 기술 채무 상환, 새로운 기능 추가

모든 사람은 레거시 시스템을 유지보수하는것을 좋아하지 않는다. 희망적인 점은, 유지보수 중 고통을 최소화할 수 있게 소프트웨어를 설계할 수 있다는 것이다. 그러기 위해서 주의를 기울어야 하는 원칙은 다음과 같다.

  1. 운용성

운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만들어라

  1. 단순성

시스템에서 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라

  1. 발전성

엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하라. 그래야 요구사항 변경 같은 예기치 않은 사용 사례를 적용하기가 쉽다. 이 속성은 유연성, 수정 가능성, 적응성으로 알려져 있다.

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

좋은 운영성이란 동일하게 반복되는 태스크를 쉽게 수행하게끔 만들어, 운영팀이 고부가가치 활동에 노력을 집중한다는 의미이다. 즉, 동일 반복 태스크를 쉽게 할 수 있게 만들어야 한다.

단순성 : 복잡도 관리

프로젝트가 커짐에 따라 시스템은 매무 복잡해지고 이해하기 어려워진다. 복잡도는 다양한 증상으로 나타난다. 상태 공간의 급증, 모듈 간 강한 커플링, 복잡한 의존성, 일관성 없는 명명과 용어, 성능 문제 해결을 목표로 한 해킹, 임시방편으로 문제를 해결한 특수 사례 등이 이런 증상이다.

개발자가 시스템을 이해하고 추론하기 어려워지면 시스템에 숨겨진 가정과 의도치 않은 결과 및 예기치 않은 상호작용을 간과하기 쉽다. 따라서 시스템은 단순해야 한다.

시스템이 그냥 복잡할 수도 있다. 이 복잡도를 해결하기 위한 취상의 도구는 추상화다. 이런 추상화는 큰 시스템의 일부를 잘 정의되고 재사용 가능한 구성 요소로 추출할 수 있게 한다.

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

시스템의 요구사항은 지속적으로 변화한다. 이 책에서는 다양한 애플리케이션이나 다른 특성을 가진 서비스로 구성된 대규모 데이터 시스템 수준에서 민첩성을 높이는 방법을 찾는다. 데이터 시스템 변경을 쉽게 하는 건 시스템의 간단함과 추상화와 밀접한 관련이 있다. 이해하기 쉬운 시스템은 수정하기 쉽다. 하지만 데이터 시스템 수준에서 민첩성을 언급할 때는 발전성이라는 개념을 따로 사용하겠다.

정리

  • 애플리케이션이 유용하려면 다양한 요구사항을 충족시켜야 한다.
    • 기능적 요구사항(데이터를 조회, 검색, 처리)
    • 비기능적 요구사항(보안, 신뢰성, 법규 준수, 확장성, 호환성, 유지보수성)
    • 이 장에서는 신뢰성, 확장성, 유지보수성을 살펴보았다.
  • 신뢰성
    • 결함이 발생해도 시스템을 올바르게 동작하게 한다.
  • 확장성
    • 부하가 증가해도 좋은 성능을 유지하기 위한 전략
  • 유지보수성
    • 시스템에서 작업하는 엔지니어와 운영 팀의 삶을 개선한다.
    • 좋은 추상화와 좋은 운용성으로 유지보수성을 증가시킬 수 있다.