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

임지규·2021년 10월 24일
9
post-thumbnail

개요

책에서는 데이터 시스템이 가져야 하는 가장 중요한 요소들 중
신뢰성(Reliability), 확장성(Scalability), 유지보수성(Maintainability)의 개념을 소개하고 이러한 목표를 달성하기 위한 방법을 설명한다.

신뢰성

개념

책에서 정의하는 개념은 다음과 같다.

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

소프트웨어에 대한 일반적인 기대치를 생각해보면 다음과 같다.

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

이러한 것들을 "올바른 동작" 이라고 정의할 때, 무언가 잘못되더라도 지속적으로 올바르게 동작함을 신뢰성의 의미로 이해할 수 있다.

결함

시스템에서 잘못될 수 있음을 결함 이라고 하고 그러한 결함을 예측하고 대처할 수 있는 시스템을 내결함성 혹은 탄력성을 지녔다고 표현한다.

이제 결함의 종류를 크게 하드웨어, 소프트웨어, 사람이라는 세가지 측면으로 나누어 알아보자.

하드웨어 결함

하드웨어 결함의 예시로는

  • 하드디스크, 램의 고장
  • 정전 사태
  • 실수로 네트워크 케이블이 뽑힘

등이 있을 수 있다.

시스템 장애율을 줄이기 위한 첫 번째 대응은 각 하드웨어의 구성 요소에 중복을 추가하는 방법이 있다.
구성 요소 하나가 죽으면 해당 구성 요소가 교체되는 동안 중복된 구성 요소를 사용함으로써 장애 발생에 대응할 수 있다.

이러한 방식에는 운영상 장점이 있다.
장비를 재부팅해야 하는 경우, 단일 서버 시스템은 중단시간이 필요하지만 이러한 시스템은 전체 시스템의 중단시간 없이 한 번에 한 노드씩 패치할 수 있다.(blue-green 배포 방식과 유사하다고 느낀다.)

소프트웨어 오류

소프트웨어 오류는 하드웨어 오류에 비해 예상하기가 더 어렵고 각 노드의 오류간 상관관계가 존재한다.
그렇기 때문에 시스템 오류를 더욱 많이 유발하는 경향이 있다.
소프트웨어 오류의 예시로는

  • 잘못된 특정 입력이 있을 때 애플리케이션 서버가 종료되는 버그
    (2012년 6월 30일 리눅스 커널 버그로 인해 많은 애플리케이션이 일제히 멈춰버린 사건처럼 - 리눅스 윤초 버그 사건)
  • CPU, 메모리, 디스크 공간, 네트워크 대역폭처럼 공유 자원을 과도하게 사용하는 일부 프로세스
  • 시스템 속도가 느려지면 바응이 없거나 잘못된 응답을 반환하는 서비스
  • 한 구성 요소의 결함이 다른 구성 요소의 결함을 야기하는 연쇄 장애(cascading failure)

등이 있다.

소프트웨어의 체계적 오류 문제는 신속한 해결책이 없다.
직접 시스템의 가정과 상호작용을 철저히 분석하고 테스트하여 프로세스 간 격리, 프로세스 재시작, 모니터링 등이 도움이 된다.
시스템이 지켜야 하는 상황이나 가정이 있다면(이를테면 특정 메시지 큐의 발송, 수신 수가 같다.)
해당 지표를 라이브 중에 지속적으로 관찰하여 상태를 점검하는 것이 좋다.

인적 오류

시스템을 구축하고 운영하는 것은 결국 사람이다.
그러나 사람은 그런 의도가 있었는가와 별개로 언제든 실수를 할 수 있다.
그렇기 때문에 우리는 시스템을 신뢰성 있게 만들기 위해 다양한 접근 방식과 규칙들을 결합해야 한다.
이를테면 다음과 같은 내용들이 있다.

  • 잘 설계된 추상화, API, 인터페이스를 사용하면 옳은 일은 쉽게 하고 잘못된 일은 막을 수 있다.
  • 사람이 가장 많이 실수하는 부분에서 장애가 발생할 수 있는 부분을 분리한다.
    특히 실제 데이터를 사용해 분석이나 실험이 가능하지만 실제 사용자에게는 영향을 주지 않은
    비 프로덕션 환경을(Sandbox) 구성하는 것이 좋다.
  • 단위 테스트, 전체 시스템 통합 테스트, 수동 테스트까지 모든 수준에서 철저히 테스트한다.
  • 빠르고 쉬운 복구를 가능하게 한다.
    설정 변경 내역을 빠르게 롤백(roll back) 하고
    새로운 코드를 배포할 때에는 서서히 롤아웃(roll out)하고 (예상치 못한 버그가 일단 일부 사용자에게만 영향을 미치게 한다.)
  • 성능 지표, 오류율과 같은 모니터링 대책을 명확하게 세운다.
    모니터링 지표를 보는 것은 장애 발생시 조기에 경고를 보내 빠른 대처가 가능하게 하며
    이후 분석을 할 때에도 중요한 자료가 된다.

결국 인적 오류가 나타날 수 있는 부분을 최소화하여 설계하는 것
오류가 나타났을 때 빠르고 쉽게 대처, 분석할 수 있는 방안을 세우는 것
최고의 시스템을 설계하는 최선의 방식이라고 생각한다.

확장성

개념

시스템이 현재 안정적으로 동작한다고 해도 미래에 부하가 증가했을 때에도 동작하지 않을 수 있다.
서비스의 사용자가 늘어감에 따라 동시간대 사용 리소스와 처리할 데이터가 계속해서 증가한다.
확장성은 이처럼 증가하는 부하에 대처할 수 있는 시스템의 능력을 설명하는 용어이다.

그러나 확장성이라는 말은 일차원적인 표식이 아니다.
"A 시스템은 확장성이 있다" 혹은 "B 시스템은 확장성이 없다" 같은 말은 의미가 없다.
확장성을 논한다는 것은 "현재 시스템이 특정 방식으로 커지면 어떻게 대처할 것인가?"
"추가 부하를 다루기 위해서는 어떤 자원을 어떻게 투입할까?" 와 같은 질문을 고려한다는 의미이다.

부하 기술하기

부하 증가를 논하기 전에 가장 먼저 해야할 것은 무엇일까?
바로 현재 시스템의 부하는 무엇인지 정의하는 것이다.
그래야만 우리는 "부하가 두 배로 증가했다"와 같은 말의 의미를 파악할 수 있다.

부하는 부하 매개변수(load parameter) 라는 몇 개의 숫자로 나타낼 수 있다.
부하 매개변수가 될 수 있는 요소들에는

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

등이 있으며 평균 지표가 중요할 수도 있고 극단적인 경우의 지표가 중요할 수도 있다.
이는 현재 시스템의 설계에 따라 달라지기 때문에 분석이 필요하다.

성능 기술하기

시스템 부하를 기술한 이후에는 해당 부하가 증가할 때 어떤 일이 발생하는지 조사할 수 있다.
그리고 조사는 다음과 같이 진행할 수 있다.

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

이 때 중요한 것은 마찬가지로 성능을 정의하는 것이다.
일반적으로 하둡과 같은 일괄 처리 시스템은 처리량(throughput)에 집중하며
WAS 같은 온라인 시스템에서는 클라이언트가 요청을 보내고 응답을 받는 사이의 시간인
응답 시간(response time)에 집중한다.

이 역시 마찬가지로 평균이나 백분위와 같은 지표 산출 방식을 결정해야 한다.
응답시간을 예로 들면 사용자가 보통 얼마나 오래 기다려야 하는지 알고 싶을 때에는
평균(산술 평균)보다는 백분위 - 중앙값을 사용하는 것이 권장된다.

백분위는 서비스 수준 목표, 서비스 수준 협약서에 자주 등장하고 기대 성능과 서비스 가용성을 정의하는 계약서에도 자주 등장하는 지표이다.
EX) "응답 시간 중앙값이 200밀리초 미만이고 99분위가 1초 미만인 경우 정상 서비스 상태로 간주하며 서비스 제공 시간은 99.9% 이상이어야 한다."

부하 대응 접근 방식

성능 측정을 위해 부하와 지표를 기술한 뒤에는 본격적인 황장성을 논의한다.
일반적으로 확장성과 관련되어 용량 확장(scaling up)과 규모 확장(scaling out)으로 구분한다.

용량 확장(scaling up) : vertical scaling(수직 확장)이라고도 하며 더 좋은 리소스를 가진 장비로 이동하는 것을 의미한다.

규모 확장(scaling out) : horizontal scaling(수평 확장)이라고도 하며 다수의 낮은 사양 장비에 부하를 분산시키는 것을 의미한다.

단일 장비에서 수행할 수 있는 시스템은 간단하지만 고사양 장비는 비싸기 때문에 작업 부하가 커질수록 규모 확장은 거의 반드시 필요해진다.
분산 시스템을 위한 도구와 추상화가 좋아지면서 이후에는 빅데이터를 다루지 않는 사용 사례에서도 분산 데이터 시스템이 기본 아키텍처로 자리 잡을 가능성이 있다.

대개 대규모 시스템 아키텍처는 해당 시스템을 사용하는 애플리케이션에 특화되어 있다.
범용적이고 모든 상황에 들어맞는 확장 아키텍처는 없다.
아키텍처는 읽기의 양, 쓰기의 양, 저장할 데이터의 양, 응답 시간 요구사항, 접근 패턴 등의 요소를 고려하여 구성된다.

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

유지보수성

개념

소프트웨어 비용의 대부분은 초기 개발보다 유지보수에 들어간다는 사실은 널리 알려져있다.
유지보수는 버그 수정, 운영 유지, 장애 조사, 새 사용 사례를 위한 변경, 기술 채무 상환, 새 기능 추가 등으로 구성된다.

하지만 대부분의 개발자들은 레거시 시스템의 유지보수 작업을 꺼려한다.
다른 사람의 실수를 고치거나 올드한 플랫폼에서 작업해야 하는 등의 이유 때문이다.
따라서 우리는 유지보수 중 고통을 최소화하고 레거시 소프트웨어를 만들지 않도록 소프트웨어를 설계할 수 있고 그래야만 한다.
그것이 바로 시스템의 유지보수성을 높이는 길이다.

이제 유지보수성을 구성하는 세 가지 측면인 운용성(operability), 단순성(simplity), 발전성(evolvability)을 설명한다.

운용성

운용성이란 운영의 편리함을 만드는 것이다.

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

데이터 시스템에서는 이러한 작업을 위해 아래와 같은 항목들을 수행할 수 있다.

  • 좋은 모니터링으로 런타임 동작과 시스템 내부에 대한 가시성 제공
  • 표준 도구를 이용하여 자동화와 통합을 위한 지원
  • 개별 장비 의존성을 회피
  • 유지보수를 위해 장비를 내리더라도 시스템에 영향을 주지 않고 운영 가능하게 함
  • 좋은 문서와 이해하기 쉬운 운영 모델(예를 들면 "X를 하면 Y가 발생한다") 제공
  • 충분한 기본 동작을 제공하고 필요하면 기본값을 재정의할 수 있는 자유를 관리자에게 부여
  • 자기 회복이 가능할 뿐 아니라 필요에 따라 관리자가 수동으로 시스템 상태를 제어할 수 있도록 함
  • 예측 가능한 동작과 예기치 못한 상황 최소화

단순성

단순성이란 복잡도를 관리하는 것이다.

소규모 소프트웨어에서는 대개 간단한 코드로 깔끔한 시스템을 작성할 수 있지만 프로젝트가 커지면서 시스템은 매우 복잡하고 이해하기 어려워진다.
이는 해당 시스템에 관여하는 모든 작업자의 유지보수 비용을 증가시키고 알다시피 유지보수가 소프트웨어 비용의 대부분을 차지하기 때문에 영향이 크다.

복잡도는 다양하게 나타나는데 모듈간 강한 커플링(tight coupling), 복잡한 의존성, 일관성 없는 명명(naming), 임시방편으로 처리한 예외 처리 등이 있다.
이러한 주제는 대표적으로 클린 코드 원칙부터 다양한 곳에서 이미 많이 회자되고 있는 내용이다.

이러한 우발적 복잡도를 줄이기 위한 최상의 도구는 추상화이다.
좋은 추상화는 명확하고 직관적인 아래로 세부 구현을 숨긴다.

추상화는 사실 어렵게 생각할 것 없이 사용자가 쉽게 사용할 수 있는 인터페이스를 제공하고 내부적으로는 세부 구현이 있는 것을 말한다.
고수준 프로그래밍 언어는 기계어, CPU 레지스터, 시스템 호출을 숨긴 추상화고
SQL은 디스크 기록과 메모리에 저장한 복잡한 데이터 구조와 다른 클라이언트의 동시 요청 등을 숨긴 추상화이다.
이러한 추상화를 통해 우리는 직접 기계어를 사용하지 않고도 프로그래밍 언어의 추상화로 보다 쉽게 개발 작업을 할 수 있는 것이다.

발전성

발전성이란 변화를 쉽게 만드는 것이다.

시스템의 요구사항은 끊임없이 변할 가능성이 크다.
새로운 사용 사례가 나타나고 비즈니스 우선순위가 바뀌고 새 기능이 만들어지고 시스템의 성장으로 아키텍처가 변화하고 심지어는 법적 규제가 변경되는 것마저도 시스템의 요구사항을 변화시킨다.

조직 프로세스 측면에서 애자일(agile) 작업 패턴은 변화에 적응하기 위한 프레임워크를 제공한다.
또한 애자일 커뮤니티는 테스트 주도 개발(TDD)과 리팩토링 처럼 변화가 잦은 환경에서 개발할 때 도움이 되는 기술 도구나 패턴을 개발하고 있다.
이러한 애자일 기법에 대한 설명은 대부분 매우 작고, 로컬 규모에 초점을 맞추고 있다.
이 책에서는 다양한 애플리케이션으로 구성된 대규모 데이터 시스템 수준에서 발전성을 높이는 방법을 제시한다.

데이터 시스템의 변경을 쉽게 하는 것은 시스템의 단순성과 추상화와 역시 밀접한 관련이 있다.
결국 복잡도를 줄인 시스템이 의미하는 것이 유지보수 작업이 편해짐을 의미하고 변경에도 민첩한 시스템임을 의미하기 때문이다.

정리

애플리케이션이 유용하려면 다양한 요구사항을 충족시켜야 한다.
이는 기능적 요구사항(데이터 저장, 조회, 검색, 처리 등의 작업)과 비기능적 요구사항(보안, 신뢰성, 확장성, 유지보수성, 호환성 등)으로 분리시킬 수 있는데 이번 장에서는 신뢰성, 확장성, 유지보수성을 집중적으로 알아보았다.
신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션을 만드는 간단한 해결책은 없지만 여러 시스템에서 반복하여 재현되는 특정 패턴과 기술이 있다.

profile
서울대학교 컴퓨터공학부 Backend(Data) Engineer

0개의 댓글