NLP Blog

3. 역할, 책임, 협력

|

3. 역할, 책임, 협력

  • 객체지향 패러다임의 관점에서 핵심은 역할, 책임, 협력
  • 클래스, 상속, 지연 바인딩은 구현 픅면에 치우쳐있어 객체지향 패러다임의 본질과는 거리가 있다.
  • 객체지향의 본질은 협력하는 객체들의 공동체를 창조 하는 것
  • 협력 구성 -> 적절한 객체 찾기 -> 적절한 책임 할당; 클래스와 상속은 객체들의 책임과 협력이 어느 정도 자리를 잡은 후에 구현하는 것
  • 이런 설계가 되기 전 구현을 해버리면 변경하기 어렵고 유연하지 못한 코드를 낳을 수 있다.

1. 협력

영화 예매 시스템 돌아보기

  • 그림 3.1 참고
  • 객체 지향 원칙을 따르는 애플리케이션의 제어 흐름은 어떤 하나의 객체에 의해 통제되지 않고 다양한 객체들 사이에 균현 있게 분배되는 것이 일반적
  • 중요한 것은 다양한 객체들이 영화 예매라는 기능을 구현하기 위해 메시지를 주고받으면서 상호작용한다는 것
  • 객체들의 상호작용을 협력, 객체가 협력에 참여하기 위해 수행하는 로직은 책임, 객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성

협력

  • 메세지 전송(message sending)은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단
  • 객체는 다른 객체의 상세한 내부 구현에 직접 접근할 수 없기 때문에 (캡슐화) 오직 메시지 전송을 총해서만 자신의 요청은 전달
  • 메시지를 수신한 객체는 메서드를 실행해 요청에 응답
    • 객체가 메시지를 처리할 방법을 스스로 선택한다는 점이 중요 -> 객체는 자신의 일을 스스로 처리할 수 있는 자율적인 존재
  • 영화 예매 예제에서 ScreeningMovie 협력을 보면, ScreeningMovie에게 caculateMovieFee 메시지를 전송하여 요금 계산을 요청
    • 이렇게 위임을 하는 이유는 요금에 대해서 가장 잘 아는 객체가 Movie이기 때문이다
    • 만약 요금 계산을 Screening이 하려고 한다면, Movie 내부 구현에 직접 접근해야하고, 이는 변경하기 어려운 코드를 만든다
    • 또한 Movie의 자율성이 훼손됨
    • 결과적으로 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화하는 것 -> 변경에 대한 파급효과 제한 가능

협력이 설계를 위한 문맥을 결정한다

  • 어떤 객체도 섬이 아니다
  • 애플리케이션 안에 어떤 객체가 필요하다면 그 이유는 그 객체가 어떤 협력에 참여하고 있기 떄문
  • 협력에 참여할 수 있는 이유는 협력에 필요한 적절한 행동을 보유하고 있기 떄문
  • 객체의 행동을 결정하는 것은 객체가 참여하고 있는 협력
    • 협력이 바뀌면 객체가 제공해야 하는 행동 역시 바뀌어야 함
  • 예제의 Movieplay 메서드가 없는 이유가 뭘까? 우리의 예제는 예매를 위한 어플리케이션이기 때문이다


  • 객체의 상태를 결정하는 것은 행동
  • 객체의 상태는 그 객체가 행동을 수행하는 데 필요한 정보가 무엇인지로 결정됨
  • MoviefeediscountPolicy라는 인스턴스 변수를 상태의 일부로 포함하는 이유는 요금 계산이라는 행동을 수행하는데 필요하기 때문
  • 상태는 객체가 행동하는데 필요한 정보에 의해 결정
  • 행동은 협력 안에서 객체가 처리할 메시지로 결정
  • 결과적으로 객체가 참여하는 협력이 객체를 구헝하는 행동과 상태 모두를 결정
  • 협력은 문맥(context)을 제공

2. 책임

책임이란 무엇인가

  • 협력이 갖춰진 이후에는 협력에 필요한 행동을 수행할 수 있는 적절한 객체를 찾아야 한다
  • 이때 협력에 참여하기 위해 객체가 수행하는 행동을 책임이라고 부른다
  • 책임이란 객체에 의해 정의되는 응집도 있는 행위의 집합
    • 객체가 유지해야 하는 정보와 수행할 수 있는 행동에 대해 개략적으로 서술한 문장, 이는 하는 것아는것으로 나뉜다


  • 하는 것
    • 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
    • 다른 객체의 행동을 시작시키는 것
    • 다른 객체의 활동을 제어하고 조절하는 것
  • 아는 것
    • 사적인 정보에 관해 아는 것
    • 관련된 객체에 관해 아는 것
    • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것


  • 그림 3.3, 객체의 책임을 CRC카드로 표현
  • 협력 안에서 객체에게 할당한 책임이 외부의 인터페이스와 내부의 속성을 결정
  • 책임의 관점에서 ‘아는 것’과 ‘하는 것’이 밀접하게 연관돼 있다.
    • 객체는 자신이 맡은 책임을 수행하는 데 필요한 정보를 알고 있을 책임이 있음
    • 자신이 할 수 없는 작업을 도와줄 객체를 알고 있을 책임이 있다
  • 책임은 객체지향 설계의 핵심
  • 적절한 협력이 적절한 책임을 제공 -> 적절한 책임을 적할한 객체에게 할당 == 단순하고 유연할 설계
  • 다시 한 번 강조 : 겍체지향 설계에서 가장 중요한 것은 책임, 구현 방법은 그 뒤의 일

책임 할당

  • 자율적인 객체를 만드는 가장 기본적인 방법은 책임을 수행하는 데 필요한 정보를 가장 잘 알고 있는 전문가에게 그 책임을 할당 -> INFORMATION EXPERT pattern
  • 객체에게 책임을 할당하기 위해서는 먼저 협력이라는 문맥을 정의해야 함
  • 협력 설계의 출발점 : 시스템이 사용자에게 제공하는 기능을 시스템이 담당할 하나의 책임으로 바라보는 것
    • 객체지향 설계는 시스템의 책임을 완료하는 데 필요한 더 작은 책임을 찾아내고, 이를 객체들에게 할당하는 반복적인 과정
  • 영화 예매 예제
1: 예매하라 -> :Screening -> 2: 가격을 계산하라 -> :Movie
  • 예매 전문가 : Screening, 가격 전문가 : Movie, 할인 전문가 : DiscountPolicy, …
  • 객체 지향 설계는 협력에 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하는 반복적인 과정을 통해 이뤄진다.
  • 이렇게 결정된 메시지가 객체의 퍼블릭 인터페이스를 구성
  • 협력을 설계하면서 객체의 책임을 식별해 나가는 과정에서 최종적으로 얻게 되는 결과물은 시스템을 구성하는 객체들의 인터페이스와 오퍼레이션의 목록

책임 주도 설계

  • 책임 주도 설계 (Resposibility-Driven Design, RDD) : 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법
    • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악
    • 시스템 책임을 더 작은 책임으로 분할
    • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당
    • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다
    • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 함

메시지가 객체를 결정한다

  • 객체에게 책임을 할당하는 데 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택
  • 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 함
  • 메시지가 객체를 선택하게 해야하는 두 가지 중요한 이유
    • 객체가 최소한의 인터페이스(minimal interface)를 가질 수 있음
      • 필요한 메시지가 식별될 때까지 객체의 퍼블릭 인터페이스에 어떤 것도 추가하지 않기 때문에, 객체는 애플리케이션에 꼭 필요한 크기의 퍼블릭 인터페이스를 가질 수 있음
    • 객체는 충분히 추상적인 인터페이스(abstract interface)를 가질 수 있게 됨
      • 객체의 인터페이스는 무엇what을 하는지는 표현해야 하지만 어떻게how 수행하는지를 노출해서는 안된다
      • 메시지는 외부의 객체가 요청하는 무언가를 의미하기 때문에 메시지를 먼저 식별하면 무성ㅅ을 수행할지에 초점을 맞추는 인터페이스를 얻을 수 있다
  • 결과적으로 협력을 구성하는 객체들의 인터페이스는 충분히 추상적인 동시에 최소한의 크기를 유지할 수 있음

행동이 상태를 결정한다

  • 객체가 존재하는 이유는 협력에 참여하기 위해서
  • 객체는 협렵에 필요한 행동을 제공해야 함
  • 객체를 객체답게 만드는 것은 객체의 상태가 아니라 객체가 다른 객체에게 제공하는 행동
  • 객체가 협력에 접합한지를 결정하는 것은 그 객체의 상태가 아니라 행동이다
  • 얼마나 적절한 객체를 창조했느냐는 얼마나 적절한 책임을 할당했느냐에 달려있고, 책임이 얼마나 적절한지는 협력에 얼마나 적절한가에 달려있음
  • 초보자들의 흔한 실수 : 객체의 상태를 결정 한 이후 상태에 필요한 행동을 결정
    • 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만드므로 캡슐화를 저해
    • 객체의 내부 구현을 변경하면 퍼블릭 인터페이스도 함께 변경되고, 결국 객체에 의존하는 클라이언트로 변경의 영향이 전파
  • 캡슐화를 위반하지 않도록 구현에 대한 결정을 뒤로 미루면서 객체의 해우이를 고려하기 위해서는 항상 협력이라는 문맥 안에서 객체를 생각해야 함
  • 개별 객체의 상태와 행동이 아닌 시스템의 기능을 구현하기 위한 협력에 초점을 맞춰야만 응집도가 높고 결합도가 낮은 객체들을 창조 할 수 있다.
  • 상태는 단지 객체가 행동을 정상저으로 수행하기 위해 필요한 재료일 뿐
  • 행동이 중요!

3. 역할

역할과 협력

  • 객체의 목적은 협력 안에서 객체가 맡게 되는 책임의 집합 -> 역할
  • 영화 예매 협력에서 예매하라라는 메시지 처리의 과정
    • 영화를 예매할 수 있는 적절한 역할이 무엇인지 찾는 것
    • 역할을 수행할 객체로 Screening 인스턴스를 선택
  • 사실은 역할을 찾고 객체를 선택

유연하고 재사용 가능한 협력

  • 역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문
  • 할인 요금을 계산 할 때 가격 할인 정책 마다 협력을 개별적으로 만들어야 하는가? -> 아니다, 코드 중복이 일어남
  • 객체가 아닌 책임에 초점을 맞추어야 함. 가격 할인 정책들은 할인 요금 계산이라는 동일한 책임을 수행한다
  • 객체라는 존재를 지우고 할인 요금을 계산하라라는 메시지에 응답할 수 있는 대표자를 생각한다면 두 협력을 하나로 통합할 수 있음, 이러한 객체의 슬록을 역할이라고 생각할 수 있다

역할은 다른 것으로 교체할 수 있는 책임의 집합이다[Wirfs-Brock03]

  • 여기서의 역할이 두 종류의 구체적인 객체를 포괄하는 추상화라는 점에 주목
  • 요점은 동일한 책임을 수행하는 역할을 기반으로 두 개의 협력을 하나로 통합할 수 있다는 것
  • 따라서 역할을 이용하면 불필요한 중복 코드를 제거할 수 있음
  • 책임과 역할을 중심으로 협력을 바라보는 것이 바로 변경과 확장이 용이한 유연한 설계로 나아가는 첫걸음

역할의 구현

  • 역할을 구현하는 가장 일반적인 방법은 추상 클래스인터페이스를 사용하는 것
  • 추상 클래스와 인터페이스는 구체 클래스들이 따라야 하는 책임의 집합을 서술한 것
  • 추상 클래스는 책임의 일부를 구현해 놓은 것, 인터페이스는 일체의 구현 없이 책임의 집합만을 나열해 놓은 것
  • 역할이 다양한 종류의 객체를 숭할 수 있는 일종의 슬록이자 구체적인 객체들의 타입을 캡슐화하는 추상화
  • 일단 협력 안에서의 역할이 어떤 책임을 수행해야 하는지를 결정하는 것이 중요
  • 역할을 구현하는 방법을 그 다음 문제
  • 객체에게 중요한 것은 행동이며, 역할은 객체를 추상화해서 객체 자체가 아닌 협력에 초점을 맞출 수 있게 함

객체 대 역할

  • 역할은 객체가 참여할 수 있는 일종의 슬록
  • 만약 오직 한 종류의 객체만 협력에 참여하는 상황에서 역할이라는 개념을 고려하는 것이 유용할까?
    • 레베카 워프스브록의 말에 따르면 협력에 참여하는 후보가 여러 종류의 객체에 의해 수행될 필요가 있다 -> 역할, 단지 한 종류의 객체만이 협력에 참여할 필요가 있다 -> 객체
  • 설계 초반에는 어떤 것이 역할이고 어떤 것이 객체인지가 또렷하게 드러나지는 않을 것
  • 설계 초반에는 적절한 책임과 협력의 큰 그림을 탐색하는 것이 가장 중요한 목표여야 하고 역할과 객체를 명확하게 구분하는 것은 그렇게 중요하지는 않다
  • 애매하다면 단순하게 객체로 시작하고, 반복적으로 책임과 협력을 정제해가면서 필요한 순각에 객체로부터 역할을 분리해내는 것이 가장 좋은 방법

  • 처음에 특정 시나리오에 대한 협력을 구상할 때는 아마도 도메인 모델에 있는 개념들을 후보로 선택해 직접 책임을 할당할 것
  • 다양한 시나리오를 설계로 옮기면서 협력을 지속적으로 정제하다 보면 두 협력이 거의 유사한 구조를 보인다는 것을 발견하게 됨
  • 이 경우 두 협력을 하나로 합치면서 두 객체를 포괄할 수 있는 역할을 고려해서 객체를 역할로 대체 가능

역할과 추상화

  • 역할은 공통의 책임을 바탕으로 객체의 종류를 숨기기 때문에 이런 관점에서 역할을 객체의 추상화로 볼 수 있음
  • 추상화의 장점
    • 세부 사항에 억눌리지 않고도 상위 수준의 정책을 쉽고 간단하게 표현할 수 있음
      • 추상화를 적절하게 사용하면 불필요한 세부 사항을 생략하고 핵심적인 개념을 강조할 수 있다
      • 93 page 예시 참조
    • 설계를 유연하게 만들 수 있음
      • 협력 안에서 동일한 책임을 수행하는 객체들은 동일한 역할을 수행하기 떄문에 서로 대체 가능
      • 프레임워크나 디자인 패턴과 같이 재사용 가능한 코드나 설계 아이디어를 구성하는 핵심적인 요소가 바로 역할

배우와 배역

  • 연극의 배역과 배우 간의 관계에 다음과 같은 특성이 존재
    • 배역은 연극 배우가 특정 연극에서 연기하는 역할
    • 배역은 연극이 상영되는 동안에만 존재하는 일시적인 개념
    • 연극이 끝나면 연극 배우는 배역이라는 역할을 벗어 버리고 원래의 연극 배우로 돌아옴
    • 서로 다른 배우들이 동일한 배여기을 연기할 수 있음
    • 하나의 배우가 다양한 연극 안에서 서로 다른 배역을 연기할 수 있음\
  • 연극 안에서 배역을 연기하는 배우라는 은유는 협력 안에서 역할을 수행하는 객체
    • 하나의 배역을 여러 배우가 연기할 수 있는 것처럼 동일한 역할을 수행하는 하나 이상의 객체들이 존재 가능 함
    • 배우가 여러 연극에 참여하면서 여러 배역을 연기할 수 있는 것처럼 객체 역시 여러 협력에 참여하면서 다양한 역할을 수행할 수 있음
      • 객체는 여러 역할을 가질 수 있지만 특정한 협력 안에서는 일시적으로 오직 하나의 역할만이 보여진다
  • 역할은 특정한 객체의 종류를 캡슐화하기 때문에 동일한 역할을 수행하고 계약을 준수하는 대체 가능한 객체들은 다형적이다.

Comment  Read more

22. 연속적 장애 다루기

|

22. 연속적 장애 다루기

처음에 성공하지 못하면 그 이상으로 늦어지게 된다 - 댄 샌들러 (Google SW Engineer) 왜 사람들은 조금만 더 신경을 쓰면 된다는 사실을 늘 잊곤 하는가? - 에이드 오시니에 (구글 개발자 전도사)

  • 연속적 장애(cascading failure)는 정상적인 것처럼 보여지는 응답때문에 시간이 지나면서 장애가 계속해서 가중되는 현상
    • 전체 시스템의 일부에서 장애가 발생했을 때 주로 나타나며, 이로 인해 시스템의 다른 부분에서 장애가 발생할 가능성도 늘어나게 됨

연속적 장애의 원인과 그 대책

서버 과부하

  • 연속적 장애를 유발하는 가장 일반적인 원인은 과부하다
  • 대부분의 연속적 장애는 서버의 과부하가 직접적인 원인이거나 혹은 이로 인해 확장된 또는 변형된 장애들이다
  • 특정 클러스터에서 장애가 발생하면, 다른 클러스터에 가용량 이상의 요청이 전달될 수 있다.
    • 이로인해 자원이 부족하게 되어 충돌이 방생하거나 지연응답 혹은 오동작이 발생하게 된다
    • 이처럼 성공적으로 완료된 작업의 수가 감소하면 그 여파는 도메인의 다른 시스템으로 퍼져나가 잠재적으로는 전체 시스템에 영향을 미칠 수 있다.
    • 예를 들어 한 클러스터에서 과부하가 발생하면 그 서버들이 충돌로 인해 강제로 종료 -> 로드밸런싱 컨트롤러가 요청을 다른 클러 스터에 보냄 -> 그 쪽의 서버들에서 과부하가 발생 -> 서비스 전체에 걸치 과부하 장애 발생
  • 위 상황은 그다지 오래 걸리지 않는다 (~분)

자원의 부족

  • 서버의 어떤 자원이 부족해지는지, 그리고 서버가 어떻게 구성되어 있는지에 따라 자원의 부족은 서버의 비효율적 동작 혹은 충돌로 인한 서버의 강제 종료를 유발할 수 있고 로드밸런서는 그 즉시 자원 부족이라는 이슈를 다른 서버들로 퍼져나가게 함

CPU 자원이 부족한 경우

  • 주로 모든 요청의 처리가 느려진다. 다음과 같은 부차적 현상 발생가능
처리중인 요청 수의 증가
  • 이는 메모리, 활성 스ㄹ드 수, 파일 디스크립터 수 및 백엔드 자원 등 거의 모든 자원에 영향을 미침
비정상적으로 증가하는 큐의 크기
  • 지연응답이 증가하고, 큐가 더 많은 메모리를 소비하게 됨
스레드 기아 (thread starvation)
  • 스레드가 잠금을 기다리느라 더 이상 처리되지 않고 대시 상태가 되면 건강 상태 체크 종단점이 제시간에 처리되지 않아 실패할 가능성이 있다
CPU 혹은 요청 기아
  • 서버 내부의 와치독이 서버가 더 이상 처리를 못하고 있다는 것을 탐지하면 CPU 기아 현상으로 인해 서버에서 충돌이 발생하기도 하고 와치독 이벤트가 원격에서 발생하고 요청 큐를 통해 처리되는 경우에는 요청 기아가 발생하기도 한다.
RPC 시간 초과
  • RPC 응답이 늦어져 클라이언트에서 시간초과 발생가능
  • 서버가 수행했던 작업은 아무런 소용이 없어지고 클라이언트는 RPC 요청을 재시도하여 더 많은 과부하 발생
CPU 캐시 이점 감소
  • 더 많은 CPU가 사용될수록 더 많은 코어에서 누수가 발생할 가능성이 커지고 그 결과 로컬 캐시 및 CPU의 효율성이 떨어지게 됨

메모리

태스크 종료
  • 예를 들어 container manager가 가용한 자원의 한계 혹은 애플리케이션의 특정한 충돌로 인해 태스크를 거부하면 이 태스크는 강제로 종료됨
자바의 GC 수행률 증가로 인한 CPU 사용률 증가
  • GC가 비정상적으로 동작하면 가용한 CPU 자원이 줄어들고 이로 인해 요청의 처리가 느려지면서 RAM 사용량이 증가하고 -> GC가 더 자주돌고 ….
  • 죽음의 GC 소용돌이 (GC death spiral)
캐시 활용률의 감소
  • 가용한 RAM이 부족해지면 애플리케이션 수준의 캐시 활용률이 감소하고 그 결과 더 많은 RPC 요청이 백엔드로 전달되어 백엔드에 과부하를 초래

스레드

  • 스레드 기아는 직접적으로 에러를 발생시키거나 혹은 건강 상태 점검의 실패를 야기한다.
  • 서버가 필요한 만큼 스레드를 생성하면 스레드 과부하로 인해 너무 많은 RAM을 소비하게 된다
  • 프로세스 ID를 모두 소비하게 될 수도 있음

파일 서술사

  • fd가 부족해지면 네트워크 연결의 초기화가 불가능해져 건강 상태 점검이 실패하게 된

자원간의 의존성

  • 자원의 부족은 또 다른 자원에 영향을 미침
  • 과부하로 고통받고 있는 서비스는 부차적 증상을 유발하는데, 이것이 마치 근본 원인처럼 보여 디버깅이 더욱 어려워 짐
  • 307p 참고

서비스 이용 불가

  • 자원의 부족은 서버의 충돌을 유발
  • 이 문제는 마치 눈덩이 처럼 불어나는 경향이 있어서 금세 모든 서버들에서 충돌이 발생
  • 행여 서버가 복구된다 하더라도 복구가 되자마자 밀려있는 요청의 폭탄을 얻어맞기 깨문에 이 상활을 탈출하기란 상당히 어렵다.

서버 과부하 방지하기

서버 수용량 한계에 대한 부하 테스트 및 과부하 상태에서의 실패에 대한 테스트

  • 실제 환경에서 테스트하지 않으면 정확히 어떤 자원이 부족해지는지, 그리고 자원의 부족이 어떤 영향을 미치는지를 예측하기가 매우 어렵다
  • python Locust와 같은 부하테스트 프레임워크 활용 가능

경감된 응답 제공하기

  • 품질은 낮지만 더 수월하게 연산할 수 있는 결과를 사용자에게 제공
  • 311 페이지 “부하 제한과 적절한 퇴보” 참고

과부하 상태에서 요청을 거부하도록 서비스를 구현하기

  • 서버는 과부하 및 충돌로부터 스스로를 보호할 수 있어야 함
  • 최대한 빠르면서도 비용이 적게 드는 방법으로 실패를 처리해야 함

고수준의 시스템들이 서버에 과부하를 유발하지 않고 요청을 거부하도록 구현하기

  • 리버스 프록시는 IP 주소 같은 조건들을 이용하여 요청의 전체 양을 제한 함으로써 DoS와 같은 공격의 영향을 최소화할 수 있음
  • 로드밸런서는 서비스가 전체적인 과부하 상태일 때는 요청을 스스로 차단한다. 차단 방식은 무조건적 차단, 선택적 차단 등 여러방법이 있음
  • 개별 작업들은 로드밸런서가 보내오는 요청의 변화에 의해 서버에 갑작스럽게 많은 요청이 몰리지 않도록 이를 제한한다.

수용량 계획을 실행하기

  • 수용량 계획은 서비스가 어느 수준의 부하에서 장애가 발생하는지를 판단하기 위해 반드시 성능 테스트와 병행되어야 함
    • N+2 공식
  • 수용량 계획은 연속적 장애의 발생 가능성을 억제할 수는 있지만, 연속적 장애로부터 시스템을 보호하는 데는 크게 도움이 되지 않는다.

큐 관리하기

  • 각 요청을 개별 스레드에서 처리하는 대부분의 서버는 요청을 처리하기 위해 스레드 풀 앞에 큐를 배치
  • 대부분 이 큐가 가득 차면 서버는 새로운 요청을 거부
  • 요청을 큐에 적재하면 더 많은 메모리를 소비, 지연응답 또한 증가
  • 폭발적인 부하가 발생할 수 있는 시스템은 현재 사용 중인 스레드의 수, 각 요청의 처리 시간, 그리고 과부하의 크기와 빈도 등에 기초해서 큐의 크기를 결정하는 것이 좋다.

부하 제한과 적절한 퇴보

  • 서버가 과부하 상태에 도달하게 되면 부하 제한 (load shedding)을 통해 유입되는 트래픽을 감소시켜 어느 정도의 부하를 덜어낼 수 있다.
  • 서버의 메모리 부족이나 건강 상태 점검 실패, 지연응답의 극단적 증가 및 기타 과부하에 관련된 증상들이 서버에서 발생하는 것을 방지하고 서버가 최대한 자신의 작업을 계속해서 수행할 수 있도록 유지하는 것
  • 부하는 배분하는 가장 직관적인 방법은 CPU, 메모리 혹은 큐의 길이에 따라 태스크별로 제한을 두는 것
    • 일정 수 이상의 클라이언트 요청이 유입되면 그 이후의 요청에 대해서는 HTTP 503에러를 리턴
  • FIFO로 동작하는 큐를 LIFO로 변경하면 처리할 필요가 없는 요청들을 제거함으로써 부하를 줄일 수 있음 (RPC 제한 시간 초과 요청)
  • PRC 마감기한을 전체 스택에 전파하는 방법을 함께 활용하면 더욱 큰 효과를 발휘
  • 어 깔끔한 방법은 처리를 거부할 작업과 더 중요하고 우선순위가 높은 요청을 선택할 때 해당 요쳥의 클라이언트를 함께 고려하는 방법
  • 적절한 퇴보 (graceful deradation)는 부하 제한의 개념에서 한 걸을 더 나아가 실행해야 할 작업의 양을 감소시키는 방법
    • 일부러 응답의 품질을 경감시켜 작업의 양이나 처리에 필요한 시간의 양을 현저히 줄일 수 있다
    • 다음의 고려가 필요
      • 어떤지표를 활용해서 부하 제한이나 적절한 퇴보의 적용 여부를 결정할 것인가?
      • 서버가 퇴보 모드로 동작하게 되면 어떤 조치를 취해야 하는가?
      • 부하 제한이나 적절한 퇴보는 어느 계층에서 구현해야 하는가?
    • 선택할 수 있는 옵션을 평가하고 배포하는 동안에는 다음과 같은 내용들을 고려하자
      • 적절한 퇴보는 너무 자주 발생해서는 안된다. 대부분 수용량 계획이 실패했거나 혹은 예상하지 못했던 부하의 이동이 발생하는 경우에만 적용되어야 함
      • 어떤 코드 경로가 한 번도 사용된 적이 없다면 이 코드 경로는 동작하지 않을 것. 안정적인 상태에서 운영 중일 때는 적절한 퇴보 모드가 사용될 일이 없지만, 적적한 퇴보 모드 운영 경험을 쌓을 수 없다
      • 이 모드에서 동작하는 서버가 너무 많아지지는 않는지 적절히 모니터링하고 경고 발송
      • 부하 제한과 적절한 퇴보를 구현하는 로직이 복잡해지면 그 자체에 문제가 발생할 수 있다.

재시도

  • 백엔드로 약간 어설프게 재시도를 요청하는 프런트엔드 코드가 있다고 가정해보자
  • 이 재시도는 어떤 요청이 실패앴을 때 이루어지며 논리 요청당 백엔드 RPC는 20번으로 제한
func exampleRpcCall(client pb.ExampleClient, request pb.Request) *pb.Response{
  // RPC 타임 5초
  opts := grpc.WithTimeout(5 * time.Second)

  // 최대 20번까지 RPC 호출 시도
  attempts := 20
  for attempts > 0 {
    conn. err := grpc.Dial(*serverAddr, opts...)
    if err != nil {
      // 연결 설정 시 에러가 발생하면 재시도
      attempts--
      continue
    }
    defer conn.Close()

    // 클라이언트 stub을 생성하고 RPC호출 시도
    client := pb.NewBackendClient(conn)
    response, err := client.MakeRequest(context.Background, request)
    if err != nil {
      // 호출 실패 시 재시도
      attempts--
      continue
    }

    return response
  }

  grpclog.Fatalf("재시도 횟수 초과")
}
  • 이 시스템은 다음과 같은 과정을 거쳐 연속적 장애를 일으킬 수 있다
  1. 백엔드 한계가 태스크당 10,000 QPS이며, 적절한 최보가 적용되면 그 이후의 모든 요청을 거부된다고 가정
  2. 프런트엔드가 MakeRequest 함수를 10,100 QPS의 비율로 일정하게 호출하면 백엔드에 100 QPS의 과부하가 발생해서 백엔드가 요청을 거부하게 됨
  3. MakeRequest함수는 매 1,000 ms 마다 실패한 100 QPS의 요청을 재시도하고 이들이 성공적으로 처리됨. 이는 100 QPS의 추가 과부하가 되어 백엔드에 200 QPS의 과부하 발생
  4. 그 결과 재시도의 비율이 증가: 그래서 처음 시도에 비해 더 적은 수의 요청이 처리되고 결국 백엔드에 전달된 요청의 일부만이 처리됨
  5. 백엔드 태스크가 부하의 증가로 인해 더이상의 요청을 처리할 수 없게 되면 계속되는 요청과 재시도 때문에 결국 충돌이 발생 -> 이 충돌로 인해 처리되지 못한 요청들은 나머지 백엔드 태스크들이 감당해야 하는 부하가 되어 이 태스크들 역시 과부하 상태에 놓인다.
  • 자동 재시도를 수행할 때는 다음과 같은 사항들을 고려해야 한다
    • 대부분의 백엔드 보호전략은 “서버 과부하 방지하기” 절에서 설명. 특히 시스템에 대한 테스트는 문제를 조명하고 적절한 퇴보를 통해 백엔드에 대한 재시도의 영향을 줄일 수 있음
    • 재시도를 실행할 때는 항상 임의의 값을 이용해 지수적으로 간격을 두어야 한다. AWS 아키텍처 블로그의 “지수에 기반한 가격 및 지터” 참고
      • 재시도가 어느 간격 내에서 임의로 분포되지 않으면 작은 변화에도 재시도 요청이 동시에 쏟아질 수 있어 장애의 확산에 영향을 주게 됨
    • 요청당 재시도 횟수에 제한은 둔다
    • 서버 수준에서 재시도에 대한 한계 수치를 책정
    • 서비스 전체를 살펴보고 특정 수준의 재시도가 정말로 필요한 것인지를 결정해야 함
      • 특히 여러 부분에서 재시도를 요청함으로써 재시도 요청이 확대되는 상황은 피해야 한다
    • 명확한 응답 코드를 사용하고 각기 다른 실패 모드를 어떻게 처리할 것인지를 생각해야 한다.
      • 에러 상황에 따라 재시도를 수행할 것과 그렇지 않을 것을 구분해야 한다

지연응답과 마감기한

  • 프런트엔드는 백엔드 서버로 RPF 요청을 보낼 때 그 응답을 기다리기 위해 자원을 소비
  • RPC 마감기한은 프런트엔드가 응답을 얼마나 오래 기다릴 것인지를 정의해서 백엔드 때문에 프런트엔드의 자원이 소비되는 시간에 제한을 두기 위한 것

마감기한의 결정

  • 마감기한이 너무 길면 스택의 상위 계층이 자원을 너무 많이 소비해서 스택의 하위 계층에서 문제가 발생하게 됨
  • 마감기한이 너무 짧으면 자원을 많이 소비하는 요청이 계속해서 처리에 실패할 수 있음

마감기한의 상실

  • 많은 연속적 장애들에서 찾아볼 수 있는 현상 중 하나는 서버가 클라이언트의 마감기한을 넘긴 요청들을 처리하느라 자원을 소비하고 있는 현상
  • 클라이언트는 마감기한이 지나면 이미 해당 요청의 처리를 포기하므로 서버가 무슨 작업을 수행하는지는 전혀 신경 쓰지 않는다.
  • 만일 요청의 처리가 여러 단계를 거쳐 실행된다면, 서버는 요청을 위해 다른 작업을 수행하기에 앞서 각 단계의 마감기한이 얼마나 남았는지를 먼저 확인해야 한다.

마감기한의 전파

  • 백엔드에 RPC를 보낼 때 설정한 적절한 마감기한을 찾아내는 것보다는 서버들이 직접 마감기한을 전파하고 작업의 취소를 전파해야 한다.
  • 마감 기한의 전파가 없다면,
    1. 서버 A가 10초의 마감기한을 설정한 RPC 요청을 서버 B로 보낸다.
    2. 서버 B는 요청의 처리를 시작하기까지 8초의 시간을 소요한 후 서버 C로 RPC 요청을 보냈다.
    3. 마감기한 전파를 제대로 구현했다면 서버 B가 RPC 요청에 2초의 마감기한을 설정해야 하겠지만 이 경우에는 20초라는 하드코드된 값을 대신 적용해서 RPC 요청을 서버 C에 보냈다고 가정해보자
    4. 서버 C는 5초 후에 큐에서 요청을 가져왔다.
  • 서버 B가 마감기한 전파를 제대로 구현했더라면 서버 C는 이미 2초라는 마감기한을 넘겼으므로 요청 처리를 즉시 포기했을 것이다.

이중 지연응답

  • 예제 319페이지 참고
  • 이런 종류의 문제를 해결하는 데 도움이 되는 가이드라인
    • 이 문제를 사전에 인지하기란 매우 어렵다. 특히 일시적인 지연응답 현상이 발생했을 때 이 장애의 원인이 이중 지연응답 (bimodal latency)이라는 것이 명확하게 드러나지 않는다. 지연응답이 증가하는 것이 확인되면 평균 값과 더불어 지연응답의 분포 역시 살펴보아야한다.
    • 요청이 마감기한까지 기다리지 않고 일찌감치 에러를 리턴하면 이 문제는 자연스럽게 피해갈 수 있다. 예를 들어 백엔드가 사용할 수 없는 경우에는 백엔드를 다시 사용할 수 있을 때까지 자원을 소비하면서 기다리는 대신 즉시 에러를 리턴하는 것이 최선. RPC 계층이 이 옵션을 지원한다면 주저 없이 사용하자
    • 통상적인 요청의ㅣ 지연응답에 비해 몇 배나 긴 마감기한을 설정하는 것은 좋지 않은 방법
    • 공유 자원을 사용할 때는 일부 키 공강에 의해 해당 자원들이 고갈될 수 있음

느긋한 시작과 콜드 캐싱

  • 어떤 프로세스든지 막 시작된 직후에는 안정적으로 동작하는 상태에 비해 응답이 느려지는 경향이 있다.
    • 초기화가 필요한 경우
      • 첫번 째 요청을 받을 때 필요한 백엔드와의 연결을 설정해야 하는 경우
    • 일부 언어, 특히 자바의 경우 런타임 성능 향상을 위한 추가 작업이 실행되는 경우
      • JIT 컴파일, 핫스팟 최적화 및 지연된 클래스 로딩 등이 수행되는 경우
  • 마찬가지로 일부 바이너리들을 캐시가 채워져 있지 않으면 효율성이 떨어지는 경우가 있다
  • 콜드 캐시를 갖는 경우
    • 새로운 클러스터를 켜는 경우
      • 이제 막 추가된 클러스터는 완전히 비어있는 캐시와 함께 실행된다.
    • 유지보수 작업 후 클러스터를 서비스에 제공하는 경우
      • 이 경우에는 캐시의 데이터가 그다지 유용하지 않을 수 있다.
    • 서비스 재시작
      • 캐시를 사용하는 태스크가 최근에 재시작되었다면 캐시를 다시 채우는 데 어느 정도의 시간이 필요
      • 이때 서버가 아니라 memcache 같은 별도의 바이너리로 캐시를 이동한다면 지연응답이 약간 있긴하지만 다른 서버들과 캐시를 공유할 수 있다
  • 만일 캐시가 서비스에 서비스에 중요한 영향을 미친다면, 다음의 전략 중 하나 혹은 그 이상을 채택할 수 있다.
    • 서비스를 overprovision한다.
      • 서비스 소유자는 서비스에 캐시를 추가하는 것에 대해 충분히 주의를 기울여야한다.
      • 새로 투입되는 캐시가 지연응답 캐시인지, 아니면 용량 캐시로써 안정하게 잘 동작할 수 있도록 충분히 잘 구현된 캐시인지 확인 필요 (??? 질문하기)
      • 간혹 서비스의 성능 향상을 위해 투입한 캐시 때문에 의존성 관리만 어려워지기도 함
    • 일반적인 연속적 장애 방지 기법 적용
      • 서버는 과부하가 발생하거나 혹은 적절한 퇴보 모드에 지입하면 유입되는 요청들의 처리를 거부해야 함
      • 대량의 서버를 재시작하는 등의 일이 발생하면 서비스가 어떻게 동작하는지를 확인하기 위한 충분한 테스트가 수반되어야 함
    • 클러스터에 부하를 위임할 때는 부하의 크기를 천천히 늘려야 함
      • 최초로 유입되는 요청의 비율을 낮게 유지하면서 캐시가 채워질 시간적 여유를 가져야 함
      • 일단 캐시가 채워진 후에 더 많은 트래픽을 구성하도록 처리

항상 스택의 아래쪽을 살펴보자

  • 내부 계층간의 통신은 다음과 같은 여러가지 이유로 문제가 될 수 있음
    • 분산 데드락 (distributed deadlock)의 영향을 받기 쉬움
      • 백엔드가 원격 백엔드로부터 지속적으로 요청을 수신하기 위해 사용하는 스레드 풀을 원격 밴엔드로 보낸 RPC 요청의 응답을 기다리기 위해 사용할 수도 있음
      • 이때 백엔드 A의 스레드 풀이 가득 찼다고 생각해보자. 그러면 백엔드 A에 요청을 보내는 백엔드 B는 백엔드 A의 스레트 풀에 여력이 생길 때까지 계속해서 스레드를 점유하게 됨 -> 스레드 기아
    • 만일 어떤 장애나 과부하(예를 들면 과부하 상태에서 부하의 재분배가 더 활발하게 이루어지는 경우)로 인해 내부 계층 간의 통신이 증가하는 경우
      • 어느 수준 이상으로 부하가 증가하면 내부 계층 간 요청이 빠르게 증가할 수 있다.
      • 예를 들어 어떤 사용자의 요청이 특정 백엔드에서 처리되며, 이 백엔드는 다른 클러스터의 차순위 백엔드로 해당 사용자의 요청의 처리를 위임할 수 있도록 설정되어 있다고 가정하자
      • 전체 시스템이 과부하 상태라면 최우선 백엔드가 차순위 백엔드로 요청을 위임하는 비율이 증가하게 되고 시스템의 부하가 더 높아지면 최우선 백엔드가 차순위 백엔드의 요청 처리 결과를 파싱하고 대기하는 비용이 증가하게 됨
    • 계층 간 통신의 중요도에 따라 시스템의 부트스트랩이 더 복잡해질 수 있음
      • 일반적으로 계층 간 통신(특히 통신의 경로가 계속 반복되는 경우)은 지양하는 것이 좋음
      • 그 대신 클라이언트가 통신을 하도록 주도해야 한다
      • 예를 들어 프런트엔드가 어떤 백엔드와 통신하는데 이 백엔드를 잘못 선택했다면 해당 백엔드는 올바른 백엔드로 요청을 위임해서는 안된다
      • 백엔드는 프런트엔드에 올바른 백엔드로 요청을 다시 전달할 것을 지시해야 한다.

연속적 장애의 발생 요인

프로세스 중단

  • 간혹 일부 서버 태스크들이 중단되어서 가용한 용량이 줄어드는 경우가 있다.
  • 아주 사소한 몇가지로 인해 서비스가 중단되기 직전의 상황까지 몰릴 수도 있다.

프로세스 업데이트

  • 새 버전의 바이너리를 배포하거나 설정을 수정했는데, 이건이 동시에 많은 수의 태스크에 영향을 주게 되면 연속적 장애를 유발할 수 있다.
  • 이런 상황을 피하려면 서비스의 업데이트 인프라스트럭처를 설정할 때 이런 오버헤드를 고려해서 용량 게획을 세우거나 가장 사용량이 많은 시점을 피해야 한다.

새로운 배포

  • 새 바이너리, 설정의 변경, 혹은 기반 인프라스트럭처 스택의 변경 등으로 인해 요청 프로파일, 자원 사용량과 제한, 백엔드 혹은 연속적 장애를 유발시킬 수 있는 다른 시스템 컴포넌트의 수 등에 변화가 생길 수 있음
  • 문제가 생기면 이런 것들을 복원해보는 것이 좋다.

유기적 성장

  • 대부분의 경우 연속적 장애는 어떤 특정한 서비스의 변화에 의해 발생하지는 않지만 사용량이 증가하는데, 그에 맞추어 수용량을 조정하지 않는다면 연속적 장애가 발생하기도 한다.

계획에 의한 변경, 자원의 감소 혹은 서버의 종료

  • 만일 서비스가 멀티호밍(multihoming)을 사용하는 경우, 서비스의 수용량 중 일부가 클러스터의 유지보수나 장애로 인해 사용이 불가능해지기도 함

요청 프로파일의 변화

  • 로드밸런싱 설정의 변경이나 트래픽의 혼합 또는 클러스터의 과적 등으로 인해 트래픽이 다른 프런트엔드 서비스로 옮겨지면서 해당 서비스와 연결된 백엔드 서비스가 다른 클러스터로부터 발신된 요청을 수신하게 되는 경우가 발생
  • 프런트엔드 코드의 변경이나 설정의 변경 때문에 개별 페이로드를 처리하는 병균 비용이 변경되기도 함

자원의 제한

  • 일부 클러스터 운영 시스템은 자원의 과다 투입을 허용하기도 함 (잉여 CPU)
  • 서비스의 안전을 이 여분의 CPU에 의존하는 것은 매우 위험

연속적 장애 테스트하기

장애가 발생할 때까지 테스트하고 조치하기

  • 부하테스트 조지기
  • 부하테스트를 하면서 우리의 시스템이 얼마나 잘 설계되어있는지 파악하기
  • 어느 지점에서 장애가 발생하는 지 파악하기
  • 조치하기
  • 한동안 과부하 상태에 놓였던 컴포넌트가 원래의 수준으로 돌아오면 어떻게 동작하는지 파악하기
    • 과부하 상태에서 퇴보 모드에 들어간다면, 사람의 개입 없이 일반 모드로 돌아올 수 있는가?
    • 과부하 상태에서 몇 개의 서버에서 충도링 발생한다면 시스템을 안정화시키기 위해서는 어느 정도의 부하가 절감되어야 하는가?
  • 프로덕션 환경의 일부를 대상으로 장애 테스트를 실행하는 것을 고려해볼 수 있다 (아주 조심해야 함)
    • 예상되는 트래픽 패턴과 더불어 태스크의 수를 빠르게 혹은 천천히 줄여본다
    • 클러스터의 수용량을 신속하게 줄여본다.
    • 다양한 백엔드를 시스템에서 감춰본다.

사용량이 높은 클라이언트 테스트하기

  • 서비스가 다운된 동안 클라이언트는 얼마나 빠르게 작업을 처리하는가?
  • 에러 상황에서 클라리언트가 임의의 지수 백오프를 사용하는가?
  • 클라이언트가 대량의 부하를 유발하는 취약점을 가지고 있는가?

상대적으로 덜 중요한 백엔드의 테스트

  • 상대적으로 덜 중요한 백에드가 사용 불가능한 상태라 하더라도 서비스의 중요한 컴포넌트들이 그에 따른 간섭을 받지 않는지 확인

연속적 장애를 처리하기 위한 즉각적인 대처

자원의 추가 투입

  • 가장 적절한 방식
  • 하지만 죽음의 소용돌이는 해결 못함

건강 상태 점검의 중지

  • 서비스가 건강 상태가 아닌 경우 건강 상태 점검으로 인해 장애 모드로 들어갈 때가 있다.

서버의 재시작

  • 자바 서버에서 죽음의 GC 소용돌이가 발생한 경우
  • 일부 처리 중인 요청들에 특별한 마감기한이 없지만 자원을 소비 중이어서 스레드 차단이 발생한 경우
  • 서버에서 데드락이 발생한 경우

  • 한가지 명심해야 할 것은 서버를 재시작하기 전에 연속적 장애의 원인을 먼저 규명해야 한다는 점이다
  • 만약 장애의 원인이 콜드캐시 같은 이슈라면 서버의 재시작으로 인해 연속적 장애가 더 크게 퍼져나갈 수도 있다.

트래픽의 경감

  1. 이 상황을 유발하는 기본적인 원인을 처리해야 한다.
  2. 충돌이 사라질 만큼 충분히 부하를 경감시켜야 한다
  3. 대부분의 서버가 양호한 상태가 된다
  4. 점진적으로 서버에 부하를 늘려간다
  • 사용자는 장애를 경험할 수 밖에 없지만, 연속적 장애로부터 복구가 가능

퇴보 모드로 들어가기

  • 이 방법은 반드시 서비스 내에 구현되어 있어야 함

일괄 작업 부타 (batch load) qowpgkrl

  • 부하가 많을 때는 batch job 죽인다

문제가 있는 트래픽 배제하기

마무리하며

  • 시스템에 과부하가 발생하면 그 상황을 해결하기 위한 조치가 필요
  • 일단 서비스가 장애가 발생할 수 있는 수준에 도달하면 사용자의 모든 요청을 완벽하게 처리해 내기보다는 에러를 리턴하거나 혹은 평소 대비 낮은 품질의 결과를 리턴해야 함
  • 연속적 장애를 피하려면 장애의 발생 지점을 이해하고 장애 발생 시 시스템이 어떻게 동작하는지 이해하는 것

  • 충분한 주의를 기울이지 않는다면 백그라운드 에러를 줄이거나 안정적인 상태를 더 향상시키기 위한 시스템의 변경사항이 오히려 서비스 전체의 장애로 번질 위험이 있음
  • 장애 시 재시도, 문제가 발생한 서버로부터 부하 위임, 문제가 발생한 서버의 중단, 캐시 추가 작업등은 일반적으로는 성능의 향상을 볼 수 있지만, 대량의 장애를 유발하기도 함.

Comment  Read more

7. 코딩하는 동안 38~40

|

Topic 38. 우연에 맡기는 프로그래밍

  • 지뢰를 찾는 병사처럼 개발자는 지뢰밭에서 일한다.
  • 하루에도 수백 개가 넘는 함정에 우리가 빠지기를 기대리고 있다.
  • 우리는 우연에 맡기는 프로그래밍, 곧 행운과 우연한 성공에 의존하는 프로그래밍을 하지 않아야 함
  • 대신 “의도적으로 프로그래밍”해야 한다.

우연에 맡기는 프로그래밍 하기 (Anti-pattern)

예시 (개발자 프레드)

  • 프레드는 정확한 검증없이 코드를 덧붙이는 작업을 몇 주간 하였다.
  • 갑자기 프로그램이 잘 돌아가지 않음
  • 몇 시간 동안 고치려고 노력했지만 원인을 찾을 수 없음

분석

  • 왜 코드가 망가졌는지 프레드가 모르는 까닭은 애초에 코드가 왜 잘 돌아가는지는 몰랐기 때문이다.
  • 프레드가 제한적으로 ‘테스트’를 했을 때에는 코드가 잘 돌아가는 것 처럼 보였지만, 그것은 단지 그때 운이 좋았기 때문

구현에서 생기는 우연

  • 어떤 루틴을 잘못된 데이터를 가지고 호출했다고 해 보자.
  • 그 루틴은 예상하지 못한 데이터에 특정한 방식으로 반응을 하고, 여러분은 그 반을을 기반으로 코드를 작성한다.
  • 하지만 루틴을 만든 사람의 의도는 그 루틴이 그런 식으로 작동하는 것이 아니었다.
  • 이상하게 코드를 짜서 우연히 동작을 하게 된다면 그것을 고치지 않으려고 할 가능성이 크다.
  • 잘 작동하는 데 괜히 건드려서 일을 만들 필요가 있을까? 우리가 보기에는 그래야 할 이유가 몇 가지 있다.
    • 정말로 제대로 돌아가는 게 아닐지도 모른다. 그저 돌아가는 듯이 보이는 것일 수도 있다.
    • 여러분이 의존하는 조건이 단지 우연인 경우도 있다. 화면 해상도가 다른 경우나 CPU 코어가 더 많은 경우 들 다른 상황에서는 이상하게 작동할지도 모른다.
    • 문서화 되지 않은 동작은 라이브러리의 다음 릴리스에서 변경될 수도 있다.
    • 불필요한 추가 호출은 코드를 더 느리게 만든다.
    • 추가로 호출한 루틴에 새로운 버그가 생길 수도 있다.
  • 다른 사람이 호출할 코드를 작성하고 있다면 모듈화를 잘하는 것, 그리고 잘 문서화한 적은 수의 인터페이스 아래에 구현을 숨기는 것 같은 기본 원칙들이 모두 도움이 됨
  • 다른 루틴을 호출할 때도 문서화된 동작에만 의존하라.
  • 어떤 이유로든 그럴 수 없다면 추측을 문서로 상세히 남겨라

비슷하다고 괜찮을 리는 없다

  • UTC 관련 썰, 책 참고

유령 패턴

  • 우연히 생긴 패턴들을 연역적으로 생긴 것이라고 생각하지 마라
  • ex. 러시아 국가 수반 패턴
  • 가정하지 말라. 증명하라.

상황에서 생기는 우연

  • 특정한 상황에서 빚어지는 우연도 있음
  • 여러분이 짜는 코드가 GUI에서만 작동한다고 가정하고 있지 않은가?
  • 언제나 한국어 또는 영어라고 가정하고 있지 않은가?
  • 언제나 사용자가 글을 읽을 수 있다고 생각하는가?
  • 현재 디렉터리에 쓸 수 있다는 것에 의존하고 있지 않은가?
  • 어떤 환경 변수나 설정 파일이 미리 존재한다는 것은?
  • 서버의 시간이 정확하다는 것은?
  • 네트워크를 쓸 수 있고, 속도가 어느 정도 이상이라는 것에 의존하지는 않는가?
  • 인터넷 검색으로 찾은 첫 번째 답에서 코드를 복사해 올 때 여러분과 동인한 상황이라고 확신하는가?
    • 아니면 의미는 신경 쓰지 않고 그냥 따라 하는 화물 숭배 cargo cult 코드를 만들고 있나?

Tip 62. 우연에 맡기는 프로그래밍을 하지 말라.

암묵적인 가정

  • 우연은 여러 단계에서 우리를 오도할 수 있다.
  • 테스트가 특히 가짜 원인과 우연한 결과로 가득 찬 영역이다.
  • X의 원인은 Y라고 가정하기는 쉽다. 하지만 가정하지 말라. 증명하라.
  • 확고한 사실에 근거하지 않은 가정은 어떤 프로젝트에서든 재앙의 근원이 된다.

의도적으로 프로그래밍하기

  • 언제나 여러분이 지금 무엇을 하고 있는지 알아야 한다.
  • 더 경험이 적은 프로그래머에게 코드를 상세히 설명할 수 있는가? 그렇지 않다면 아마 우연에 기대고 있는 것일 터이다.
  • 자신도 잘 모르는 코드를 만들지 말라. 완전히 파악하지 못한 애플리케이션을 빌드하거나, 이해하지 못한 기술을 사용하면 우연의 함정에 빠질 가능성이 높다. 이것이 왜 동작하는지 잘 모른다면 왜 실패하는지도 알 리가 없다.
  • 계획을 세우고 그것을 바탕으로 진행하라. 머릿속에 있는 계획이든, 냅킨이나 화이프보드에 적어놓은 계획이든 상관없다.
  • 신뢰할 수 있는 것에만 기대라. 가정에 의존하지 말라. 무언가를 신회할 수 있을지 판단하기 어렵다면 일단 최악의 상황을 가정하라.
  • 가정을 기록으로 남겨라. 자신의 마음속에서 가정을 명확하게 하는 데 도움이 될 뿐더러, 다른 사람과 그에 대해 소통하는 데에도 도움이 된다.
  • 코드뿐 아니라 여러분이 세운 가정도 테스트해 보아야 한다. 어떤 일이든 추측만 하지 말고 실제로 시험해 보라. 여러분의 가정을 시험할 수 있는 단정문을 작성하라. 가정이 맞았다면 코드를 더 이해하기 쉽게 만든 셈이고, 가정이 틀렸다면 일찍 발견했으니 운이 좋았다고 생각하라.
  • 노력을 기울일 대상의 우선순위를 정하라. 중요한 것에 먼저 시간을 투자하라. 중요한 부분이 가장 어려운 부분이기도 한 경우가 많다. 기본이나 기반 구조가 제대로 되어 있지 않다면 현란한 부가 기능도 다 부질없다.
  • 과거의 노예가 되지 말라. 기존 코드가 앞으로 짤 코드를 지배하도록 놓아두지 말라. 언제나 리팩터링 할 자세가 되어 있어야 한다. 이런 결정이 프로젝트 일정에 영햐을 줄지도 모른다. 그러니 필요한 변경을 하지 않을 경우의 비용보다 일정이 늦어져서 발생하는 비용이 적어야 한다는 것을 염두에 두어라.

Topic 39. 알고리즘의 속도

  • 대문자 O 표기법 (Big-O notation) 사용

알고리즘을 추정한다는 말의 의미

  • 대부분의 알고리즘은 가변적인 입력 데이터를 다룬다
    • n개의 문자열 정렬하기
    • m x n 행렬의 역행렬 만들기
    • n비트 키를 이용해서 메시지 암호화하기 등
  • 일반적으로 입력의 크기는 알고리즘에 영향을 줌
    • 입력의 크기가 클수록 알고리즘의 수행 시간이 길어지거나 사용하는 메모리 양이 늘어난다
  • 중요한 알고리즘은 대부분 선형적이지 않다
  • 특히 몇몇 알고리즘은 증가 폭이 선형보다 훨씬 크다

대문자 O 표기법

  • 다들 아시죠…?

  • O(1) : 상수 (배열의 원소 접근, 단순 명령문)
  • O(logn) : 로그 (이진 검색) 로그의 밑은 중요치 않다
  • O(n) : 선형 (순차 검색)
  • O(nlogn) : 선형보다는 좋지 않지만, 그래도 그렇게 많이 나쁘지는 않음. (퀵 정렬과 힙 정렬의 평균 수행 시간)
  • O(n^2) : 제곱 (선택 정렬과 삽입 정렬)
  • O(n^3) : 세제곱 (두 n x n 행렬의 곱)
  • O(C^n) : 지수 (여행하는 외판원 문제, 집합 분할 문제)

상식으로 추정하기

단순 반복문

  • 단순 반복문 하나가 1부터 n까지 돌아간다면 O(n)일 가능성이 크다
  • ex: 소진 탐색, 배열에서 최댓값 찾기, 체크섬 생성하기 등

중첩 반복문

  • 반복문 안에 또 반목문이 들어 있다면, 알고리즘은 O(m x n)이 된다. 이런 코드를 작성하면 보통 코딩 테스트에서 떨어진다

반씩 자르기

  • 반복문을 돌 때마다 작업 대상의 수를 반으로 줄여 나가는 알고리즘
  • ex: 정렬된 목록의 이진 검색, 이진 트리의 탐색, 정수의 2진수 표현에서 첫 번째 1인 비트를 찾는 문제

분할 정복 (divide and conquer)

  • 입력 데이터를 둘로 나눠서 각각 돌립적으로 작업한 다음, 결과를 합치는 알고리즘
  • ex: quick sort

조합적 (combinatoric)

  • 알고리즘이 항목의 permutation을 다루기 시작하면 대부분의 경우 수행 시간은 걷잡을 수 없이 늘어남
  • 난해 hard 하다고 분류되는 문제를 푸는 알고리즘이 대부분 여기에 속한다
  • ex: 여행하는 외판원 문제, bin packing problem, partition problem
  • 종종 한정된 도메인 안에서 수행 시간을 줄이기 위해 휴리스틱을 동원

실전에서의 알고리즘 속도

Tip 63. 사용하는 알고리즘의 차수를 추정하라

  • O(n^2) 알고리즘이 있다면 분할 정복을 사용하여 O(nlogn)으로 줄일 수 없는지 시도해 보라
  • 코드의 실행 시간이 얼마나 될지 또는 메모리를 얼마나 사용할지 확실하지 않다면 직접 실행해 보라
  • 입력 데이터의 크기를 바꾸어가며 프로파일링 몇번 해보면 쉽게 감을 잡을 수 있다
  • 실무에서는 다른 효과들 (ex: thrashing)이 발생하여 실행시간이 폭발적으로 증가 할 수도 있다
  • 추정을 이미 했다 하더라도 실제 서비스에서 실제 데이터로 돌아가는 코드의 수행 시간만이 정말로 의미 있는 수치다

Tip 64. 여러분의 추정을 테스트하라

최고라고 언제나 최고는 아니다

  • 가장 빠른 알고리즘이 언제나 가장 좋은 알고리즘은 아니다
  • 성급한 최적화를 조심하라

연습 문제

연습 문제 28

  • 실행결과
Using Bubble Sort
10000, 1179
20000, 4592
30000, 10608
40000, 19484
50000, 32297
60000, 43393
70000, 59731
80000, 74558
90000, 92325
100000, 114256
110000, 139129
Using Insertion Sort
10000, 611
20000, 2445
30000, 5484
40000, 9772
50000, 15251
60000, 22104
70000, 29970
80000, 39033
90000, 49383
100000, 61391
110000, 73869
Using Selection Sort
10000, 729
20000, 2932
30000, 6584
40000, 11703
50000, 18292
60000, 26365
70000, 35840
80000, 46824
90000, 61504
100000, 74047
110000, 89045
Using Quicksort
1000000, 324
2000000, 687
3000000, 1059
4000000, 1423
5000000, 1811
6000000, 2218
7000000, 2582
8000000, 2971
9000000, 3377
10000000, 3755
11000000, 4273

bubble_sort insertion_sort selection_sort quick_sort

Topic 40. 리팩터링

이 천지 만물 모두 변하나… (H. F. 라이트)

  • 프로그램이 발전함에 따라 점점 초기에 내린 결정을 다시 고려하고 코드의 일부분을 다시 작성할 일이 생긴다.
  • 소프트웨어의 비유로 가장 널리 쓰이는 메타포는 건축 이지만, 저자는 조경에 비하고 싶다.
    • 어떤 루틴이 너무 크게 자라거나 너무 많은 것을 하려고 한다면 둘로 나눠야 한다
    • 계획한 대로 잘 되지 않는 것들은 잡초 제거하듯 뽑아내거나 가지치기를 해야 한다

밖으로 드러나는 동작은 그래도 유지한 채 내부 구조를 변경함으로써 이미 존재하는 코드를 재구성하는 체계적 기법 (마틴 파울러 - 리팩터링)

  • 이 활동은 체계적이다. 아무렇게나 하는 것이 아니다
  • 밖으로 드러나는 동작은 바뀌지 않는다. 기능을 추가하는 작업이 아니다
  • 밖으로 드러나는 동작이 바뀌지 않는다는 것을 보장하려면 코드의 동작을 검증하는 좋은 자동화된 단위 테스트가 필요하다

리팩터링은 언제 하는가?

  • 리팩터링은 여러분이 무언가를 알게 되었을 때 한다. 여러분이 작년이나 어제, 심지어 10분 전과 비겨ㅛ해서 더 많이 알게 되었다면, 리팩터링을 한다.

중복

  • DRY 원칙 위반을 발견했다.

직교적이지 않은 설계

  • 더 직교적으로 바꿀 수 있는 무언가를 발견했다.

더 이상 유효하지 않은 지식

  • 사물은 변하고, 요구 사항은 병경되며, 지금 처리하고 있는 문제에 대한 여러분의 지식은 점점 늘어난다.
  • 코드는 지금 상화엥 뒤떨어지지 않아야 한다.

사용 사례

  • 진짜 사람들이 실제 상황에서 시스템을 사용하게 되면, 여러분은 어떤 기능은 예전에 생각했던 것보다 더 중요하고, 꼭 필요하다고 생각했던 기능은 그렇지 않은 경우도 있다는 것을 깨닫게 될 것이다.

성능

  • 성능을 개선하려면 시스템의 한 영역에서 다른 영역으로 기능을 옮겨야 한다.

테스트 통과

  • 여러분이 코드를 조금 추가한 후 추가한 테스트를 통과했을 때가, 방금 추가한 코드로 다시 뛰어들어 깔끔하게 정리하기에 최고의 타이밍

고통 관리

  • 현실을 피하지 말자
  • 소스 코드를 이곳저곳 변경하는 것은 굉장히 고통스러운 작업이다 (단위테스트가 잘 되어있다면 그렇게 고통스럽진 않다!)
  • 많은 개발자들이 코드에 조금 개선할 부분이 있다는 이유만으로는 다시 돌아가서 코드 열기를 주저한다.

현실 세계의 복잡한 문제들

  • 고객한테 리팩터링 해야하니까 1주일 더 달라고 하는 것은 할 수 없다
  • 하지만 리팩터링의 타이밍을 놓치면 더 고통스러워 진다.

Tip 65. 일찍 리팩터링하고, 자주 리팩터링하라.

리팩터링은 어떻게 하는가?

  • 리팩터링의 본질을 재설계다

마틴 파울러의 조언

  1. 리팩터링과 기능 추가를 동시에 하지 말라
  2. 리팩터링을 시작하기 전 든든한 테스트가 있는지 먼저 확인하라. 할 수 있는 한 자주 테스트를 돌려 보라. 망가진 것을 빠르게 알아챌 수 있다.
  3. 단계를 작게 나누어서 신중하게 작업하라. 단계를 작게 나누고, 한 단계가 끝날 때마다 테스트를 돌린다면 기나긴 디버깅 작업을 피할 수 있다.

Comment  Read more

15. 포스트모텀 문화: 실패로부터 배우기

|

15. 포스트모텀 문화: 실패로부터 배우기

실패의 비용은 교육이다 - 데빈 캐러웨이

구글의 포스트모텀 철학

  • 장애에 대한 내용을 문서화하고, 장애가 발생하게 된 원인에 대해 이해하며, 무엇보다 장애를 해결하기 위한 취한 조치들이 향후 장애의 재발을 막는 데도 유용하게 사용될 수 있도록 하기 위함
  • 포스트모텀은 불이익을 주기 위한 것이 아님
  • 회사 전체가 실패로부터 새로운 것을 배울 수 있는 기회
  • 보통 포스트모텀은 다음과 같은 상황이 발생했을 때 수행
    • 사용자가 다운타임을 경험했거나 신뢰성이 목표치 이하로 떨어진 경우
    • 종류에 관계 없이 데이터 손실이 발생한 경우
    • 비상 대기 엔지니어의 중재가 발생한 경우
    • 장애의 해결 시간이 목표치보다 오래 걸린 경우
    • 모니터링 장애 시 (장애를 사람이 직접 발견한 경우)
  • 서로를 비난하지 않는 포스트모텀은 SRE문화의 신조

협업과 지식의 공유

  • 포스트모텀 절차에는 모든 단계마다의 협업과 지식 공유가 담겨있음
  • 포스트모텀 템플릿 (부록 D참조), Confluence에도 있음

실시간 협업

열린 댓글/주석 시스템

이메일 알림

리뷰 과정

  • 나중을 대비해 장애에 대한 핵심 데이터를 수집하고 있는가?
  • 장애의 영향이 완벽하게 처리되었는가?
  • 장애의 근본 원인이 충분히 사려 깊게 분석되었는가?
  • 후속 조치 계획이 적절하며 버그 수정 작업들의 우선순위가 적절하게 조정되었는가?
  • 관련 의사 결정자들에게 이 결과를 공유했는가?

모든 포스트모텀 문서는 반드시 리뷰를 거쳐야 한다.

포스트모텀 문화 도입하기

  • 포스트모텀 문화를 소개하는 것은 말처럼 쉬운 일은 아니다
  • 선임 관리자가 적극적으로 리뷰 및 협업 절차에 개입하게 함으로써 협력적인 포스트모텀 문화를 정착시키기 위해 노력함
  • 특정인을 비방하지 않는 포스트모텀은 스스로 동기를 부여하는 엔지니어의 산출물으로써 태어나는 것이 이상적

이달의 포스트 모텀

구긆츨러스 포스트모텀 그룹

포스트모텀 읽기 클럽

불행의 바퀴 (wheel of misfortune)

  • 새로 입사한 SRE들은 종종 불행의 바퀴 연습을 수행한다.
  • 이전의 포스트모텀 중 하나를 선정해서 엔지니어들이 그 안에 기록된 대로 역할을 수행해보는 것
  • 실제와 같은 경험을 위해 당시 장애 제어를 담당했던 사람이 함께 참여 함

어려움

  • 구성원들이 준비 과정 대비 실제 가치에 의문을 제기할 수 있음

해결 전략

  • 포스트모텀을 작업의 일부로 편입. 일정한 시범 기간을 거쳐 성공적인 포스트모텀 문서를 작성해보면 그 가치를 입증할 수 있음
  • 앞서 언급한 사회적 방법들을 통해 공개적으로, 그리고 개인과 팀의 역량 관리를 통해 효과적으로 작성된 포스트모텀 문서에 보상을 부여하고 축하해 줌
  • 선임 리더들의 관심과 참여를 독려한다. 래리 페이지마저도 포스트모텀의 높은 가치를 역설함

결론 및 지속적인 개선

  • 매달 구글 전체가 만들어내는 엄청난 양의 포스트모텀 문서를 수집하는 도구 역시 점점 더 유용하게 활용되고 있음
  • GPT로 뭔가 할 수 있지 않을까…?

Comment  Read more

14. 장애 관리하기

|

14. 장애 관리하기

미흡한 장애 관리

  • 책 참고
  • 총체적 난국
    • 기술적인 문제에 대한 날카로운 집중 부재
    • 소통의 부재
    • 프리랜서의 고용

장애 관리 절차의 기본 요소들

  • 구글의 장애 관리 시스템은 장애 제어 시스템을 기반으로 함

책임에 대한 재귀적인 분리

  • 자신의 역할을 명확히 이해하고 다른 이의 영역을 침범하지 않도록 하는 것이 중요
  • 만일 누군가의 업무 부하가 감당하기 힘들 정도로 높아지면 그 사람을 리더에게 더 많은 지원 인력을 요청해야 함

장애 제어

  • 장애 제어자 (incident commander)는 장애에 대한 높은 수준의 상태를 확인
  • 장애 조치를 위한 팀을 구성하고 필요와 우선순위에 따라 책임을 나누어줌
  • 다른 사람에게 위임되지 않은 모든 역할을 수행해야 함

운영 업무

  • Ops 조직의 리드는 장애 제어자와 협력하여 운영 도구들을 이용해 실제 업무를 수행할으로써 장애에 대처
  • 장애를 조치하는 동안에는 오직 운영팀만이 시스템을 변경할 수 있어야 한다

의사 소통

  • 장애 조치팀의 대외 창구
  • 장애 대응팀의 현황을 정기적으로 의사 결정자들에게 (주로 이메일을 통해) 전달하는 것
  • 장애 조치 문서를 최신의 상태로 유지하는 역할도 겸할 수 있음

계획

  • 운영팀을 도와 버그를 수집하거나, 저녁 식사를 주문하거나, 업무 분장을 돕거나…

확실한 컨트롤 타워

  • 장애 조치 팀원들은 소위 War Room으로 활용할 목적으로 만들어진 공간에 함께 있는 것이 좋음
  • 구글은 IRC(Internet Relay Chat)를 사용함

실시간 장애 조치 문서

  • 장애 조치 문서의 예시는 부록 C에서 볼 수 있음

명확하고 즉각적인 업무 이관

  • 교대 작업 중요함!

적절하게 관리한 장애 조치

  • 책 참고

언제 장애를 선언할 것인가?

  • 문제가 심각해졌을 때 부랴부랴 장애 관리 프레임워크를 발동시키는 것보다는 장애를 빨리 선언하고 최대한 간결한 해결책을 찾아 장애를 조치하는 것이 더 나음
  • 다음 중 어느 한 조건이라도 해당된다면 이를 장애라고 판명
    • 문제를 해결하기 위해 다른 팀의 도움이 필요한가?
    • 문제가 사용자에게 영향을 미쳤는가?
    • 문제 발생 이후 한 시간 동안 집중적으로 분석했는데도 문제가 해결되지 않았는가?

요약

장애 조치에 대한 모범 사례

  • 우선 순위 : 우선 출혈을 막고 서비스를 되살린 후에 근본 원인에 대한 증거를 찾자
  • 사전 준비 : 장애 조치에 참여한 사람들의 자문을 받아 장애 관리 절차를 미리 개발하고 문서화 해 두자.
  • 신뢰 : 장애 조치에 참여 중인 모든 사람들에게 충분한 자율권을 보장하자
  • 감정 조절 : 장애를 조치하는 동안 스스로의 감정적 상태에 주의를 기울이자. 만일 너무 부담이 된다면 다른 이에게 도움을 청하자
  • 대체 방안에 대한 모색 : 주기적으로 현재 선택할 수 있는 방법에 대해 다시 생각하고 이 방법이 여전히 유효한지, 아니면 다른 방법을 찾아야 하는지를 판단하자.
  • 실습 : 이 과정을 정기적으로 수행해서 자연스럽게 활용할 수 있는 수준으로 만들자
  • 개선 : 그리고 계속해서 개선하자. 모든 팀 구성원들이 모든 역할에 익숙해질 수 있도록 독려하자

Comment  Read more