어려운 코드를 잘 설명하는 법
코드는 가능하면 쉽게 작성해야 한다. 지나치게 복잡한 코드는 구조적인 문제가 있을 확률이 높고, 유지보수와 협업을 어렵게 만들기 때문이다. 이는 아주 기본적인 원칙이지만, 조금만 방심해도 코드는 쉽게 복잡해진다. 나 역시 코드를 복잡하게 만들어놓고 팀원들에게 리뷰를 요청한 경험이 꽤 있다. 당시에는 나름의 이유가 있었지만, 리뷰를 받고 나면 언제나 이전보다 간결하고 명료해졌다. 몇 번 그런 경험을 하고 나자, PR(코드 머지 요청)을 올리기 전에 셀프 리뷰를 훨씬 더 꼼꼼하게 하게 되었다. 그럼에도 불구하고 쉬운 코드를 작성하는 일은 늘 어렵다.
최근 나는 아주 복잡한 코드를 작성한 뒤 PR을 올릴 수밖에 없는 상황을 겪었다. 내 선에서는 도무지 개선 방법이 떠오르지 않았다. 그런데 팀장님과 함께 꼼꼼히 검토하고 고민한 끝에, 코드를 깔끔하게 개선할 수 있었다. 그 과정에서 복잡한 코드를 설명하기 위해 다양한 시도를 했고, 이를 통해 커뮤니케이션에 대한 중요한 통찰을 얻었다. 오늘은 그 경험을 바탕으로, 개발자가 어려운 코드를 설명해야 하는 상황에서 어떤 접근이 더 효과적인지를 공유하고자 한다. 비슷한 상황에 처해 있는 누군가에게 도움이 될 수 있길 바란다.
개발자 미션: 팀원들을 이해시키기
1. 무엇보다 쉽게 짜야 한다(가장 중요한 원칙!)
“읽기 좋은 코드가 좋은 코드다”라는 유명한 말이 있다. 코드는 프로그램을 실행시키는 도구인 동시에 소통의 도구다. 컴파일러는 가독성이 나쁘다는 이유만으로 성능이 나빠지지 않지만, 사람은 복잡한 코드를 해독할 때 그렇지 않은 경우보다 더 많은 시간이 걸린다. 그래서 쉽게 작성하는 것은 협업할 때 정말 중요하다. 그리고 경험상 간결하고 명료하게 작성된 코드가 성능도 좋은 경우가 많았다. 왜 그런지 생각을 해봤는데, 구조화가 잘 된 코드일수록 실수를 발견하기에도 좋다. 이러면 개발하는 과정에서도 버그를 발견하기 쉽고, 이후 유지보수를 할 때도 기존 패턴을 따르면 되기에 버그를 유발할 가능성도 적다.
복잡한 코드를 마주했을 때 가장 먼저 해야 할 일은 왜 복잡해졌는지에 대한 원인 분석이다. 흔한 원인은 다음과 같다.
- 여러 기능이 한 곳에 몰려 있음
- 나쁜 추상화로 인해 책임이 불분명함
책임과 역할을 분명히 하고 ‘모듈화’만 잘해도 괜찮은 결과물이 나온다. 과도하게 긴 문장이 중심을 쉽게 잃어버리듯, 너무 많은 라인을 가진 함수는 불필요하게 많은 역할을 하게 되는 경우가 많다. 역할이 많으면 일단 이름 짓기부터 애매해진다. 이렇게 애매한 함수는 소통할 때 많은 오해를 불러일으킨다.
코드가 복잡하면, 구조적인 문제가 있을 가능성이 크다. 예를 들어, 내가 최근에 개발한 기능은 ‘럭키박스’였다. 이 럭키박스 안에는 확률형 상품과 무조건 받는 보장형 상품이 섞여 있었다.
문제는, 이 두 가지가 섞여 있을 때 사용자가 최종적으로 어떤 상품들을 받게 되는지를 계산하는 일이었다. 처음에 나는 이렇게 접근했다. 먼저 재귀함수를 사용해서, 럭키박스 안에 포함된 모든 최하위 상품들을 수집했다. 여기서 말하는 ‘최하위 상품’이란, 더 이상 내부에 럭키박스를 포함하지 않는, 실제 아이템 단위의 상품을 의미한다.
각 상품은 다음과 같은 형태의 자료구조를 갖는다.
{
"items": { "ItemId": 개수 },
"probability": 확률 (0.0 ~ 1.0)
}
예를 들어,
{
"items": { "character card": 1, "sticker": 100 },
"probability": 0.1
}
이 상품은 10% 확률로 캐릭터 카드 1장과 스티커 100장을 지급하는 상품이다. 나는 이 상품들을 리스트로 만들고, 확률형 상품이 하나 뽑히면 그에 연결된 보장형 상품들도 함께 뽑히도록 처리했다. 즉, 확률형 상품과 그에 종속된 보장형 상품을 하나의 묶음으로 처리한 것이다.
하지만 이 방식에는 치명적인 문제가 있었다. 보장형 상품 안에 다시 럭키박스가 포함될 수 있다는 점이다. 예를 들어,
- 확률형 상품 A가 뽑힘
- → 보장형 상품 B도 함께 나옴
- → 그런데 B는 또 다른 럭키박스
- → 그 안에 또 다른 확률형/보장형 상품이 존재
이러한 구조에서는 단순히 한 번 재귀로 끝나는 것이 아니라, 조합의 수가 계속해서 늘어난다.

물론 현실적으로 무한히 깊어질 일은 거의 없겠지만, 2단계 깊이에 다시 럭키박스가 등장할 가능성은 충분히 고려해야 했다. 예를 들어, 보장형 상품이 럭키박스고 그 안에 또 다른 확률형 상품이 있다면, 그 확률까지 포함해서 전체 조합을 계산해야 정확한 결과가 나온다.
자, 그렇다면 이 상황에서 사용자가 받을 수 있는 모든 상품 조합은 어떻게 계산해야 할까? 바로 조합(combination)을 사용하는 것이었다. 모든 하위 상품들을 재귀적으로 수집한 뒤, 가능한 모든 조합을 계산하면 최종 결과가 만들어졌다. 조합은 알고리즘 문제에서는 자주 쓰던 개념이지만, 실무에서 바로 떠올리기는 쉽지 않았다. 그동안 헛공부를 한 기분이 들어 조금 허탈하기도 했다.
결과적으로 확률형 상품과 보장형 상품을 다루는 복잡한 로직을 이 조합 함수로 분리하고 나니 코드가 훨씬 깔끔해졌다.
IEnumerable<List<T>> CartesianCombine<T>(List<List<T>> sources)
{
IEnumerable<List<T>> seed = new[] { new List<T>() };
foreach (var source in sources)
{
seed = seed.SelectMany(prefix => source, (prefix, item) =>
{
var next = new List<T>(prefix) { item };
return next;
});
}
return seed;
}
쉬운 구조로 개선할 방법은 언제나 있다. 데이터 구조를 바꾸거나, 함수와 객체의 역할을 더 작은 단위로 나누는 방식으로 복잡도를 줄일 수 있다. 그렇게 하다 보면 현재의 복잡함이 구조적인 문제인지, 불가피한 업무 제약인지 판단할 수 있게 된다.
2. 어쩔 수 없이 복잡한 코드를 설명해야 하는 상황
짧은 일정이나 갑작스러운 기획 변경으로 부득이하게 복잡한 코드를 작성해야 하는 상황이 온다 해도, ‘최소한 무엇을 간소화할 수 있을지’ 혹은 ‘미래에 어떤 부분을 개선할 여지가 있는지’를 꾸준히 고민해야 한다. 이러한 고민의 흔적이 리뷰 과정에서 드러나면, 팀원들도 해당 코드를 이해하기가 상대적으로 쉬워진다.
물론 우리가 완벽하지 않은 환경과 일정 속에서 일하다 보면, 언제나 코드가 이상적으로 쉽고 우아하지는 못하다. 다음과 같은 현실적인 상황이 있을 수 있다.
- 시간이 촉박해서 일단 동작하게 만드는 것에만 집중해야 하는 경우
마감이나 배포 일정이 너무 임박해, 일단 기능이 돌아가도록 구현하는 데 집중해야 할 때가 있다. 이런 경우에는 테스트코드로 주요 로직이 문제가 없다는 것을 보장해야 한다. 그리고 리팩토링할 부분을 주석으로 표시해 둔다. 이렇게 함으로써 버그에 대한 팀원들의 불안감과, 복잡한 코드를 지속적으로 유지 보수해야 한다는 부담감을 줄일 수 있다.
- 더 좋은 방법이 생각나지 않고, 팀원들도 아이디어가 없는 경우
물론 매 순간 정답을 찾을 수는 없다. 내가 찾지 못한 정답을 팀원이 찾아준다면 좋겠지만, 팀원 없이 혼자 일하고 있거나, 혹은 팀원들 중 아무도 최적의 디자인 패턴이나 알고리즘을 떠올리지 못할 수도 있다. 고민할 시간을 갖기 위해 일정을 조정할 수도 있겠지만, 그것도 한계가 있다. 그런 경우에는, 일단 어떻게든 기능을 구현하는 것에 집중해야 한다. 시행착오를 겪으면서 유지보수를 하다 보면, 점진적으로 좋은 코드로 개선해 나갈 수 있을 것이다. 미래의 나를 위해 주석으로 현재 적용한 방식의 한계가 무엇인지 적어두면 좋다.
이렇듯 현실적인 이유로 코드가 어려워질 수밖에 없는 경우, 최선을 다해서 팀원들을 설득해야 한다. 코드는 의사소통의 도구이다. 내 코드가 깔끔하고 성능이 좋으면 팀원들은 쉽게 설득된다. 그게 아니라면 설득할 다른 방법을 찾아야 한다. 팀원들의 승인을 받아야 코드를 머지할 수 있기 때문이다. 리뷰어도 공동의 책임을 지는 만큼, 그들의 부담감을 고려하자.
3. 이해하기 쉽게 설명하는 방법
리뷰어가 PR을 이해하기 쉽도록 도와주는 설명을 작성하자. 다음 요소들은 가능하면 꼭 포함하는 것이 좋다.
- 어떤 기능을 구현한 것인지
- 조언이나 피드백을 중점적으로 받고 싶은 부분
- 현재 코드의 한계와, 개선하기 위해 고민한 방법들
그리고 다음과 같은 내용도 넣으면 팀원들을 설득하는 데 도움이 된다.
- 추후 리팩토링을 계획하고 약속하기
임시방편으로 처리한 부분을 언제 개선할 것인지 되도록 구체적인 시점을 약속한다. 단순히 계획만 말하는 것이 아니라, 스프린트 보드에 태스크를 등록해 두면 팀원들이 이 부분은 언젠가 반드시 고칠 부분이라는 점을 인지하게 된다.
- 코드의 흐름을 플로우차트로 시각화하기
복잡한 분기나 반복 로직을 말이나 글로만 설명하려고 하면, 상대방이 헷갈리기 쉽다. 주요 로직을 플로우 차트로 그리면, 리뷰어가 더 직관적으로 이해할 수 있다. 도식화는 코드리뷰 뿐 아니라 많은 경우에 이해를 도와주는 중요한 수단이 된다.
- 데이터 타입을 그림이나 테이블로 표현하기
많은 경우, 코드 복잡도의 출발점은 어떤 데이터를 입력받아서, 어떤 데이터를 산출하는가에서 비롯된다. 데이터의 자료구조, 함수의 인풋과 아웃풋, 객체 간 의존 관계 등을 표나 다이어그램으로 표현하면 시각적 이해가 빨라진다.
- 주석과 문서로 히스토리 남기기
앞서 말했듯, 어쩔 수 없이 코드가 어려워질 수 있다. 시간이 부족하거나 실력이 부족해서, 아니면 기획의 한계로 인해 예외 처리가 많아져서 등 다양한 이유가 있을 수 있다. 이런 맥락이 주석이나 문서에 전혀 남아있지 않으면, 동료들이 맥락을 이해하기 어렵다. 현재 상황에 대한 기록을 주석과 문서로 잘 남겨두면, 나중에도 코드를 볼 때 기억을 되살리는 데 도움이 된다.

좋은 코드는 이해할 수 있는 코드다
좋은 코드는 누구나 잘 이해할 수 있는 코드다. 개발자로서 복잡한 코드를 그대로 두기보다는 단순화하거나, 복잡한 이유를 명확히 설명하여 팀원들을 설득하는 것이 핵심이다. 한 번 체계적으로 정리해 두면, 이후 반복되는 유지보수 과정에서 시간과 에너지를 크게 절약할 수 있다. 또 지나치게 복잡한 코드는 종종 구조적인 문제의 신호일 때가 많다. 이럴 때는 원인을 면밀히 분석하고, 필요하다면 기획 단계부터 다시 검토해보는 용기도 필요하다. 만약 현실적인 제약 때문에 불가피하게 복잡해진 코드라면, 이유를 명확히 설명하고 팀원들과 함께 책임질 수 있도록 하는 자세가 중요하다.
결국 복잡한 코드를 잘 설명하는 가장 효과적인 방법은 명확한 구조, 시각적 자료, 미래 지향적인 개선 계획을 제시하는 것이다. 이렇게 하면 협업을 원활하게 만들고, ‘코드를 쉽게 짜는 것’이 자연스러운 팀 문화로 자리 잡을 수 있는 든든한 토대가 된다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.