본문 바로가기

Vibe Coding

[리팩토링] LLM으로 테스트 불가능한 구조를 개선하기

Disclamer: 이 문서는 사내용 세미나 진행을 위해, 사외에서 취득 가능한 정보만를 활용하여 사외에서 작성되었습니다.

◼︎ 문제는 테스트할 수가 없다는 것입니다.

대부분의 코드에서 가장 흔히 부딪히는 문제는 "테스트할 수 없는 구조"입니다.

데이터베이스 연동, 외부 API 호출, 파일 핸들링 등이 코드에 박혀있으면 단위 테스트는 사실상 불가능합니다.

이런 문제를 근본적으로 해결하기 위해서는 다음과 같은 활동이 필요합니다.

  • 의존성 분리
  • 인터페이스 추출
  • 추상화 계층 설계

이러한 활동은 프로젝트 구조를 충분히 이해하고, 경험이 많은 개발자가 수동으로 직접 해내야 했습니다. 불가능한 것은 아니지만 비용이 크고 시간이 오래 걸리는 활동입니다.

이러한 활동을 기반으로 테스트 가능한 구조를 확보하는 리팩토링을, 이제는 LLM을 기반으로 빠르게 수행할 수 있습니다.

◼︎ LLM을 활용한 구조 개선 접근 방법

앞선 문서에서 계속 살펴보고 있는 것 처럼, LLM에게 단순히 "테스트 가능하게 구조를 바꿔줘"와 같이 요청하면, 모호한 추상화나 겉보기에만 화려한 불필요한 패턴들이 덧붙을 수 있습니다. 원하는 결과를 얻기 위해서는 구체적인 리팩토링 목적과 맥락을 함께 전달해야 합니다.

"OOO 코드를 테스트 가능한 구조로 바꿔줘.
DB 접근 로직은 인터페이스로 추상화하고, Python + SQLAlchemy 환경에 맞게 작성해줘"

◼︎ LLM의 장점을 최대로 활용하기

LLM이 대규모 코드베이스를 컨텍스트로 활용할 수 있다는 점을 적극적으로 활용하면 아래와 같은 베네핏을 얻을 수 있습니다.

  • MSA와 같이 다수의 프로젝트가 얽힌 환경에서 테스트 가능한 구조를 빠르게 얻을 수 있습니다.
  • 한 명이 여러개의 프로젝트를 개발, 운영하는 경우 코드 리팩토링 비용을 크게 줄일 수 있습니다. 
  • 기존 운영 코드에 테스트를 붙이기 전에, 대략적인 구조 변경 결과를 예측하기 위한 "초안"으로 활용하기 좋습니다.

다만 주의할 점도 있습니다.

  • 추상화 수준이 너무 지나치거나, 실제 환경에는 적용하기 어려울 수 있습니다.
  • 따라서 반드시 프롬프트에 충분한 목적, 환경, 제약 사항을 포함해야 합니다. 예를 들어
    • 언어: Python 3.10 (버전 명시)
    • 프레임워크: FastAPI 0.115
    • DB: MySQL 8.0.33, pymysql
  • 프로젝트 제약 사항에 대한 충분한 컨텍스르를 제공하면, 리팩토링 결과에 대한 불필요한 복잡도를 피할 수 있습니다.

◼︎ 예제: 테스트가 불가능한 코드 개선하기

DB 접근 로직을 처리하는 아래와 같은 코드가 있습니다. 이 코드는 다음 몇가지 문제점이 있습니다.

  • DB 연결/쿼리 로직이 비즈니스 로직에 직접 포함됨
  • 단위 테스트에서 DB를 접근해야함 > 느리고 불안정함
  • mocking이 불가능함 > 테스트 작성이 어려움
import pymysql

def get_user(user_id: int):
    conn = pymysql.connect(
        host="localhost",
        user="root",
        password="password",
        database="app_db"
    )
    cur = conn.cursor()
    cur.execute("SELECT id, name FROM users WHERE id=%s", (user_id,))
    row = cur.fetchone()
    conn.close()
    return {"id": row[0], "name": row[1]}

 

리팩토링을 위한 프롬프트를 작성합니다. 이번에도 구조화된 프롬프트를 사용합니다.

목적: 이 코드를 테스트 가능한 구조로 리팩토링하세요
코드: 
(붙여 너힉)
환경:
(1) Python 3.10+
(2) DB: MySQL 8.0.33
(3) 라이브러리: pymysql
(4) 테스트 프레임워크: pytest

요구사항:
1. DB 접근 로직을 abc.ABC 기반 인터페이스로 추상화해주세요.
2. 서비스 로직은 인터페이스에 의존하도록 변경해주세요.
3. 단위 테스트는 DB 대신 fake repository를 주입하도록 변경하세요.
4. 실제 구현은 MySQL + pymysql을 사용하세요.

출력형식:
1. 인터페이스 정의 코드
2. mysql 구현체 코드
3. 서비스 로직 코드
4. 단위 테스트 코드 (Fake Repository 활용)

 

LLM에게 프롬프트를 입력하고 결과를 확인합니다. 출력 형식에서 4개의 파일 출력 형식을 지정했기 때문에 아래와 같은 결과를 얻을 수 있습니다.

결과1. 인터페이스 정의 코드

from abc import ABC, abstractmethod
from typing import Dict

class UserRepository(ABC):
    @abstractmethod
    def get_user(self, user_id: int) -> Dict:
        pass

결과2. mysql 구현체 코드

import pymysql
from typing import Dict
from db_interface import UserRepository

class MySQLUserRepository(UserRepository):
    def __init__(self, host="localhost", user="root", password="password", database="app_db"):
        self.host = host
        self.user = user
        self.password = password
        self.database = database

    def get_user(self, user_id: int) -> Dict:
        conn = pymysql.connect(
            host=self.host,
            user=self.user,
            password=self.password,
            database=self.database
        )
        cur = conn.cursor()
        cur.execute("SELECT id, name FROM users WHERE id=%s", (user_id,))
        row = cur.fetchone()
        conn.close()
        if not row:
            return {}
        return {"id": row[0], "name": row[1]}

결과3. 서비스 로직 코드

from typing import Dict
from db_interface import UserRepository

class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

    def get_user_info(self, user_id: int) -> Dict:
        return self.repo.get_user(user_id)

결과4. 단위 테스트 코드 (Fake Repository 활용)

from user_service import UserService
from db_interface import UserRepository

class FakeUserRepo(UserRepository):
    def get_user(self, user_id: int):
        return {"id": user_id, "name": "Test User"}

def test_get_user_info():
    service = UserService(FakeUserRepo())
    user = service.get_user_info(123)
    assert user["name"] == "Test User"

◼︎ Wrap-up

테스트 불가능한 구조를 LLM으로 개선하는 과정은 단순히 "코드를 깔끔하게 정리"하는 차원을 넘어서, 운영 환경에서 안정적으로 유지 보수 가능한 구조를 확보하는데 의미가 있습니다.

  • 개발자가 직접 수행하는 방식은 경험 많은 개발자가 직접 의존성 분리, 인터페이스추출을 설계해야 했습니다. 시간이 오래 걸리고, 여러 서비스 개발을 동시에 담당하는 상황에서는 현실적으로 불가능한 방법입니다.
  • LLM을 활용하면 "DB 접근을 인터페이스로 추상화 해줘"처럼 구체적인 요구를 전달해서, 빠르게 구조적 개선안을 확보할 수 있습니다.
  • 특히 우리 프로젝트와 같이 단위 테스트가 없거나 부족한 운영중인 코드라는 현실적인 제약 속에서, LLM은 짧은 시간 안에 테스트 가능한 구조를 제안해주는 강력한 도구로 활용할 수 있습니다.
  • 하지만 실제로 사용해보면 추상화 수준이 과도하거나, 실제 프레임워크/DB와 맞지 않는 코드를 제시할 수 있다는 점에서 항상 주의 해야 합니다. 따라서 반드시 "우리 프로젝트의 환경 정보, 제약 사항"을 프롬프트에 포함시켜야 합니다.

즉, LLM은 설계를 대신하는 것이 아니라, 설계/리팩토링 과정에서 초안을 빠르게 만들어주는 역할을 주로 수행하고, 개발자는 이 초안을 검토하고, 실제 서비스와 맞게 다듬으면 됩니다.

 

LLM이 리팩토링 과정을 도울 수는 있지만, 최종적으로 책임을 지는 것은 개발자라는 것을 기억해야겠습니다.

이러한 리스크를 인지하고, LLM을 안전하고 효과적으로 활용하는 것이 실무에서 필요한 바이브 코딩 역량이라 생각합니다.

 

다음 글에서는 기존 코드를 테스트 가능한 형태로 리팩토링하는 몇 가지 사례에 대해 알아보겠습니다.

 

좋아요 & 댓글 많이 남겨주세요~~