경험이 많은 사람이 소프트웨어를 만드는 속도가 느리다면, 가진 도구들로 할 수 있는 일이 무엇인지 명확하지 않은 상태에서 코드를 작성하는 좋지 않은 습관을 가지고 있기 때문일지 모른다. 잘 모르는 상태에서 작성하는 코드가 점진적으로 확장할 것을 지나치게 염두에 두면 과한 추상화가 된다.

좋은 코드를 작성하겠다는 욕심의 가치는 양면적이다. 욕심은 코드의 퀄리티에 대한 고민을 하게 만들고, 단순성을 얻을 기회를 제공한다는 점에서 긍정적이다. 잘 정리된 코드가 나의 정신을 맑게 한다. 디자인 패턴은 코드의 반복이나 잘못된 추상화를 막을 수 있는 일반적인 전략들을 제시한다. 잘 추상화된 개념들은 생각을 단순하게 만들고, 단순화된 생각은 여유를 만들어 다른 더 중요한 일에 몰입할 수 있도록 돕는다. 하지만 이러한 정리에 대한 욕심이 종종 과할 수 있다. 무엇을 만들지 정확히 모르는 채로 리팩터링을 시도하는 자신을 발견한다면, 부정적인 측면이 강하게 작용하고 있음을 인지해야 한다.

이틀간[1,2] 약 10시간 정도 소스코드를 작성했다. 이 10시간 중 앞의 7시간이 넘는 시간동안 작성한 코드의 90%는 버려졌고, 기능도 완성하지 못했다. 반면 팀 리더인 전대위님은 1.5시간 만에 어쨌든 작동하는 코드를 만들어 냈다. 비둘기의 날개가 펄럭이지 않고 대가리가 헬리콥터처럼 돌아가는 한이 있더라도, 모든 개발 프로젝트는 잘 작동하는 최소한의 프로그램을 만드는 것에서부터 시작이다. 나는 부끄럽게도 실패했다.

이유가 무엇이었을까? 긴 회고 끝에 "잘 모르는 상태에서의 불필요한 추상화 시도"가 가장 근본적인 원인이라고 결론내렸다. OpenAI compatible 언어모델 n개가 저마다 다른 인트라넷 서버에 호스팅되고 있었다. 이 산개된 모델들은 번역 기능 제공을 위한 모델들이었다. 언어모델마다 잘 하는 언어가 다르기 때문에, 언어마다 서비스에서 사용되는 모델이 달라졌다. 나는 번역 애플리케이션 개발자가 모델을 선택하는 로직이나 각각의 주소같은 세부사항을 모른 채, 단일 언어모델을 사용하는 느낌을 준다면 코드가 잘 정리된 느낌을 줄 것 같다는 생각이 들어 키보드를 두들겼다. 눈치챘겠지만, 욕심에 의한 불필요한 추상화다.

이뿐만이 아니다. LLM의 경우 스트리밍 응답이 굉장히 자주 사용된다. 나는 번역 프롬프트를 작성하는 팀 개발자가 스트리밍 응답과 관련된 부분을 신경쓰지 않아도 되도록 완전히 은닉하고 싶었다. 스트리밍 응답을 처리하는 일에 익숙하지 않고, 프롬프트를 관리하는 방법에도 익숙하지 않고, 스트리밍 정보를 처리하는 기본적인 문법(yield)에도 익숙하지 않고, 그 어떤 구성 요소를 포함한 작은 애플리케이션도 end-to-end 작동을 확인한 적도 없는데 섣부르게 추상화를 시도했다. 더 큰 문제는 LangChain에 이미 OpenAI Compatible 모델을 ChatOpenAI라는 클래스로 묶어 사용할 수 있고, 프롬프트를 관리하는 방법, 비동기 호출 방법, 스트리밍 호출 방법에 대한 훌륭한 추상화 계층을 제공하고 있다는 사실을 아주 뒤늦게 깨달았다는 것이다. 왜 어떤 부분에서의 추상화가 필요한지 제대로 파악하지 않고, 추상화를 하고 싶다는 욕심에 섣부르게 추상화했다. 일단 개념검증이 중요하다는 것도 알고, 확장성 있는 코드가 처음에는 별로 중요하지 않다는 것은 뇌로 알고 있음에도 불필요한 시행착오를 스스로 만들어 7시간과 90%의 코드를 버리게 만들었다.

나는 반복을 예상하려는 습관이 있다. 내가 만들고 싶은 것을 잘 못 만들던 시절부터 그렇게 교육을 받았기 때문에 너무 깊숙한 습관으로 자리잡고 있는듯하다. 물론 숙련된 프로그래머는 반복을 예상하고 처음부터 훨씬 더 좋은 코드를 작성할 수 있지만, 나같이 어정쩡한 프로그래머들은 코드상에서 반복을 충분히 확인한 다음, 반복되는 부분을 리팩터링하는 편이 반복을 예상해 대비하는 것보다 훨씬 효율적일 수 있다. 반복을 먼저 확인하게 되면 그 과정에서 이미 제공하는 도구들이 어떤 추상 계층을 제공하는지 알게 된다는 장점도 있다. 쉽게 말해, 도구들이 제공하는 추상화 계층을 더 잘 이해할 수 있는 코드 스니펫이 쌓이는 것이다.

점심 시간, 펜과 종이만을 챙겨간 카페에서 마음을 가다듬고 40분 동안 고민했다. 우선 동료 개발자와 '그... 그... 다의어… 쭉 들어 있는 딕셔너리인데 그 안에 이중 리스트인 것 있잖아요.' 라고 하던 것을 명쾌한 단어로 잘 표현해 보아야겠다는 생각이 들었다. 머지않아 이런 용어에 대한 추상화가 가장 절실했음을 깨닫게 됐다. 유비쿼터스 언어가 약간 구체화될 때마다, 우리의 핵심 로직도 명료해졌다. 로직이 명료해지니, 유비쿼터스 언어도 다시 조금 더 구체화됐다. 우리의 핵심 로직이 무엇인지 파악되면서, ‘언어모델에 넣는 프롬프트는 도메인에 속해야 할까, 인프라에 속해야 할까?’같은 고민이 한결 수월해졌다. ‘현재 번역기능의 핵심은 프롬프트이므로, 나중에 필요해지면 도메인 내의 별도 파일, 인프라로 점진적으로 분리해 넘기고, 지금은 일단 도메인 소스코드에 포함시키자’ 같은 결정을 내렸다. 점심시간이 끝나고 빠르게 언어모델에게 명령할 수 있었다. 10시간 중 가장 생산적인 40분이었다.

처음부터 좋은 코드를 작성하려는 노력 자체를 하지 말라는 이야기를 하고자 함은 아니다. 이 단계에서 좋은 코드를 작성하는 것은 일단 생각하지 말라고 스스로에게 조언하고 싶다. 무엇을 어떻게 할 것인지 충분히 고민한 이후에 좋은 코드를 고민해도 괜찮다. 무엇을 어떻게 할 것인지 고민하는 것과, 좋은 코드를 작성하는 것을 분리해서 생각할 수 있도록 습관이 들어야 한다. 소프트웨어공학이나 설계라고 하면 디자인패턴을 떠올리는데, 설계의 시작은 ‘무엇을 할지, 어떻게 할 수 있을지’를 구체화하는 것이 시작임을 잊지 말자.


parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.

  1. None

from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.

  1. a0_4.1_1. [entry] title: 생성형 AI와 포멀하고 딱딱한 소프트웨어공학의 조합

supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.


opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.