잘한다는 것에 대한 고찰

2026. 7. 26. 19:47·일상

이력서나 채용 사이트의 기술 숙련도 항목을 채울 때마다 늘 걸리는 게 있다. 프로그래밍 언어 옆에 상/중/하 중 하나를 고르라는 칸이다. 별생각 없이 지나칠 수도 있지만, 나는 이게 사실 "당신은 잘한다는 게 무엇인지 스스로 정의할 수 있는가"를 묻는 질문이라고 생각한다.

수학을 전공한 사람 입장에서 이건 낯설지 않은 문제다. 수학에서 무언가를 "잘한다"고 스스로 말하는 것은 확실히 오만이다. 이 오만함은 지금 자신이 알고 있는 것이 마치 수학의 전부인 것처럼 여기는 데서, 더 정확히는 자신이 무엇을 모르는지조차 모르고 있다는 사실을 인지하지 못하는 데서 온다. 수학은 신이 아니라면 아무도 전체를 알 수 없는 영역이고, 아무리 뛰어난 탐구자라 해도 그가 아는 것은 존재하는 모든 진리에 비하면 우주의 모래알 하나조차 되지 못한다. 만약 자신이 모르고 있는 영역이 우주의 모든 모래알 개수보다도 많다는 걸 정말로 체감하고 있다면, 애초에 "잘한다"는 말이 입 밖으로 나올 수 없을 것이다. 공부를 하면 할수록 이 사실은 위안이 아니라 무게로 다가온다. 어떤 개념을 깊이 파고들수록 그 주변의 미해결 영역, 표면만 훑었던 지점들이 끝없이 드러나고, 그때마다 자신이 모르고 있던 영역이 얼마나 넓은지를 새로 발견하게 되기 때문이다.

다행히 프로그래밍 언어는 수학만큼 무한하지는 않다. 표준 문서로 명세되어 있고, 원칙적으로는 한 사람이 전체를 다 알 수 있는 유한한 대상이다. 그래서 "잘한다"를 정의하는 일이 수학에서만큼 절망적이지는 않다. 다만 나는 그 정의를 탐구하고 싶었다. 그리고 이 정의는 언어마다 다를 수밖에 없다. 각 언어가 애초에 다른 철학으로 설계되었기 때문이다.

언어는 각자 다른 태도로 말을 건다

C는 이렇게 말하는 것 같다.

"나는 너에게 아무것도 강제하지 않을 거야. 배열의 경계도, 메모리 관리도 다 네 몫이야. 잘 통제하면 아름답고 빠른 걸 만들 수 있을 거고, 서툴다면 그건 쓰레기가 될 거야."

정확히 말하면 C는 자유를 "주는" 게 아니라 애초에 관심이 없다. 하드웨어를 최대한 가깝게 노출시키는 게 목적이지, 프로그래머에 대한 신뢰가 목적이 아니다. C가 탄생한 시대에 컴파일러는 지금처럼 똑똑하지 않았고, 운영체제를 짜야 하는 사람에게 필요한 건 안전망이 아니라 하드웨어를 있는 그대로 다룰 수 있는 통로였다. 그래서 C에게 "위험"이라는 개념은 언어 안쪽의 문제가 아니라 애초에 언어 바깥, 즉 프로그래머의 몫으로 밀려나 있다.

 

C++은 C의 자유를 그대로 물려받으면서도 조금 다른 태도를 취한다.

"나도 너에게 C만큼의 자유를 줄게. 그런데 원한다면 안전장치도 마련해뒀어. 다만 그 안전장치는 전부 옵션이야. 쓸지 말지, 언제 쓸지는 네가 골라. 그러니 이제 무엇을 선택할지 아는 것 자체가 네 실력이야."

C++의 태도는 자유를 주는 C와 안전을 주는 Java 사이 어딘가에 걸쳐 있는 게 아니라, 그 둘을 동시에 손에 쥐여주면서 선택의 책임까지 프로그래머에게 넘기는 쪽에 가깝다. 안전장치가 전부 "옵션"이라는 게 핵심이다. 쓰지 않으면 비용을 내지 않아도 되지만, 그 말은 곧 언제 어떤 도구를 쓸지 선택하는 것 자체가 실력이라는 뜻이 된다. C가 "위험은 네 몫"이라고 말하고 끝낸다면, C++은 "위험을 감수할지 말지도 네가 정해야 한다"고 한 겹 더 되묻는 셈이다.

 

Java는 C와 정반대다.

"너 하나를 못 믿는 게 아니야. 이 코드는 10년 뒤 너 말고 다른 100명이 유지보수할 거야. 그러니 안전장치는 내가 깔아둘게. 대신 성능은 조금 포기해줘."

Java의 불신은 개인이 아니라 규모와 시간을 향한 것이다. GC, 강제 타입 체크 - 전부 개인의 천재성보다 조직의 평균적인 안정성을 뽑아내기 위한 트레이드오프다. C가 "위험은 언어 바깥의 일"이라고 한다면, Java는 정반대로 "위험은 언어 안쪽에서 내가 미리 다 처리해두겠다"고 나선다. 그 대가로 치르는 게 성능과 로우레벨 제어권이라는 것도 분명히 한다.

 

Python은 또 다르다.

"네가 쓸 재료와 도구는 내가 다 마련해뒀어. 너는 그냥 빨리 멋진 집을 지으면 돼."

Python의 친절함은 C처럼 위험을 아예 신경 쓰지 않는 무관심도, Java처럼 프로그래머를 못 믿어서 안전장치를 까는 것도 아니다. 애초에 메모리 관리나 타입 같은 저수준의 결정을 프로그래머가 매번 내리게 하는 것 자체가 비생산적이라고 보는 태도에 가깝다. Python의 설계 철학을 담은 'The Zen of Python'에는 "명료함이 함축성보다 낫다", "복잡함보다 단순함이 낫다"는 원칙과 함께, "무언가를 하는 명확한 방법이 하나 있어야 한다"는 문장이 있다. C++이 하나의 문제에 여러 해법(가상 함수냐 템플릿이냐, 원시 포인터냐 스마트 포인터냐)을 늘어놓고 그 선택을 프로그래머에게 떠넘기는 것과 정반대로, Python은 애초에 선택지를 좁혀서 "이렇게 하면 된다"는 하나의 경로를 제시하려 한다. 그 대가로 세부적인 제어권 - 메모리가 어떻게 배치되는지, 연산이 내부적으로 몇 단계를 거치는지 - 을 인터프리터 쪽으로 가져가 프로그래머의 시야에서 감춰버린다. 그래서 Python으로 짠 코드는 대개 짧고 읽기 쉽지만, 그 아래에서 실제로 무슨 일이 벌어지고 있는지는 의도적으로 가려져 있다.

C를 잘한다는 것

C에서 "잘한다"는 것을, 나는 프로그래머 스스로 엄격한 규율을 세우고 그 규율 아래에서 언어가 부여한 자유를 온전히 누리는 것이라고 정의하고 싶다. C는 프로그래머에게 어떠한 족쇄도 채우지 않는다. 하고 싶은 건 다 할 수 있게 내버려둔다. 문제는 이 자유를 그대로 두면 코드가 중구난방이 되기 쉽다는 데 있다. 그래서 진짜 실력은 언어가 강제하지 않는 규율을 프로그래머가 스스로 세우고 지키는 데서 나온다.

이 규율이 가장 먼저 필요한 이유는, C가 위험을 전혀 걸러주지 않기 때문이다. 컴파일러는 프로그래머가 "이런 상황은 절대 일어나지 않는다"고 암묵적으로 약속했다고 가정하고 코드를 최적화하는데, 이 약속이 깨지는 순간이 undefined behavior(UB)다. 해제된 메모리를 다시 참조하는 use-after-free가 대표적이다. free(p) 이후 p를 건드리는 순간 UB가 발동하지만, 겉보기엔 멀쩡하게 동작하다가 메모리가 재할당되는 시점에야 뒤늦게 문제가 터진다. 그래서 개인 차원에서는 "포인터를 해제한 직후 NULL로 초기화한다", "모든 변수는 선언과 동시에 초기화한다", "malloc의 반환값은 반드시 확인한다" 같은 규칙을 스스로 세우고 지켜야 한다.

혼자 짤 때는 이런 규율이 개인의 습관으로도 충분하지만, 협업으로 넘어가면 이 습관은 조직의 coding convention으로 명문화되어야 한다. 함수 하나의 길이를 몇 줄로 제한할지, 중괄호를 여는 위치를 어디에 둘지, 매크로 이름은 전부 대문자로 쓸지, 전역 변수를 허용할지 말지, 에러 코드를 리턴값으로 처리할지 errno로 처리할지 - 이런 세세한 규칙 하나하나가 언어가 주지 않는 안전망을 조직이 스스로 만들어내는 과정이다. C가 아무것도 강제하지 않기 때문에, 오히려 이 규율을 얼마나 촘촘하게 그리고 예외 없이 세울 수 있는가가 실력을 가른다.

그런데 이 규율은 위험을 막는 데서 끝나지 않는다. 재밌는 지점은, 규율을 정확한 위치에 세워두면 오히려 그 덕분에 다른 곳에서는 더 자유롭게 움직일 수 있다는 것이다. const 키워드가 그렇다. 어떤 변수를 const로 선언하는 것은 사람에게도 분명히 의미가 있다. 협업 상황에서 다른 사람의 코드를 읽거나 디버깅할 때, 이 변수가 const로 선언되어 있다는 걸 보는 순간 "이 값은 이 함수 안에서 절대 바뀌지 않는다"는 확신을 갖고 나머지 코드를 안심하고 따라갈 수 있다. 가독성과 협업을 생각한다면 이 신호는 그 자체로 충분히 가치가 있다. 그런데 const는 여기서 그치지 않고 컴파일러에게도 똑같은 신호를 보낸다. "이 메모리 영역은 상수를 담고 있으니 그 사실을 이용해서 최적화해도 좋다"는 걸 명시적으로 알려주는 것이다. 컴파일러는 이 값을 레지스터에 캐싱해두고 다시 메모리에서 읽어올 필요가 없다고 판단하거나, 이 값에 의존하는 계산을 컴파일 타임에 미리 할 수 있다. const를 붙이는 규율 하나가 사람에게는 안심하고 읽을 수 있는 가독성을, 컴파일러에게는 더 과감하게 최적화할 자유를 동시에 만들어주는 셈이다.

같은 구조가 함수 매개변수의 유효성 검사에서도 나타난다. 함수를 짤 때마다 "이 값이 NULL은 아닌지, 범위를 벗어나지 않았는지"를 내부에서 다시 확인하는 방어적 코드를 짤 수도 있다. 하지만 나는 대신 "매개변수의 유효성은 호출하는 쪽에서 미리 보장한다"는 규율을 코드베이스 전체에, 마치 팀의 coding convention처럼 세우는 쪽을 선호한다. 이 규율을 계약처럼 지키고 나면, 함수 내부에서는 그 유효성을 다시 검사하지 않아도 되는 자유를 얻는다. 코드는 더 짧아지고, 매 호출마다 반복되는 검사 비용도 사라진다. 이건 검사를 게을리하는 게 아니다. 검사의 책임을 코드베이스 전체에서 정확히 한 지점(호출부)으로 못박아 두었기 때문에, 그 계약이 지켜지는 한 나머지 모든 곳에서 자유롭게 움직일 수 있는 것이다. 규율이 자유를 없애는 게 아니라, 정확히 규율을 세운 지점에서 자유가 태어난다.

돌이켜보면 나는 이 방향으로 C를 즐겨왔다. AddressSanitizer와 UndefinedBehaviorSanitizer로 스스로 세운 규율이 실제로 지켜지고 있는지 검증하고, Makefile과 CI/CD로 코드베이스가 커져도 그 규율이 자동으로 지켜지는 구조를 만드는 일. 그 위에서 프로파일러를 돌려가며 실행 속도를 한 사이클씩 줄여나가는 집념. 이건 알고리즘 효율성 문제와는 결이 다르다. 그보다 한 단계 더 물리적인 층위, C가 하드웨어에 얼마나 가깝게 붙어있는지를 실감하며 최적을 찾아가는 감각이었다.

C++을 잘한다는 것

C++에서 "잘한다"는 것을, 나는 내가 만들고자 하는 시스템에 어울리는 기능을 선택하거나 제한하는 능력이라고 정의하고 싶다. C++은 템플릿, 예외, 다중 상속, 저수준 포인터 조작까지 다 던져주는 언어다. 이 기능을 다 쓰는 게 좋은 설계는 아니다. 시스템의 성격 - 임베디드냐, 대규모 백엔드냐, 게임 엔진이냐 - 에 맞춰 어떤 기능을 쓰고 어떤 기능을 제한할지 판단하는 것이 실력의 본질이다. 어떤 서비스에서는 최선인 기능이, 다른 서비스에서는 그대로 장애 요인이 되기도 한다.

그런데 이 선택을 제대로 하려면 전제조건이 하나 있다. C++이 제공하는 각 기능의 장단점을 정확하게 알고 있어야 한다는 것이다. 장단점을 모른 채로는 애초에 "이 상황에는 이 기능이 맞다"는 판단 자체가 성립하지 않는다. 그러니 C++을 잘한다는 것은 결국 두 단계로 이루어진다. 먼저 각 기능이 무엇을 대가로 요구하는지 명확히 알고 있는 것, 그리고 그 지식을 바탕으로 지금 만들고 있는 시스템에 맞는 선택을 내리는 것이다.

이 두 단계가 어떻게 맞물리는지 잘 보여주는 예가 컴파일 타임 다형성(템플릿)과 런타임 다형성(가상 함수) 사이의 선택이다. 먼저 장단점부터 보면, 가상 함수는 vtable을 통한 간접 호출 비용을 매번 지불하는 대신 실행 중에 동작을 바꾸는 유연성(플러그인 아키텍처)을 얻는다. 템플릿은 컴파일 시점에 타입이 확정되어 간접 호출 비용이 없고 인라이닝까지 가능하지만, 런타임에 동작을 바꿀 수 없고 타입 조합마다 코드가 찍혀 나와 바이너리가 커진다. 이 장단점을 정확히 알고 있어야 비로소 선택이 가능해진다. 초당 수백만 건을 처리하는 고빈도 거래 시스템은 이 정보를 바탕으로 CRTP 같은 정적 다형성을 택하고, 런타임 플러그인이 필요한 애플리케이션은 같은 정보를 바탕으로 가상 함수를 택한다. 같은 지식에서 출발했는데도 시스템이 다르면 결론이 정반대로 갈리는 것이다. 둘 중 하나가 항상 옳은 게 아니라, 시스템이 요구하는 게 유연성인지 순수한 성능인지에 따라 갈린다.

같은 원리는 value category와 move semantics에서도 반복된다. C++11 이후 표현식은 lvalue, prvalue, xvalue로 분류되고, std::move(x)는 실제로 아무것도 이동시키지 않는다. 그냥 x를 xvalue로 캐스팅할 뿐이며, 실제 이동은 받는 쪽의 move constructor가 수행한다. 이 메커니즘을 정확히 알고 있으면, 대용량 객체의 복사가 병목이 되는 시스템 - 실시간 데이터 파이프라인이나 게임 엔진의 리소스 관리 같은 곳 - 에서는 처음부터 값을 값으로 주고받는 대신 이동을 강제하는 인터페이스(우측값 참조, perfect forwarding)를 적극적으로 설계에 반영할 수 있다. 반대로 이런 최적화가 크게 중요하지 않은 소규모 서비스라면, 그 복잡성을 감수할 이유가 없으니 단순한 값 전달 방식을 유지하는 것도 합리적인 선택이다. 여기서도 같은 지식이 시스템에 따라 다른 선택으로 이어진다. std::move가 실제로 무엇을 하는지 모르면, 애초에 이런 선택지가 존재한다는 것조차 알 수 없다.

예외 안전성 보장 수준(no-throw / strong / basic guarantee)을 구분하는 것도 같은 이유에서 중요하다. 실시간성이 중요한 임베디드나 항공 전자 소프트웨어에서는 예외 처리의 예측 불가능한 실행 시간 때문에 컴파일러 옵션으로 예외 자체를 꺼버리고(-fno-exceptions) 오류 코드나 std::expected로 대체하는 경우가 흔한 반면, 일반적인 애플리케이션 서버라면 예외를 적극적으로 쓰는 게 코드를 더 명료하게 만든다. 이 상반된 선택 모두, 예외가 무엇을 보장하고 무엇을 대가로 요구하는지(스택 되감기 비용 등) 정확히 알고 있다는 같은 전제에서 출발한다. RAII도 "소멸자에서 자원을 해제한다"는 문장으로는 부족하다. 스택 되감기 도중 소멸자에서 또 예외가 발생하면 std::terminate가 호출되므로, 소멸자는 원칙적으로 예외를 던지면 안 된다는 것까지 알아야 한다. 멀티스레딩까지 가면 memory_order의 차이를 알고 왜 lock-free 자료구조에서 이 순서 지정이 정확성에 직결되는지 설명할 수 있어야 한다. 이 모든 지식이 결국 "이 기능을 지금 이 시스템에 써도 되는가"라는 하나의 질문에 답하기 위해 필요한 재료들이다.

Java를 잘한다는 것

Java에서 "잘한다"는 것을, 나는 자바가 제공해준 안전한 틀을 이용하여 조직 내에서 잘 협업할 수 있는 능력이라고 정의하고 싶다. Java의 안전장치들은 개인의 실력을 못 믿어서가 아니라 여러 사람이 오래 함께 유지보수한다는 전제에서 나온 설계다. 그래서 Java를 잘한다는 것은 혼자 최대한 많은 걸 해내는 능력이 아니라, 다른 사람이 짠 코드를 정확히 읽고 팀 전체의 코드베이스가 시간이 지나도 무너지지 않게 지탱하는 능력에 가깝다.

여기서 흥미로운 점은, GC나 동시성 모델, 타입 시스템처럼 겉보기엔 전혀 다른 영역에 속한 Java의 기능들이 사실 전부 같은 목적 하나로 수렴한다는 것이다. 바로 조직이 안전하게 협업하며 코드를 짤 수 있도록 언어 차원에서 판을 깔아주는 것이다. 이 기능들을 각각 뜯어보면 그 사실이 더 분명해진다.

GC가 협업에 기여하는 지점은, 각자 다른 모듈을 짜는 여러 사람이 메모리 해제 시점을 서로 조율하지 않아도 된다는 데 있다. C나 C++이었다면 "이 객체는 누가 언제 해제할 책임을 지는가"를 모듈 경계마다 문서화하고 합의해야 하지만, Java에서는 GC가 그 책임을 통째로 떠맡는다. 다만 이걸 "자동이니까 신경 안 써도 된다"로 받아들이면 곤란하다. Java는 처리량과 지연시간 사이 트레이드오프가 다른 여러 GC 알고리즘(Parallel, G1, ZGC 등)을 제공하는데, 이 트레이드오프를 팀 전체가 공유하고 있어야 한다. "우리 서비스는 지연시간이 중요하니 ZGC를 표준으로 쓴다"는 걸 팀 컨벤션으로 정해두면, 새로 합류한 팀원이 GC 내부를 몰라도 왜 이 옵션이 켜져 있는지 이해하고 같은 기준으로 코드를 짤 수 있다.

동시성 쪽은 조금 더 직접적으로 협업과 맞닿아 있다. 동시성 버그는 대개 한 사람이 짠 코드 하나의 문제가 아니라, 서로 다른 시점에 다른 사람이 짠 코드 조각들이 예상치 못하게 얽히면서 생긴다. JMM(Java Memory Model)의 happens-before 관계를 이해하고 있으면, 코드 리뷰 중에 "이 필드는 여러 스레드에서 접근하는데 volatile이 빠져 있다"는 걸 남의 코드에서 짚어낼 수 있다. volatile이나 synchronized의 진짜 역할은 "잠금"이 아니라 happens-before 관계를 강제해서 한 스레드의 쓰기가 다른 스레드에게 확실히 보이도록 만드는 것인데, 이걸 정확히 아는 사람이 팀 내에서 동시성 코드의 사실상 검증자 역할을 하게 된다. 이건 "내 코드가 정확한가"의 문제가 아니라 "여러 사람이 각자 짠 코드가 안전하게 결합될 수 있는가"를 판단하는 능력이다.

이 협업의 감각은 타입 시스템 차원에서도 똑같이 나타난다. checked exception이 대표적이다. 메서드 시그니처에 throws IOException이 붙어 있으면, 다른 팀원은 코드를 열어보지 않아도 "이 호출은 실패할 수 있다"는 계약을 컴파일 타임에 강제로 인지한다. 인터페이스와 구현을 엄격히 분리해 의존성을 역전시키는 설계도 마찬가지다. 한 팀이 인터페이스만 정해두면 각자 다른 구현을 병렬로 개발할 수 있고, Spring 같은 프레임워크가 의존성 주입을 표준으로 만든 것도 결국 서로의 구현 세부사항을 몰라도 협업할 수 있게 하기 위해서다. 제네릭의 타입 소거(type erasure) - List<String>과 List<Integer>가 런타임엔 똑같이 List인 것 - 도 이 제약 안에서 API를 설계해 팀원이 타입을 잘못 쓸 여지를 컴파일 타임에 막아준다는 점에서 같은 맥락에 있다. 결국 GC, JMM, checked exception, 인터페이스, 제네릭 - 언어 차원에서 보면 전혀 다른 기능들이지만, 전부 "혼자 잘 짜는 코드"가 아니라 "여럿이 오래 다뤄도 무너지지 않는 코드"를 만들기 위해 Java가 마련해둔 장치라는 점에서 하나로 묶인다.

Python을 잘한다는 것

Python은 방향이 다르다. 두꺼운 추상화 뒤에 기계장치를 숨겨두는 언어이기 때문에, "잘한다"는 언어가 다 봐주는 척하지만 사실은 안 봐주는 지점을 꿰뚫어보는 능력이다. 다만 이건 파이썬을 의심하며 쓰라는 뜻이 아니다. 언어가 건네주는 추상화된 도구를 매번 넙죽 받아 쓰는 게 아니라, 그 도구가 언제 값비싼 대가를 요구하는지 알고 필요하면 다른 방식으로 대체할 수 있는 능력이 핵심이다.

이 능력은 먼저 병목을 진단하고 실제로 고칠 수 있는가로 나타난다. GIL(Global Interpreter Lock)이 대표적이다. CPython의 참조 카운팅이 스레드 안전하지 않기 때문에 한 번에 하나의 스레드만 바이트코드를 실행하도록 강제하는 락인데, 이 때문에 CPU-bound 작업은 멀티스레딩을 해도 실제 병렬로 돌지 않는다. 여기서 그치지 않고 CPU-bound 작업은 multiprocessing으로 프로세스를 나누거나, 아예 그 부분만 C 확장 모듈이나 Cython으로 새로 짜서 인터프리터 바깥에서 실행되게 만들 수 있어야 한다. CPython의 객체 모델도 같은 맥락이다. Python에서는 정수 하나조차 참조 카운트와 타입 포인터를 가진 완전한 객체(PyObject)라서 C의 int보다 훨씬 크고 힙 여기저기 흩어져 할당되고, 리스트도 값이 아니라 객체를 가리키는 포인터만 연속으로 저장한다. 그래서 단순한 반복문 하나에도 포인터를 쫓아 객체를 찾고 타입을 확인하는 과정이 매번 끼어든다. numpy, pandas 같은 라이브러리가 순수 Python보다 수십 ~ 수백 배 빠른 이유가 여기 있다. 이 라이브러리들은 얇은 Python 껍질 밑에 C나 Rust로 짜인 엔진을 두고, 컴파일된 반복문이 연속된 메모리를 그대로 훑게 만든다. "지금 이 코드가 순수 Python 레이어에서 도는지, 컴파일된 레이어에 위임되는지"를 코드를 보자마자 구분할 수 있어야, 병목이 어디 있는지 찾고 그 부분만 골라내 numpy로 벡터화하거나 직접 C 확장으로 새로 짜 넣는 결정을 내릴 수 있다.

이 능력의 또 다른 축은 Pythonic하게 짜는 것이다. Python으로 C 스타일의 코드를 짜는 사람들이 있다. 인덱스를 직접 증가시키며 for i in range(len(lst))로 순회하거나, 컴프리헨션 대신 매번 빈 리스트를 만들고 append를 반복하는 식이다. 이렇게 짤 거라면 애초에 왜 Python을 쓰는가. Python을 선택했다는 것 자체가 C를 쓰지 않기로 한 선택이다. 그러니 Python이 제공하는 강력한 추상화 도구들 - 리스트/딕셔너리 컴프리헨션, 제너레이터, 컨텍스트 매니저(with), 이터레이터 프로토콜, 언패킹 - 의 쓰임새를 명확히 알고 적재적소에 쓰는 것도 실력의 핵심이다. 제너레이터는 단순히 메모리를 아끼는 트릭이 아니라 lazy evaluation이 실제로 언제 이득이 되는지 판단해서 쓰는 도구이고, 컨텍스트 매니저는 리소스 해제를 try/finally로 매번 반복하지 않고 언어 차원에서 보장받는 장치다. typing이나 mypy로 타입 힌트를 다는 것도 마찬가지다. 런타임엔 아무것도 강제되지 않는다는 걸 알면서도, 정적 분석과 협업에 도움이 된다는 이유로 이걸 적절히 쓰는 균형 감각 역시 Pythonic한 태도의 일부다.

결국 이 두 가지는 같은 태도의 양면이다. 병목을 진단하는 것은 언어가 숨긴 비용을 아는 것이고, Pythonic하게 짜는 것은 언어가 마련해둔 이점을 제대로 쓰는 것이다. 둘 다 이 언어가 나에게 무엇을 주고 무엇을 감추고 있는지를 정확히 알아야 가능하다.

무지를 인지하는 것에서 시작되는 즐거움

C에서는 이렇게 나름의 방식으로 자유를 통제해왔지만, C++이나 Java, Python 앞에서는 아직 많이 부족하다고 느낀다. 그런데 이 부족함을 인지하는 것 자체가 오히려 재밌다. 발전은 스스로 부족하다는 사실을 인지하는 지점에서 시작되는 것 같다.

초등학생 때 고등학생 형 누나들의 수학 교과서를 본 적이 있다. "1, 2" 대신 "x, y" 같은 알파벳이 잔뜩 적혀 있는 걸 보고 "수학이 영어예요?"라고 물었다. 나중에 보니 별거 아니었다. 그냥 미지수였다. 이 경험이 재밌는 이유는 그 사이의 간극 때문이다. 지금 내가 C++의 value category나 Java의 메모리 모델, Python의 GIL 앞에서 느끼는 부족함도 똑같은 구조다. 지금은 낯선 개념들이 시간이 지나 숙련되면 "별거 아니었다"고 돌아볼 수 있는 날이 올 거라는 상상. 그 상상과 믿음이 나에게는 즐거움의 원천이다. 지금은 잘 몰라도 이렇게 하나씩 익혀 나가면 언젠가 지금의 C만큼 자신 있게 이야기할 수 있는 날이 올 거라는 상상이, 낯선 개념 앞의 두려움을 줄여주고 계속 도전하게 만든다.

결국 같은 곳으로 수렴한다

C, C++, Java, Python은 겉보기엔 완전히 다른 철학을 가진 언어들인데, "잘한다"의 정의는 결국 하나의 형태로 수렴한다. C는 위험을 그대로 드러내고 프로그래머가 규율 속에서 직접 통제해야 한다. Java는 위험을 안전장치 뒤에 눌러 담고, 그 위에서 여럿이 오래 함께 일할 수 있어야 한다. C++은 위험을 감수할지 프로그래머의 선택에 맡기고, 그 선택이 만들고자 하는 시스템에 맞는지 판단할 수 있어야 한다. Python은 위험을 친절하게 포장하고, 그 포장 뒤에 숨은 비용 구조를 꿰뚫어봐야 한다.

방식은 다 다르지만, 결국 "잘한다"는 언어가 나에게 숨기고 있는 진실 - 비용, 위험, 트레이드오프 - 을 얼마나 정확히 알고 있는가로 귀결된다. 어떤 도구를 잘 다룬다는 것은, 그 도구가 나에게 무엇을 대신 해주고 그 대가로 무엇을 감추고 있는지를 아는 것과 같은 말일지도 모른다.

이렇게 보면 이력서의 상/중/하는 너무 조악한 척도다. "프로덕션 코드를 짜본 적 있다"와 "이 언어가 나를 배신할 수 있는 지점까지 안다" 사이의 간극을 세 칸짜리 선택지는 담을 방법이 없다. 그래도 적어도 스스로에게는 물어볼 수 있다. 내가 "잘한다"고 말할 때 그 정의를 나는 얼마나 구체적으로 세우고 있는가. 상/중/하를 고르는 순간보다, 그 정의를 스스로 세워보는 과정이 더 중요한 것 같다.

다만 이 정의를 아무리 엄격하게 세워도, 그것이 자신에 대한 관대한 평가로 이어져서는 안 된다. 수학에서 안다고 말하는 것이 오만인 이유는, 지금 아는 것을 전부인 양 여기며 자신이 무엇을 모르는지조차 모르고 있기 때문이었다. 프로그래밍 언어도 한 사람이 그 전체를 소진할 수 있는 대상은 아니다. 스스로 세운 엄격한 정의를 통과했다고 해서, 그 바깥에 여전히 자신이 모르고 있다는 사실조차 모르는 영역이 남아 있다는 게 사라지진 않는다. 정의를 엄격하게 세우는 일과, 그 정의를 통과한 뒤에도 겸손을 유지하는 일은 서로 다른 노력이고, 둘 다 필요하다.

'일상' 카테고리의 다른 글

군 생활하면서 자격증 공부하는 방법  (0) 2026.08.09
AI시대 물결에 올라탄다는것  (0) 2026.08.02
장인에서 모험가로  (0) 2026.07.19
내가 풀고 싶은 문제  (1) 2026.07.11
일을 잘한다는 것, 그리고 작은 변화를 만드는 일  (0) 2026.07.05
'일상' 카테고리의 다른 글
  • 군 생활하면서 자격증 공부하는 방법
  • AI시대 물결에 올라탄다는것
  • 장인에서 모험가로
  • 내가 풀고 싶은 문제
juwkim
juwkim
예측 불가능한 런타임 환경 속에서 최선의 해를 찾습니다. 눈앞의 배움을 탐욕스럽게 캐시에 담으며, 목표를 향해 끝없이 최적화 해 나가는 치열한 연산과 디버깅의 기록.
  • juwkim
    The Heuristic Learner
    juwkim
  • 전체
    오늘
    어제
    • 분류 전체보기 (27)
      • CS (13)
        • Kdata (4)
        • AI (1)
        • Algorithm (2)
        • 정보처리기사 (1)
        • 정보보안기사 (1)
        • 정보통신기사 (4)
      • Standard Library (4)
        • 어학 (0)
        • 오피스 (4)
        • 한국사 (0)
      • 일상 (10)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    워드프로세서
    PCCP
    컴퓨터활용능력1급
    빅데이터분석기사
    DAsP
    정보보안기사
    정보통신기사
    AICE
    Cospro
    건강
    SQLD
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
juwkim
잘한다는 것에 대한 고찰
상단으로

티스토리툴바