이 글은 데이터 중심 애플리케이션 설계 라는 책의 내용을 기반으로 제가 이해한 것들을 작성했습니다.
데이터 중심 애플리케이션 설계 라는 책에서는 소프트웨어 시스템이 신뢰성, 확장성, 유지보수성 이라는 세 가지 관심사에 중점을 둔다고 한다.
신뢰성: 시스템은 지속적으로 올바르게 동작해야 한다.
확장성: 시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 방법이 있어야 한다.
유지보수성: 모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야 한다.
신뢰성
- 사용자가 기대한 기능 수행한다.
- 사용자가 범한 실수, 예상치 못한 사용법을 허용할 수 있다.
- 성능은 예상된 부하와 데이터 양에서 필수적인 사용 사례를 충분히 만족한다.
- 허가되지 않은 접근과 오남용을 방지한다.
즉, 무언가 잘못되더라도 지속적으로 올바르게 동작한다는 것이 신뢰성의 의미이다.
신뢰할 수 있는 시스템에 만들기 위해선 결함과, 장애에 대해 알아야 한다.
결함은 잘못될 수 있는 일을 뜻한다. 이 결함을 예측하고 대처할 수 있는 시스템을 내결함성 또는 탄력성을 지녔다고 말할 수 있다.
장애는 사용자에게 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우이다.
결함이 발생할 확률이 0이 될 순 없다. 즉, 결함으로 시스템이 장애가 발생하지 않도록 설계해야 한다. 넷플릭스에서는 내결함성을 가질 수 있도록 카오스 몽키 를 이용?한다고 한다. 간단하게 얘기하면 카오스 몽키를 이용해 무작위로 서버 인스턴스나 컨테이너가 내려가더라도 내결함성을 가질 수 있도록 보완한다고 생각하면 된다.
책에서 얘기하는 건 하드웨어, 소프트웨어로 인한 오류보다 인적 오류가 더 많다고 하며 인적 오류를 대비할 수 있게 시스템을 설계하고 구축하라고 한다. 아래를 통해 신뢰할 수 있는 시스템을 만들 수 있다고 하니 참고하자.
- 오류의 가능성을 최소화하는 방향으로 시스템 설계하기
- ex) 예를 들면 API, 인터페이스를 잘 설계하면 의도한 대로 잘 사용할 수 있기에 잘못 사용하여 문제가 생기는 일을 최소 할 수 있다.
- 모든 수준에서 철저하게 테스트하기
- ex) 정상 케이스 말고 예외 케이스까지 고려해 테스트 코드를 잘 짜자.
- 장애 발생 시 영향을 최소화하기 위해 쉽게 복구할 수 있게 하기
- 성능지표, 오류율 같은 모니터링 대책 마련하기
- 조작 교육과 실습 시행하기
지금까지 나는 개발하면서 신뢰성 있는 서비스를 잘 만들지는 못한 것 같다. 앞으로는 계속 머릿속에서 생각하면서 개발할 수 있도록 해야겠다.
확장성
시스템이 안정적으로 동작한다고 해도 미래에도 안정적으로 동작하는다는 보장은 없다. 그렇기에 확장성 있는 시스템을 만들어야 한다.
먼저, 확장성은 부하가 증가해도 좋은 성능을 유지하기 위한 전략을 의미한다. 그리고 증가한 부하에 대처하는 시스템 능력을 설명하는데 사용하는 용어이기도 하다. 단순히 "X 는 확장 가능하다", "Y 는 확장성이 없다" 같은 말이 아니라 "시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇인가?", "추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?" 와 같은 질문을 고려한다는 의미이다.
먼저 부하 매개변수라는 것을 알아야 한다. 부하 매개변수는 특정 기능 또는 웹 서버의 초당 요청 수, DB 의 읽기/쓰기 비율, 동시 활성 사용자 수, 캐시 적중률 등이 될 수 있다.
우리는 이 부하 매개변수를 통해 현재 서비스의 부하를 기술할 수 있어야 한다. 그래야 부하가 증가할 때 어떤 일이 일어날지 조사할 수 있으며 "~ 에 부하가 2배로 되면?" 과 같은 부하 성장 질문에 대해서 논의할 수 있다. 단순히 "특정 기능에 트래픽이 몰리면 이렇게 할거다" 가 아니다.
이제 우리는 부하 성장 질문을 부하 매개변수를 이용해 질문할 수가 있어졌다. 그러면 성능을 기술할 차례이다. 가령 아래와 같은 질문이 나올 때처럼 말이다.
- 부하 매개변수를 증가시키고 시스템 자원(CPU, 메모리, 네트워크 대역폭 등)은 변경하지 않을 때 시스템 성능은 어떻게 영향을 받을까?
- 부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되길 원한다면 자원을 많이 늘려야 할까?
위 질문 모두 성능 수치가 필요하기에 성능을 기술하는 것도 중요하다.
이제 성능 기술에 대해서 얘기해보도록 하겠다. 한 가지 알아야 할 점은 시스템마다 성능에 대한 관점이 다르다는 것이다. 예를 들어 일괄 처리 시스템은 처리량, 온라인 시스템에선 응답 시간을 주요 성능으로 생각할 수 있다. 응답 시간을 주요 성능으로 보는 시스템을 기준으로 이야기하겠다.
클라이언트는 동일한 요청을 반복해서 날리더라도 매번 응답 시간이 다르다. 그렇기에 다양한 요청을 다루는 실제 서비스에선 응답 시간이 매번 다양하게 변한다. 그래서 응답 시간은 단일 숫자가 아닌 측정 가능한 값의 분포라고 생각해야 한다는 것이다.
예를 들어보겠다. 동일한 기능에 N개의 요청이 있다고 했을 때 가끔 특정 요청에 대해 오래 걸리는 특이 값이 존재할 수 있다. 컨텍스트 스위칭이나 네트워크 패킷 손실로 인한 TCP 재전송, GC, disk 이슈 등으로 인해 말이다. 그렇기에 M개의 요청은 다른 요청들보다 오래 걸릴 수 있기에 "특정 기능의 응답 시간은 어떻게 되나요?" 와 같은 질문에 "평균적으로 1초 걸립니다" 와 같이 단순 평균을 얘기하기보단 백분위를 이용해 얘기하는 게 더 좋다. N개의 요청들의 응답 시간을 정렬하여 "99분위 응답 시간은 0.7초 입니다" 라는 것처럼 말이다. 100개의 요청이라 하면 99개의 요청은 0.7초 이하로 걸리는 것이고 1개의 요청은 0.7초를 초과한다는 것이다.
이렇게 부하 매개변수와 성능 기술하는 것에 대해 얘기했으니 확장을 어떻게 할 것인가에 대해 얘기를 할 수 있어졌다.
보통 확장성을 얘기하면 스케일업, 스케일아웃에 대해서 얘기할 수 있다. 일부 시스템은 탄력적이기에 부하 증가를 감지하면 컴퓨팅 자원을 자동으로 추가할 수도 있기도 하다. stateless 한 서비스를 스케일아웃하는 건 쉽지만 DB 와 같은 시스템은 스케일아웃하기엔 아주 많은 복잡도가 추가적으로 발생한다. 그렇기에 DB 를 분산해야 한다는 요구가 있기 전까진 스케일업을 하는 것이 일반적이다.
중요한 점은 범용적이고 모든 상황에 맞는 확장 아키텍처는 없다는 것이다. 아키텍처를 결정하는 요소는 읽기/쓰기 양, 저장할 데이터의 양, 데이터의 복잡도, 응답 시간 요구사항, 접근 패턴 등이 있다. 가령 크기가 1KB 인 요청을 초당 100,000건 처리하는 시스템과 크기가 1GB 인 요청을 분당 3건 처리하는 시스템은 매우 다르다.
그렇기에 특정 애플리케이션에 적합한 확장성을 갖춘 아키텍처는 주요 동작, 잘하지 않는 동작을 가정해 구축된다. 여기서의 가정은 부하 매개변수가 된다. 가정이 잘못되면 확장에 대한 엔지니어링 노력이 헛수고가 되고 역효과를 낳으니 주의해야 한다.
유지보수성
소프트웨어의 비용의 대부분은 초기 개발이 아니라 지속해서 이어지는 유지보수에 들어간다는 사실이 잘 알려져 있다. 그렇기에 유지보수 중 고통을 최소화하고 레거시를 직접 만들지 않도록 주의해야 한다. 이 때 주의하여야 할 원칙이 있다.
- 운용성 (운영팀이 시스템을 원활히 운영할 수 있도록 쉽게 만들어야 함)
- 단순성 (새로운 엔지니어가 시스템을 이해하기 쉽게 만들어야 함)
- 발전성 (엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 해야 함)
위에서 얘기한 신뢰성, 확장성을 달성하기 위한 쉬운 해결책은 없다. 그보다 운용성, 단순성, 발전성을 염두해야 한다.
운용성에 대해 얘기해보도록 하겠다 (운용과 운영의 차이를 검색해서 알았다;;). 좋은 운영은 종종 나쁜 소프트웨어의 제약을 피하는 대안이 될 수 있다. 물론 좋은 소프트웨어라도 나쁘게 운영할 경우 작동을 신뢰할 수 없기도 하다. 항상 운영의 일부를 자동화해야 한다는 것을 잊지 말자.
좋은 운영을 하려면 다음과 같아야 한다.
- 시스템 상태를 모니터링하고 상태가 좋지 않다면 빠르게 서비스를 복원
- 시스템 장애, 성능 저하 등의 문제 원인을 추적
- 소프트웨어와 플랫폼을 최신 상태로 유지
- 서로 다른 시스템끼리 어떻게 영향을 주는 확인
- 미래에 발생 가능한 문제를 예측해 문제 발생 전에 해결 (용량 등)
- 배포, 설정 관리 등을 위한 모범 사례, 도구 마련
- 설정 변경으로 생기는 시스템 보안 유지보수
- 예측 가능한 운영과 안정적인 서비스 환경을 유지하기 위한 절차 정의
- 개인 인사이동에도 시스템에 대한 조직의 지식 보전
좋은 운용성이란 동일하게 반복되는 태스크를 쉽게 수행하도록 만들어야 한다는 것이다. 그래야 운영팀이 고부가가치 활동에 집중할 수 있기 때문이다.
단순성에 대해서 얘기해보도록 하겠다. 복잡도를 관리하며 최대한 단순하게 만들어야 한다는 것이다. 프로젝트가 커짐에 따라 시스템은 매우 복잡하고 이해하기 어려워지는데 이는 유지보수 비용을 증가시킨다. 복잡도는 다양하게 나타나는데 "모듈 간 강한 커플링", "복잡한 의존성", "일관성 없는 네이밍과 용어", "임시방편으로 문제를 해결한 케이스" 등이 있다. 이런 것들로 인해 개발자는 시스템을 이해하기 어려워지고, 사이드 이펙트가 발생하기에 유지보수가 어려워진다. 즉, 복잡도를 줄여야 유지보수를 쉽게 할 수 있다.
단순히 기능을 줄여 단순하게 만든다는 의미가 아닌 우발적 복잡도를 줄여야 한다. 여기서 말하는 우발적 복잡도란 소프트웨어의 구현에서 발생하는 것을 얘기한다. 이때 추상화가 중요하다. 직관적인 외관 아래에 많은 세부 구현을 숨길 수 있기 때문이다. 이런 추상화는 큰 시스템의 일부를 잘 정의하고 재사용 가능한 구성 요소로 추출할 수 있게 한다.
발전성에 대해서 얘기해보도록 하겠다. 시스템의 요구사항은 영원히 바뀌지 않을 가능성이 매우 적다. 그렇기에 변화를 쉽게 할 수 있도록 해야 한다는 것이다.
기존에 이 책을 읽기 전에는 신뢰성, 확장성, 유지보수성에 대해서5초 안에 말할 수 있었다. 그러나 그렇게 짧은 시간 안에 각각의 의미를 얘기할 수 있는 단어들이 아니라는 것을 알게 되었다. 또한, 개발자로서 항상 좋은 소프트웨어를 만들어야 하는데 그러면 좋은 소프트웨어란 무엇일까에 대해서 개발자 입장에서 다시 생각해볼 수 있는 기회가 된 것 같기도 하다.
이 책은 앞으로 신뢰할 수 있고, 확장성 있으며 유지 보수하기 쉬운 소프트웨어를 만드는데 도움이 될 것 같고 책에는 관련 내용에 대해 더 자세히 설명하니 다른 사람들도 한 번씩 읽어보았으면 좋겠다.
그리고 확장성 얘기할 때 부하에 관한 내용이 있는데 지금까지 내가 정말 잘못하고 있음도 깨달았다. 앞으론 저렇게 얘기할 수 있도록 해야겠다.