2026년 상반기 프로젝트 회고

9 분 소요


나이를 먹을수록 시간이 빠르게 지나간다는 게 새삼 와닿는다. 올해를 시작한 게 엊그제 같은데, 벌써 7월 중반을 넘어가고 있다. 상반기 가장 굵직한 일정이었던 고객사 인게이지먼트(engagement)를 한 차례 마무리했다. 나는 올해 상반기에 어떤 것들을 했고, 무엇을 배웠는지 회고록으로 정리해본다.

AI 코딩 에이전트로 인해 생기는 인지 부족과 기술적 부채

2026년 상반기에 두 프로젝트 팀에 인게이지먼트했다. 하나는 신규 서비스, 다른 하나는 제법 오랜 시간 유지보수되고 있는 서비스였다. 두 팀은 공통적인 문제가 있었다. AI 코딩 에이전트를 적극적으로 사용하면서 개발자들의 인지 부족 현상이 발생하고 기술적 부채는 늘어나는 중이었다.

  • 코드 이해도 저하
    • AI가 코드를 작성하면서 개발자가 코드의 동작을 충분히 이해하지 못하는 현상
  • 코드 컨텍스트(context) 상실
    • 코드에 대한 질문을 하면 AI가 작성한 코드라 코드가 작성된 이유나 컨텍스트를 기억하지 못하는 현상
  • 기능 딜리버리(feature delivery) 위주 개발
    • AI 에이전트는 주변 코드를 컨텍스트 삼아 기존 복잡했던 코드를 따라 비슷한 패턴의 코드를 추가해 복잡도가 커지는 현상

사람 인지 능력을 벗어난 AI의 개발 속도는 악순환의 고리를 만든다.


AI는 정말 강력한 도구이지만, 양날의 검과 같다. 적절한 사용은 개발자에게 훌륭한 무기가 되지만, 제어 없이 마구잡이로 사용하면 시스템을 망가트리기 쉽겠다는 생각이 들었다. 시스템을 장기간 유지보수할 생각이라면 의도적으로 속도를 늦추고, 사람이 인지할 수 있는 수준까지 복잡도를 낮추는 작업을 수행해야 할 것 같다. 복잡도를 낮추는 작업에 AI를 사용하는 것은 찬성이다. AI의 빠른 속도가 가져다주는 도파민에 중독되어 기능을 계속 밀어 넣지 말아야 한다는 것이 말하고 싶은 포인트다.

기술적 부채는 새로운 기능을 만들 때가 아니라 기존의 기능을 수정/확장할 때 드러난다. 기술 부채가 쌓인 코드를 수정/확장하는 일은 쉽지 않다. 기존의 코드를 충분히 이해하지 못하고 변경하는 순간 문제가 발생한다. 이를 방지하기 위해 테스트 코드라는 방어책이 필요하지만, 테스트 코드도 리팩토링 작업의 결과가 100% 안정하다는 사실을 보장하지 못한다. 소프트웨어 엔지니어의 경험과 판단이 테스트 코드가 커버하지 못하는 부분을 채워준다.

내가 작성한 코드도 한두 달 지나면 잊어버리는데, 시스템을 유지보수하다 보면 남이 작성한 코드를 들여다보는 일이 더 많다. 역량에 따라 다르겠지만, 기존 코드를 분석하는 일은 쉽지 않다. if-else 구문이 너무 많아서 읽기 어렵다거나, 연관된 의존성이 많다거나, 로직이 너무 길어서 중간에 컨텍스트를 잃는다거나, 타입이 모호해서 런타임에 도무지 무슨 값이 들어 있는지 모르겠다거나 등의 순간이 있다. 책임을 나누고, 로직을 의미 있는 단위로 나누고, 추상화를 통해 단순화하는 등의 리팩토링이 필요하다.

경력이 긴 소프트웨어 엔지니어는 이를 감각적으로 느끼고 수행한다. 과거의 모든 삽질들, 코드를 수정하면서 겪은 고통, 실패로부터 배워온 경험이 지금 살펴보고 있는 코드의 어색함과 복잡도를 인지하게 해준다. 그런데 경험이 아직 부족해서 코드 스멜을 감지하지 못하는 주니어 개발자에게 지금 보고 있는 코드가 복잡하고 문제가 있는 코드라는 사실을 어떻게 인지시킬 수 있을까? 가독성, 복잡도는 전혀 상관없이 코드를 전부 읽고 일을 척척 해내는 AI에게 코드 상태가 사람이 인지하기 어려울 정도로 복잡하니 리팩토링이 필요한 시점이라는 신호를 어떻게 줄 수 있을까? 정적 코드 분석을 통해 얻어낼 수 있는 몇 가지 신호가 있다.

  • 순환 복잡도(cyclomatic complexity)
    • 함수나 메서드 안에 존재하는 독립적인 실행 경로의 수를 나타낸다.
    • 순환 복잡도가 높으면 테스트해야 할 조건 조합이 많고, 누락된 테스트 케이스가 생기기 쉽다는 의미이다.
    • 수정 시 회귀 버그 가능성이 높아지고, 함수가 여러 책임을 담당하고 있다는 신호가 될 수 있다.
  • 인지 복잡도(cognitive complexity)
    • 코드를 읽는 사람이 제어 흐름을 이해하기 위해 얼마나 많은 정신적 노력을 사용해야 하는지를 측정한다.
    • 순환 복잡도는 분기 개수에 집중하지만, 인지 복잡도는 특히 중첩 구조를 중요하게 본다.

이번 인게이지먼트 중에도 팀 동료가 너무 복잡하고 커다란 컴포넌트에 대한 리팩토링 작업의 우선순위를 높이기 위한 명분으로 순환 복잡도와 코드 변동률(code churn)을 제시했다. 이 지표는 사람이 리팩토링의 명분을 찾기 위해 사용하는 것뿐만 아니라 코드의 복잡도나 사람의 인지 능력을 AI가 신경 쓸 수 있도록 훅(hook) 같은 하네스(harness)에 설정하는 것도 좋은 아이디어라는 생각이 들었다. 몇 가지 코드 정적 도구들을 훅에 설정해서 원하는 대로 동작하는지 검증해 볼 생각이다.

AI 시대지만 깨진 유리창 이론, 보이스카웃 규칙(Boy Scout Rule) 같은 클린 코드를 작성하기 위한 습관이 더 중요해졌다는 생각이 든다.

이미지 처리(image handling)

공교롭게도 두 프로젝트 모두 이미지를 처리하는 작업에 문제가 있었다. 그래서 비슷한 작업을 두 번이나 연달아 하게 됐다. 나에겐 좋은 경험이었고, 반복 학습처럼 기억에 오래 남을 작업이었다. 관련 내용들은 정리해서 블로그에 글로 남길 생각이다.

  • A 프로젝트 - AWS 클라우드프론트와 람다(lambda)를 사용한 서버리스(serverless) 아키텍처
    • 동영상과 사진을 업로드해서 썸네일 이미지를 생성했다. 사진에 대한 썸네일 생성은 문제가 없었지만, 동영상 썸네일을 만들 때 람다 메모리 부족 에러가 발생했다.
    • 브라우저에서 S3에 접근하기 위해 pre-signed URL을 사용했다. 매번 랜덤한 URL이기 때문에 캐시 컨트롤(cache-control) 헤더가 동작하지 않았고, 브라우저는 매번 이미지를 다운로드받았다.
  • B 프로젝트 - 스프링 프레임워크와 ECS 컨테이너를 사용한 아키텍처
    • 브라우저가 API 서버를 경유해서 S3 버킷으로부터 이미지를 가져왔다. 동일한 URL이기 때문에 브라우저가 캐시 컨트롤 헤더를 사용할 수 있었지만, API 서버의 부하가 심했다.
    • 용량이 큰 이미지를 S3 버킷으로부터 가져올 때 스레드가 오랜 시간 점유되기 때문에 서버가 느려지고, 메모리가 크게 오르내렸다.
    • 썸네일 사진을 읽을 때 매번 생성해서 반환하고 있었기 때문에 불필요한 연산과 메모리가 필요했다.

이제 문제들을 어떻게 해결했는지 하나씩 돌이켜보자. 먼저 A 프로젝트에서 동영상의 썸네일 이미지를 람다에서 생성할 때 발생했던 문제를 살펴보자. 람다 아키텍처에서 동영상의 썸네일을 만들기 위해 ffmpeg 레이어 위에서 동작하는 구조였다. 동영상의 프레임을 추출해서 썸네일을 만들기 위해 주로 사용하는 방식이지만, 로직이 효율적이지 않았다.

  1. 동영상 전체를 S3 버킷에서 가져와 람다 파일 시스템의 임시 디렉토리에 저장한다.
  2. 파일 시스템에 저장한 영상을 읽은 후 첫 번째 프레임에 대한 썸네일 이미지를 파일 시스템에 생성한다.
  3. 파일 시스템에 저장한 썸네일 이미지를 S3 버킷에 업로드한다.
  4. 파일 시스템에 저장된 영상과 이미지를 모두 정리한다.

언뜻 보면 문제가 없지만, 영상을 모두 다운로드받고 파일 시스템에 저장 후 한 프레임만 갖고 썸네일을 만드는 작업은 불합리하다. pre-signed URL과 ffmpeg 도구에서 제공하는 시크(seek) 옵션과 frames 옵션을 사용하면 이를 개선할 수 있다. ffmpeg가 S3 URL을 직접 열고, 필요한 프레임만 디코딩해서 썸네일을 생성하기 때문에 메모리나 스토리지 측면에서 훨씬 유리하다.

1GB 사이즈의 4K 고해상도 영상에 대한 썸네일을 동일한 스펙 람다 컨테이너에서 실행했을 때 이전 로직은 디스크가 부족하다는 에러가 발생했지만, 후자의 방식은 성공하고 메모리도 더 적게 사용했다. 제한된 리소스만 사용할 수 있는 람다에선 후자 방식이 더 적절했다. 아래 이미지는 개선된 방식과 이전 방식의 메모리 사용량을 클라우드워치(CloudWatch)에서 확인해보니 많이 개선된 것을 확인할 수 있었다.


B 프로젝트는 이미지 원본만 저장하고, 매번 읽을 때마다 썸네일 이미지를 생성하는 방식으로 구현되어 있었다. 이로 인해 이미지를 읽을 때마다 불필요한 썸네일 생성 과정을 수행했다. 대시보드에서 수십 개의 썸네일 이미지를 보여주고 있었기 때문에 동시 접속자 수가 조금만 늘어도 메모리 사용량이 불안정하게 날뛰었다. 이 문제를 개선하기 위해 이미지를 업로드할 때 썸네일 이미지를 생성해서 업로드하는 방식으로 리팩토링했다. 기존 S3 디렉토리 구조와 새로운 방식은 호환되지 않았기 때문에 다음과 같은 작업이 필요했다.

  • 디렉토리 구조를 바꿔야 했다. 원본 이미지와 앞으로 저장할 썸네일 이미지의 위치를 재정의했다.
  • 기존 이미지에 대해 썸네일 이미지로 리사이징 후 저장하는 마이그레이션(migration) 작업이 필요했다.

기존 이미지를 썸네일 이미지로 변경 후 업로드하는 마이그레이션 과정에서 PNG 이미지를 JVM 애플리케이션에 그대로 올리면 OOM(Out of Memory) 에러가 발생하는 시행착오가 있었다. PNG 압축 방식에 의해 이미지 사이즈가 수 MB 정도로 작을 수 있지만, 실제로 JVM 메모리에 올릴 경우 수 GB까지 올라간다는 사실을 배웠다. 하나의 프로세스로 모든 이미지를 마이그레이션하려고 했지만, 메모리 문제와 OOM 에러로 서버가 다운됐을 때 진행 상황을 잃는 문제가 있어서 비동기 태스크로 분리해서 각 이미지를 개별로 처리했다.


마지막으로 A, B 프로젝트에서 작업을 수행했던 CDN(Content Delivery Network) 구축에 관련된 내용을 정리해보자. A, B 프로젝트는 서로 다른 문제였지만, CDN이라는 동일한 솔루션이 필요했다.

  • A 프로젝트는 브라우저에서 S3에 직접 접근할 때 보안적인 차원에서 pre-signed URL을 사용 중이었다. 다만, pre-signed URL은 매번 쿼리 파라미터가 변경되기 때문에 브라우저의 성능 향상을 위해 캐시 컨트롤 헤더를 사용할 수 없었다. 이 문제를 해결하기 위해 AWS 클라우드프론트를 CDN을 구축하는 작업을 수행했다.
  • B 프로젝트는 API 서버를 경유해서 S3로부터 이미지를 가져오는 중이었다. 트래픽이 높은 시간대에 메모리 사용량이 크게 올라갔다. 앞서 말했듯이 썸네일 이미지를 읽을 때마다 API 서버에서 만들어 반환하는 것도 서버 부하를 일으키는 주된 원인이었다.

CDN을 구축하는 작업을 단기간 동안 두 번이나 수행했기 때문에 B 프로젝트는 A 프로젝트보다 더 빠르게 작업을 마무리 지을 수 있었다. 보안 관점에서 리소스를 보호하는 메커니즘에 대해 배움이 있었다.

  • CDN을 통해 S3 리소스에 접근할 때 인증된 사용자 외 접근을 막기 위해 서명된 쿠키(signed cookie)를 사용했다.
  • 보안을 강화하기 위해 외부 접근을 차단하고 오직 AWS 클라우드프론트만 허용하는 OAC(Origin Access Control) 설정을 수행했다.

CDN 구축은 처음이었지만, 그리 어렵지 않았다. 테라폼(terraform) 코드로 인프라를 관리 중이었기에 필요한 리소스, (비)공개키 등록, 정책들을 추가하는 작업은 수월했다. 코드를 재사용할 수도 있었고, AI가 빠르게 테라폼 코드를 작성해주었기 때문에 우리에게 맞는 세부 옵션들만 정리해주면 됐다. AI 발전으로 코드 작성을 통해 처리할 수 있는 작업은 비용이 제로(0)에 수렴하게 된 것 같다.

B 프로젝트에서 이미지에 관련된 기술 부채들을 덜어내니 메모리 사용량이 매우 안정적으로 내려왔다.


이미지 관련된 기술적 부채들을 해소하면서 얻은 인사이트(insight)들은 다음과 같다.

리소스를 늘려서 당장 급한 불은 끌 수 있지만, 좋은 해결 방법은 아니다. 근본적인 문제를 해결하지 않으면 더 큰 기술 부채로 돌아온다.

A 프로젝트와 B 프로젝트는 비슷한 문제를 갖고 있었지만, 해결할 때 공수는 완전히 달랐다. A 프로젝트는 시작한 지 얼마 안 돼서 변경이 만드는 영향 범위를 검토하는 게 상당히 쉬웠다. 코드 변경도 람다 함수 하나만 변경하는 것으로 끝났다. B 프로젝트는 몇 년 동안 유지보수되어 왔다. 이미지를 조회하는 API 엔드포인트를 여러 곳에서 사용하고 있었기에 구조적 변경이 어느 비즈니스에 영향을 미치는지, 어떻게 작업하는 것이 변경과 영향을 최소한으로 줄일 수 있는지, 작업이 완료된 후에 정상적으로 시스템이 동작하는지 확인하는 데 시간이 오래 걸렸다.

처음 B 프로젝트의 개발자들은 가장 쉬운 방법으로 컨테이너 리소스를 늘리는 방법을 선택했을 것이다. 하지만, 근본적인 문제를 해결하지 못한 상태에서 메모리만 늘린 임시방편은 더 커진 작업량과 함께 부메랑처럼 돌아왔다. 소프트웨어 엔지니어는 항상 기술적 탁월성(technical excellence)을 추구하며, 더 나은 방향을 탐구하며, 최고의 아키텍처나 솔루션을 추구하기보다는 그 순간에 맞는 최선의 선택을 해야 함을 다시금 배웠다.

처음엔 없던 문제가 시간이 지나고 상황이 바뀌면서 드러난다. 작은 기술 부채라도 상황이 바뀌면 큰 기술 부채가 된다. 기술 부채는 적절한 시점에 반드시 갚아야 한다.

비슷한 내용을 개발자를 위한 기술 부채 실무 가이드구글 엔지니어는 이렇게 일한다라는 책에서 봤던 기억이 있다. 읽으면서 많이 공감했던 기억이 난다. 이번 프로젝트를 통해 다시 경험했고, 이번에 함께 참여한 동료도 비슷한 이야기를 했다.

처음 B 프로젝트에서 이미지 처리를 API 서버에서 수행하기로 결정했을 때는 전혀 문제가 없었을 것이다. 사용자도 수십 명 정도밖에 안 됐을 것이고, 화면에서 보는 이미지도 수가 적었을 거다. 인프라 작업을 하거나 외부 방화벽 신청을 하는 것보다 API 서버에서 이미지를 처리하는 편이 비용이 작고, 팀에서 빠르게 진행할 수 있는 선택이었을 거다. 프로젝트도 이렇게 커질지 몰랐기에 당시에 그 선택은 타당했다고 본다.

시간이 지나고 사용자 수가 많아짐에 따라 전혀 문제가 없던 이미지 기능이 팀의 발목을 잡기 시작했을 거다. 서버 사양이 부족해져서 에러가 발생하고, 필요 이상의 리소스를 할당해서 해결했을 것이다. 이 시점이라도 문제의 근본 원인을 고민해서 기술 부채를 갚았다면, 신속하게 아키텍처 전환을 하고, 짧은 리팩토링으로 끝낼 수 있었을 거다.

시간이 지나 해당 API를 사용하는 위치가 많아지고, 주변 코드들이 얽히고, 데이터가 많아져서 구조적 변경이 더 어려워진 뒤에 다시 문제가 터졌다. 영향을 받는 코드들을 분석하고, 안전한 리팩토링과 마이그레이션 작업 계획을 세우고, 실제 작업을 했다. 이 모든 시간과 작업을 고려하면, 적절한 시기에 갚았을 때의 작업량보다 분명히 더 컸을 것이다.

시스템 운영에 필요한 것들

주니어 시절 약 1년 정도 시스템 운영을 했었지만, 그 이후로 거의 5년 동안 시스템 개발을 했다. 신규 프로덕트 개발, 기존 레거시 시스템을 현대화하는 프로젝트, 기존 프로덕트를 개선하는 프로젝트처럼 시스템 운영보다는 신규 개발이나 현 프로덕트를 개선하는 개발 일을 주로 수행했다. 그러다 보니 시스템 운영과 관련된 내용을 깊게 고민해본 적이 없었다. 항상 내게 부족한 소양이라고 생각하는 부분이다. 이런 일도 소프트웨어 엔지니어링의 한 부분이기 때문에 나도 역량을 키우고 싶지만, 지금 이 회사에 계속 다닌다면 시스템 운영에 대한 고민을 할 기회가 적을 거라 생각했다.

운이 좋게도 이번 인게이지먼트의 B 프로젝트 팀은 시스템을 오래 운영해왔고, 이런 부분을 이제부터 구체적으로 정립하자는 이야기가 나오는 중에 합류해서 함께 고민해볼 수 있는 기회가 있었다. 아쉽게도 내가 주축으로 관련 업무를 맡아 진행하진 못했지만, 매일 스탠드업이나 필요하면 시간을 내서 공유해줬기 때문에 공부가 많이 됐다.

다만, 모든 대화가 일본어이기 때문에 내용을 전부 이해하지는 못했다는 점이 아쉽다. 일본어 실력이 많이 늘어 언어적 장벽을 많이 허물었다고 생각했지만, 여전히 어려움이 남아 있다. 일본어 넋두리는 그만하고, 내가 어떤 것들을 배웠는지 정리해보자. 같이 인게이지먼트를 수행한 유시-상이 이전 직장의 경험들을 바탕으로 고객사 개발자들과 함께 잘 정리해줬다. 랩스 조직에 제법 오래 있었던 나로선 나와 전혀 다른 경험을 가진 유시-상에게 많은 배움이 있었고, 이것도 그것들 중 하나다.

  • 시스템 장애(incident)가 발생 시 대처 플레이북(playbook) 만들기
    • 장애에 대한 심각도 판단 기준과 공유 및 보고 프로세스
    • 장애가 난 시스템을 빠르게 정상화하는 프로세스
    • 장애 발생 과정을 타임라인으로 분석하고 근본 원인을 파악하여, 재발 방지를 위한 액션 아이템을 도출하는 포스트모템(postmortem) 활동
  • 현재 팀 컨텍스트에 적합한 SLI(Service Level Indicator)와 SLO(Service Level Objective) 정의와 지표들
    • LCP(Largest Contentful Paint), INP(Interaction to Next Paint), CLS(Cumulative Layout Shift) 같은 사용자 경험(UX) 성능 지표
    • API Latency, Throughput(RPS), Error Rate, Availability, DB Query Time 같은 서비스 성능 및 안정성 지표
    • Error Budget, Burn Rate 같은 신뢰성 관리 지표
  • SLI/SLO를 실시간으로 확인할 수 있는 모니터링 시스템 구축
    • 뉴레릭(newrelic) 대시보드 구축
    • 모니터링하면 좋은 지표들과 구체적인 수치 정리

이번엔 유시-상의 많은 도움을 받았으니 다음 인게이지먼트에선 이 경험과 배움을 다른 사람들에게도 전달하고 싶다. 여전히 지식과 경험이 부족하니 독서를 통해 미흡한 부분을 채워볼 생각이다. 예전에 사두고 책장에만 두었던 사이트 신뢰성 엔지니어링을 읽어봐야겠다. 읽다가 중간에 멈춘 구글 엔지니어는 이렇게 일한다도 다시 읽어볼 생각이다.

카테고리:

업데이트:

댓글남기기