네 Python은 느립니다, 하지만 저는 신경쓰지 않습니다
부제: 생산성을 위한 성능 희생에 대한 불평
이 글은 Yes, Python is Slow, and I Don't Care의 번역문입니다. 저는 전문 번역가가 아니기에 일부 오역이 존재할 수 있습니다. 이에 대해서는 댓글로 남겨주시면 수정하도록 하겠습니다.
저는 최근 asyncio에 대한 토론을 진행했었고 이제는 Python 성능에 대해 이야기해보려 합니다. 모르는 분들을 위해 설명드리자면 저는 Python의 광팬이고 가능한 모든 곳에서 Python을 적극적으로 사용하고 있습니다. 사람들이 Python을 사용하면서 하는 가장 큰 불평 중 하나는 "Python은 느리다"입니다. 몇몇 사람들은 "X언어보다 느리니까"와 같은 이유로 제대로 사용해보지도 않습니다. 이 글에서는 Python이 느림에도 왜 우리가 Python을 사용할 가치가 있는지를 적었습니다.
속도는 중요하지 않습니다
속도는 정말 오랜 시간동안 돌아가는 프로그램에서 중요합니다. CPU와 메모리는 비쌌습니다. 프로그램의 러닝타임은 매우 중요한 척도로 사용됩니다. 컴퓨터는 매우 비쌌습니다, 물론 컴퓨터를 돌리는 전기도 비쌌습니다. 이러한 자원의 최적화는 끊임없는 사업법으로 이뤄졌습니다:
가장 비싼 자원을 최적화하세요.
역사적으로 가장 값비쌌던 자원은 컴퓨터가 켜져있는 시간(런타임)이었습니다. 이것은 알고리즘의 효율에 초점을 맞춘 컴퓨터공학 연구를 주도하기도 했습니다. 하지만 실리콘값이 떨어진 오늘날에는 조금 거리가 있는 이야기입니다. 실리콘값은 과거에 비해 정말 값싸졌습니다. 런타임은 더이상 가장 비싼 자원이 아닙니다. 오늘날 회사에서의 가장 비싼 자원은 직원의 시간입니다. 다른 말로 하면 바로 당신이죠. 어떠한 것을 더 빠르게 만드는 것은 과거에 비해 더 중요해졌습니다. 실제로요.
아마 이렇게 말할 수도 있겠습니다, "우리 회사는 속도를 중요시합니다, 저는 웹 앱을 만들고 모든 웹 앱의 응답은 x밀리초보다 빨라야 합니다." 또는 "우리는 우리 앱이 느리다고 생각해서 우리 앱 사용을 중단하는 고객을 접했었습니다." 저는 속도가 모든 면에서 중요하다고는 말하지 않습니다, 단순히 속도는 더 이상 가장 중요한 것이 아니라고만 말합니다. 속도는 더 이상 가장 비싼 자원이 아닙니다.
속도는 중요한 한가지일 뿐입니다
프로그래밍을 이야기할 때 속도에 대해 이야기한다면 이것은 일반적으로 성능(CPU 사이클)을 의미합니다. 회사의 대표가 프로그래밍에서의 속도를 이야기한다면 이것은 비즈니스 속도를 의미하겠죠. 가장 중요한 것은 신제품을 출시하는 속도입니다. 결국 얼마나 당신이 만든 제품/앱이 빠르게 돌아가는지는 중요하지 않습니다. 어떤 언어로 짜여져 있는지도 중요하지 않습니다. 심지어 만든 앱을 돌리는데 얼마나 큰 돈이 들어가는지 또한 중요하지 않습니다. 결국 가장 중요한 것은 회사가 사냐 죽냐를 결정하는 신제품 출시 속도입니다. 저는 만든 제품으로 돈을 벌기까지 얼마나 오랜 시간이 걸리는지에 대해서만 이야기하는 것이 아니라 "아이디어를 구상하고 고객의 손 안에 들어가기까지 걸리는 시간"에 대해서도 이야기하고 있습니다. 사업을 하면서 살아남는 유일한 방법은 경쟁사보다 더 빠르게 혁신하는 것입니다. 경쟁사가 우리보다 먼저 제품을 출시한다면, 우리가 얼마나 많은 아이디어를 생각해내는지는 중요하지 않습니다. 시장에서 승리하기 위해 당신은 시장에서의 최초 자리를 선점해야 합니다, 아니면 최소한 따라잡기라도 해야합니다. 조금이라도 뒤쳐지면 그 순간 끝나게 됩니다.
사업을 하면서 살아남는 유일한 방법은 경쟁사보다 더 빠르게 혁신하는 것입니다.
마이크로서비스의 예
아마존, 구글, 넷플릭스와 같은 회사들은 빠르게 움직이는 것의 중요성을 이해하고 있습니다. 이 회사들은 회사들이 빠르게 움직이고 빠르게 성장할 수 있는 비즈니스 시스템을 만들어왔습니다. 마이크로서비스는 그중 하나입니다. 이 글에서는 당신이 마이크로서비스를 이용해야할지 아닐지에 대해서는 이야기하지 않습니다, 하지만 최소한 아마존과 구글이 마이크로서비스를 이용하고 있다는 사실을 인지하기를 바랍니다.
마이크로서비스는 본질적으로 느립니다. 마이크로서비스의 핵심 개념은 서비스 사이의 네트워크 연결로 경계를 깨는 것입니다. 이것은 기존의 함수 호출을 네트워크 호출로 전환하는 것을 의미합니다. 성능면에서 당신이 할 수 있는 것 중 이것보다 나쁜 것은 별로 없습니다. 네트워크 호출은 CPU 호출에 비해 매우 느립니다. 하지만 큰 회사들은 여전히 마이크로서비스를 선택하고 있습니다. 제가 알기로 마이크로서비스보다 느린 아키텍쳐는 없습니다. 마이크로아키텍쳐의 가장 큰 단점은 성능입니다, 하지만 가장 큰 장점은 출시 속도입니다. 회사는 작은 프로젝트와 코드 단위로 팀을 구성하여 더 빠른 속도로 반복하고 혁신할 수 있습니다. 이것이 보여주는 것은 스타트업을 넘어 매우 큰 회사들조차 출시 속도에 대해 고민하고 있다는 사실입니다.
CPU는 병목을 일으키지 않습니다
만약 당신이 웹 서버와 같은 네트워크 앱을 만든다면 CPU가 병목을 일으킬 확률은 매우 적습니다. 당신이 만든 웹 서버가 요청을 처리할 때, 웹 서버는 데이터베이스 쿼리, Redis와 같은 캐시 서버로 보내는 네트워크 요청을 만들어냅니다. 데이터베이스, Redis와 같은 서비스는 빨라도 이 서비스들로 보내는 네트워크 요청은 느립니다. 특정 작업에 따른 속도 변화에 대한 매우 좋은 글이 있습니다. 이 글에서 저자는 CPU 사이클 시간을 더 쉽게 이해할 수 있게 사람들이 사용하는 시간으로 변환해서 보여줍니다. 한 번의 CPU 사이클이 1초와 같았다면, 캘리포니아에서 뉴욕으로 보내는 네트워크 요청은 거의 4년과 맞먹습니다. 이것은 네트워크가 얼마나 느린지를 보여줍니다. 대략적인 견적을 내보면 같은 데이터센터 내에 있는 네트워크 요청은 3ms가 소요됩니다. 이것은 사람의 시간으로 3달과 맞먹습니다. 자 이제 당신의 프로그램이 CPU를 매우 크게 요구한다고 상상해봅시다, 이 때 단일 처리에 응답하는데에는 100,000 사이클이 요구됩니다. 이를 사람의 시간으로 바꾸면 1일이 약간 넘는 수준일 뿐입니다. 이제 당신이 쓰는 프로그래밍 언어가 5배 느리다고 가정해봅시다, 그래봐야 5일입니다. 3달이 걸리는 네트워크 요청과 비교했을 때 4일의 차이는 전적으로 정말 중요하지 않은 차이입니다. 만약 누군가가 택배를 받기 위해 최소 3개월을 기다려야 한다면 4일이 더 걸려도 크게 중요하지 않을 거라고 생각합니다.
결정적으로 이것이 의미하는 바는 정말로 Python이 느려도 그것은 중요하지 않다는 것입니다. 언어의 속도는 거의 문제가 되지 않습니다. 구글은 이 부분에 대한 연구를 진행했었고 이에 대한 논문도 내놓았습니다. 이 논문에서는 처리량이 많은 시스템을 디자인하는 것에 대해 이야기하고 있습니다. 결론을 보면 다음과 같이 이야기합니다:
인터프리터 언어를 처리량이 많은 환경에서 사용하는 것은 모순같아 보입니다, 하지만 우리는 CPU 시간은 제한 요소가 아니라는 사실을 발견했습니다. 프로그래밍 언어의 표현력은 대부분의 프로그램은 작고 대부분의 시간을 I/O 처리와 런타임 코드를 위해 보내고 있다는 것을 의미합니다. 더욱이 인터프리터 언어 구현의 다루기 쉬운 특성은 매우 큰 도움이 되어왔습니다. 이러한 부분들은 언어학적인 실험을 쉽게 해줬고 연산을 여러 머신에 나누어 분산처리할 수 있는 방안을 모색할 수 있게 해줬습니다.
짧게 이야기해서 가장 중요한 부분은 다음 문장입니다:
하지만 우리는 CPU 시간은 제한 요소가 아니라는 사실을 발견했습니다.
CPU 시간이 문제라면?
이렇게 말할 수도 있을겁니다, "다 완벽하고 좋아요, 하지만 우리는 CPU가 병목을 일으켜 우리 웹 앱을 느리게 한 문제를 접했었습니다.", "X 언어는 Y 언어보다 서버를 돌릴 때 요구하는 하드웨어 성능이 훨씬 낮습니다." 이것 모두 맞습니다. 웹 서버의 가장 멋진 부분은 거의 무한대로 로드밸런싱을 할 수 있다는 점입니다. 다른말로 하면 하드웨어를 더 붙이는 거죠. 예 Python은 C와 같은 다른 언어에 비해 더 좋은 하드웨어를 요구할겁니다. CPU와 관련된 문제가 생겼을 때 그냥 하드웨어를 더 붙이세요. 하드웨어는 당신의 시간에 비해 매우 저렴합니다. 만약 당신이 1년에 몇주를 생산성을 위한 가치있는 시간으로 활용한다면 하드웨어를 추가하는 비용보다 더 큰 가치를 발휘할 것입니다.
그래서 Python은 빠른가요?
지금까지 저는 어떻게 가장 중요한 것이 개발 시간인지에 대해 이야기했습니다. 이제 다음 질문들이 남아있습니다: Python이 개발 시간면으로 볼 때 X언어보다 빠른가요? 일화로 저, 구글 그리고 더 많은 사람들이 Python이 얼마나 생산적인지를 말해주고 있습니다. Python은 정말 많은 부분을 추상화하여 당신이 벡터(vector)를 사용해야 할지, 배열(array)을 사용해야 할지와 같은 잡다한 고민할 필요 없이 정말로 코드로 무엇을 할지에 대해 집중할 수 있게 해줍니다. 하지만 다른 사람의 말을 듣고싶지 않을 수도 있으니 좀 더 실무에 가까운 자료를 살펴봅시다.
대부분 Python이 더 생산적인지 또는 Python이 정적 타입 언어에 반대되는 스크립팅(또는 동적 언어)인지 아닌지에 대한 논쟁이 있습니다. 제 생각으로 정적 타입 언어는 일반적으로 생산성이 떨어지는 것으로 알려져 있고 여기 이에 대해 설명한 논문이 있습니다. 특히 Python 면에서 보면, 여러 언어들을 이용해 문자열 처리를 하는 코드를 짜는데 얼마나 걸린지에 대해 확인한 연구로부터 좋은 자료가 있습니다.
위 연구 결과로 볼 때 Python은 Java보다 2배 이상 더 생산적입니다. 이와 비슷한 결과를 보여주는 다른 연구도 많이 있습니다. Rosetta Code는 프로그래밍 언어마다의 차이에 대한 공정한 깊은 연구를 했습니다. 이 논문에서 저자들은 Python을 다른 스크립팅/인터프리터 언어와 비교하며 다음과 같이 이야기하고 있습니다:
Python은 함수형 언어와 비교해서도 가장 간결한 언어가 되려는 경향이 있습니다. (1.2–1.6 times shorter on average)
일반적인 트렌드로 볼 때 Python에서의 "코드의 줄 수"는 항상 적었습니다. 코드의 줄 수는 별로 좋지 않은 척도인 것처럼 들립니다, 하지만 앞서 본 연구를 포함한 여러 연구에서는 코드의 한 줄을 짜는데 걸리는 시간은 모든 언어에서 거의 같다고 말합니다. 따라서 코드의 줄 수를 제한한다면 더 나은 생산성을 보여줍니다. 심지어 Coding Horror(C# 개발자) 자신도 Python이 얼마나 생산적인지에 대한 글을 작성했습니다.
Python이 다른 많은 언어에 비해 더 생산적이라고 말하는 것은 매우 타당하다고 생각합니다. 이것은 Python이 많은 것을 포함한 상태로 나왔고 수많은 서드파티 라이브러리가 있다는 사실에서 기인합니다. 여기 Python과 다른 언어들의 차이를 비교한 간단한 글이 있습니다. 만약 당신이 왜 Python이 정말 "작고" 생산적인지를 모르겠다면 저는 이 기회를 빌어 Python을 좀 배워보도록 권유합니다. 여기 당신의 첫 프로그램이 있습니다:
import __hello__
속도가 정말로 중요하다면 어떻게 하죠?
위에서 말하고 지적한 말투가 최적화와 속도가 아예 중요하지 않다는 것처럼 들릴 수도 있겠습니다. 하지만 실제로는 런타임 성능이 정말로 중요할 때가 많습니다. 한 예로 당신이 어떤 웹 앱을 만들었고 응답하는데 오랜 시간이 소요되는 어떤 루틴이 있는 경우입니다. 당신은 이게 얼마나 빨라져야 할지를 알고 있고 얼마나 개선되어야 할지 알고 있습니다.
우리 예로 볼 때 2가지 상황이 발생합니다:
- 우리는 매우 느리게 처리하는 루틴을 발견했습니다
- 우리는 그게 느리다는 것을 인지했습니다, 왜냐하면 우리는 이게 얼마나 빨라야 충분히 빠른 것인지에 대한 기준점이 있기 때문입니다. 그리고 1번은 그 기준점을 충족시키지 않습니다.
우리는 세세하게 애플리케이션 내에 있는 모든 것을 최적화할 필요는 없습니다. 그저 모든 것들은 "충분히 빨라지기만" 하면 됩니다. 만약 루틴이 응답하는데 몇초가 걸린다면 사용자들은 쉽게 알아차릴 수 있을 겁니다, 하지만 당신이 응답 시간을 35ms에서 25ms로 줄인다면 사용자들은 알아차리지 못할 겁니다. "이정도면 충분하다"가 바로 당신이 꼭 이뤄야 할 부분입니다. Disclaimer: 저는 실시간 경매 애플리케이션과 같은 몇몇 앱들은 미세하게 최적화할 필요가 있고 매 밀리초가 의미있을 수도 있다는 것을 밝힙니다. 하지만 이 사례는 예외일 뿐입니다.
어떻게 루틴을 최적화해야 하는지 알기 위해 우선 코드 프로파일링을 하고 어디서 병목현상이 발생하는지 알아야 합니다. 그 후:
병목현상 외에 개선된 점이 보인다면 그것은 환상입니다. - Gene Kim
만약 당신의 최적화가 병목현상을 해결하지 않았다면 당신은 그저 시간을 낭비했고 실제 이슈를 해결하지 않은 것입니다. 당신은 병목현상을 최적화하기 전까지 의미있는 개선을 얻을 수 없습니다. 만약 당신이 무엇이 병목인지를 알기 전에 최적화를 하려 한다면, 당신은 그저 코드와 두더지잡기 게임을 한 것에 불과합니다. 어디에 병목이 있는지 알아내기 전에 최적화를 하는 것은 "premature optimization(섣부른 최적화)"라고 부릅니다. Donald Knuth는 종종 다음 인용문 덕분이라고 하지만 그는 그가 다른 사람의 인용문을 훔쳤다고 주장합니다:
Premature optimization is the root of all evil. (섣부른 최적화는 모든 악의 근원입니다.)
코드 베이스를 유지보수하는 것에 대해 Donal Knuth는 다음과 같이 말합니다:
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. (우리는 자잘한 효율에 대해서는 잊고 시간의 97%에 대해 생각해봅시다: 섣부른 최적화는 모든 악의 근원입니다. 하지만 우리는 나머지 3%의 기회를 놓쳐서는 안됩니다.)
다른 말로 하면 그는 대부분의 시간동안 코드 최적화에 대해서는 잊어야 한다고 말합니다. 코드 최적화는 항상 충분히 잘 되어있는 상태입니다. 충분하지 않은 상태라면 우리는 오직 코드의 3%만 건드리면 됩니다. 당신은 루틴을 몇 나노초 정도 빠르게 만든다고 상을 받지 않습니다 왜냐하면 예로 당신은 함수 호출 대신에 if 구문을 사용해서 루틴을 빠르게 만들 것이기 때문입니다. 최적화는 충분한 검토 후에 하세요.
섣부른 최적화에는 특정 메소드를 더 빠르게 호출하는 것도 포함하고 심지어 더 빠르다는 이유로 특정 자료구조를 이용하는 것도 포함됩니다. 컴퓨터공학에서는 만약 메소드 또는 알고리즘이 다른 것들과 같이 동일한 점근적 증가율(asymptotic growth, 또는 Big-O)를 가진다면 어떤 것이 실제로는 2배 느려도 그것들은 같다고 주장합니다. 컴퓨터는 매우 빠르기 때문에 데이터와 같은 알고리즘 계산량 증가는 실제 처리 속도보다 더 중요합니다. 다른 말로, 만약 2개의 O(log n) 함수가 있고 하나는 2배 느리다면 그것은 실제로 중요하지 않습니다. 데이터 양이 증가할수록 이 2개의 함수는 같은 속도로 "느려집니다". 이것이 왜 섣부른 최적화가 악의 근원인지에 대한 이유입니다. 섣부른 최적화는 시간 낭비일 뿐이고 실질적 성능 향상에는 전혀 도움이 되지 않습니다.
Big-O로 보면 당신은 모든 언어가 O(n)이고 n이 코드 줄 수 또는 명령 수라고 주장할 수 있습니다. 이것들은 모두 같은 명령에 대해서는 같은 비율로 증가합니다. 점근적 증가율로 볼 때 언어나 런타임이 얼마나 느린지는 중요하지 않습니다. 모든 언어는 동일하게 만들어집니다. 이 논리로 보면 언어를 선택할 때 단순히 그 언어가 빠르다는 이유만으로 선택하는 것은 어리석은 행동입니다. 대부분 당신은 실제로 돌려보지도 않고 단순히 어떠한 것이 빠르다고 해서 그것을 선택합니다. 실제로 어디서 병목이 발생하는지 이해하는 것은 제쳐두고 말이죠.
Python 최적화하기
제가 Python을 좋아하는 이유 중 하나는 Python 코드는 조금씩 조금씩 최적화할 수 있기 때문입니다. Python에서 병목을 일으키는 메소드를 찾았다고 생각해봅시다. 그리고 여기와 여기에 나와있는대로 코드를 계속해서 최적화했다고 봅시다. 그리고 그 후 Python 그 자체가 병목을 일으킨다고 확정지을 수 있는 상황에 와있다고 봅시다. Python은 C 코드를 직접 실행할 수 있는 기능을 갖고 있습니다. 이것은 Python에서 병목이 일어나는 이 코드를 C로 다시 작성하여 성능 문제를 최소화할 수 있다는 것을 의미합니다. 우리는 이러한 작업을 각 메소드마다 따로따로 진행할 수 있습니다. 이런 특성은 C와 호환되는 어셈블리로 컴파일할 수 있는 어떤 언어로도 병목이 일어나는 메소드를 최적화할 수 있게 해줍니다. 또한 대부분의 시간을 Python 코드를 짜는데 보낼 수 있게 해주고 정말로 필요한 때에 로우레벨 영역으로 갈 수 있게 해줍니다.
Python의 슈퍼셋인 Cython이라는 언어가 있습니다. Cython은 거의 Python과 C를 합쳐둔 형태이고 이를 단계적 타이핑 언어(progressively typed language or gradual typing)라고 부릅니다. 그리고 Python 코드는 Cython 코드로 옮겨도 유효한 코드이고 Cython은 이를 C 코드로 컴파일합니다. Cython을 이용하면 C 타입과 성능의 이점을 가진 채로 모듈 또는 메소드를 작성할 수 있게 됩니다. 물론 C 타입과 Python만의 타입을 합칠 수도 있습니다. Cython을 이용해 Python의 멋진 부분을 이용하면서도 병목만 최적화할 수 있는 셈입니다.
우리가 만약 Python 성능 문제에 닥치게 되어도 결국 모든 코드 베이스를 다른 언어로 옮길 필요가 전혀 없습니다. 그저 성능이 중요한 부분을 Cython으로 다시 작성하기만 하면 되는 겁니다. 이건 이브온라인이 선택한 전략입니다. 이브는 매우 큰 규모의 멀티플레이어 컴퓨터 게임인데 모든 개발 스택에 Python과 Cython을 사용하고 있습니다. 그들은 게임 레벨에서 발생하는 병목현상을 C/Cython을 이용해 해결했습니다. 이 사례가 성공적이었듯 다른 곳에서도 동일하게 적용될 수 있을겁니다. 꼭 이 방법을 사용하지 않아도 Python 코드를 최적화하는 방법은 수도없이 많습니다. 예로 PyPy는 웹 서버와 같은 오랜 시간동안 작동하는 애플리케이션에 의미있는 런타임 성능 향상을 줄 수 있는 JIT 구현체입니다. 간단히 널리 쓰이고 있는 CPython을 PyPy로 변경하기만 하면 됩니다.
이제 중요한 몇 가지를 다시 집어봅시다:
- 가장 비싼 자원을 최적화하세요. 그것은 바로 컴퓨터가 아닌 당신입니다.
- Python과 같이 개발을 가장 빨리 할 수 있게 도와주는 언어/프레임워크/아키텍쳐를 선택하세요. 단순히 어떠한 것이 빠르다고 그것을 선택하지 마세요.
- 성능 문제를 겪는다면 병목을 찾으세요.
- 대부분의 병목은 CPU 또는 Python 그 자체에서 일어나지 않습니다.
- 만약 Python이 병목의 원인이고 이미 알고리즘과 같은 당신이 사용하고 있는 코드의 최적화를 마친 상태라면 해당 부분만 Cython/C로 옮기세요.
- 빠르게 일을 끝낼 수 있다는 점을 즐기세요.
저는 당신이 제가 이 글을 썼을 때처럼 이 글을 즐겨주셨으면 좋겠습니다. 만약 당신이 감사함을 표현하고 싶다면 그냥 아래에 있는 하트 버튼만 눌러주세요. 그리고 저와 Python에 대해 이야기하고 싶다면 제 트위터(@nhumrich) 계정으로 연락주시거나 Python Slack 채널에서 저를 찾으시면 됩니다.