카테고리 보관물: How to be a Programmer

성능 문제를 이해하는 법


실행 중인 시스템의 성능을 이해하는 법을 배우는 것은 디버깅을 배우는 이유와 것과 같은 이유로 피할 수 없는 일입니다. 여러분이 작성하는 코드의 비용을 완벽하고 정확하게 이해하고 있더라도 그 코드에서는 여러분이 통제하거나 속을 들여다보기가 힘든 다른 소프트웨어 시스템을 호출하게 될 것입니다. 하지만 실무에서 성능 문제는 일반적으로 디버깅과 조금 다르고 조금 더 쉽습니다.

시스템이나 하위 시스템이 너무 느리다고 고객이 느낀다고 가정해 봅시다. 그것들을 좀 더 빠르게 만들려고 하기에 앞서 왜 그것이 느린가에 대한 멘탈 모델을 만들어야 합니다. 이를 위해 프로파일링 도구나 로그를 이용해 어느 부분에서 시간이나 다른 리소스가 실제로 소비되는지 파악할 수 있습니다. 코드의 10%에서 90%의 시간이 소모된다는 격언이 있습니다. 저는 이와 더불어 성능 문제에 대한 입출력 비용(I/O)의 중요성을 더하고 싶습니다. 대부분의 시간이 대체로 I/O에서 소모되는 경우가 많습니다. 비용이 높은 I/O와 코드 중에서 높은 비용이 드는 10%를 찾는 것이 멘탈 모델을 만들기 위한 첫 번째 단계입니다.

컴퓨터 시스템의 성능에는 여러 차원이 있으며, 그리고 많은 자원들이 소비됩니다. 맨 먼저 측정해야 할 리소스는 벽시계 시간(wall-clock time), 즉 계산을 하는 동안 경과된 총 시간입니다. 벽시계 시간(wall-clock time)을 로깅하는 것은 특히 중요한데, 시뮬레이션에서 발생하는 예상 불가능한 상황에 관해 알려줄 수 있기 때문이며, 이는 다른 프로파일링에서는 불가능한 것입니다. 하지만 그렇다고 해서 이것이 항상 전체 그림을 보여주는 것은 아닙니다. 간혹 시간은 조금 더 걸리지만 너무나도 많은 프로세서 시간을 소모하지는 않는 뭔가가 실제로 여러분이 다뤄야 할 컴퓨팅 환경에서는 더 나을 것입니다. 이와 비슷하게, 메모리, 네트워크 대역폭, 데이터베이스나 기타 서버 접근은 결국 프로세서 시간보다 훨씬 더 비용이 높을 수 있습니다.

동기화된 공유 자원에 대한 경합은 교착상태(deadlock)와 기아상태(starvation)를 유발할 수 있습니다. 교착상태는 부적절한 동기화나 자원 요구 때문에 진행할 수 없는 것을 말합니다. 기아상태는 어떤 컴포넌트를 적절히 스케줄링하는 데 실패하는 것입니다. 어쨌거나 이를 예상할 수 있다면 프로젝트 초기부터 이러한 경합을 측정하는 수단을 마련하는 것이 가장 좋습니다. 이러한 경합이 일어나지 않더라도 자신 있게 이를 단정(assert)할 수 있는 것이 아주 도움될 것입니다.

로그를 이용해 디버깅하는 법


로깅은 로그라고 하는 정보성 기록을 만들어도록 시스템을 작성하는 실천법입니다. 임시 코드 출력(Printlining)은 간단하고, 대개 임시로 쓸 로그를 만들어내는 것입니다. 초보자는 프로그래밍 지식이 제한적이기 때문에 로그를 이해하고 사용해야 하고, 시스템 아키텍트는 시스템의 복잡성 때문에 로그를 이해하고 사용해야 합니다. 로그에서 제공하는 정보의 양은 설정 가능해야 합니다(이상적인 경우 프로그램이 실행 중인 동안에도). 일반적으로 로그는 세 가지 기본적인 이점을 제공합니다.

  • 로그는 재현하기 힘든 버그(배포 환경에서는 발생하지만 테스트 환경에서는 재현할 수 없는 것과 같은)에 관한 유용한 정보를 제공할 수 있습니다.
  • 로그는 성능과 관련된 통계와 데이터를 제공할 수 있습니다(예: 두 문장 사이에 소요된 시간).
  • 로그를 설정할 수 있는 경우, 예상치 못한 특정 문제를 디버깅하기 위해 해당 문제만를 해결하는 코드를 수정 및(또는) 재배포할 필요 없이 일반적인 정보를 확보할 수 있습니다.

로그에 출력하는 내용의 양은 정보와 간결성 사이에서 절충 가능합니다. 정보가 너무 많으면 로그를 처리하는 비용이 높아지고 스크롤의 압박이 생겨서 원하는 정보를 찾기가 힘들어집니다. 반면 정보가 너무 적으면 필요한 정보가 포함돼 있지 않을지도 모릅니다. 이러한 이유로 로그에 출력하는 내용을 설정 가능하게 만들면 아주 유용합니다. 대개 로그상의 각 레코드는 소스 코드 상의 위치, 실행된 스레드, 정확한 실행 시간, 그리고 공통적으로 특정 변수의 값, 여유 메모리의 양, 데이터 객체의 수 등과 같이 추가적인 유용한 정보를 파악하게 해줍니다. 이러한 로그문은 소스코드 도처에 흩어져 있으며, 특히 주요 기능 지점과 위험성 있는 코드 주변에 지정돼 있습니다. 각 로그문에는 레벨을 할당할 수 있고 시스템이 현재 해당 레벨로 설정돼 있는 경우에만 로그 레코드를 출력할 것입니다. 로그문은 예상되는 문제를 해결하도록 설계해야 합니다. 성능 측정의 필요성을 예상해 보십시오.

영구적인 로그가 있다면 로그 레코드 측면에서 임시 코드 출력(Printlining)을 해볼 수 있으며, 아마도 그러한 디버깅 문장의 일부는 영구적으로 로깅 시스템에 추가될 것입니다.

오류를 제거하는 법


지금까지는 일부러 프로그램 실행을 검사하는 활동과 오류를 고치는 활동을 나눴습니다. 하지만 물론 디버깅은 버그를 제거하는 것을 의미하기도 합니다. 이상적인 경우라면 코드를 완벽하게 이해하고 오류와 그것을 고치는 방법이 훤히 드러나는 ‘아하!’라고 깨닫는 순간에 도달해야 할 것입니다. 하지만 프로그램은 여러분이 명확하게 알기 힘든, 불충분하게 문서화된 시스템을 사용하는 경우도 있을 것이므로 이러한 순간이 늘 오지는 않을 것입니다. 어떤 경우에는 코드가 너무 복잡해서 완벽하게 이해하지 못하는 경우도 있을 것입니다.

버그를 고칠 때는 가장 작은 부분만 변경해서 해당 버그를 고치고 싶을 것입니다. 개선이 필요한 다른 부분이 보일지도 모릅니다. 하지만 그것들을 동시에 고치지는 마십시오. 한 번에 딱 하나만 변경하는 과학적 방법론을 활용하려고 하십시오. 이를 위한 최선의 방법은 버그를 손쉽게 재현한 다음, 고친 코드를 올바른 자리에 놓고, 프로그램을 다시 실행해서 버그가 더는 존재하지 않는지 관찰하는 것입니다. 물론 때로는 한 줄 이상을 변경해야 하겠지만 그럼에도 개념적으로는 단 하나의 최소한의 변경사항을 적용해 버그를 고쳐야 합니다.

간혹 여러 개의 버그가 하나로 보일 때도 있습니다. 버그를 정의하고 그것을 한번에 고치는 것은 여러분에게 달려 있습니다. 때로는 프로그램이 어떻게 동작해야 하거나 원 제작자가 의도한 바가 명확하지 않은 경우도 있습니다. 이 경우 경험과 판단을 발휘해 해당 코드에 여러분만의 의미를 부여해야 합니다. 프로그램이 어떻게 동작해야 할지를 결정하고 그것을 주석으로 남기거나 다른 어떤 방법으로 분명히 밝힌 다음 코드가 여러분이 의미한 바를 따르게 하십시오. 이는 맨 처음에 원래의 함수를 작성하는 것보다 더 어려운 중급 또는 고급 기술이며, 현실 세계는 깔끔하게 맞아떨어지지 않을 때가 많습니다. 어쩌면 재작성할 수 없는 시스템을 고쳐야 할 수도 있습니다.