사람들은 인터넷이 너무 잘 만들어진 나머지 사람이 만들었다는 사실을 잊어버리고 태평양처럼 천연자원인 마냥 주어진 것이라고 여긴다. 이렇게 규모 있는 기술이 오류로부터 자유로웠던 적이 있었던가?
앨런 케이, 돕 박사와의 인터뷰 중 (2012)
현대의 애플리케이션은 데이터 집중적이다. CPU 성능은 충분히 좋아져 애플리케이션의 앞길을 가로막진 않는다. 그러나, 데이터의 크기, 복잡성, 변경빈도가 문제다.
데이터 집중적인 애플리케이션은 데이터를 처리(CRUD, 캐시, 스트림, 배치 등)하는 일을 한다. 데이터 시스템은 이런 일을 할 수 있게 도와주고, 개발자는 레고처럼 쌓아 애플리케이션을 만든다. 예를 들어 이미 있는 데이터베이스(MySQL, Oracle 등)를 쓰지 처음부터 만들진 않는다.
데이터를 처리하는데 단 하나의 도구만 쓸 수는 없을까? 그러나 흔히들 '은총알은 없다'고 한다. 한 도구로 모든 데이터를 처리할 수 없기에 각자의 분야에서 뛰어난 여러 도구를 조합해 사용해야 한다. 예를 들어 데이터를 저장하는 메인 데이터베이스(RDB, NoSQL 등), 텍스트 검색을 위한 서버(ElasticSearch, Solr 등), 캐싱을 위한 데이터베이스(Memcached, Redis 등)를 조합할 수 있다.
데이터시스템은 이런 도구들을 부르는 추상적인 말이다. 그래서 작은 데이터시스템을 조합해 만든 API도 데이터시스템이라고 부를 수 있다. 즉, 우리는 애플리케이션 개발자이면서 데이터시스템 디자이너라고 부를 수 있다.
어떻게 데이터시스템을 만들 때 고려해야 하는 점이 뭘까? 이를 판단하는 기준은 여러 개겠지만 아래 세 가지를 집중적으로 살펴보자.
일이 틀어져도 견딜 수 있는 성질을 말한다. 이를 보고 결함 내성(fault-tolerant)또는 결함 회복탄성(fault-resilient)이라고 부른다.
결함(fault)과 실패(failure)는 다른 말이다. 결함이란 시스템의 한 부분이 정해진 규약을 벗어난 행동을 할 때를 말하고, 실패란 시스템 전체가 멈춰버려 먹통을 겪는 상태를 말한다.
하드웨어 결함
가장 먼저 생각나는 결함을 꼽자면 하드웨어 결함을 생각할 수 있다.
하드디스크가 죽었다던가, 네트워크 선이 잘못 연결됐다거나 등 여러 경우가 있다.
이는 흔히 중복(Redundancy)으로 예방한다.
최근까지만 해도 서버의 부품을 다중화하는 것만으로 충분했다. 서버 자체가 완전히 죽는 일은 드물었기 때문이다. 디스크(RAID), 전원(UPS, 디젤발전기), CPU(hot-swappable) 등을 다중화해서 한 부품이 죽으면 다른 부품으로 빠르게 대체되도록 했다.
그러나 데이터양과 애플리케이션이 처리해야 하는 요청은 더 많아졌다. 이를 대응할 서버대수는 더 많아지고, 자연스레 부품이 망가질 가능성도 커진다. 특히, AWS 같은 클라우드는 단일 서버가 신뢰성 있게 동작하는 걸 보장하지 않는다. 이는 다중 서버를 사용한 유연함과 탄력적 처리를 중요시한 설계에서 비롯됐다. 따라서 서버 자체를 다중화하는 방향으로 변화해 갔다.
소프트웨어 오류
하드웨어가 사용하는 소프트웨어(OS, 커널 프로세스, 사용자 프로세스 등)에서 발생한 문제를 말한다.
이런 문제들은 깊게 동면해 있다가 트리거되면서 갑작스럽게 나타나는 경우가 꽤 많다. 그래서 예상하기 어렵고, 한번 일어나면 시스템에 크게 오류가 생길 수 있다.
심하게는 시스템 전체가 먹통이 될 수도 있다. 예를 들어, 2012년 6월 30일에 일어난 윤초 버그로 인해 많은 애플리케이션이 동시에 멈췄던 적이 있다.
이를 쉽게 해결할 수 있는 명료한 방법은 따로 없다. 그러나, 테스팅, 프로세스 격리, 모니터링, 데이터 흐름 검증 등 여러 작은 것들이 도움이 될 수 있다.
인적 오류
인간은 믿음직스럽지 못하다. 그런데 시스템을 만들고 운영하는 주체는 인간이다. 당연하게도 인간의 실수들이 시스템의 결함을 초래한다.
우리는 고객에 대한 책임이 있다.
핵 발전소, 비행기, 로켓 발사체, 수술 로봇 등 사람의 생명과 안전에 직접적으로 영향이 있는 거대한 규모의 애플리케이션만 신뢰성이 중요한 게 아니다.
일상적인 애플리케이션도 중요하다. 기업용 애플리케이션의 장애는 큰 생산성 하락 및 법적 문제까지 야기할 수 있다. 또 쇼핑몰같이 사용자에게 서비스를 제공하는 애플리케이션은 매출과 명성이 크게 줄어들 수도 있다. 클라우드 사진 저장소가 문제가 생겨 가족사진이 다 없어진 부모의 마음은 어떻겠는가?
우리는 고객에 대한 책임이 있다.
서비스가 성장함에 따라 시스템 부하(load)가 늘어났을 때 대응할 수 있는 능력이 있는 지를 말한다. 단순히 이 시스템은 확장성이 있어, 저 시스템은 확장성이 없어라고 구분할 수 없다. 확장성은 "시스템의 특정한 부하가 늘어날 때, 대응할 방법은 무엇인가?", "부하가 늘어날 때 컴퓨팅 자원을 어떻게 추가할 수 있을까?"와 같은 질문에 대답하는 것이다.
부하를 정의하기
그래서 현재 시스템의 부하가 뭔지 알아야 대응할 방법을 논의할 수 있다.
부하는 특정한 숫자들로 표현될 수 있는데, 이를 부하 인자(load paramters)라고 한다.
시스템마다 다르지만, 예를 들어보면 웹 서버의 초당 요청 수, 데이터베이스 읽기/쓰기의 비율, 병목이 되는 특정 API의 동시 처리량 등 여러 경우가 있을 수 있다.
트위터를 생각해보자. 트위터는 내가 쓴 트윗이 날 팔로우하는 사람들의 홈에 보여야한다. 트위터는 이를 처리하기위해 두가지 방법을 생각했다.
tweets
테이블에 모두 저장되고, 홈 타임라인 조회 시 join
으로 팔로우한 사람의 트윗을 가져온다.1번은 쓰기 단계를 최소화했고, 2번은 읽기 단계를 최소화했다. 트위터는 트윗을 쓰는 것보다, 읽는 게 평균 2배 정도 요청이 많다. 그래서 이럴 때는 2번 방법이 효율적이다.
그러나, 팔로워의 수는 굉장히 넓게 분포되어있다. 십만, 백만 단위 팔로워를 가진 사람도 꽤 있다. 이런 사람들이 트윗을 쓰면 1초당 몇백, 몇천만개의 데이터를 동시에 써야한다. 이를 수초 내에 처리하긴 어렵다. 그래서 이럴 때는 1번 방법이 효율적이다.
이렇게 팔로워 수 분포에 따라 확장성을 어떻게 고려해야하는지 논의할 수 있기 때문에, 이를 주요 부하 인자라고 볼 수 있다.
성능을 표현하기
이제 부하를 알고 있으니, 부하 인자를 늘렸을 때 성능이 어떻게 변하는지 확인해볼 수 있다.
그렇다면 성능은 어떻게 표현할 수 있을까?
성능은 시스템의 종류에 따라 다르게 표현할 수 있다.
성능 수치는 고정적이지 않다. 예를 들어 응답시간은 애플리케이션의 쓰레드 풀 상태, GC Pause time, TCP 패킷 유실, 디스크 캐시 폴트, 하드웨어 발열 진동 등 여러 변수로 인해 유동적이다.
응답시간을 분석할 때 평균을 사용하기도한다. 그러나 평균은 극단적인 수치에 영향을 받고, 평균값이 대략 몇명의 사용자가 겪는지 알 수 없다.
그래서 백분위수(percentile)을 사용한다. 응답시간의 중간값(median)은 대체로 사용자 절반이 해당 수치 이하로 응답을 받는다고 말할 수 있다. 중간값은 곧 50번째 백분위수이며, 라고 표현한다. 극단적인 응답시간을 겪는 경우를 알아보려면 흔히 , , 를 본다. 이렇게 큰 백분위수를 꼬리 지연(tail latencies)이라 한다.
응답시간이 길어지면 서비스에 나쁜 영향이 갈 수 있다. 실제로 아마존은 응답시간이 100ms 늘 경우, 매출의 1%가 줄어든다고 분석했다. 아마존은 특히 꼬리 지연 시간을 신경쓰는데, 응답시간이 느렸던 사용자는 구매이력이 많은, 곧 데이터가 많은 핵심 사용자일 수 있기 때문이다. 그러나 일반적으론 꼬리 지연 시간을 줄이는 노력에 비해 효과는 미미하다.
그리고 응답시간은 클라이언트 쪽에서 측정해야한다. 서버는 동시에 처리가능한 요청의 양이 정해져 있다. 유독 처리가 늦는 요청이 있다면 뒤이은 요청을 빠르게 처리하더라도 클라이언트가 받는 응답이 전반적으로 느려지기 때문이다. 이를 HOL 블로킹(head-of-line)이라고 한다.
부하를 극복하는 방법
데이터베이스 같이 하나의 상태(데이터)를 서로 공유해야하는 시스템을 유상태(stateful) 데이터 시스템이라한다. 상태를 공유하는 만큼 부하를 분산시키기는 어렵다. 그래서 최후의 수단으로 분산 환경을 구축한다. 반면, 무상태(stateless, share-nothing) 데이터 시스템은 그런 걱정이 없어 분산 환경을 구축하기 쉽다.
큰 규모의 연산을 처리하는 시스템의 아키텍쳐는 그 애플리케이션에 크게 좌지우지된다. 마찬가지로 모든 경우를 커버해주는 은총알은 없다. 그래도 확장성 있는 시스템의 아키텍처는 일반적으로 패턴이 있다. 입/출력의 용량, 데이터의 용량 및 복잡도 등 여러 사항을 고려하고 적절한 패턴을 조합해 문제를 해결할 수 있다. 앞으로 이 책에서는 이런 패턴에 대해서 알아볼 것이다.
확장성을 고려한 시스템 설계에선 어떤 연산이 흔하고, 드문지에 대한 추정을 기반한다. 즉, 부하 인자를 예상해야 하는데, 잘못 예상하면 여태 작업한게 말짱도루묵이 되어버린다. 생산성에 크게 악영향을 줄 수 있기때문에, 초기 스타트업이나 신규런칭 서비스의 경우 미래를 예상해 대비하기보다 기능 출시에 맞춰 빠르게 대응할 수 있어야 한다.
가장 많은 비중을 차지하는 소프트웨어의 비용(돈, 시간)은 초기개발이 아니라 유지보수다. 특히 레거시(legacy) 시스템를 유지보수하는 건 정말 끔찍하다. 모든 이들이 기피하고 싶어하는 영역이다. 그러나 이런 시스템도 누군가가 만들었기에 레거시 시스템이 된 것이다.
그렇기에 우리가 만든 시스템이 레거시가 되어 또 다른 이들의 발목을 잡지 않도록 노력해야한다. (물론 완벽할 순 없겠지만) 유지보수가 용이한 시스템을 만들기 위해서 신경써야할 부분 세 가지를 보자.
언어의 한계는 곧 세상의 한계이다.
루드비히 비트겐슈타인, 논리-철학 논고 (1922)
데이터 모델이 중요한 이유는 소프트웨어의 작성 방식을 가를 뿐만 아니라 우리가 문제를 접근하는 방식도 가르기 때문이다. 대다수의 애플리케이션은 데이터 모델을 여러 레이어로 나눈다. (ex: 실제세계 <-> 애플리케이션 <-> 데이터베이스) 이는 하위 레이어의 복잡함을 가려 여러 그룹의 사람들이 서로 효율적으로 일 할 수 있도록 만든다. 데이터 모델은 소프트웨어가 할 수 있는 역량의 범위를 결정한다. 때문에 어떤 데이터 모델을 써야할지 아는게 중요하다.
관계형 모델은 1970년에 에드가 코드가 제안되어 80년대 중반에 관계형 데이터베이스로 구현됐다. 관계형 모델은 관계(SQL: 테이블)와 튜플(SQL: ROW)로 이루어졌다. 관계형 데이터베이스는 깔끔한 인터페이스를 바탕으로 애플리케이션 개발자가 세부 구현을 알 필요가 없었다.
그동안 관계형 모델의 경쟁자로 여러 모델이 있었다. 1970년대와 1980년대 초엔 네트워크 모델과 계층형 모델, 1980년대와 1990년대에는 객체 데이터베이스, 2000년대에는 XML 데이터베이스가 나타났으나 많이 사용되진 않았다.
그러다 2010년대에 NoSQL이 등장했다. NoSQL은 Not Only SQL(SQL 말고)의 약어다. 관계형 데이터베이스의 한계나 불만에서 비롯되어 만들어진 개념이다.
객체-관계 패러다임의 불일치
현대의 애플리케이션 개발은 대부분 OOP 언어로 만들어진다.
이는 관계형 테이블과의 조화가 좋지 않다. 이를 패러다임의 불일치(impedance mismatch)라고 한다.
이와 관련한 내용은 여기서
더 알아볼 수 있다.
ActiveRecord, Hibernate 같은 객체-관계 매핑(ORM) 프레임워크가 두 모델 사이의 차이를 극복할 수 있도록 도와주지만, 차이를 완전히 숨길 수는 없다.
일대다 관계
페이스북 프로필 예로 들어보자. 프로필에는 여러 정보가 들어갈 수 있다.
학력, 경력, 친구 목록, 팔로우 목록 등이 있을 수 있다.
이 중 사용자-학력 관계를 한번 표현해보자.
한 사용자는 여러 학력을 가질 수 있다. 즉 일대다 관계를 가질 수 있는데 이는 여러 방식으로 표현될 수 있다.
작성중...
작성중...