본문 바로가기
TIL/데이터 엔지니어링

데이터 중심 어플리케이션 설계 - 1장. 신뢰성, 확장성, 유지보수성

by yeon_zoo 2023. 4. 23.

데이터 중심 어플리케이션 설계라는 아마존 1위의 개발 서적 북리뷰 스터디를 시작하게 되었다. 쓰고 기록하지 않으면 휘발성 공부가 되어 버리니 매주 한 장씩 꼭 블로그를 작성해야 겠다는 다짐으로 시작해본다..

 

1장의 학습 목표 : 신뢰성, 확장성, 유지보수성의 의미를 정확히 이해하고 이를 고려하는 몇 가지 방법을 개략적으로 설명한다. 

 

각각의 개념을 간략하게 설명하면 다음과 같다.

 

신뢰성 (Reliability)

하드웨어나 소프트웨어의 결함, 심지어 인적 오류 같은 문제를 직면하더라도 시스템은 지속적으로 원하는 성능 수준과 정확한 기능 수행을 해야 한다. 

소프트웨어에 대한 일반적인 기대치는 다음과 같다. 

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

 

확장성 (Scalability)

시스템의 데이터 양, 트래픽 양, 복잡도가 올라갈 수록 이를 처리할 수 있는 방법이 있어야 한다. 

 

유지보수성 (Maintainability)

모든 사용자가 시스템 상에서 생산적으로 작업할 수 있도록 해야 한다. 


좀 더 자세하게 살펴보자. 

 

1. 신뢰성

신뢰성을 논하면서 결함을 빼먹을 수 없다. 결함은 잘못될 수 있는 일을 말한다. 반면 내결함성 (fault-tolerance) / 탄력성 (resilient)이 있는 시스템은 결함을 예측하고 대처할 수 있는 시스템을 말한다. 

 

결함과 장애는 다르다. 결함은 사양에서 벗어난 시스템의 한 구성 요소이고, 장애는 사용자에게 필요한 서비스가 제공되지 않고 시스템 전체가 멈춘 경우를 말한다. 결함 확률을 0으로 줄이는 것은 불가능하지만 우리의 목표는 결함이 장애로 이어지지 않도록 하는 내결함성 구조를 갖추는 것이다. 

더보기

* 카오스 몽키란? 시스템을 무작위로 망가뜨리는 테스트용 시스템이다. 넷플릭스에서 실제로 랜덤한 근무시간 중에 실서비스에 위험한 상황을 연출하는 카오스 몽키 시스템을 띄우면서 유명해졌다. 이 시스템은 장애를 유도하는데 담당자는 이것을 수습하려고 하면서 장애 복구 능력이 증가한다. 웹 시스템 상 다운타임(완전히 시스템이 죽은 시간)이 0이 되는 것이 불가능하다는 것을 인정하고, 장애를 예방하는 것보다 복구하는 것에 투자한 것이다. 하지만 복구보다 예방이 무조건 우선시되어야 하는 측면도 존재하는데, 바로 보안 분야가 그런 예시이다. 

 

하드웨어 결함

구성 요소 하나가 죽으면 고장 난 구성 요소가 교체되는 동안 중복된 구성 요소를 대신 사용할 수 있다. 최근까지는 단일 장비의 전체 장애 발생 가능성이 낮았지만, 데이터의 양과 어플리케이션에 요구되는 계산의 양이 많아지면서 더 많은 어플리케이션이 많은 수의 장비를 사용하고 AWS 같은 클라우드 플랫폼에 위탁하게 되었다. 이를 통해 소프트웨어 내결함성 기술을 사용하고 하드웨어 중복성을 추가하면서 전체 장비 손실을 견딜 수 있는 시스템으로 발전하게 되는 것이다. 중복 구성 요소 중 하나로 여러 개의 서버를 둔다면, 운영상의 장점도 따라 온다. OS 보안패치를 업데이트할 때도 전체 서버를 끌 필요 없이 하나씩 교체해나가면 서비스 전체에 주는 영향을 줄일 수 있다. 

 

 

소프트웨어 결함

하드웨어 결함은 서로 연쇄적인 영향이 적은 독립적이고 무작위적인 결함인 반면, 소프트웨어 오류는 연쇄적인 오류를 발생시킬 가능성이 높다. 여기에는 하드웨어 교체와 같은 신속한 해결책이 없다. 테스트를 빈틈없이 하거나, 시스템에 대해 주의깊게 생각하기 등 작은 일들이 쌓여서 방어할 수 있고, 수행 중에 지속적으로 확인해서 보장하고 싶은 부분이 보장되지 않을 때 경고를 받는다거나 하는 모니터링 방식으로 확인해야 한다. 

 

 

인적 오류

인간은 최선을 다해도 미덥지 않다는 평가를 받는다. 그런 인간이 만드는 시스템을 신뢰성 있게 하려면 오류의 가능성을 최소화하는 설계, 비프로덕션 환경인 sandbox 제공, 철저한 테스트, 장애 발생의 영향 최소화(빠른 롤백, 서서히 롤아웃), 상세하고 명확한 모니터링 대책 마련 등과 같은 방법을 취해야 한다. 

 

 

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

모든 어플리케이션은 사용자에 대한 책임이 있으므로 신뢰성은 소프트웨어에서 중요한 요소이다. 초기 시제품이나 이익률이 매우 작은 서비스의 경우, 신뢰성에 희생이 필요한 경우도 존재하지만 이런 경우라면 그 비용을 줄이는 시점을 잘 파악하여 줄여 나가야 한다. 

 

2. 확장성

증가한 부하에 대처하는 시스템 능력을 설명하는 데 사용하는 용어이다. 확장성 논의는 이런 질문들을 공유하는 것이다.

  • 시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇일까?
  • 추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?

이런 질문을 하기 위해서는 부하 기술, 성능 기술이 우선되어야 한다. 

 

부하 기술

현재 시스템의 부하가 어느정도인지를 간결하게 기술할 수 있어야 한다. 그래야 부하가 두 배로 늘면 어떻게 될까? 와 같은 부하 성장 질문을 논의할 수 있다. 이런 부하 정도를 논의할 때 부하 매개변수(load parameter, 시스템이나 소프트웨어가 견디는 부하를 측정하고 제어하기 위한 매개변수)가 필요하다. 

부하 매개변수는 예를 들어 웹 서버의 초당요청 수, DB의 읽기 대 쓰기 비율, 대화방의 active user, 캐시 hit rate 등이 있다. 시스템마다 가장 적합한 부하 매개변수가 무엇일지는 설계에 따라서 다르다. 어떤 시스템에서는 평균적인 경우를, 다른 시스템에서는 소수의 극단적인 경우를 삼기도 한다. 둘 다 병목 현상의 주요 원인이 될 수 있다. 

 

더보기

트위터의 부하 제어 예시를 보자. 

트위터의 주요기능은 트윗 작성, 타임라인 보기 두 개가 있다. 트윗 작성은 초당 4600개의 요청이 발생하고 (피크일 땐 12000개 정도), 타임라인 보기는 초당 300,000개의 요청 정도가 발생한다. 트위터의 확장성 문제는 트윗의 작성 양 때문이 아니라 fan-out 때문에 발생한다. 

*fan-out : 하나의 수신 요청을 처리하는 데 필요한 다른 서비스의 요청 수. 여기서는 어떤 사용자의 새 포스팅을 그 사용자의 친구 관계에 있는 모든 사용자에게 전달하는 과정을 얘기한다. 

 

트위터가 생각한 팬아웃을 작동하는 방식은 두 가지다. 

  1. 새로운 트윗을 전역 컬렉션에 삽입한다. 사용자가 타임라인 보기를 요청하면 해당 사용자가 팔로우하고 있는 모든 사용자를 찾고, 그 사용자들의 트윗을 찾아 시간 순으로 정렬해서 합치는 방식이다. 
  2. 개별 사용자의 타임라인 캐시를 유지한다. 한 사용자가 트윗을 작성했을 때 해당 사용자를 팔로우하는 사람을 모두 찾고 그들의 타임라인 캐시에 새로운 트윗을 삽입한다. 

트위터는 전통적으로 1번 방법을 사용하고 있었지만 홈 타임라인 질의에 대한 부하를 견디기가 힘들었다. 그래서 2번 방식을 도입해서 쓰기 시점의 일을 늘리는 대신 읽기 시점의 부하를 줄였다.

하지만 2번 방식도 문제는 있었다. 평균적으로 하나의 트윗은 75명의 팔로워에게 전달되었지만 예외의 경우가 있다. (평균값의 무의미함이 여기에 있다.) 유명 인플루언서는 3000만 명 이상의 팔로워를 갖기도 하는데, 그런 인플루언서가 새로운 트윗을 올리면? 3000만 번의 쓰기가 발생해야 했다. 따라서 트위터는 1번과 2번 방법을 혼합하여 일반적인 유저의 경우에는 2번 방식으로, 소수의 유명 인플루언서들의 경우는 1번 방식으로 쓰기를 진행했고 읽는 시점에 1번을 합치는 것으로 대체했다. 이렇게 팬아웃의 부하를 줄일 수 있었던 것이다. 

 

성능 기술하기

부하 기술한 후에는 부하가 증가하면 어떤 일이 일어나는지 조사할 수 있어야 한다. 부하 매개변수의 수치가 올라갔는데 시스템 자원을 유지하면 성능은 얼마나 떨어질까? 혹은 부하 매개변수의 수치가 올라갔지만 성능을 유지하려면 자원을 얼마나 많이 올려야 할까? 같은 고민에 답을 주기 위함이다. 

 

 

시스템의 성능 수치

처리량 (throughput) 은 대용량 데이터 분산 저장/처리 시스템 같은 일괄 처리 시스템에서 사용하는 수치이다. 초당 처리할 수 있는 레코드 수나 일정 크기의 데이터 집합으로 작업 수행 시 걸리는 전체 시간을 뜻한다. 

응답 시간 (response time)은 웹 어플리케이션에서 자주 사용하는 수치로, 클라이언트가 요청을 보내고 응답을 받는 사이의 시간이다. 

*지연 시간과 혼동해서 사용되기도 하는데, 응답 시간은 요청을 처리하는 실제 서비스 시간으로 네트워크 지연이나 큐 지연도 포함되는 반면 지연 시간은 요청이 처리되길 기다리는 시간으로 서비스를 기다리며 휴지 상태인 시간으로 살짝 다른 점이 있다. 

 

응답 시간의 측정 기준은 단일 숫자보다는 특이값(outlier)이 존재하는 분포가 더 적합하다. 

  • 평균 (= 산술 평균) : 좋은 지표가 아니다. 얼마나 많은 사용자가 실제로 지연을 경험했는지 알 수 없다. 
  • 백분위 : 백분위를 이용해서 다음과 같은 지표들이 생성된다.
    • 중앙값 : 사용자가 보통 얼마나 오래 기다려야 하는가를 나타내 줄 수 있다. 단일 요청에 대한 응답 시간을 참고하기 때문에 한 페이지를 로드하는 데 여러 요청이 사용된다면 중앙값보다 오래 기다려야 할 가능성은 50%를 훨씬 넘게 된다. 
    • 상위 백분위(95분위, 99분위, 99.9분위) : 요청의 95%, 99%, 99.9%가 특정 기준치보다 빠르면 해당 기준치가 각 백분위의 응답 시간 기준치가 된다. 
    • 꼬리 지연 시간 (tail latency) :  시스템에서 가장 느린 요청의 처리 시간을 말한다. 아마존에서는 내부 서비스의 응답시간 요구사항을 99.9분위로 기술한다. 응답 시간이 가장 느린 요청을 경험한 고객들은 보통 가지고 있는 데이터 양이 너무 많아서이고, 이는 즉 해당 고객들이 구매를 많이한, 사랑하는.. 고객들이기 때문이다. 이를 위해서 99.9분위까지의 처리 시간을 높이기 위해 노력하게 된다. 하지만 99.99분위 최적화(10000명 중 한 명이다)는 비용이 너무 높아 효용 가치가 떨어진다. 99.99%라면 해당 처리 시간은 외부적인 요인으로 인해서 발생한 지연 시간일 가능성이 높기 때문이다. 

 

이러한 측정 기준은 서비스 수준 목표나 서비스 수준 협약서에 자주 등장한다. 실제로 협약서에는 응답 시간 중앙값이 200ms 미만이고, 99분위가 1초 미만인 경우 정상 서비스 상태로 간주한다고 기술되어 잇으며 이런 지표는 기대치를 설정하기 때문에 지키지 않으면 환불을 요구할 수 있게 되기도 한다. 

 

큐 대기 지연은 응답 시간의 상당 부분을 차지한다. 소수의 느린 요청 처리가 후속 요청의 처리를 느리게 하게 된다(=선두 차단이라고 한다). 따라서 부하 테스트를 위한 부하 생성 클라이언트는 응답 시간과 독립적으로, 응답을 대기하지 않고 요청을 지속적으로 보내야 큐 대기 지연까지 테스트가 가능하다. 

 

 

부하 대응 접근 방식 (확장성 논의)

1의 부하에 대응할 수 있도록 설계된 서비스가 10의 부하에 대응하는 좋은 방안은 없다. 급성장하는 서비스라면 부하 규모의 자릿수가 바뀔 때마다 (혹은 더 자주) 아키텍처 재검토가 필요하다. 

확장성에는 용량 확장(수직 확장)규모 확장(수평 확장)이 존재한다. 용량 확장은 좀 더 강력한 장비로 이동하는 것이고, 규모 확장은 다수의 낮은 사양 장비에 부하를 분산하는 방식이다. 

다수의 장비에 부하를 분산하는 아키텍처는 비공유(shared-nothing) 아키텍처라고 한다. 실용적인 접근 방식의 조합이 필요하다. 적절한 사양의 장비 몇 대가 다량의 낮은 사양 가상 장비보다 훨씬 가격과 성능 측면에서 효율적일 수 있기 때문이다. 

 

탄력적인 시스템(elastic)은 부하 증가 감지 시 자원을 자동으로 추가해준다. 부하가 예측할 수 없을 만큼 높은 경우에는 유용하다.

반면 탄력적이지 않은 시스템은 수동으로 확장하게 되는데, 더 간단하면서 운영상 예상치 못한 일이 발생할 가능성이 적다. 

 

일반적인 최근까지의 통념은 확장 비용이나 DB를 분산으로 만들어야 하는 고가용성 요구가 있을 때까지 단일 노드에 DB를 유지하고 수평 확장을 해왔다. 하지만 분산 시스템을 위한 도구, 추상화가 좋아지면서 적어도 일부 어플리케이션에선 이 통념이 변화하고 있다. 많은 종류의 분산 데이터 시스템을 다루고 확장성 뿐만 아니라 손쉬운 사용과 유지보수를 어떻게 달성하는지를 알아가야 한다. 

 

특정 어플리케이션에 적합한 확장성을 갖춘 아키텍처는 어떤 가정을 전제로 하고 있다. 주요하게 사용되는 동작과 그렇지 않은 동작을 나누고 그럴 것이다 하는 가정이다. 이 가정은 결국 부하 매개변수가 된다. 그래서 잘못된 가정은 확장성을 높이지 못하고 부하가 발생하는 역효과가 난다. 따라서 스타트업이나 검증되지 않은 제품의 경우에는 미래에 가정하는 부하에 대비한 확장을 진행하기 보다는 빠르게 반복해서 제품 기능을 개선하는 것이 더 중요하다. 

 

3. 유지 보수성

유지 보수를 위한 작업에는 버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적응, 새 유스케이스를 위한 변경, 기술 채무 상환, 새로운 기능 추가 등이 있다. 레거시 시스템 유지보수는 여전히 개발자들이 하기 싫어하기 때문에 레거시 시스템을 더 이상 양산하지 않도록 시스템 설계 원칙을 준수하는 것이 필수다. 

 

시스템 설계 원칙

  1. 운용성 : 운영팀이 시스템을 원활하게 운영할 수 있도록 쉽게 만들어라. 좋은 운용성이란 동일하게 반복되는 테스크를 쉽게 수행하게끔 하여 운영팀이 고부가가치 활동에 노력을 집중할 수 있도록 하는 것이다. 
  2. 단순성 : 시스템에서 복잡도를 최대한 제거해서 새로운 개발자가 시스템을 이해하기 쉽게 만들어야 한다. 복잡도가 높아지면 유지보수 비용이 증가하고 예산과 일정을 초과하게 된다. 여기서 개념적으로 우발적 복잡도와 필수적 복잡도가 나뉜다. 우발적 복잡도는 개발자나 프로젝트 제약 사항, 개발 방법과 같은 외부 요인으로 발생하는 복잡도 문제로 불필요한 복잡성을 말한다. 필수적 복잡도는 복잡한 비즈니스 문제 자체를 해결하기 위해 필요한 복잡성으로 이를 충족시키는 소프트웨어는 문제를 잘 해결하고 있는 것이다. 
    우발적 복잡도 제거를 위한 최상의 도구는 추상화이다. 추상화를 통해 외관은 깔끔하고 직관적으로 유지하되 그 밑의 많은 세부 구현을 두면서 재사용할 수 있도록 한다. 
  3. 발전성 : 이후에 개발자가 시스템을 쉽게 변경할 수 있게 해라. 시스템의 요구 사항은 끊임없이 변하기 때문에 애자일 작업 패턴이 등장하여 변화에 적응하려고 한다. 애자일에서는 또 TDD나 리팩토링으로 이를 만족시키려고 하는데, 이는 로컬 규모에 초점을 두고 있다. 이 책에선 대규모 데이터 시스템 수준에서의 민첩성을 높이는 방법을 소개할 것이다. 앞으로는 민첩성 대신 발전성이란 단어로 사용하게 될 것이다. 

댓글