🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

코파일럿 아키텍처 설계 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 24. · 5 Views

코파일럿 아키텍처 설계 완벽 가이드

기본 코파일럿 시스템의 핵심 아키텍처를 초급 개발자 눈높이에서 배웁니다. Input-Process-Output 구조부터 FastAPI 기반 실습까지 실무에 바로 적용할 수 있는 코드 생성 시스템을 만들어봅니다.


목차

  1. 코파일럿의_Input-Process-Output_구조
  2. 사용자_요청에서_코드_생성까지의_루프
  3. 컨텍스트_윈도우_관리_전략
  4. 스트리밍_응답으로_실시간_코드_출력
  5. 실습_FastAPI로_코드_생성_API_서버_구축
  6. 실습_간단한_웹_인터페이스_만들기

1. 코파일럿의 Input-Process-Output 구조

입사 2개월 차 신입 개발자 김코드 씨는 회사 내부 AI 코파일럿 프로젝트에 배정되었습니다. "코파일럿이 어떻게 동작하는지 아시나요?" 팀장님의 질문에 김코드 씨는 막막했습니다.

그저 VSCode에서 탭만 누르면 코드가 생성되는 마법 같은 도구라고만 생각했거든요.

코파일럿 시스템은 Input-Process-Output이라는 명확한 3단계 구조로 동작합니다. 사용자의 요청이 입력되면, LLM이 이를 처리하고, 생성된 코드를 출력합니다.

마치 공장의 컨베이어 벨트처럼 입력된 재료가 가공 과정을 거쳐 완성품으로 나오는 것과 같습니다.

다음 코드를 살펴봅시다.

# 코파일럿의 기본 구조
class CopilotArchitecture:
    def __init__(self, llm_client):
        # LLM 클라이언트 초기화
        self.llm = llm_client

    def process_request(self, user_input):
        # Input: 사용자 요청 받기
        prompt = self._prepare_prompt(user_input)

        # Process: LLM으로 처리
        response = self.llm.generate(prompt)

        # Output: 결과 반환
        return self._format_response(response)

    def _prepare_prompt(self, user_input):
        # 프롬프트 준비 로직
        return f"Generate code for: {user_input}"

김코드 씨가 가장 먼저 배워야 할 것은 코파일럿의 기본 구조였습니다. 선배 개발자 이아키 씨가 화이트보드에 큰 그림을 그리며 설명하기 시작했습니다.

"코파일럿은 복잡해 보이지만, 사실 매우 단순한 구조로 되어 있어요. 세 단계만 기억하면 됩니다." Input-Process-Output, 이 세 단계가 전부입니다.

쉽게 비유하자면, 레스토랑에서 주문을 받고 요리를 만들어 서빙하는 과정과 똑같습니다. 손님이 주문하면(Input), 주방에서 요리하고(Process), 완성된 음식을 내놓는(Output) 것처럼 말이죠.

코파일럿이 없던 시절에는 어땠을까요? 개발자들은 모든 코드를 직접 타이핑해야 했습니다.

반복적인 보일러플레이트 코드도, 비슷한 패턴의 함수도 매번 손으로 작성했습니다. 더 큰 문제는 새로운 라이브러리를 사용할 때마다 문서를 찾아보고, 예제 코드를 복사 붙여넣기 해야 했다는 점입니다.

바로 이런 문제를 해결하기 위해 AI 코파일럿이 등장했습니다. 코파일럿을 사용하면 반복 작업을 자동화할 수 있습니다.

또한 실시간으로 코드 제안을 받아 생산성을 크게 높일 수 있습니다. 무엇보다 초보 개발자도 베테랑처럼 코드를 작성할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 __init__ 메서드에서 LLM 클라이언트를 초기화합니다.

이것이 핵심 처리 엔진입니다. 다음으로 process_request 메서드에서는 세 단계가 순차적으로 실행됩니다.

사용자 입력을 받고, 프롬프트로 변환하고, LLM에게 전달하고, 결과를 받아 포맷팅합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 사내 개발 도구를 만든다고 가정해봅시다. 개발자가 "FastAPI 엔드포인트 만들어줘"라고 요청하면, 이 구조를 통해 자동으로 라우터 코드, 스키마, 테스트 코드까지 생성할 수 있습니다.

GitHub Copilot, Cursor, Amazon CodeWhisperer 모두 이런 구조를 기반으로 합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 Input과 Output만 신경 쓰고 Process를 무시하는 것입니다. 이렇게 하면 품질 낮은 코드가 생성되거나, 보안 문제가 발생할 수 있습니다.

따라서 중간 처리 과정에서 프롬프트 엔지니어링, 컨텍스트 관리, 필터링 로직을 반드시 추가해야 합니다. 다시 김코드 씨의 이야기로 돌아가봅시다.

이아키 씨의 설명을 들은 김코드 씨는 고개를 끄덕였습니다. "아, 결국 입력받고, 처리하고, 출력하는 거군요!" 코파일럿의 기본 구조를 제대로 이해하면 나만의 AI 도구를 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Input 단계에서 사용자 의도를 명확히 파악하는 것이 전체 품질을 결정합니다

  • Process 단계에 캐싱을 추가하면 동일한 요청에 대해 속도를 10배 이상 높일 수 있습니다
  • Output 단계에서 코드 검증을 추가하면 오류를 사전에 방지할 수 있습니다

2. 사용자 요청에서 코드 생성까지의 루프

김코드 씨는 기본 구조를 이해했지만, 또 다른 의문이 생겼습니다. "그럼 사용자가 요청을 보내면 정확히 어떤 과정을 거쳐서 코드가 생성되나요?" 이아키 씨는 미소를 지으며 말했습니다.

"좋은 질문이에요. 지금부터 요청-프롬프트-생성 루프를 알려드릴게요."

코파일럿의 핵심은 사용자 요청 → 프롬프트 구성 → LLM 호출 → 코드 생성의 반복 루프입니다. 이 과정은 마치 통역사가 한국어를 영어로 번역하고, 상대방의 답변을 다시 한국어로 번역하는 것과 비슷합니다.

요청을 LLM이 이해할 수 있는 형태로 변환하고, 생성된 결과를 사용자가 원하는 형태로 변환합니다.

다음 코드를 살펴봅시다.

# 요청-프롬프트-생성 루프
class CodeGenerationLoop:
    def __init__(self, llm_client):
        self.llm = llm_client
        self.conversation_history = []

    def generate_code(self, user_request):
        # 1. 대화 히스토리에 사용자 요청 추가
        self.conversation_history.append({
            "role": "user",
            "content": user_request
        })

        # 2. 프롬프트 구성 (시스템 + 히스토리)
        prompt = self._build_prompt()

        # 3. LLM 호출
        generated_code = self.llm.generate(prompt)

        # 4. 응답 히스토리에 추가
        self.conversation_history.append({
            "role": "assistant",
            "content": generated_code
        })

        return generated_code

    def _build_prompt(self):
        # 시스템 프롬프트 + 대화 히스토리 결합
        system = "You are an expert code generator."
        return system + "\n" + str(self.conversation_history)

김코드 씨는 이제 더 깊은 단계로 들어가야 했습니다. 단순히 입력과 출력만 아는 것으로는 부족했습니다.

실제로 어떤 일이 일어나는지 알아야 했죠. "코파일럿은 한 번의 요청으로 끝나지 않아요." 이아키 씨가 설명을 이어갔습니다.

"사용자가 계속해서 수정 요청을 하고, 코파일럿은 그에 맞춰 코드를 개선합니다. 이게 바로 루프 구조예요." 쉽게 비유하자면, 루프는 마치 친구와 카톡으로 대화하는 것과 같습니다.

내가 질문을 보내면 친구가 답장을 보내고, 내가 다시 추가 질문을 하면 친구는 이전 대화 내용을 기억하며 답변합니다. 코파일럿도 똑같이 이전 대화 맥락을 기억하며 코드를 생성합니다.

루프가 없던 시절에는 어땠을까요? 매번 새로운 요청을 할 때마다 처음부터 다시 설명해야 했습니다.

"아까 말한 FastAPI 엔드포인트에 인증 추가해줘"라고 하면, "어떤 엔드포인트요?"라고 되물었습니다. 더 큰 문제는 복잡한 코드를 여러 단계로 나눠서 생성할 수 없다는 점이었습니다.

바로 이런 문제를 해결하기 위해 대화 히스토리 기반 루프가 등장했습니다. 루프를 사용하면 이전 대화 맥락을 유지할 수 있습니다.

또한 점진적으로 코드를 개선하고 수정할 수 있습니다. 무엇보다 사용자가 자연스럽게 대화하듯이 요청할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 conversation_history 리스트에 모든 대화가 저장됩니다.

이것이 맥락 유지의 핵심입니다. generate_code 메서드는 네 단계로 동작합니다.

사용자 요청을 히스토리에 추가하고, 전체 히스토리를 포함한 프롬프트를 만들고, LLM을 호출하고, 생성된 코드를 다시 히스토리에 추가합니다. _build_prompt 메서드가 중요합니다.

시스템 프롬프트와 대화 히스토리를 결합하여 완전한 컨텍스트를 만듭니다. 이렇게 하면 LLM이 "아, 이 사용자는 아까 FastAPI에 대해 물어봤구나"라고 이해할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 사용자가 "로그인 API 만들어줘"라고 요청했다고 가정해봅시다.

코파일럿이 기본 코드를 생성하면, 사용자는 "JWT 토큰도 추가해줘"라고 추가 요청합니다. 루프 구조 덕분에 코파일럿은 이전 코드를 기억하고 있어서, 처음부터 다시 만들지 않고 JWT 부분만 추가합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 히스토리를 무한정 쌓아두는 것입니다.

이렇게 하면 LLM의 컨텍스트 윈도우를 초과하여 오류가 발생합니다. 따라서 일정 개수 이상이 되면 오래된 대화를 요약하거나 제거해야 합니다.

다시 김코드 씨의 이야기로 돌아가봅시다. 이아키 씨의 설명을 들은 김코드 씨는 감탄했습니다.

"아, 그래서 ChatGPT처럼 계속 대화가 이어지는 거군요!" 요청-프롬프트-생성 루프를 제대로 이해하면 더 자연스러운 코파일럿 경험을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 대화 히스토리는 최근 10개 정도만 유지하면 성능과 품질의 균형을 맞출 수 있습니다

  • 시스템 프롬프트에 코딩 가이드라인을 포함하면 일관된 스타일의 코드가 생성됩니다
  • 사용자 요청을 정제하는 단계를 추가하면 불필요한 LLM 호출을 줄일 수 있습니다

3. 컨텍스트 윈도우 관리 전략

며칠 후 김코드 씨는 첫 번째 난관에 부딪혔습니다. 테스트 중이던 코파일럿이 갑자기 "컨텍스트 길이 초과" 오류를 뱉어냈습니다.

"이게 무슨 오류죠?" 이아키 씨는 고개를 끄덕이며 말했습니다. "드디어 컨텍스트 윈도우 관리를 배울 때가 왔네요."

LLM은 한 번에 처리할 수 있는 토큰 수에 제한이 있습니다. 이를 컨텍스트 윈도우라고 합니다.

GPT-4는 약 8K~128K 토큰, Claude는 최대 200K 토큰까지 처리할 수 있습니다. 코파일럿을 만들 때는 이 제한을 고려하여 대화 히스토리와 코드 컨텍스트를 효율적으로 관리해야 합니다.

다음 코드를 살펴봅시다.

# 컨텍스트 윈도우 관리 전략
class ContextWindowManager:
    def __init__(self, max_tokens=4000):
        self.max_tokens = max_tokens
        self.history = []

    def add_message(self, role, content):
        # 메시지 추가
        self.history.append({"role": role, "content": content})

        # 토큰 수 계산 (간단히 문자 수 / 4로 추정)
        total_tokens = sum(len(msg["content"]) // 4 for msg in self.history)

        # 윈도우 초과 시 오래된 메시지 제거
        while total_tokens > self.max_tokens and len(self.history) > 2:
            # 시스템 메시지는 유지, 가장 오래된 대화 제거
            self.history.pop(1)  # 인덱스 0은 시스템 메시지
            total_tokens = sum(len(msg["content"]) // 4 for msg in self.history)

    def get_context(self):
        # 현재 컨텍스트 반환
        return self.history

    def summarize_old_messages(self):
        # 오래된 메시지를 요약으로 대체
        if len(self.history) > 10:
            old_messages = self.history[1:6]
            summary = "이전 대화 요약: 사용자가 FastAPI 로그인 API를 요청함"
            self.history = [self.history[0]] + [{"role": "system", "content": summary}] + self.history[6:]

김코드 씨는 처음으로 실전 문제에 부딪혔습니다. 코드는 잘 작성했는데, 실제로 돌려보니 예상치 못한 오류가 발생한 것입니다.

"컨텍스트 윈도우는 LLM의 단기 기억 용량이라고 생각하면 돼요." 이아키 씨가 비유를 들어 설명했습니다. "사람도 한 번에 너무 많은 정보를 기억하지 못하잖아요?" 쉽게 비유하자면, 컨텍스트 윈도우는 마치 공책의 페이지 수와 같습니다.

공책이 100페이지라면, 101페이지째부터는 쓸 수 없습니다. LLM도 똑같이 정해진 토큰 수를 넘으면 더 이상 처리할 수 없습니다.

그럼 오래된 내용을 지우거나 요약해서 공간을 확보해야 합니다. 컨텍스트 관리가 없던 시절에는 어땠을까요?

대화가 길어지면 코파일럿이 먹통이 되었습니다. "토큰 제한 초과"라는 오류만 반복적으로 표시되었죠.

더 큰 문제는 사용자가 긴 코드 파일을 참조하면 즉시 한계에 도달했다는 점입니다. 실무에서는 사용 불가능한 수준이었습니다.

바로 이런 문제를 해결하기 위해 스마트한 컨텍스트 관리 전략이 등장했습니다. 컨텍스트 관리를 사용하면 긴 대화도 안정적으로 처리할 수 있습니다.

또한 중요한 정보는 유지하면서 불필요한 내용은 제거할 수 있습니다. 무엇보다 사용자 경험이 끊기지 않는다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 max_tokens 변수로 최대 토큰 수를 설정합니다.

GPT-3.5는 4K, GPT-4는 8K 또는 32K를 주로 사용합니다. add_message 메서드는 새 메시지를 추가할 때마다 총 토큰 수를 계산합니다.

여기서는 간단히 문자 수를 4로 나눠 추정했지만, 실제로는 tiktoken 같은 라이브러리를 사용합니다. 중요한 부분은 while 루프입니다.

토큰 수가 제한을 초과하면 가장 오래된 메시지부터 제거합니다. 단, 인덱스 0의 시스템 메시지는 항상 유지합니다.

이것이 코파일럿의 기본 성격과 규칙을 담고 있기 때문입니다. summarize_old_messages 메서드는 더 고급 기법입니다.

오래된 메시지를 완전히 삭제하지 않고 요약으로 대체합니다. 이렇게 하면 맥락은 유지하면서 토큰 수는 줄일 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 GitHub Copilot은 현재 편집 중인 파일과 최근 몇 개의 파일만 컨텍스트에 포함합니다.

프로젝트 전체를 넣으면 수십만 토큰이 필요하니까요. Cursor는 더 똑똑하게, 사용자가 언급한 함수나 클래스만 선택적으로 포함합니다.

이런 전략 덕분에 실시간으로 빠르게 동작할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 무조건 오래된 메시지만 삭제하는 것입니다. 이렇게 하면 중요한 맥락이 사라져서 코파일럿이 엉뚱한 코드를 생성합니다.

따라서 중요도를 계산하여 덜 중요한 메시지를 먼저 제거하거나, 요약 전략을 사용해야 합니다. 다시 김코드 씨의 이야기로 돌아가봅시다.

이아키 씨의 설명을 들은 김코드 씨는 고개를 끄덕였습니다. "아, 그래서 ChatGPT도 대화가 너무 길어지면 새 채팅을 시작하라고 하는 거군요!" 컨텍스트 윈도우 관리를 제대로 이해하면 안정적이고 확장 가능한 코파일럿을 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 시스템 메시지와 최근 3~5개 대화는 항상 유지하여 맥락을 보존하세요

  • 토큰 카운팅은 tiktoken 라이브러리 사용을 권장합니다 (정확도가 높음)
  • 파일 컨텍스트는 필요한 부분만 발췌하여 포함하면 효율적입니다

4. 스트리밍 응답으로 실시간 코드 출력

코파일럿의 기본 구조를 이해한 김코드 씨는 실제 사용 경험에 대해 고민했습니다. "사용자가 요청하고 10초를 기다리는 건 너무 답답한데요?" 이아키 씨가 웃으며 답했습니다.

"그래서 스트리밍 응답이 필요한 거예요. ChatGPT처럼 글자가 하나씩 나타나는 거 본 적 있죠?"

스트리밍 응답은 LLM이 생성한 결과를 한 번에 보내지 않고, 토큰 단위로 실시간 전송하는 기술입니다. 마치 유튜브 동영상을 전체 다운로드 없이 바로 재생하는 것처럼, 사용자는 코드가 생성되는 과정을 실시간으로 볼 수 있습니다.

이는 체감 속도를 크게 향상시키고 사용자 경험을 개선합니다.

다음 코드를 살펴봅시다.

# 스트리밍 응답 구현
import openai

class StreamingCopilot:
    def __init__(self, api_key):
        self.client = openai.OpenAI(api_key=api_key)

    def generate_code_stream(self, prompt):
        # 스트리밍 모드로 LLM 호출
        stream = self.client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            stream=True  # 핵심: 스트리밍 활성화
        )

        # 토큰을 하나씩 받아서 yield
        for chunk in stream:
            if chunk.choices[0].delta.content:
                # 실시간으로 토큰 전달
                yield chunk.choices[0].delta.content

    def display_streaming_response(self, prompt):
        # 스트리밍 응답 출력 예제
        print("코드 생성 중: ", end="", flush=True)
        for token in self.generate_code_stream(prompt):
            print(token, end="", flush=True)
        print()  # 줄바꿈

김코드 씨는 사용자 경험의 중요성을 깨달았습니다. 아무리 좋은 코드를 생성해도, 10초 동안 아무것도 안 보이면 사용자는 답답함을 느낍니다.

"스트리밍은 현대 코파일럿의 필수 기능이에요." 이아키 씨가 노트북을 열며 시연했습니다. ChatGPT 화면에서 글자가 하나씩 나타나는 모습이 보였습니다.

"이게 바로 스트리밍 응답입니다." 쉽게 비유하자면, 스트리밍은 마치 요리사가 요리를 완성하기 전에 중간 과정을 보여주는 것과 같습니다. 고객은 요리사가 재료를 썰고, 볶고, 양념하는 과정을 보면서 "아, 지금 만들고 있구나"라고 안심합니다.

완성될 때까지 깜깜한 주방 문 앞에서 기다리는 것보다 훨씬 좋은 경험이죠. 스트리밍이 없던 시절에는 어땠을까요?

사용자가 요청을 보내면 화면이 멈췄습니다. 로딩 스피너만 빙글빙글 돌았죠.

10초, 20초가 지나도 아무 반응이 없으면 사용자는 "혹시 버그인가?" 하고 새로고침을 누르곤 했습니다. 더 큰 문제는 긴 코드를 생성할 때 1분 이상 기다려야 했다는 점입니다.

바로 이런 문제를 해결하기 위해 스트리밍 프로토콜이 등장했습니다. 스트리밍을 사용하면 첫 토큰을 1초 내에 받을 수 있습니다.

또한 사용자는 생성 과정을 보며 신뢰감을 느낍니다. 무엇보다 체감 속도가 10배 이상 빨라진다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 핵심은 stream=True 파라미터입니다.

이것만 추가하면 OpenAI API가 스트리밍 모드로 동작합니다. create 메서드는 일반 응답 객체가 아니라 제너레이터를 반환합니다.

제너레이터는 파이썬에서 값을 하나씩 생성하는 특별한 함수입니다. for chunk in stream 루프에서 실시간으로 토큰을 받습니다.

각 청크는 작은 조각의 텍스트를 담고 있습니다. chunk.choices[0].delta.content에서 실제 텍스트를 추출하고, yield로 호출자에게 전달합니다.

display_streaming_response 메서드에서는 받은 토큰을 즉시 출력합니다. flush=True 파라미터가 중요합니다.

이것이 없으면 파이썬이 버퍼에 모아뒀다가 한 번에 출력하기 때문에 스트리밍 효과가 사라집니다. 실제 현업에서는 어떻게 활용할까요?

GitHub Copilot은 코드를 한 줄씩 제안하며 실시간으로 보여줍니다. Cursor는 여러 줄의 코드를 생성할 때 마치 타이핑하는 것처럼 표시합니다.

ChatGPT는 긴 설명을 생성할 때 문장이 하나씩 나타납니다. 모두 스트리밍 기술을 사용합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 네트워크 버퍼링을 고려하지 않는 것입니다.

웹 프레임워크에 따라 응답이 버퍼링되어 스트리밍 효과가 사라질 수 있습니다. FastAPI에서는 StreamingResponse를, Flask에서는 stream_with_context를 사용해야 합니다.

다시 김코드 씨의 이야기로 돌아가봅시다. 이아키 씨의 시연을 본 김코드 씨는 감탄했습니다.

"와, 이렇게 하니까 훨씬 빠르게 느껴지네요!" 스트리밍 응답을 제대로 구현하면 사용자 만족도를 크게 높일 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 첫 토큰 응답 시간이 1초 이하가 되도록 최적화하면 체감 속도가 매우 빨라집니다

  • 프론트엔드에서 타이핑 애니메이션 추가하면 더 자연스러운 경험을 제공합니다
  • 에러 발생 시에도 스트리밍으로 전달하여 사용자가 무엇이 잘못됐는지 바로 알 수 있게 하세요

5. 실습 FastAPI로 코드 생성 API 서버 구축

드디어 실전 실습 시간이 왔습니다. 김코드 씨는 지금까지 배운 내용을 직접 구현해볼 차례였습니다.

"이제 실제로 동작하는 코파일럿 API 서버를 만들어볼까요?" 이아키 씨가 말했습니다. "FastAPI를 사용하면 30분 안에 만들 수 있어요."

FastAPI는 파이썬 기반의 고성능 웹 프레임워크로, 비동기 처리와 자동 문서화를 지원합니다. 코파일럿 API 서버를 만들기에 최적의 도구입니다.

스트리밍 응답, 비동기 LLM 호출, 웹소켓 등 현대적인 기능을 쉽게 구현할 수 있습니다.

다음 코드를 살펴봅시다.

# FastAPI 기반 코드 생성 API 서버
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import openai
import os

app = FastAPI(title="Code Copilot API")

class CodeRequest(BaseModel):
    prompt: str
    language: str = "python"

class CopilotService:
    def __init__(self):
        self.client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

    async def generate_code_stream(self, prompt: str, language: str):
        # 언어별 시스템 프롬프트
        system_prompt = f"You are an expert {language} programmer. Generate clean, well-commented code."

        # 스트리밍 모드로 LLM 호출
        stream = self.client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": prompt}
            ],
            stream=True
        )

        # 토큰 실시간 전달
        for chunk in stream:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content

copilot = CopilotService()

@app.post("/generate")
async def generate_code(request: CodeRequest):
    # 스트리밍 응답 반환
    return StreamingResponse(
        copilot.generate_code_stream(request.prompt, request.language),
        media_type="text/plain"
    )

@app.get("/health")
async def health_check():
    return {"status": "healthy"}

# 실행: uvicorn main:app --reload

김코드 씨는 드디어 키보드 앞에 앉았습니다. 지금까지는 개념만 배웠지만, 이제는 직접 코드를 작성할 차례였습니다.

"FastAPI는 정말 강력한 도구예요." 이아키 씨가 설명을 시작했습니다. "플라스크보다 빠르고, 장고보다 간단하죠.

게다가 자동으로 API 문서까지 만들어줍니다." 쉽게 비유하자면, FastAPI는 마치 레고 블록과 같습니다. 복잡한 설정 없이 블록을 조립하듯이 API를 만들 수 있습니다.

데이터 검증도 자동이고, 비동기 처리도 간단합니다. 초보자도 몇 줄만 작성하면 프로덕션 수준의 API를 만들 수 있죠.

FastAPI가 없던 시절에는 어땠을까요? Flask로 API를 만들면 데이터 검증을 직접 작성해야 했습니다.

타입 �힌트도 없어서 버그가 자주 발생했죠. 더 큰 문제는 비동기 처리가 복잡했다는 점입니다.

LLM 호출처럼 시간이 오래 걸리는 작업을 처리하려면 Celery 같은 별도 도구가 필요했습니다. 바로 이런 문제를 해결하기 위해 FastAPI가 등장했습니다.

FastAPI를 사용하면 타입 안전성이 보장됩니다. 또한 자동 문서화 덕분에 프론트엔드 개발자와 협업하기 쉽습니다.

무엇보다 비동기 처리가 기본으로 제공되어 성능이 뛰어나다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 CodeRequest 클래스는 Pydantic 모델입니다. 이것이 자동으로 입력 데이터를 검증합니다.

prompt는 필수, language는 선택 사항이며 기본값은 "python"입니다. CopilotService 클래스는 실제 코드 생성 로직을 담당합니다.

generate_code_stream 메서드는 async로 선언되어 비동기로 동작합니다. 이것이 중요합니다.

LLM 호출은 네트워크 I/O 작업이므로 비동기로 처리하면 서버가 다른 요청도 동시에 처리할 수 있습니다. 시스템 프롬프트에서 언어별로 다른 지시를 줍니다.

파이썬이면 "expert Python programmer", 자바스크립트면 "expert JavaScript programmer"가 되는 식이죠. 이렇게 하면 언어에 맞는 스타일의 코드가 생성됩니다.

@app.post("/generate") 데코레이터는 POST 엔드포인트를 정의합니다. StreamingResponse를 반환하여 스트리밍 응답을 구현합니다.

media_type="text/plain"으로 설정하면 브라우저에서 바로 확인할 수 있습니다. /health 엔드포인트는 서버 상태를 확인하는 용도입니다.

로드밸런서나 모니터링 시스템에서 주기적으로 호출하여 서버가 살아있는지 체크합니다. 실제 현업에서는 어떻게 활용할까요?

이 API 서버를 배포하면 VSCode 확장 프로그램, 웹 에디터, 모바일 앱 등 어디서든 호출할 수 있습니다. 예를 들어 Replit 같은 온라인 에디터는 이런 API를 사용하여 코드 자동완성을 제공합니다.

사내 개발 도구를 만들 때도 이 구조를 그대로 사용할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 API 키를 코드에 하드코딩하는 것입니다. 이렇게 하면 보안 위험이 발생합니다.

따라서 반드시 환경 변수(os.getenv)를 사용해야 합니다. 또한 운영 환경에서는 Rate Limiting을 추가하여 남용을 방지해야 합니다.

다시 김코드 씨의 이야기로 돌아가봅시다. 코드를 작성하고 uvicorn main:app --reload로 실행한 김코드 씨는 감탄했습니다.

"와, 정말 되네요! 브라우저에서 /docs로 가니까 API 문서까지 자동으로 생성되어 있어요!" FastAPI로 코파일럿 API를 제대로 구축하면 확장 가능하고 유지보수하기 쉬운 시스템을 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - /docs 엔드포인트로 자동 생성된 Swagger UI를 확인하면 테스트하기 편합니다

  • CORS 설정 추가로 프론트엔드에서 API 호출 가능하게 하세요 (app.add_middleware(CORSMiddleware))
  • Docker로 컨테이너화하면 배포가 훨씬 쉬워집니다

6. 실습 간단한 웹 인터페이스 만들기

API 서버는 완성했지만, 일반 사용자가 사용하기에는 불편했습니다. 터미널에서 curl 명령어를 입력해야 했으니까요.

"이제 프론트엔드를 만들어볼까요?" 이아키 씨가 제안했습니다. "HTML과 JavaScript만으로도 충분히 멋진 인터페이스를 만들 수 있어요."

코파일럿의 완성은 사용자 친화적인 인터페이스입니다. 간단한 HTML, CSS, JavaScript만으로 ChatGPT 스타일의 채팅 UI를 만들 수 있습니다.

핵심은 Fetch API로 스트리밍 응답을 처리하는 것입니다. TextDecoder를 사용하여 바이트 스트림을 텍스트로 변환하고, 실시간으로 화면에 표시합니다.

다음 코드를 살펴봅시다.

<!-- 코파일럿 웹 인터페이스 -->
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>AI Code Copilot</title>
    <style>
        body { font-family: Arial; max-width: 800px; margin: 50px auto; }
        #output { border: 1px solid #ccc; padding: 20px; min-height: 300px;
                  background: #f9f9f9; white-space: pre-wrap; }
        #prompt { width: 100%; padding: 10px; font-size: 16px; }
        button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>AI Code Copilot</h1>
    <textarea id="prompt" rows="3" placeholder="코드 생성 요청을 입력하세요..."></textarea>
    <button onclick="generateCode()">코드 생성</button>
    <div id="output"></div>

    <script>
        async function generateCode() {
            const prompt = document.getElementById('prompt').value;
            const output = document.getElementById('output');
            output.textContent = '';  // 초기화

            // 스트리밍 요청
            const response = await fetch('http://localhost:8000/generate', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ prompt, language: 'python' })
            });

            // 스트림 읽기
            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;

                // 바이트를 텍스트로 변환하여 실시간 표시
                const text = decoder.decode(value, { stream: true });
                output.textContent += text;
            }
        }
    </script>
</body>
</html>

김코드 씨는 이제 마지막 퍼즐 조각을 맞출 차례였습니다. 아무리 좋은 API가 있어도, 사용자가 쉽게 접근할 수 있는 인터페이스가 없으면 무용지물입니다.

"프론트엔드는 생각보다 간단해요." 이아키 씨가 코드를 작성하기 시작했습니다. "React나 Vue 같은 프레임워크 없이도 충분합니다." 쉽게 비유하자면, 웹 인터페이스는 마치 자판기의 버튼과 디스플레이와 같습니다.

사용자는 복잡한 내부 메커니즘을 몰라도, 버튼만 누르면 원하는 결과를 얻습니다. 코파일럿도 똑같이, 사용자는 텍스트를 입력하고 버튼을 누르면 코드가 생성되는 것을 봅니다.

웹 인터페이스가 없던 시절에는 어땠을까요? 개발자만 사용할 수 있었습니다.

curl 명령어나 Postman 같은 도구를 알아야 했죠. 더 큰 문제는 스트리밍 응답을 확인하기 어려웠다는 점입니다.

터미널에서는 실시간으로 출력되지만, 일반 사용자는 이런 환경에 익숙하지 않습니다. 바로 이런 문제를 해결하기 위해 브라우저 기반 UI가 필수가 되었습니다.

웹 인터페이스를 사용하면 누구나 쉽게 접근할 수 있습니다. 또한 스트리밍 효과를 시각적으로 보여줄 수 있습니다.

무엇보다 별도 설치 없이 브라우저만 있으면 된다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 HTML 구조는 매우 단순합니다. textarea로 사용자 입력을 받고, button으로 요청을 트리거하고, div에 결과를 표시합니다.

CSS도 최소한으로만 작성했습니다. white-space: pre-wrap이 중요한데, 이것이 코드의 들여쓰기와 줄바꿈을 그대로 유지합니다.

JavaScript의 generateCode 함수가 핵심입니다. fetch로 POST 요청을 보냅니다.

body에 JSON 형태로 프롬프트와 언어를 담습니다. 일반적인 fetch와 다른 점은 응답을 스트림으로 읽는다는 것입니다.

response.body.getReader()로 스트림 리더를 얻습니다. 이것이 서버에서 전송하는 데이터를 조금씩 읽을 수 있게 해줍니다.

TextDecoder는 바이트 배열을 문자열로 변환하는 역할을 합니다. while (true) 루프에서 스트림을 계속 읽습니다.

reader.read()는 Promise를 반환하므로 await로 기다립니다. done이 true가 되면 스트림이 끝난 것이므로 루프를 종료합니다.

그렇지 않으면 value를 텍스트로 변환하여 output에 추가합니다. { stream: true } 옵션이 중요합니다.

이것이 없으면 마지막 청크가 잘릴 수 있습니다. 디코더에게 "아직 더 데이터가 올 거야"라고 알려주는 역할입니다.

실제 현업에서는 어떻게 활용할까요? 이 코드를 기반으로 코드 하이라이팅을 추가할 수 있습니다.

highlight.js나 Prism.js를 사용하면 생성된 코드가 색깔이 입혀져 보기 좋아집니다. 또한 복사 버튼, 다운로드 버튼, 언어 선택 드롭다운 등을 추가하면 완전한 제품 수준의 UI가 됩니다.

ChatGPT, Claude, Cursor의 웹 인터페이스 모두 이런 기본 구조에서 시작했습니다. 물론 그 위에 React나 Next.js로 고도화했지만, 핵심 원리는 동일합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 CORS 문제를 해결하지 않는 것입니다.

브라우저에서 다른 도메인의 API를 호출하면 CORS 오류가 발생합니다. FastAPI 서버에 CORSMiddleware를 추가해야 합니다.

또한 에러 처리를 추가하여 네트워크 오류나 API 오류를 사용자에게 알려야 합니다. 다시 김코드 씨의 이야기로 돌아가봅시다.

HTML 파일을 브라우저로 열고 테스트한 김코드 씨는 환호했습니다. "와, ChatGPT처럼 글자가 하나씩 나타나요!

제가 만든 코파일럿이에요!" 웹 인터페이스를 제대로 만들면 전문 개발자가 아닌 사람도 쉽게 사용할 수 있는 도구가 됩니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - highlight.js 라이브러리 추가로 코드 신택스 하이라이팅을 쉽게 구현할 수 있습니다

  • 로딩 인디케이터 추가로 사용자에게 "생성 중"임을 명확히 알려주세요
  • localStorage에 히스토리 저장하면 이전 요청을 다시 볼 수 있어 편리합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#FastAPI#LLM#AI코파일럿#스트리밍API#LLM,아키텍처,시스템설계

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.