part I에서는 지금까지 만들어 본 몇 가지 간단한 프로토타입과, 그것들이 어떻게 나를 LLM 기반 제품을 위한 안정적이고 확장 가능하며 유연하고 규모를 키울 수 있는 스택이 필요한 지점까지 데려왔는지 이야기했다. 만들 만한 흥미로운 것들이 너무 많기 때문이다. 레지스트리와 베이스 클래스를 써서 우리가 구상한 A.I. 기반 소프트웨어 묶음의 구성 요소를 유연하게 갈아 끼우고, 개발자들이 더 쉽게 협업할 수 있게 하는 방식도 이야기했다. 그런데 최종 사용자의 경험은 어떻게 할 것인가? 내가 노리는 유연성과 모듈성 수준이라면, 레지스트리 패턴과 인터페이스 중심 설계를 결합하는 방식이 꽤 이상적인 접근일 수 있다. 이 글에서는 레거시 코드베이스 리팩터링 자동화든, 이메일과 문서를 바탕으로 한 지식베이스든, 소셜 미디어 콘텐츠 생성과 발행이든, 관객을 위한 새로운 데이터 소비 방식이든, 관심 있는 거의 모든 배포 형태에 일반화할 수 있을 법한 내 A.I./LLM 엔지니어링 스택을 훑어보려 한다.
계속 가기 전에, 작품 어노테이션 문제를 다루는 방식에서 전통적인 소프트웨어 개발과 ML 기반 접근이 어떻게 다른지 다시 짚고 넘어가자.
기준
작품 어노테이션과 태깅을 위한 전통적 소프트웨어 개발
작품 어노테이션과 태깅을 위한 머신러닝 기반 개발(예: OpenAI의 CLIP)
무엇을 하는가
미리 정의한 규칙과 메타데이터를 사용해 작품에 설명과 태그를 붙인다.
데이터에서 학습한 패턴을 사용해 작품에 동적으로 설명과 태그를 붙인다.
어노테이션의 깊이
명시적인 규칙과 사용 가능한 메타데이터 안에 머문다.
더 넓은 범위의 특징, 주제, 요소를 식별할 수 있다.
적응성
새로운 스타일과 흐름이 나오면 사람이 직접 업데이트해야 한다.
재학습을 통해 새로운 스타일과 흐름에 적응할 수 있다.
사용자 참여
사용자 피드백을 사람이 직접 반영한다.
모델 파인튜닝을 위해 사용자 피드백을 자동으로 반영할 수 있다.
계산 복잡도
대체로 계산 부담이 낮다.
더 깊은 분석을 하려면 더 많은 계산 자원이 필요하다.
해석 가능성
규칙이 명시적이어서 해석하기가 더 쉽다.
흔히 “블랙박스”로 여겨지며, 그래서 해석하기가 더 어렵다.
커버리지
메타데이터의 존재 여부와 정확도에 의존한다.
작품의 시각 요소 자체를 직접 분석할 수 있다.
수작업 부담
규칙과 템플릿을 정의하는 데 상당한 노력이 든다.
모델이 학습 데이터에서 일반화할 수 있으므로 수작업 부담이 줄어든다.
이런 접근 방식의 차이 때문에 워크플로와 개발 방식에도 중요한 차이가 생긴다. A.I.의 이점을 제대로 가져가려는 조직이라면 반드시 신경 써야 하는 부분이다. 두 접근이 만나는 지점과 갈라지는 지점은 대략 다음과 같다.
겹치는 지점:
-
통합: ML 모델은 더 큰 소프트웨어 시스템 안의 구성 요소로 들어가는 경우가 많고, 그 시스템 자체는 전통적인 소프트웨어 엔지니어링 기법으로 만들어진다.
-
테스트와 검증: 두 접근 모두 코드나 모델이 기대한 대로 동작하는지 확인하기 위해 테스트를 사용한다. 다만 테스트의 구체적인 방식은 다를 수 있다.
-
배포와 유지보수: 머신러닝 모델이든 전통적인 소프트웨어든, 결국 배포하고 모니터링하고 유지보수해야 한다.
-
버전 관리: 두 개발 방식 모두 변경 이력 관리를 위해 Git 같은 버전 관리 시스템을 자주 사용한다. 다만 머신러닝에서는 데이터, 모델, 하이퍼파라미터, 결과까지 버전으로 관리해야 한다.
갈라지는 지점:
-
결정론 대 확률론: 전통적인 소프트웨어 엔지니어링은 대체로 명시적인 규칙과 로직을 따르는 결정론적 방식인 반면, 머신러닝은 데이터에서 패턴을 배우는 확률론적 방식이다.
-
해석 가능성: 전통적인 소프트웨어는 대체로 디버깅하고 해석하기가 더 쉽지만, 머신러닝 모델, 특히 복잡한 모델은 이해하기가 훨씬 까다로울 수 있다.
-
데이터 의존성: 머신러닝은 학습, 검증, 테스트를 위해 데이터에 크게 의존한다. 반면 전통적인 소프트웨어 엔지니어링은 이 단계들에 데이터가 본질적으로 필요하지는 않다.
-
피드백 루프: 머신러닝에는 새 데이터로부터 모델이 계속 배우는 지속적인 피드백 루프가 들어가는 경우가 많다. 전통적인 소프트웨어에는 이런 특성이 본질적으로 들어 있지 않다.
-
필요 역량: 전통적인 소프트웨어 엔지니어링과 머신러닝은 서로 다른 역량을 요구한다. 물론 소프트웨어 엔지니어가 점점 더 데이터를 잘 다루고, 데이터 과학자가 소프트웨어 베스트 프랙티스를 더 많이 익히면서 겹치는 영역은 커지고 있다.
- ML 특화 역량: 통계와 수학에 대한 이해(확률, 통계, 선형대수, 미적분), 데이터 역량(데이터 정제, 전처리, 피처 엔지니어링, 시각화), 모델 학습과 평가(알고리즘 선택, 하이퍼파라미터 튜닝, 평가 지표), 전문 도구와 라이브러리(머신러닝 프레임워크, 데이터 조작 라이브러리), 실험(A/B 테스트, 모델 해석 가능성)이 포함된다.
위 예시들이 A.I. 엔지니어링의 아키텍처와 디자인 패턴이 왜 그 자체로 하나의 새 실천 영역으로 떠오르고 있는지 감을 주면 좋겠다.
레지스트리 기반, 인터페이스 중심 디자인 패턴
객체지향 프로그래밍, 특히 Java, C#, TypeScript 같은 언어에서 “인터페이스”는 일종의 계약을 명시하는 타입 정의다. 어떤 클래스가 반드시 구현해야 하는 메서드 묶음, 그리고 경우에 따라 속성까지 지정한다. 인터페이스 자체에는 구현이 들어 있지 않다. 그 인터페이스를 구현한다고 주장하는 클래스라면 어떤 메서드를 가져야 하는지만 정의할 뿐이다.
안정성과 실험의 유연성이 모두 필요한 A.I. 기반 또는 LLM 기반 애플리케이션에서는 인터페이스와 추상 클래스를 쓰는 일이 특히 유용하다. A.I. 개발에는 실험이 아주 많이 들어간다. 모델을 조금씩 바꾸고, 전처리 단계를 갈아엎고, 서로 다른 데이터 소스를 써 본다. 시스템이 처음부터 모듈성을 염두에 두고 설계되어 있다면(인터페이스와 추상 디자인 패턴이 가능하게 해 주는 바로 그것), 실험을 위해 구성 요소를 바꿔 넣는 일이 훨씬 쉬워진다. 시스템의 다른 부분을 부숴 먹을 위험도 줄어든다. 게다가 A.I. 시스템을 확장하거나 배포할 때 모듈식 설계는 안정성과 유지보수성을 지켜 준다.
짧게 말하면, 인터페이스는 무엇을 기대해도 되는지에 대한 명확한 계약을 제공한다.
레지스트리, 추상 클래스, 인터페이스를 결합하면 다음과 같다.
-
각 기능 유형마다 인터페이스를 둔다. 예를 들어 파일 처리, 청킹, 임베딩, 저장, 검색 같은 것들이다. 이렇게 하면 각 구성 요소가 무엇을 해야 하는지 분명해진다.
-
그다음 레지스트리를 사용해 런타임에 이 인터페이스들의 적절한 구현체를 관리하고 제공할 수 있다. 덕분에 시스템은 서로 다른 시나리오나 설정에 맞춰 동적으로 적응할 수 있다.
예를 들어 서로 다른 파일 형식을 처리한다고 해 보자.
-
인터페이스 중심 부분:
-
먼저 인터페이스를 정의하는 것부터 시작하자. 예를 들어 데이터를 수집하고 메타데이터를 가져오는 메서드를 가진
DataSource인터페이스를 생각해 볼 수 있다. 이 인터페이스는 모든 데이터 소스 프로세서가 무엇을 해야 하는지에 대한 명확한 계약을 세운다.from abc import ABC, abstractmethod class DataSource(ABC): @abstractmethod def ingest(self): pass @abstractmethod def get_metadata(self): pass
-
-
레지스트리 부분:
-
데이터가 수집되고 나면(예컨대 Git 저장소의 파일들), 시스템은 각 파일의 확장자를 기준으로
FILE_TYPE_PROCESSORS레지스트리에서 적절한 프로세서를 찾을 수 있다.FILE_TYPE_PROCESSORS = { ".py": PythonFileProcessor, ".md": MarkdownFileProcessor, # ... }
-
시스템이 데이터를 수집해야 할 때는 이 레지스트리에 질의해 각 파일의 확장자에 맞는 프로세서를 찾으면 된다.
-
DataSource서브클래스 내부의 통합 워크플로:DataSource서브클래스 안에서는 이 두 단계를 하나로 묶을 수 있다. 먼저 데이터를 수집한 뒤, 수집된 각 파일에 적절한 파일 프로세서를 적용한다.
class GitRepoSource(DataSource):
def ingest(self):
files = self._clone_and_filter_files()
processed_data = []
for file_path in files:
ext = self._get_file_extension(file_path)
processor_class = FILE_TYPE_PROCESSORS.get(ext)
if processor_class:
processor = processor_class()
processed_data.append(processor.process(file_path))
return processed_data결합했을 때의 장점
이 결합 접근은 몇 가지 장점을 준다.
-
쉬운 확장: 앞으로 새로운 데이터 소스 유형을 지원해야 할 때는
DataSource인터페이스를 따르는 새 클래스를 만들고 **DataSourceRegistry**에 등록하면 된다. 기존 코드는 건드릴 필요가 없다. -
명확한 기대치:
DataSource인터페이스는 모든 데이터 소스가 공통 API를 따르도록 보장하므로, 수집 파이프라인의 동작을 예측 가능하게 만든다. -
동적 적응성: 중앙 레지스트리가 있으면 메인 워크플로에 영향을 주지 않고 서로 다른 데이터 소스 프로세서를 쉽게 바꿔 넣거나 뺄 수 있다. 유연성이 꽤 많이 생긴다.
서로 다른 에이전트를 두는 다양한 사용 사례, 예컨대 하나는 문서와 PDF용, 다른 하나는 코드 저장소용으로 두는 경우에는 이 조합이 특히 좋다. 인터페이스를 통해 명확한 계약을 제공하면서도, 레지스트리를 통해 확장하거나 적응할 여지를 남겨 두기 때문이다. 안정성은 인터페이스에서 오고, 유연성은 레지스트리에서 온다.
그럼 A.I./LLM 기반 앱에 새로운 초능력을 주고 싶을 때는 어떻게 될까?
상호작용과 소비 방식 확장하기:
예를 들어 내가 “The Sound of Stories”에서 하고 싶은 일을 보자. LLM이 생성한 텍스트를 파싱해 다국어 내레이션이라는 추가 소비 방식을 붙이는 것이다. 생성된 이야기를 바탕으로 이미지 생성도 가능하게 하고 싶을 수 있다. 소비 주체는 다른 도구일 수도 있고 사람일 수도 있다. 내가 염두에 둔 소셜 미디어 콘텐츠 생성 사용 사례에서는 소셜 미디어 API가 소비할 수 있는 구조화된 .json도 출력해야 한다. 레지스트리 기반, 인터페이스 중심 접근을 쓰면 아래처럼 소비 방식을 확장할 수 있다.
-
레지스트리 만들기: 앞선 예시들과 마찬가지로 소비 방식의 레지스트리를 둘 수 있다. 예를 들어 텍스트 표시, 음성 출력, 애니메이션 비디오 같은 방식들이다.
-
소비 인터페이스:
class ConsumptionInterface(ABC):
@abstractmethod
def display(self, content):
pass- 구체 구현체:
class TextDisplay(ConsumptionInterface):
def display(self, content):
# Simply print the content
class VoiceOutput(ConsumptionInterface):
def display(self, content):
# Use Text-to-Speech to vocalise the content
class AnimatedVideo(ConsumptionInterface):
def display(self, content):
# Convert content into animated video그럼 이제 한 걸음 물러나, A.I. 애플리케이션을 위한 데이터 기반 개발의 전체 순환을 처음부터 끝까지 보자.
A.I. 앱을 끝까지 만들기 위한 일반화된 단순 아키텍처
A.I./LLM 앱 스택은 이제 막 모습을 갖춰 가는 중이고, a16z는 매우 친절하게도 이를 프롬프트 구성(retrieval), 프롬프트 실행(inference), 그리고 생태계 안의 몇 가지 도구라는 일반 단계로 정리해 두었다. 데이터 흐름과 파이프라인은 이 생태계의 백엔드 배관이다. 이것 역시 전통적인 소프트웨어 엔지니어링과 구분되는 중요한 차이다. 내가 개발에서 집중하려는 일반 아키텍처와 구성 요소를 아래처럼 단순화해 보았다.

예시: 작품 신경망 검색의 흐름
이전에 나는 작품 신경망 검색 데모를 만든 적이 있다. 그 데모를 프로덕션에 맞게 확장한다면 대략 아래 단계들이 필요할 수 있다.
단계
관련 구성 요소
동작
메모
1
오케스트레이터
작품 메타데이터와 이미지 임베딩의 초기 데이터 수집
배치 모드 작업
2
사용자 인터페이스(UI)
사용자가 검색어 또는 필터 조건을 입력한다
3
메인 애플리케이션
사용자 입력을 검증하고 설정을 가져온다
Configuration Manager와 확인한다
4
오케스트레이터
Vector Database에서 관련 임베딩을 가져온다
Retrieval Engine을 사용할 수도 있다
5
응답 생성기
사용자가 읽기 좋은 출력을 구성한다
작품 메타데이터와 이미지를 화면 표시용으로 정리한다
6
로깅 모듈
작업을 로그로 남긴다
사용자 질의, 응답 시간 등을 저장한다
7
검증 및 평가
결과를 자동으로 평가한다
기본 보안 검사
8
사용자 인터페이스(UI)
사용자에게 검색 결과를 보여 준다
9
피드백 수집기
있다면 사용자 피드백을 수집한다
4개월 개발 로드맵의 우선순위
그러니까 내가 관심을 두는 이 일반 프레임워크와 아키텍처 안에서는 할 수 있는 일이 정말 많다.
-
작품 신경망 검색
-
생성된 이야기를 실시간으로 다국어 내레이션하기
-
코드베이스 리팩터링과 테스트 작성
-
문서와 데이터를 바탕으로 한 지식베이스와 채팅하기
-
코드 생성
-
소셜 미디어 콘텐츠 생성과 발행
우선순위를 정하자면, 독해와 창작은 서로 다른 두 역량이고 전자가 후자에 매우 큰 도움이 될 것이므로, 내 계획은 LLM에게 배우는 법을 먼저 가르치는 쪽에 무게를 두는 것이다. 그래서 작품 신경망 검색과 지식베이스처럼 덜 복잡한 사용 사례부터 시작하려 한다. 아주 거친 일정은 아래처럼 흘러갈 수 있다.
-
1-2개월차: 작품 대상 신경망 검색 - 기초 데이터 파이프라인을 만들기 위해.
-
2-3개월차: 문서와 데이터를 바탕으로 한 지식베이스와 채팅하기 - 수집과 검색 방법, UI 구성 요소를 개발하기 위해.
-
3-4개월차: 코드베이스 리팩터링과 테스트 작성 - 채팅/지식베이스 단계에서 만든 통찰과 방법을 활용해 더 똑똑한 자동 테스트와 리팩터링으로 이어가기 위해.
위 계획대로 신경망 검색과 지식베이스 구축에 집중하더라도, LLM을 활용해 코드베이스에 질의하고, 그 결과를 코드 품질 서비스로 넘긴 뒤, 체이닝이나 프롬프트 템플릿을 사용해 리팩터링과 테스트 작성 같은 관련 자동화를 수행하는 단순화된 워크플로도 시작해 두면 좋겠다고 생각한다. 처음에는 브랜치로 쪼개 사람이 검토하고, 이후에는 모든 테스트를 통과하면 자동화하는 식으로 갈 수 있다.
한편 내 Git 저장소에서 프로젝트를 정리하려는 방식은 이렇다. 아직 더 바뀔 것으로 보고 있으니 아래는 여전히 아주 많이 진행 중인 작업이지만, 각 폴더가 맡아야 할 역할은 대략 이렇게 생각하고 있다.
-
db - CRUD 같은 모든 데이터베이스 관련 작업
-
demos - 어느 정도 “안정적인” 데모와 워크플로를 정의하는 곳
-
gui - 사용자가 보게 되는 모든 것
-
src - 인터페이스와 구현체를 위한 소스 코드 대부분
-
tests - 단위 테스트와 통합 테스트, 그리고 테스트용 픽스처
아직 CI/CD, 모니터링, 평가 구성 요소가 빠져 있고 프로젝트 구조도 계속 정리 중이다. 그래도 다른 개발자들과 분할 정복을 해 보고 싶다. National Arts Council Arts x Tech Lab을 위해 “The Sound of Stories”(생성된 이야기를 실시간으로 다국어 내레이션하기)도 제대로 만들어 내야 하기 때문이다. 일이 잘 풀린다면, 이 여러 개발 흐름은 서로 꽤 강하게 시너지를 내고 Berlayar A.I.라는 회사를 위한 기반 요소가 되어 줄 것이다. ;)
├── agents
│ └── __init__.py
├── dataloader.ipynb
├── db
│ ├── chunks.py
│ ├── connectors.py
│ ├── __init__.py
│ ├── main.py
│ ├── ops
│ │ ├── deeplake.py
│ │ └── __init__.py
│ └── utils.py
├── demos
│ ├── chat_adf
│ │ ├── chat.py
│ │ ├── tests.py
│ │ └── utils.py
│ ├── neural_search
│ │ └── upload.py
│ └── thesoundofstories
│ ├── audio
│ └── chat.py
├── diagram.py
├── gui
│ └── __init__.py
├── ingest_pdf.log
├── __init__.py
├── log_init.py
├── main.py
├── notebooks
│ ├── architecture.ipynb
│ ├── chat_adf.ipynb
│ └── dataloader.ipynb
├── poetry.lock
├── pyproject - Copy.toml
├── pyproject.toml한편, 곧 있을 “Good Economics for Bad Times” 시험에 집중하러 가야 하니 이건 당분간 미뤄 둔다.
원래 PubPub의 erniesg.pubpub.org/pub/wpnv7rhs에 게시한 글.