출퇴근에 상당한 시간과 노력이 소요되어 불행할 때, 어떻게 하면 좋을까? 출퇴근을 없애는 극단적인 방법도 있겠지만 일반적으로는 더 나은(불행하지 않는) 방법을 찾기 위한 여정을 떠나야 한다. 그 방법에는 다양한 선택지가 있다.
● 걷는 데 편한 신발을 사는 등 작은 것부터 개선하기
● 도움이 된다면 전기 스쿠터나 자동차 구매하기
● 이동 거리나 시간이 덜 소요되도록 계획하기 (예: 최적 경로 찾기)
● 출퇴근 시간에 이북이나 동영상 강의를 보는 등 취미 활동하기
● 직장 가까이 이사하거나 직장 바꾸기(!)
더 나은 방법을 위해 모든 선택지를 골라 수행할 수도 있지만 최적화에 드는 투자, 기회 비용 (예: 자동차 구입 비용), 노력은 모두 다르다. 가능하면 최소한의 노력으로 최대의 가치를 얻도록 하는 것이 최선이다.
이러한 수준에는 중요한 측면이 또 있다. 더 높은 수준의 최적화를 수행한다면 낮은 수준의 최적화는 영향을 받거나 평가 절하될 수 있다. 예를 들어 출퇴근을 최적화하기 위해 특정 수준의 최적화를 여러 번 수행했다고 가정하자. 더 나은 차를 사고, 기름값을 아끼기 위해 카풀을 모집하고, 교통체증을 피하기 위해 근무 시간을 변경하는 등 여러 방법을 써봤다. 그런데 근무지까지 도보로 이동할 수 있는 아파트로 이사 (더 높은 수준의 최적화)한 경우, 지금까지 해온 많은 노력과 투자는 그 가치가 떨어진다. 이는 엔지니어링 분야에서도 마찬가지다. 최적화 노력을 언제 어디에 사용해야 하는지 알아야 한다.
컴퓨터 과학을 공부할 때, 학생들은 알고리즘과 자료 구조에 대한 이론을 배우며 최적화를 처음 접한다. 학생들은 더 나은 시간 및 공간 복잡도를 가진 여러 알고리즘을 사용하며 어떻게 프로그램을 최적화하는지 탐구한다. 코드에서 사용했던 알고리즘을 바꾸는 작업은 중요한 최적화 기술이지만, 소프트웨어 효율성을 향상시키기 위해서는 그 이상의 많은 변수와 영역을 고려해야 한다. 정확히 말하면, 소프트웨어가 의존하는 수준은 더 다양하다.
아래 그림은 소프트웨어 실행에서 중요한 역할을 수행하는 수준들을 제시한다. 이 수준들의 목록은 1982년에 작성된 존 루이스 벤틀리의 목록Jon Louis Bentley’s list에서 영감을 얻었는데 아직까지도 매우 정확하다.
이 글에서는 5개의 설계 수준을 설정했다. 각 수준에는 최적화 접근법과 검증 전략이 존재한다.
⠀
1. 시스템 수준
대부분 하나의 소프트웨어는 더 큰 시스템의 일부이다. 배포된 많은 프로세스 중 하나이거나 더 큰 모놀리스 애플리케이션의 한 스레드일 것이다. 이때 모든 시스템은 다중 모듈의 주변에 구성돼 있다. 하나의 모듈은 메서드, 인터페이스, 다른 API를 통해 특정한 기능을 상호 교환하고 더 쉽게 수정되도록 캡슐화하는, 소프트웨어의 작은 구성 요소다.
각각의 Go 애플리케이션은, 가장 작은 요소조차도 다른 모듈로부터 코드를 불러올 수 있는 실행 가능한 모듈이다. 다시 말해 소프트웨어는 다른 구성 요소에 의존한다. 시스템 수준에서 최적화하는 작업은 어떤 모듈이 사용되었고, 해당 모듈이 다른 모듈과 어떻게 연결돼 있는지, 누가 어느 구성 요소를 요청하는지 그리고 얼마나 자주 요청하는지 등을 바꾸는 것이다. 모듈과 API를 통해 작동하는 알고리즘을 설계한다고 할 수 있으며, 이 모듈과 API는 개발자의 자료 구조다.
이 작업은 여러 사람의 노력과 좋은 설계 디자인이 선행되는 복잡한 작업이다. 그러나 성능은 엄청나게 개선된다.
⠀
2. 모듈 간 알고리즘과 자료 구조 수준
입력과 예상되는 결과라는 해결해야만 하는 문제를 고려할 때, 모듈 개발자는 절차상 두 가지 중요한 요소를 설계하며 일을 시작한다. 첫 번째는 ‘알고리즘’이다. 이는 정확한 결괏값을 산출하는 등의 문제를 해결하기 위해 데이터를 기반으로 작동하며 문제를 해결할 수 있는 유한한 연산으로 구성된다. 이진 탐색binary search, 퀵 정렬quicksort, 병합 정렬merge sort, 맵리듀스map-reduce 등 유명한 알고리즘들은 많이 들어봤을 것이다. 하지만 프로그램이 수행하는 맞춤식 단계는 무엇이든 알고리즘으로 불릴 수 있다.
두 번째 요소는 종종 선택된 알고리즘에 의해 함축되는 ‘자료 구조’다. 이들은 데이터(입력값, 결괏값, 주기적인 데이터 등)를 컴퓨터에 저장한다. 이때 배열, 해시맵, 연결 리스트, 스택, 큐 등을 섞어 사용하거나, 직접 만든 자료 구조 등 거의 무제한으로 선택할 수 있다. 하지만 모듈 내에서 알고리즘을 확실하게 선택하는 일은 매우 중요하다. 이들은 구체적인 목표(요청 레이턴시 등)와 입력 특성들을 위해 반드시 개선돼야 한다.
⠀
3. 실행 (코드) 수준
모듈에서 알고리즘은 코드로 작성되고, 기계어로 컴파일하기 전까지는 존재하지 않는다. 개발자들은 이 과정에서 아주 큰 인내심을 가진다. RAER를 충족시키도록 비효율적인 알고리즘을 효율적으로 실행시킬 수 있는 반면, 효율적인 알고리즘을 형편없이 실행해서 의도치 않은 시스템 성능 저하를 일으킬 수도 있다. 코드 수준에서의 최적화는 Go와 같은 고수준 언어를 사용하여 프로그램이 특정 알고리즘을 실행하도록 작성하는 작업과, 동일한 알고리즘을 사용하면서 동일하고 정확한 결괏값을 산출하고, 원하는 모든 측면에서 더 효율적인 프로그램을 생산하는 작업을 말한다.
일반적으로 알고리즘과 코드 수준에서 최적화가 함께 진행된다. 한편 하나의 알고리즘을 확정하고 오직 코드 최적화에만 집중하는 방법도 있는데, 이 방법이 더 쉽기는 하다.
⠀
4. 운영체제 수준
요즘 소프트웨어는 하드웨어 기기에서 직접 실행되지도 않고 단독으로 작동하지도 않는다. 대신 각각의 소프트웨어 실행을 프로세스(그리고 스레드)로 나누고, CPU 코어에 스케줄링하고 메모리나 I/O 관리, 장치 접근 등의 다른 필수적인 서비스들을 제공하는 운영체제를 작동시킨다. 그 위에 가상 머신이나 컨테이너 같은 가상화 레이어를 둘 수 있다. 특히 가상화 레이어를 클라우드 네이티브 환경과 같은 운영체제의 서비스 자원에 놓을 수 있다.
다만 모든 레이어는 오버헤드를 유발한다. 운영체제 개발 및 설정을 제어하는 사람들은 이 오버헤드를 최적화할 수 있다.
⠀
5. 하드웨어 수준
특정 시점에 코드에서 번역된 명령어 세트가 RAM이나 로컬 디스크, 네트워크 인터페이스, 입력 및 출력 장치와 같은 메인 보드의 다른 필수 부품에 연결된 내부 캐시와 함께 컴퓨터 CPU 단위에 의해 실행된다. 개발자 또는 운영자는 종종 앞서 언급된 운영체제 수준 덕분에 복잡한 것들을 추상화할 수 있다(이는 하드웨어 제품마다 다르다). 그러나 응용 프로그램의 성능은 하드웨어 조건에 의해 제한된다. 예를 들어 멀티 코어인 기계에서 NUMA 노드의 존재와 이것이 성능에 어떤 영향을 미치는지 알고 있는가? CPU와 메모리 노드 사이의 메모리 버스가 대역폭을 제한한다는 것을 알고 있는가? 이는 소프트웨어 효율성 최적화 프로세스에 영향을 미칠 수 있는 포괄적인 주제다.
문제의 영역을 수준별로 나누는 것의 실질적인 장점은 무엇일까? 우선 연구들을 살펴보면 응용 프로그램 속도에 관해 언급된 수준 중 하나에서 10~20의 계수로 속도를 높이는 것이 종종 가능하다는 사실을 알 수 있다.
결론적으로 요구되는 시스템 효율성을 얻기 위해 최적화의 한 수준에만 집중할 수 있다는 것을 의미하므로 좋은 소식이라고 볼 수 있다. 그러나 한 수준에서 10~20번 시스템 구현을 최적화했다고 가정하자. 그랬을 때 개발 시간, 가독성 및 유지보수성(아래 그림의 적정 지점)에서 상당한 희생이 있어야 이 수준을 추가로 최적화할 수 있게 된다. 그래서 최적화를 달성하기 위해 다른 수준을 더 많이 살펴보아야 한다.
나쁜 소식은 특정 수준을 변경하지 못할 가능성이 있다는 것이다. 예를 들어 개발자에게는 일반적으로 컴파일러, 운영체제 또는 하드웨어를 쉽게 변경할 수 있는 권한이 없다. 마찬가지로 시스템 관리자는 소프트웨어가 사용하는 알고리즘을 변경할 수 없다. 대신에 그들은 시스템을 교체하고 설정하거나 조정할 수 있다.
⠀
이 글은 『Go 성능 최적화 가이드』 도서 내용을 발췌한 글입니다. Go 언어를 통해 소프트웨어의 효율성 및 성능 개선을 진행하는 더 다양하고 자세한 내용은 아래 도서에서 확인하실 수 있습니다.