AI의 리팩터링, 과연 진정 개선일까?

2026년 6월 22일 AM 10:22·15분 읽기
AI의 리팩터링, 과연 진정 개선일까?

처음엔 저도 꽤 뿌듯했습니다. GitHub Copilot이 비슷하게 생긴 코드 두 덩어리를 보더니 슥 하나의 함수로 묶어줬거든요. 함수 이름도 그럴싸했고, 중복이 사라졌으니 코드가 깔끔해진 것 같았습니다. 그 제안을 별 고민 없이 Accept했습니다.

문제는 3주 후에 터졌습니다. 새 요구사항이 들어왔는데, 그 공통 함수가 "거의 맞지만 딱 맞지는 않은" 상황이었습니다. 그래서 파라미터(함수가 동작을 바꾸기 위해 외부에서 받아오는 값 — 리모컨 버튼처럼 함수 안에서 조건 분기를 만드는 장치)를 하나 추가했습니다. 또 2주 후에 다른 요구사항이 왔고, 파라미터가 하나 더 붙었습니다. 어느 순간 그 함수 시그니처(함수 이름과 파라미터 목록을 한 눈에 보여주는 첫 줄)가 이렇게 생겨 있었습니다 — processItem(data, type, isLegacy, skipValidation, overrideFlag). 파라미터가 다섯 개입니다.

Sandi Metz가 정확히 이 패턴을 묘사한 바 있습니다. 처음 추상화를 만든 사람은 행복하게 사라지고, 이후 개발자들이 "기존 코드를 유지해야 한다"는 의무감에 파라미터와 조건문을 계속 덧붙인다고요. 저도 그 순서 그대로 밟았습니다. 함수 내부엔 if isLegacy, if skipValidation, if overrideFlag가 뒤엉켜 있었고, 어떤 호출자(caller — 이 함수를 실제로 가져다 쓰는 코드)가 어떤 경로로 실행되는지 머릿속에서 추적이 안 됐습니다. 공통 함수라고 불렸지만, 실제로는 서로 다른 세 가지 로직을 억지로 한 지붕 아래 구겨 넣은 것이었습니다.

더 황당한 건, 그 함수를 호출하는 세 곳 중 한 곳은 isLegacy=false, skipValidation=false, overrideFlag=false를 항상 넘겼습니다. 나머지 두 조건 분기는 그 호출자 입장에서 사실상 죽은 코드(dead code)였던 거죠. 중복을 없애려고 묶었는데, 실제로는 각 호출자마다 필요한 코드가 달랐던 겁니다. Copilot은 코드의 생김새가 비슷하다는 이유만으로 추상화를 제안했지만, 그 비슷함이 진짜 같은 개념을 표현하는지까지는 판단하지 않았습니다. 그 판단은 제 몫이었는데, 저는 제안을 너무 빨리 수락해버렸습니다.

매몰 비용의 함정은 그다음부터 시작됐습니다.

중복을 없애려다 오히려 더 복잡해진 이유 — 매몰 비용의 함정

파라미터가 하나씩 늘어나기 시작한 건 첫 번째 예외 케이스가 생겼을 때부터였습니다. 처음엔 isAdmin이라는 플래그 하나였어요. 관리자일 때만 특정 로직을 건너뛰어야 했거든요. "이 정도는 괜찮겠지" 싶었습니다. 그런데 두 주 뒤에 isMobileClient가 붙었고, 그다음 스프린트엔 skipValidation이 추가됐습니다. 함수 하나에 파라미터가 세 개 넘어가는 순간부터 저도 뭔가 잘못됐다는 감이 왔는데 — 이미 그 함수는 프로젝트 열두 군데에서 호출되고 있었습니다.

Sandi Metz는 이 패턴을 아주 정확하게 설명합니다.

"The code no longer represents a single, common abstraction, but has instead become a condition-laden procedure which interleaves a number of vaguely associated ideas. It is hard to understand and easy to break."

출처

그러니까, 함수가 처음엔 하나의 개념을 표현했지만 — 요구사항이 붙을 때마다 조건문이 덕지덕지 붙으면서 "여러 개의 어렴풋이 연관된 아이디어들이 뒤섞인 절차"로 변해버린다는 겁니다. 제 함수가 딱 그 꼴이었습니다. 함수 이름은 formatUserPayload였는데, 안을 열어보면 관리자 분기, 모바일 분기, 유효성 검사 생략 분기가 뒤엉켜 있었습니다. 이름과 실제 동작이 전혀 다른 함수가 된 거죠.

문제는 그때 제가 왜 계속 그 함수를 고치려 했냐입니다. 솔직히 말하면 — 이미 거기에 시간을 많이 썼기 때문입니다. 매몰 비용의 함정(sunk cost fallacy)이란, 이미 쓴 비용이 아까워서 계속 같은 방향에 투자하는 심리를 말합니다. 주식으로 치면 손실 종목을 팔지 못하고 물타기를 반복하는 것과 같습니다. Metz는 이걸 아주 냉정하게 짚어냅니다. "코드가 복잡하고 이해하기 어려울수록, 우리는 오히려 그걸 유지하려는 압박을 더 강하게 느낀다"고요. 저도 정확히 그랬습니다. 파라미터가 세 개인 함수를 이해하느라 30분을 쓴 사람은, 그 함수를 버리는 결정을 내리기가 더 어렵습니다. "이걸 이해하는 데 이렇게 걸렸는데, 분명 중요한 거겠지"라는 착각이 생기는 거예요.

실제로 저는 그 함수를 고치려고 두 번 더 시도했습니다. 한 번은 파라미터를 객체로 묶어 options로 넘기는 방식으로 리팩터링했고 (리팩터링이란, 겉으로 보이는 동작은 그대로 두고 코드 내부 구조만 개선하는 작업입니다), 또 한 번은 함수 내부에 전략 패턴을 적용하려고 했습니다. 두 시도 모두 함수를 더 복잡하게 만들었을 뿐이었습니다. 결국 제가 한 건 잘못된 추상화 위에 또 다른 추상화를 쌓은 것이었으니까요. 문제의 근본 — 애초에 이 함수가 하나의 개념을 표현하고 있지 않다는 사실 — 은 건드리지 않은 채로요. 앞으로 가려고 발버둥칠수록 진흙에 더 깊이 빠져드는 느낌이었습니다.

⚠️ 주의: 잘못된 추상화 위에 리팩터링을 쌓는 건 문제를 해결하는 게 아니라 숨기는 것입니다. options 객체로 묶든, 전략 패턴을 얹든 — 근본이 흔들리면 구조는 더 복잡해질 뿐입니다.

'뒤로 가는 것'이 사실 가장 빠른 길이었다

그 시점에서 저를 구한 건 Sandi Metz의 조언 한 줄이었습니다. "잘못된 추상화를 다룰 때, 가장 빠른 전진 방향은 후퇴다(the fastest way forward is back)." 처음 읽었을 때는 솔직히 납득이 잘 안 됐습니다. 지금껏 쌓아온 걸 다 되돌린다는 게 퇴보처럼 느껴졌거든요. 그런데 며칠을 더 씨름하다 보니, 앞으로 나아가는 모든 시도가 이미 실패하고 있다는 게 분명해졌습니다. 결국 저는 그 공통 함수를 호출하던 모든 지점에 코드를 다시 직접 집어넣기로 했습니다. 이걸 '인라인(inline)'이라고 부릅니다 — 요리로 치면, 완성된 소스를 다시 재료 단위로 분해해서 각 냄비에 따로 넣는 것과 비슷합니다.

실제로 해보니 예상보다 훨씬 빠르게 끝났습니다. 공통 함수를 호출하던 곳이 여섯 군데였는데, 각 호출 지점에 코드를 풀어 넣고 나서 "이 호출은 파라미터 A만 실제로 쓰고 있고, 저 호출은 파라미터 C만 쓰고 있다"는 게 눈에 보이기 시작했습니다. 함수에 쌓인 조건 분기들이 사실은 서로 관계없는 여섯 가지 독립 로직이 한 함수 안에 뭉쳐 있던 것이었습니다. 공통 함수라고 불렀지만, 실제로 공통된 건 거의 없었던 셈입니다. 인라인을 마치고 나니, 여섯 개의 호출 지점 각각이 자신에게 필요한 코드만 갖게 되었고 — 중복이 다시 생겼지만 — 코드를 읽는 데 걸리는 시간이 현저히 줄었습니다. 파라미터 네 개짜리 함수를 머릿속에서 추적할 필요가 없어진 것만으로도 체감 차이가 컸습니다.

그 다음에 비로소 흥미로운 일이 생겼습니다. 여섯 덩어리를 나란히 놓고 보니, 그 중 두 곳은 진짜로 동일한 패턴을 공유하고 있었습니다. 파라미터도 없이, 조건도 없이, 하나의 개념을 표현하는 작은 함수로 추출해도 되는 케이스였습니다. 나머지 네 곳은 그냥 각자 따로 두는 게 맞았고요. 처음부터 이렇게 됐어야 했던 구조가, 억지로 통합하려는 시도를 멈추고 나서야 자연스럽게 드러났습니다. Metz가 말한 "재추상화(re-extracting)"는 이 단계를 가리키는 겁니다 — 억지로 찾는 게 아니라, 중복을 허용한 뒤에 진짜 공통점이 스스로 모습을 드러낼 때까지 기다리는 것. 그 기다림을 참지 못하고 AI가 제안하는 첫 번째 추출을 그대로 수용했던 것이 애초의 실수였다는 걸, 코드를 되감고 나서야 완전히 이해했습니다.

AI 코딩 도구는 중복을 보면 반사적으로 추상화한다 — 그게 문제다

코드를 되감고 나서 든 첫 번째 생각은 "다음엔 AI 제안을 좀 더 의심해야겠다"였습니다. 그런데 그 이후에도 Copilot이나 Claude에게 코드를 보여줄 때마다 비슷한 패턴이 반복됐습니다. 중복처럼 보이는 코드가 두 군데 이상 있으면, 도구들은 거의 예외 없이 "공통 함수로 추출하는 게 좋겠습니다"라고 권했습니다. 타이밍도 맥락도 가리지 않고요.

이게 왜 그런지 생각해보면, LLM(거대 언어 모델 — 방대한 텍스트를 학습해 텍스트를 예측·생성하는 AI)은 기본적으로 코드 패턴의 통계적 확률을 따릅니다. 인터넷에 있는 수많은 코드와 기술 문서에는 DRY(Don't Repeat Yourself — "같은 코드를 두 번 쓰지 말라"는 원칙)가 미덕으로 반복 등장합니다. 모델 입장에서는 "중복이 있다 → 추상화해야 한다"가 거의 조건반사에 가까운 패턴으로 학습되어 있는 겁니다. 책을 수만 권 읽었는데 그 책들이 전부 "중복은 나쁘다"고 가르쳤다면, 그 독자도 비슷하게 반응할 겁니다.

문제는 그 판단이 코드의 의미가 아니라 형태를 기준으로 한다는 점입니다. 두 함수가 지금은 똑같이 생겼어도, 각자 다른 도메인 개념을 표현하고 있다면 앞으로 다르게 진화할 가능성이 높습니다. 예를 들어 사용자 알림을 보내는 코드와 관리자 알림을 보내는 코드가 지금 당장 90% 동일해 보여도, 6개월 뒤에 요구사항이 갈라지면 억지로 묶어놓은 공통 함수는 짐이 됩니다. AI는 이 "앞으로의 갈림길"을 알 수 없습니다. 현재 스냅샷만 봅니다. 저도 직접 실험해봤는데, Claude에게 맥락 설명 없이 두 함수를 붙여넣으면 열 번 중 아홉 번은 추출을 권했고, "이 두 함수는 앞으로 독립적으로 변경될 예정"이라고 명시적으로 덧붙였을 때만 "그렇다면 분리 유지가 낫겠습니다"라는 답이 돌아왔습니다. 맥락을 주면 달라지지만, 맥락을 주지 않으면 패턴 반사가 기본값입니다.

💡 핵심: AI 도구에게 맥락은 자동으로 전달되지 않습니다. "이 두 함수는 앞으로 독립적으로 변경될 예정"이라는 한 줄이 없으면, 도구는 항상 통합을 권합니다. 맥락을 먼저 주는 습관이 잘못된 추상화를 막는 첫 번째 방어선입니다.

그래서 저는 AI 코딩 도구를 쓸 때 추상화 제안만큼은 다른 제안들과 다른 눈으로 봅니다. 오타 수정이나 보일러플레이트(반복되는 틀에 박힌 코드) 생성은 믿고 쓰지만, "이 두 곳을 하나로 묶으세요"라는 말은 일단 멈추고 직접 묻습니다. 이 두 코드가 우연히 닮은 건지, 본질적으로 같은 개념인지. 그 질문에 명확히 답하지 못하면, 저는 AI 제안을 보류합니다. 판단 주도권이 개발자에게 남아야 하는 이유가 여기 있습니다 — 도구는 지금 이 순간의 코드만 보지만, 개발자는 이 코드가 어디서 왔고 어디로 가는지를 압니다.

그래서 나는 이 기준으로 리팩터링 여부를 결정한다

지금까지 겪은 것들을 돌아보면, 저는 결국 세 가지 질문으로 수렴하게 됐습니다. 코드 리뷰를 하든, AI 제안을 검토하든, 제 손으로 직접 리팩터링 여부를 결정할 때마다 이 순서대로 물어봅니다.

  • "이 추상화는 호출자(caller — 이 함수를 불러 쓰는 쪽)를 몰라도 존재 이유가 자명한가" — 함수 이름과 시그니처(함수 이름, 받는 값, 돌려주는 값의 조합)만 보고 "아, 이게 뭘 하는 건지 바로 알겠다"는 느낌이 오면 추상화가 제 역할을 하고 있는 겁니다. 반대로, 이 함수가 어디서 어떻게 쓰이는지를 먼저 따라가 봐야 이해가 되는 구조라면, 그건 이미 추상화가 아니라 맥락을 숨기는 블랙박스가 된 겁니다. 앞서 제가 파라미터를 계속 추가하다 망가뜨린 함수가 정확히 이 케이스였습니다. processData(type, mode, flag, extraConfig)라는 시그니처 앞에서 저는 항상 내부를 열어봐야 했으니까요.
  • "파라미터가 분기를 만들고 있나" — 파라미터를 받아서 내부에서 ifswitch로 동작을 갈라치기 시작하는 순간, 그 함수는 하나의 개념이 아니라 여러 개의 개념을 억지로 묶어둔 것일 가능성이 높습니다. Sandi Metz가 말한 그 루프 — 새 요구사항이 올 때마다 파라미터 하나씩 추가 — 가 실제로 어떻게 진행되는지는 저도 직접 겪어봐서 압니다. 파라미터가 세 개를 넘어가는 시점부터 저는 일단 멈춥니다. 수치가 다소 임의적으로 느껴질 수 있는데, 실제로 Martin Fowler의 리팩터링 책에서도 파라미터 세 개 이상은 "객체로 묶을 신호"로 본다고 나와 있습니다. 저는 그 기준을 "함수를 쪼갤 신호"로도 함께 씁니다.
  • "두 호출자가 미래에도 같은 방향으로 변할 것인가" — 앞에서 다루지 않은 기준인데, 저는 개인적으로 이걸 가장 자주 놓칩니다. 지금 이 순간 두 코드가 완전히 똑같아 보여도, 한쪽은 결제 로직과 함께 진화하고 다른 한쪽은 알림 로직과 함께 진화할 운명이라면, 지금 공통 함수로 묶는 건 나중에 그 함수를 두 개로 도로 쪼개야 하는 부채를 만드는 겁니다. 변경 이유가 다른 코드는, 닮아 보여도 같은 개념이 아닙니다. 이 질문에 답하려면 도메인(서비스나 제품의 업무 영역)을 알아야 합니다. AI 도구가 이 판단을 대신할 수 없는 이유도 여기에 있고요.

세 질문 중 하나라도 "모르겠다"가 나오면, 저는 추상화를 보류하고 중복을 그냥 둡니다. 나중에 세 번째 중복이 생길 때쯤이면 패턴이 훨씬 선명해져 있고, 그때 추출해도 늦지 않았던 경험이 훨씬 많았습니다.

당신의 코드에서 지금 파라미터가 몇 개 늘어나고 있나요?

돌아보면, 제가 이 문제를 처음 인식한 건 코드를 읽다가가 아니라 함수 시그니처(함수를 호출할 때 넘기는 입력값 목록)를 보다가였습니다. 파라미터가 다섯 개, 여섯 개로 늘어나 있을 때 — 그게 이미 신호였죠. "이 함수, 지금 여러 사람의 요구사항을 억지로 한 몸에 우겨넣고 있구나."

지금 작업 중인 코드베이스에서 한번 들여다보면 좋습니다. 가장 최근에 수정한 공통 함수가 있다면, 파라미터가 처음 만들어졌을 때보다 몇 개 늘었는지 세어보세요. 두 개 이상 늘었고, 그중 하나라도 isSpecialCase, useNewLogic, skipValidation 같은 이름이라면 — 저는 그걸 잘못된 추상화의 징후로 읽습니다. 함수가 "모든 경우를 처리하겠다"고 욕심을 부리기 시작했다는 뜻이니까요.

완벽하게 마무리된 글을 쓰고 싶었지만, 사실 이 주제는 마무리가 없습니다. 저도 지금 이 순간 어딘가에서 비슷한 실수를 하고 있을 가능성이 충분히 있고요. 다만 예전과 달라진 게 하나 있다면, AI 도구가 "이 코드 중복입니다, 추출할까요?"라고 제안할 때 반사적으로 수락하지 않게 됐다는 겁니다. 잠깐 멈추고, 호출하는 쪽 두 곳을 나란히 펼쳐놓고, 이게 정말 같은 개념인지 한 번 더 보게 됐습니다.

그걸로 충분합니다.

Q. 코드 중복과 잘못된 추상화 중 어느 쪽이 더 나쁜가요?

A. 상황에 따라 다르지만, 잘못된 추상화는 고치기가 훨씬 어렵습니다. 중복은 눈에 보이지만, 잘못된 추상화는 이미 여러 곳에서 의존하고 있어서 건드리는 순간 연쇄 영향이 생깁니다. Sandi Metz도 '잘못된 추상화보다 중복이 낫다'고 명시적으로 말한 바 있습니다.

Q. AI 코딩 도구가 제안하는 리팩터링을 언제 수락하면 안 되나요?

A. 두 코드 블록이 생김새만 비슷하고 변경 이유가 다를 때는 수락을 보류하는 게 낫습니다. AI는 코드의 구조적 유사성을 보지만, 그 코드가 표현하는 도메인 개념이 같은지는 판단하지 못합니다. 호출하는 쪽 두 곳을 나란히 펼쳐놓고 '이게 정말 같은 개념인가'를 먼저 확인하세요.

Q. 추상화가 잘못됐다는 걸 어떻게 알아챌 수 있나요?

A. 가장 빠른 신호는 함수 파라미터 개수입니다. 처음 만들었을 때보다 두 개 이상 늘었고, 그중 하나가 isSpecialCase나 skipValidation 같은 이름이라면 의심해볼 만합니다. 함수가 여러 호출자의 요구사항을 억지로 한 몸에 담기 시작했다는 뜻이기 때문입니다.