
AI를 쓰면서 가장 어려운 지점은
읽어서 이해할 수 있다랑 빈 화면에서 그 모델링을 직접할 수 있다가 다르다
는 부분에 구분을 하기 어려워진다는 것이다.
코드를 보는 것은 사실 많이 보면 발전한다라고 나는 생각한다. 좋은 코드를 많이 봐라. 좋은 형태를 많이 느껴라. 하면 이를 패턴 매칭처럼 저장한다.
사실 프로그래밍을 하다보면 기본적으로 좋은 코드 나쁜 코드를 많이 알게된다. 왜냐하면 보통 누구나 한명쯤은 팔로우하는 프로그래밍 인플루언서나 나같이 글을 쓰는 프로그래머들은 선호하는 프로젝트가 있고, 그 프로젝트를 따라서 추적하다보면은 결국 어떤 특정한 틀에 있다는 것을 느끼기 때문이다.
그래서 오픈소스나 깃허브에 공개된 소스코드라는 광활한 토양과 자기가 속한 회사의 코드라는 사유지를 누비다보면 자기가 좋아하고, 잘 맞는 것 같다라는 코드를 보게 된다. 그럼 보통 그런 관점에 정착하게 된다.
그래서 프로그래밍을 하다보면 일반적으로 하나의 관점에서 쓰게 된다. 그리고 그 관점은 특정 분야에 국한되있고, 그걸 직업삼게 되는 것이다.
그런면에서 보통 프로그래밍은 일종의 도제 시스템 처럼 진행된다. 첫 회사나 또는 처음으로 프로그래밍을 교류하게 된 오픈소스 스타일을 대체로 프로그래머가 간직한 채 그 세계관 내에서 프로그래밍을 하는 것이다. 물론 일부 '독학'이라고 하는 사람들은 대체로 자기가 처음 성공한 그 성공 경험을 바탕으로 독특한 모양새를 만들긴한다.
그렇기때문에 프로그래밍 패러다임의 논박은 일종의 세계관의 부정처럼 들리는 경우가 있다.
보통 첫 프로그램에서 사람들은 이런 것들을 배운다.
'여기서 service가 책임 진다'
'exception 처리는 이렇게 한다'
'DB transaction은 이렇게 해야한다'
이걸 몇년 반복하면 무의식적으로 코드는 원래 이런 모양이어야만 하는구나라는 감각을 가진다. 그것 일종의 암묵지(tacit knowledge)라고 한다.
각자가 수년동안 문제를 푸는 데 사용해 온 압축 방식을 건드는 것이기때문에 감정적으로 변하기 쉽다.
예를 들어서 OOP를 10년 한 사람에게
'객체라는 추상화 자체가 잘못됐다'
라고 하면 단순한 API 선택 비판이 아니라,
'네가 지난 10년 동안 문제를 바라온 방식이 잘못됐다'
라고 들릴 수 있다.
보통 ACM 커리큘럼이나 대학에서 프로그래밍의 기초를 배우고 나오면 프로그램을 만들기 위한 재료가 있다는 것은 알지만, 이걸 어디에 어떻게 적용하는지는 순전히 경험이다.

요리로 따지면 대학생에서 칼질을 배우고, 어떤 분야를 할지 생각을 하게된다.
요리사 지망생이 있다하자.
학교에서 칼질, 식재료의 성질, 위생, 기본적인 조리법을 배운 뒤 이제 직업을 가져야 한다. 프로그래밍으로 치면 기초 알고리즘과 자료구조, 운영체제, 네트워크 같은 것을 배운 상태다.
맛있는 소스와 정교한 조리 과정을 중시하는 프랑스 요리도 있고, 재료 본연의 맛과 계절성을 중시하는 일식도 있다. 거의 모든 식재료를 다양한 방식으로 다루는 중식도 있고, 발효와 저장의 시간이 중요한 한식도 있다. 여러 문화가 뒤섞인 미국 요리도 있고, 향신료의 조합이 중요한 인도 요리도 있다.
어느 것을 선택하든 칼질이라는 기초는 필요성이 높다. (필수라고 하지 않은 것은 요새는 대부분 추상화가 잘되있기때문이기도 하다.) 하지만 좋은 프랑스 요리사가 되는 법과 좋은 일식 요리사가 되는 법은 같지 않다.
같은 칼을 쓰더라도 무엇을 자르는지, 얼마나 얇게 자르는지, 언제 불에 올리는지, 무엇을 버리고 어떤 재료의 무엇을 남기는지가 달라진다. 심지어 어떤 요리에서는 칼질보다 발효가 중요하고, 어떤 요리에서는 불을 다루는 기술이 중요하며, 어떤 요리에서는 재료의 숙성 상태를 판단하는 것이 더 중요하다.
프로그래밍의 실무도 비슷하다.
학교에서는 정렬 알고리즘과 자료구조를 배우지만, 실제 회사에 들어가면 갑자기 service, repository, transaction boundary, API contract, retry, logging, deployment 같은 것을 배운다. 웹 개발자는 HTTP와 데이터베이스를 중심으로 생각하게 되고, 게임 개발자는 frame과 state, memory layout을 중심으로 생각하며, 임베디드 개발자는 timing과 hardware boundary를 먼저 생각한다.
이것들은 학교에서 배운 기초와 충돌하는 것은 아니며, 기초 위에 특정한 주방의 세계관이 올라가는 것이다.
그래서 흔히 말하는 '실무 코딩'이라는 것은 단순히 학교에서 배우지 않은 코드 위생에 관해서 배우는 것이 아니다. 어느 문제를 나의 문제로 삼고, 무엇을 중요하게 여기고, 무엇을 위험하다고 느끼는지를 배우는 과정에 가깝다.
프랑스 요리사는 소스를 보면서 음식의 완성도를 판단할 수 있고, 일식 요리사는 생선의 상태나 칼집만 보고도 많은 것을 알아챈다.한식 요리사는 발효 재료를 앞에 두고 숙성의 시간을 읽는다. 김치의 시큼함, 장의 깊은 향, 젓갈의 염도, 장독의 온도와 계절을 겹쳐 보며 지금이 어느 단계인지 판단한다.
그래서 그들은 재료를 보는 것이 아니라, 그 재료가 지나온 것과 앞으로의 방향을 본다.
마찬가지로 웹 백엔드를 오래 한 사람은 transaction과 데이터 일관성에 민감하고, 게임 프로그래머는 allocation이나 frame spike를 먼저 본다.
결국 직업적 경험이란 문제를 푸는 기술만 축적하는 것이 아니라, 무엇을 문제라고 인식할 것인가를 배우는 과정이다.
그런데 AI는 이 요리학교와 도제 시스템 사이에 이상한 존재로 존재한다.
AI는 프랑스 요리도 만들고, 일식도 만들고, 중식도 만든다. 내가 “프랑스식으로 해줘”라고 하면 그렇게 하고, “이번에는 일본식으로 바꿔줘”라고 하면 또 그럴듯하게 바꾼다. 심지어 나보다 잘한다.
그래서 나는 예전보다 훨씬 많은 요리(코드)를 볼 수 있게 되었다.
문제는 내가 그 음식을 먹어보고 맛을 평가할 수 있다는 사실이 냉장고를 열어 아무것도 정해지지 않은 상태에서 같은 요리를 설계할 수 있다는 사실과는 다르다는 것이다.
이미 완성된 접시를 보며, 소스의 플레이팅이 이상하다. 이건 염도가 너무 높아서 일식을 좋아하는 사람들은 좋아하겠지만 한국 음식에는 맞지 않다. 이건 너무 매워서 일식을 좋아하는 사람에게는 잘못된 것이다 같은 세계관이 형성이 되지 않는 것이다.
AI는 내가 기획서와 SPEC만 주면 나보다 디테일의 측면에서 훨씬 잘한다.(이는 내가 영어권 사람이 아니기때문에 가지는 한계점이다) 더 가독성이 높은 함수명을 작성하고, 더 나은 성능의 알고리즘을 쓴다. 그리고 SPEC대로 하라고 하면 내가 구현을 못해서 성능에 타협하는 지점에 대해서도 경계를 확실히 나누고 성능도 더 좋게 만들 수 있다.
오늘은 객체지향적인 코드를 만들어달라고 하면 객체와 책임으로 나누고, 내일은 함수형으로 작성해달라고 하면 immutable data와 transformation의 흐름으로 바꾼다. DDD를 요구하면 aggregate와 domain event를 만들고, 단순한 CRUD가 필요하다고 하면 그런 복잡함도 버리고 쉽게 만든다.
나는 기본적으로 방어 코드를 과잉하게 작성하지만, AI는 나보다 더 과잉하게 아니면 정말 간결하게 작성한다.
나는 3년차에 처음으로 나 혼자 처음부터 바닥부터 돌아가는 5만줄 짜리 프로그램을 완성했다. 그리고 이 세계관을 바탕으로 단순 유지보수와 기존 레거시 코드 수정에서 프로그래밍 완전 납품으로 전환 했다. 하지만 요즘은 AI에게 하루도 안되어서 저런 코드를 만들어낼 수 있다.
내가 인간 선배에게 배우던 시절에는 적어도 그 사람이 가진 하나의 세계관에 오래 노출되었다. 하지만 AI는 수많은 세계관을 동시에 흉내낼 수 있다.
그래서 AI를 많이 쓰면 굉장히 많은 형태의 코드를 보게 된다. 이것 자체는 엄청난 장점이다. 과거라면 몇 년 동안 여러 회사를 다녀야 볼 수 있었던 코드 형태를 몇 달 안에 볼 수도 있다.
그러다보면 나는 큰 착각에 빠지게 된다. 내가 그 관점을 배운 것인가, 아니면 단지 그 관점으로 작성된 결과물을 많이 본 것인지 구분하기 어려워진다.
예를 들어보자, AI가 Result<T>를 중심으로 실패를 모델링하고, sum type으로 상태를 나눈다고 가정하자.
exhaustive pattern matching으로 분기를 처리하는 코드를 만들어줬다고 하자. 나는 그 코드를 읽고 왜 좋은지도 설명할 수 있다. 잘못된 branch가 하나 빠져 있다면 그것을 찾아낼 수도 있다. 이건 그냥 많이 읽으면 느는 일종의 스킬이다.
type Result<T, E> =
| { kind: "ok"; value: T }
| { kind: "err"; error: E };
type PaymentState =
| { kind: "pending"; orderId: string }
| { kind: "authorized"; orderId: string; paymentId: string }
| { kind: "captured"; orderId: string; paymentId: string }
| { kind: "failed"; orderId: string; reason: string };
type PaymentError =
| { kind: "invalidAmount" }
| { kind: "gatewayTimeout" }
| { kind: "cardDeclined" };
function capturePayment(
state: PaymentState
): Result<PaymentState, PaymentError> {
switch (state.kind) {
case "pending":
return {
kind: "err",
error: { kind: "invalidAmount" }
};
case "authorized":
return {
kind: "ok",
value: {
kind: "captured",
orderId: state.orderId,
paymentId: state.paymentId
}
};
case "captured":
return {
kind: "ok",
value: state
};
case "failed":
return {
kind: "err",
error: { kind: "cardDeclined" }
};
default:
return assertNever(state);
}
}
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}그런데 코드를 전부 지운 뒤, 다시 모델링을 하라고 하면 나는 할 수 있을까? 이건 굉장히 어려운 문제다.
어떤 상태를 하나의 타입으로 만들어야 하는지, 어떤 실패를 별도의 경우로 분리해야 하는지, 무엇이 invariant인지, 어느 객체가 그 상태를 소유해야 하는지를 처음부터 다시 결정할 수 있는가가 전혀 다른 문제기 때문이다.
여기서 나는 인식(recognition)과 생성(generation)이 다르다고 생각한다.
이미 만들어진 구조를 보고 좋은지 나쁜지 판단하는 능력과, 아무것도 없는 상태에서 가능한 구조들을 만들어보고 그중 하나를 선택하는 능력은 서로 연결되어 있지만 동일한 것은 아니다.


좋은 평론가와 좋은 작품을 만드는 거장은 서로 다른 능력을 가진다고 할 수 있다. 뛰어난 영화 평론가는 한 장면의 구도와 편집, 서사의 약점을 정확하게 분석할 수 있지만 반드시 좋은 영화를 만들 수 있는 것은 아니라고 할 수 있다. 반대로 위대한 감독이나 소설가는 자신이 왜 그렇게 만들었는지를 이론적으로 잘 설명하지 못할 수도 있다.
히치콕의 손녀가 히치콕의 영화 리포트를 인터뷰를 해서 제출했는데 C를 받은 것을 보고 사람들은 흔히 알지도 못하면서, 작가의 의도를 해석하느냐라고 말한다.
하지만 생각해보면 히치콕이 자신의 영화를 만들 수 있었다고 해서, 그 영화가 만들어낸 사회적 파장과 더불어 대중에게 설득하는 것을 가장 잘 언어화할 수 있는 사람이라는 보장은 없다. 창작자는 선택을 하고, 평론가는 그 선택이 만들어낸 구조를 분석하며, 창작자가 의식하지 못한 반복이나 패턴을 평론가가 발견할 수도 있고, 반대로 평론가가 만들어낸 해석이 실제 제작자의 의도와 전혀 무관할 수도 있다.
즉, 만드는 것과 알아보는 것은 서로 다른 종류의 숙련이라는 점이다.
프로그래밍도 비슷하다.
어떤 사람은 코드를 보면 책임이 잘못 배치된 부분, 상태가 새는 부분, 필요 없는 추상화, 미래에 문제가 될 경계를 빠르게 발견한다. 하지만 빈 화면에서 같은 수준의 프로그램을 처음부터 만들어내는 데에는 어려움을 겪을 수 있다.
반대로 어떤 사람은 새로운 시스템을 굉장히 잘 만든다. 요구사항 몇 줄만 보고도 적절한 데이터 모델과 경계를 잡고 프로그램을 완성한다. 그런데 막상 다른 사람의 코드를 리뷰하거나, 자신이 왜 그런 구조를 선택했는지를 언어로 설명하는 데에는 서툴 수 있다.
그런면에서 AI를 쓰는 것은 일종의 평론가적 능력을 쓴다라고 생각하게 된다. 문제는 그게 창작의 부분까지 들어가서 내 무능함을 세련되게 가려버린다는 것이다.
AI가 작성한 완벽한 Result 패턴과 유니온 타입을 보며 "음, 상태 분리가 아주 잘 되었군" 하고 승인(Approve) 버튼을 누르는 순간, 나는 내가 그 아키텍처를 직접 설계했다고 착각하게 된다. 프롬프트로 지시를 내렸으니 내가 창작자라고 믿지만, 실상은 AI가 차려놓은 뷔페에서 완성된 요리를 맛보고 평가하는 '미식가'의 위치로 물러난 것이다.
평론가의 눈이 높아졌다고 해서 창작의 근육이 단련되는 것은 아니다. 오히려 AI가 던져주는 압도적인 퀄리티의 정답들을 쉼 없이 소비하다 보면, 빈 화면 앞에서 서툴게 변수를 선언하고 경계를 짓던 고통스러운 모델링의 감각은 빠르게 퇴화한다.
그렇다고해서, 빈 화면만 보면서 의도를 만들어 내기에는 시장성과 또한 AI 활용을 바탕으로 한 압도적인 구현 능력을 버려야한다. 그런 면에서 어쩌면 AI 활용 능력은 프로그래머가 관리직으로 승진하는 것과 비슷하다고 할 수 있다.
관리자가 된 사람은 더 이상 조직에서 엄청난 양을 코드를 쓰는 능력을 필요로 하지 않는다. 대신 누가 어떤 일을 해야 하는지 정하고, 결과물이 요구사항을 만족하는지 판단하고, 잘못된 방향으로 가고 있다면 중간에 멈추게 해야한다. 흔히 좋은 상사와 좋은 부하직원이 다른 것이 이때문이다. 이때문에 직접 생산하는 능력보다 생산물을 평가하고 방향을 결정하는 능력의 비중이 커진다.
AI를 사용하는 프로그래머도 점점 비슷한 위치로 이동한다.
내가 직접 함수 하나하나를 작성하는 대신, 문제를 나누고, 제약을 전달하고, AI가 만든 결과를 검토하고, 마음에 들지 않으면 다시 작성하게 한다. 테스트를 통과시키고, 기존 코드와 충돌하는 부분을 찾아내고, 최종적으로 어느 구현을 받아들일지 결정한다.
이것은 분명 필요한 능력이다. 그리고 생산성, 그러니까 어떤 기능을 빠르게 만드는 것을 놓고 보면 직접 모든 코드를 작성하는 것보다 압도적으로 강하다.
한 명의 관리자가 여러 명의 작업자를 움직이듯이, 한 명의 프로그래머가 여러 에이전트를 동시에 움직일 수 있기 때문이다. 그래서 AI 시대의 생산성은 점점 작성 속도보다 판단 속도에 의해 제한된다.
하지만 관리직에도 오래된 문제가 하나 있다.
현장을 떠난 관리자는 시간이 지나면서 자신이 관리하는 일을 직접 수행할 능력을 잃을 수 있다.
개발에서도 똑같은 일이 일어난다.
처음에는 내가 직접 작성할 수 있는 코드를 AI에게 맡긴다. 조금 지나면 내가 작성하기에는 귀찮은 코드를 맡긴다. 더 지나면 내가 작성하기 어려운 코드를 맡긴다. 그리고 어느 순간에는 내가 처음부터 작성할 수 없는 코드까지 AI가 만들어주고, 나는 그것을 읽고 승인한다.
이상한 것은 이 과정이 자연스럽다는 것이다. 갑자기 능력을 잃는 순간은 없다.
각 단계 사이의 이동은 아주 작다. 그래서 어느 순간 경계를 넘었는지 알기도 어렵다. 그 선을 넘는 순간 잃게 된다. 내가 본 일부 관리직 프로그래머들의 연차가 쌓일수록 요즘 코드들은 이해하지 못하고, 자기 바로 아래의 직원에게 리뷰를 맡기는 경우가 거의 이때문이었다.
이제 시장은 나에게 AI로 코드를 작성하라고 요구한다. 그리고 또 AI로 코드를 작성하는 것은 재밌다. 물론 싫다는 사람들도 있을 것이다.
하지만,
코드를 평가할 수 있다는 것과 그 프로그램의 모델을 내가 소유하고 있다는 것은 같은가?
늘 그 생각이 가시처럼 목구멍에 걸린 것 같다. 그래서 내 생각을 기록해둔다. 내 프로그래밍 모델을 외부로 빼두어서 언젠가 다시 읽으며 복구할 수 있게.