RicoCheesethe studio log · v2.0
Live · KRRead posts
목록으로
뉴스PUBLISHED · 2026년 8월 31일·16 MIN READ

피터 노빅의 벽시계를 이기려다 라이언 고슬링이 된 개발자

CSS 하차푸리 글에 달린 댓글 하나로 시작된 코딩 챌린지. 5억 번의 상자 이동을 7초에서 89밀리초로 줄인 방법은, 상자를 아예 옮기지 않는 것이었다.

#programming#performance#javascript#webdev#tech
I Tried to Beat Peter Norvig and Accidentally Became Ryan Gosling

개요 #

HTML과 CSS만으로 조지아 전통 빵 하차푸리를 그린 프론트엔드 챌린지 출품작. 개발자 Thea가 기대한 건 CSS에 대한 댓글이었다. 대신 낯선 사람이 찾아와 "영웅의 여정"에 초대했다. 피터 노빅을 구출해야 하고, 상대는 그의 벽시계 시간(wall-clock time)이라는 것이었다.

수락한 뒤 GitHub 저장소로 따라가 보니, 자기 bio가 손을 타 있었다. 상대가 프로필을 훑어보고 공백 하나를 넣고 대문자 두 개를 바꿔서, Thea를 라이언 고슬링으로 만들어 놓은 것이다. 평범한 코딩 챌린지가 아니라는 게 그때 확실해졌다.

결과만 먼저 말하면, 7초짜리 코드가 89밀리초로 줄었다. 상자를 더 빨리 옮기는 방법을 찾아서가 아니라, 옮기기를 그만둬서였다.

챌린지는 이렇게 시작됐다 #

설정은 이랬다. 피터 노빅 — 구글에 있고 그 유명한 AI 교과서를 쓴 그 사람 — 이 챌린지 주최자를 인질로 잡았다. 코드가 메모리를 전부 먹어치우고 CPU를 붙잡아둔 채 답을 내놓지 않고 있었다. ThePrimeagen의 Rust 풀이도 손을 못 썼다.

그래서 도움을 요청받은 사람이 Thea였다. 아니면 라이언 고슬링. 아니면 존 스노우. 본인도 중간부터는 헷갈렸다고 한다.

이어서 조지아에는 არმატურის გოგონა, 즉 "철근 소녀"라는 옛 표현이 있다는 설명이 붙었다. 온갖 첨단 마법을 상대하게 된 소녀가 마침 옆에 있던 커다란 철근을 집어 들고 일을 시작한다는 뜻이라고.

문제는 이 표현이 존재하지 않는다는 것이었다. 조지아에서 아무도 이렇게 말하지 않는다. 그 자리에서 지어낸 말이었다. 조지아어 문법은 정확했다는 점이 더 웃겼다고 한다.

그리고 이런 챌린지를 2년째 공유해 왔는데 아직 아무도 풀지 못했다는 말이 따라왔다. 그동안 100명 정도의 개발자가 거쳐 갔다고. 겁이 날 만한 이야기였지만 이미 수락한 상태였고, 호기심이 이미 너무 커져 있었다.

여기서부터 레퍼런스가 쏟아졌다. 이그리트, 존 스노우, 테넷, 시카리오, 도로 표지판, 새, 힌트일 수도 농담일 수도 함정일 수도 있는 영상들. 이그리트의 눈썹 움직임에 대한 수상한 집착. 그리고 상황마다 딱 맞는 에이프릴 러드게이트 GIF.

April Ludgate GIF

어느 게 진짜 힌트고 어느 게 그냥 농담인지 알 수 없었으니, 결국 전부에 신경을 쓰게 됐다.

실제 문제: 86,000줄의 악의적인 입력 #

이 모든 소동 뒤에 있던 진짜 문제는 Advent of Code 2022 Day 5 — Supply Stacks였다.

번호가 붙은 더미에 상자들이 쌓여 있고, move 4 from 2 to 1 같은 지시가 주어진다. 지시를 다 따른 다음 각 더미의 맨 위 상자를 출력하면 된다. 여기까지는 어렵지 않다.

문제는 입력이었다. 원래 AoC 입력은 평범했지만, 이번에 받은 입력은 약 86,000줄이동 지시 30,000개였다. 한 번에 수만 개의 상자를 옮기라는 지시도 섞여 있었다. 전부 합치면 상자 이동 횟수가 5억 회를 넘는다. 심지어 더미에 실제로 있는 상자보다 더 많은 개수를 옮기라는 지시도 있었다.

처음엔 이그리트 GIF가 "You know nothing, Jon Snow" 농담인 줄 알았다고 한다. 아니었다. 봐야 할 건 그 수상한 눈썹 움직임이었다.

Ygritte

그 눈썹 힌트가 기술적 한계에 대한 논의로 이어졌다. 더미 개수는 입력 형식 자체에 묶여 제한되지만, 이동 횟수는 얼마든지 커질 수 있다. 그리고 더미에 있는 것보다 많은 상자를 옮기라는 지시는 나중에 밝혀지듯 전부 의도된 것이었다.

피터 노빅의 파이썬 풀이도, ThePrimeagen의 Rust 풀이도 이 입력을 합리적인 시간 안에 처리하지 못했다. 목표는 하나였다. 그들의 벽시계 시간을 이기는 것.

작업은 자바스크립트로 시작했다가 최종 풀이를 파이썬으로 옮겼다. 새벽 5시쯤에 이 챌린지가 시작된 저장소가 자기 파이썬 LeetCode 저장소였다는 걸 깨달았기 때문이라고 한다.

1차 시도: 답은 맞았지만 7초 #

첫 풀이는 단순했다. 더미를 배열로 두고, 상자를 하나씩 옮기는 대신 splice()로 블록 전체를 떼어내 뒤집은 다음 목적지 더미에 붙였다.

작동했다. 출력은 JUAREZ.

Juarez sign

그러자 또 다른 레퍼런스, 시카리오의 그 장면이 이해됐다.

답은 맞았고 후아레스로 가는 길도 찾았다. 문제는 실행 시간 약 7초. 용병들이 여전히 이기고 있었다.

2차 시도: 복사하지 말고 가리키기 #

다음 아이디어는 상자 데이터 자체를 옮기지 않는 것이었다. 상자를 복사하는 대신 각 더미를 {array, start, end, reversed} 형태의 참조로 표현해 원본 데이터를 가리키게 했다. 이동은 상자 복사가 아니라 참조 재배치가 된다.

실행 시간이 약 2초로 줄었다. 나아졌지만 여전히 부족했다.

이 거대한 입력에서는 범위가 계속 잘게 쪼개졌고, 결국 많은 조각을 순회해야 했다. 빨라지긴 했어도 하는 일 자체가 너무 많았다. 그게 진짜 문제였다.

테넷은 사실 문서였다 #

주최자의 답은 이미 풀었다는 것이었다. 프로그램이 필요 이상의 일을 하고 있을 뿐이라고. 더 빨리 끝내려면 일을 덜 해야 한다는 것.

첫 조언은 문제 설명으로 돌아가서 아주 문자 그대로 질문해 보라는 것이었다. 이 문제가 실제로 요구하는 출력이 뭔가? 그다음 조언은 IDE를 닫고 컴퓨터에서 떨어져서 색연필을 들고 상자를 그려 보라는 것이었다.

그리고 곧바로 테넷의 한 장면이 도착했다. 클레망스 포에지가 엔트로피와 시간, 거꾸로 흐르는 것들에 대해 설명하는 그 씬.

처음엔 그냥 또 영화 레퍼런스구나 싶었다고 한다. 그러다 멈췄다. 거꾸로. 아.

하지만 핵심은 단순히 루프를 뒤집는 게 아니었다.

문제는 최종 상태 전체를 요구하지 않는다. 각 더미의 맨 위 상자만 묻는다. 그게 전부다. 그런데 왜 모든 상자가 어디로 갔는지 계산하고 있었을까?

필요한 정보는 하나였다. 각각의 최종 맨 위 자리는 어디서 왔는가?

초기 더미에서 출발해 상자를 전부 앞으로 옮기는 대신, 최종 맨 위 자리에서 출발해 이동 기록을 거꾸로 따라가면 된다. 각 이동마다 추적 중인 자리에 무슨 일이 일어났는지만 계산하고 계속 뒤로 간다. 상자는 하나도 움직일 필요가 없다.

이 악의적인 입력에는 문제가 하나 더 있었다. 원본 더미에 있는 것보다 많은 상자를 옮기라는 지시들. 그래서 역추적 전에 더미의 크기만 추적하는 값싼 정방향 패스를 한 번 돌렸다. 상자 데이터 없이 정수만 다룬다.

untitled
python
sizes = [len(stack) for stack in initial_stacks]
moves = []

for count, src, dst in raw_moves:
    count = min(count, sizes[src])
    sizes[src] -= count
    sizes[dst] += count
    moves.append((count, src, dst))

이렇게 각 이동이 실제로 옮길 수 있는 상자 개수를 얻는다. 그다음 각 최종 맨 위 자리를 거꾸로 추적한다.

untitled
python
for count, src, dst in reversed(moves):
    if count == 0:
        continue

    if curr_stack == dst:
        if depth_from_top < count:
            curr_stack = src
            depth_from_top = count - 1 - depth_from_top
        else:
            depth_from_top -= count

    elif curr_stack == src:
        depth_from_top += count

curr_stack은 그 시점에 이 자리가 어느 더미에 속했는지, depth_from_top은 맨 위에서 얼마나 떨어져 있었는지를 담는다. 거꾸로 들고 갈 정보는 이 두 개뿐이다.

여기서 터무니없던 이동 크기가 무서움을 잃는다. move 4 from 2 to 1move 101198 from 2 to 6을 비교해 보자. 상자를 실제로 옮기는 코드라면 두 번째는 끔찍하다. 하지만 역추적 풀이는 101198개를 순회하지 않는다. 추적 중인 자리가 옮겨진 블록에 대해 어디쯤 있는지만 묻는다. 지시가 4101198이든 그 자리에 대해서는 작고 일정한 양의 계산만 한다. 그리고 신경 쓸 최종 자리는 각 더미의 맨 위, 몇 개뿐이다.

즉 역추적의 복잡도는 O(더미 개수 × 이동 개수)다. 지시가 옮기라고 요구하는 상자 총수와는 무관하다.

실행 결과, 자바스크립트에서 약 89ms. 7초에서 내려온 숫자다. 같은 풀이를 파이썬으로 옮겼을 때는 대체로 100~200ms 사이였다.

5억 번의 상자 이동을 더 빨리 시뮬레이션하는 방법을 찾은 게 아니다. 시뮬레이션을 그만뒀을 뿐이다.

남은 것 #

명확한 교훈은 불필요한 일을 하지 말라는 것이다. 코드를 최적화할 때는 "이 연산을 어떻게 더 빠르게 만들까"를 묻기 쉽다. 하지만 더 나은 질문은 "이 연산을 할 필요가 있는가"일 때가 있다. 앞의 두 시도는 상자를 더 효율적으로 옮기려는 싸움이었다. 최종 풀이는 애초에 옮길 필요가 없다는 걸 알아차린 데서 나왔다.

Thea는 다른 것도 하나 배웠다고 적었다. 진짜 챌린지를 얼마나 그리워했는지 잊고 있었다는 것. 자기 코드와 몇 시간씩 씨름하는 게 오히려 이상한 방식처럼 느껴지는 AI 시대에, 그 씨름이 이만큼 재미있을 수 있다는 걸 한참 모르고 지냈다는 얘기다.

어디로 가는지 모르는 상태가 좋았고, 막히고, 뭔가 알아냈다고 생각했다가 또 다른 단서를 발견해서 되돌아가고, 뭘 놓쳤는지 알아낼 때까지 그 멍청한 상자들을 놓아주지 않는 게 좋았다고 한다. 2년, 100명이라는 말은 끝까지 머리 뒤편에 남아 있었다고도 했다.

새벽 2시에 코딩하고, 클레망스 포에지가 무슨 말을 하는지 이해하려 애쓰고, 피터 노빅의 벽시계와 싸우고, 이그리트의 눈썹을 따라가다, 어쩌다 라이언 고슬링이 되는 사이에 프로그래밍이 다시 우스울 만큼 재미있어졌다는 것. 그게 이 모험에서 얻은 것 중 가장 마음에 드는 부분이었다.

상자를 더 빨리 옮기는 방법을 묻는 것으로 시작해서, 옮길 필요가 없다는 걸 깨닫는 것으로 끝났다. CSS 하차푸리 아래에서 시작된 일치고는 나쁘지 않은 결말이다.


이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.