우아한테크코스 프리코스 스타일의 TDD 연습 문제를 AI로 자동 생성하는 시스템
우아한테크코스 프리코스를 진행하면서 TDD의 가치를 체감했습니다. 하지만 우테코 스타일의 연습 문제가 더 필요했고, "그럼 AI로 만들어보자!"라고 생각했습니다.
TeDDie는 세 가지 핵심 컴포넌트로 구성된 완전 자동화된 TDD 문제 생성 시스템입니다:
- 실제 우테코 과제를 RAG로 학습 (TeDDie-RagSystem)
- 비슷한 스타일의 새로운 문제 생성 (LLM + RAG API)
- 바로 풀 수 있는 프로젝트 자동 생성 (TeDDie Java Application)
- ✅ 완전 자동화: CLI 명령 하나로 프로젝트 생성부터 테스트 코드까지
- ✅ 우테코 스타일 재현: RAG로 실제 프리코스 과제 학습
- ✅ 로컬 실행: LM Studio로 비용 없이 무제한 생성
- ✅ TDD 중심: 생성된 테스트 케이스가 자동으로 JUnit 코드로 변환
- ✅ Java 생태계: 우테코 환경과 동일한 Java 21 + Gradle 프로젝트
TeDDie는 3-Tier 하이브리드 아키텍처를 채택했습니다:
┌─────────────────────────────────────────────────────────────────┐
│ TeDDie (Java Application) │
│ │
│ 사용자 입력 처리 → 미션 생성 요청 → 프로젝트 자동 생성 │
│ │
│ • CLI 인터페이스 (--topic, --difficulty) │
│ • HTTP Client (Java 21 HttpClient) │
│ • Project Generator (Gradle 프로젝트 생성) │
│ • Test Code Generator (JUnit 코드 자동 생성) │
│ │
└────────────┬──────────────────────────────┬────────────────────┘
│ │
▼ ▼
┌────────────────┐ ┌─────────────────┐
│ LM Studio │ │ TeDDie-RagAPI │
│ (로컬 LLM) │ │ (FastAPI) │
│ Port 1234 │ │ Port 8000 │
└────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ TeDDie-RagSystem│
│ (Python Core) │
│ │
│ • FAISS Index │
│ • Embeddings │
│ • Search Engine │
└─────────────────┘
초기 고민:
- Java만으로 RAG를 구현? → FAISS, sentence-transformers가 Python에 더 성숙
- Python만 사용? → 우테코가 Java 중심, TDD 연습도 Java로 하고 싶음
결정:
- Java: 메인 애플리케이션, TDD 연습, 도메인 로직
- Python: RAG 시스템 (벡터 검색, 임베딩)
- FastAPI: Java ↔ Python 브릿지
./gradlew run --args="--topic lotto --difficulty medium"실행 결과:
- 바탕화면에
java-lotto프로젝트 자동 생성 - README.md에 미션 내용 작성
- ApplicationTest.java에 테스트 코드 자동 생성
- 바로 실행 가능한 Gradle 프로젝트
TeDDie는 우테코 프리코스 과제를 RAG로 학습하여, 주제와 관련된 과제를 자동으로 찾아 프롬프트에 포함합니다.
검색 가능한 과제:
- java-racingcar (자동차 경주)
- java-lotto (로또)
- java-baseball (숫자 야구)
- java-christmas (크리스마스 프로모션)
- java-oncall (비상 근무)
- 그 외 10+ 과제
LLM이 생성한 테스트 케이스를 실제 JUnit 코드로 자동 변환합니다.
LLM 응답 (테스트 케이스 섹션):
## 테스트 케이스
### 기능 테스트
- 입력: pobi,woni\n1
- 출력: pobi : -
### 예외 테스트
- 입력: pobi,javaji\n1자동 생성되는 코드:
package racingcar;
import camp.nextstep.edu.missionutils.test.NsTest;
import org.junit.jupiter.api.Test;
import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ApplicationTest extends NsTest {
@Test
void 기능_테스트() {
assertSimpleTest(() -> {
run("pobi,woni\n1");
assertThat(output()).contains("pobi : -");
});
}
@Test
void 예외_테스트() {
assertSimpleTest(() ->
assertThatThrownBy(() -> runException("pobi,javaji\n1"))
.isInstanceOf(IllegalArgumentException.class)
);
}
@Override
public void runMain() {
Application.main(new String[]{});
}
}생성되는 프로젝트는 우테코 프리코스와 동일한 구조를 가집니다:
java-lotto/
├── build.gradle
├── settings.gradle
├── gradlew
├── gradlew.bat
├── README.md # 미션 내용
├── src/
│ ├── main/
│ │ └── java/
│ │ └── lotto/
│ │ └── Application.java
│ └── test/
│ └── java/
│ └── lotto/
│ └── ApplicationTest.java # 자동 생성된 테스트
└── gradle/
└── wrapper/
| 분류 | 기술 | 버전 | 용도 |
|---|---|---|---|
| Language | Java | 21 | 메인 애플리케이션 |
| Build Tool | Gradle | 8.14 | 빌드 자동화 |
| Testing | JUnit 5 | 5.10.0 | 단위 테스트 |
| AssertJ | 3.26.0 | 테스트 Assertion | |
| Mockito | 4.3.1 | Mock 객체 | |
| MockWebServer | 4.12.0 | HTTP 테스트 | |
| Libraries | Gson | 2.10.1 | JSON 파싱 |
| java-dotenv | 5.2.2 | 환경 변수 관리 |
| 분류 | 기술 | 버전 | 용도 |
|---|---|---|---|
| Language | Python | 3.10+ | RAG 시스템 |
| Vector Search | FAISS | - | 벡터 유사도 검색 |
| Embeddings | sentence-transformers | - | 텍스트 임베딩 |
| paraphrase-multilingual-MiniLM-L12-v2 | - | 다국어 임베딩 모델 | |
| Testing | pytest | 9.0.0 | 테스트 프레임워크 |
| 분류 | 기술 | 버전 | 용도 |
|---|---|---|---|
| Framework | FastAPI | 0.104.1 | REST API 서버 |
| Server | Uvicorn | 0.24.0 | ASGI 서버 |
| Validation | Pydantic | 2.6.4 | 데이터 검증 |
| Testing | pytest | 9.0.0 | API 테스트 |
| httpx | 0.25.2 | HTTP 클라이언트 | |
| Config | python-dotenv | 1.0.1 | 환경 변수 |
| 항목 | 내용 |
|---|---|
| 플랫폼 | LM Studio (GUI) |
| 포트 | 1234 |
| 프로토콜 | HTTP/1.1 (OpenAI Compatible) |
| 사용 모델 | a.x-4.0-light |
- Java 21 (JDK 21)
- Python 3.10+
- LM Studio (로컬 LLM 실행용)
- Gradle (프로젝트에 포함)
# 메인 프로젝트
git clone https://github.com/your-username/TeDDie.git
cd TeDDie
# RAG 시스템 (서브모듈 또는 별도 클론)
git clone https://github.com/your-username/TeDDie-RagSystem.git
git clone https://github.com/your-username/TeDDie-RagAPI.gitcd TeDDie-RagSystem
pip install -r requirements.txt
python -m crawler.CrawlWoowacourseRagDataset결과: woowacourse_rag_dataset.jsonl 생성
from rag.RagSearch import WoowacourseRAG
rag = WoowacourseRAG()
rag.build_index()
rag.save_index("faiss_index.bin")cd ../TeDDie-RagAPI
pip install -r requirements.txt
python main.py접속 확인: http://localhost:8000/docs
- LM Studio 다운로드 및 설치
- 모델 다운로드 (오픈소스 모델: a.x-4.0-light)
- Local Server 시작
- 포트 확인:
http://localhost:1234
프로젝트 루트에 .env 파일 생성:
LM_STUDIO_URL=http://localhost:1234/v1/chat/completions
RAG_API_URL=http://localhost:8000/api/searchcd TeDDie
./gradlew clean build
./gradlew test./gradlew run --args="--topic [주제] --difficulty [난이도]"| 옵션 | 설명 | 필수 | 예시 |
|---|---|---|---|
--topic |
생성할 문제의 주제 | ✅ | lotto, string, collection |
--difficulty |
난이도 | ✅ | easy, medium, hard |
./gradlew run --args="--topic lotto --difficulty medium"결과:
🧸 TeDDie: 문제 생성 완료!
--- (미션 내용) ---
# 🧩 로또 게임
## 기능 요구 사항
- 로또 번호 생성
- 당첨 번호 입력
- 당첨 통계 출력
...
____________________
✅ 프로젝트 생성: ~/Desktop/java-lotto
./gradlew run --args="--topic racing --difficulty easy"./gradlew run --args="--topic string --difficulty hard"cd ~/Desktop/java-lotto
./gradlew testTeDDie-Project/
│
├── TeDDie/ # Java 메인 애플리케이션
│ ├── src/main/java/teddie/
│ │ ├── Application.java
│ │ ├── api/ # API 통신
│ │ │ ├── RagClient.java
│ │ │ └── dto/
│ │ ├── common/ # 공통 설정
│ │ │ ├── config/
│ │ │ └── util/
│ │ ├── controller/ # 흐름 제어
│ │ ├── domain/ # 도메인 모델
│ │ ├── exception/ # 예외 처리
│ │ ├── generator/ # 프로젝트 생성
│ │ ├── prompt/ # 프롬프트 관리
│ │ ├── service/ # 비즈니스 로직
│ │ └── view/ # 출력 담당
│ ├── src/test/java/teddie/
│ ├── src/main/resources/template/
│ ├── build.gradle
│ └── README.md
│
├── TeDDie-RagSystem/ # Python RAG 코어
│ ├── rag/
│ │ ├── Loader.py
│ │ ├── Embedder.py
│ │ ├── SearchEngine.py
│ │ └── RagSearch.py
│ ├── crawler/
│ │ └── CrawlWoowacourseRagDataset.py
│ ├── test/
│ ├── requirements.txt
│ └── README.md
│
└── TeDDie-RagAPI/ # FastAPI 서버
├── app.py
├── main.py
├── controller/
├── domain/
├── infra/
├── service/
├── util/
├── test/
├── requirements.txt
└── README.md
역할: 사용자 인터페이스, 프로젝트 생성, 테스트 코드 생성
핵심 클래스:
TeDDieController: CLI 인터페이스 및 전체 흐름 제어MissionService: LLM 호출 및 응답 파싱TestGenerator: JUnit 테스트 코드 자동 생성ProjectGeneratorController: Gradle 프로젝트 생성
역할: 우테코 과제 데이터 임베딩 및 유사도 검색
핵심 모듈:
DocumentLoader: JSONL 파일 로드Embedder: 텍스트 → 벡터 변환FaissSearchEngine: FAISS 인덱스 관리 및 검색WoowacourseRAG: Facade 패턴 (통합 인터페이스)
역할: Java ↔ Python 브릿지
주요 엔드포인트:
POST /api/search: RAG 검색 요청GET /health: 서버 상태 확인
문제: LM Studio가 HTTP/2를 완벽 지원하지 않아 연결 실패
해결:
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1) // ✅ 명시
.build();이유:
- 불변 객체로 DTO 안전성 보장
- getter/setter 없이 깔끔한 구조
- Java 17+의 현대적 기능 활용
예시:
public record ApiRequest(
String model,
List<ApiMessage> messages,
double temperature,
int max_tokens,
boolean stream
) {}고민: LLM 응답을 어떻게 구조화할까?
해결:
public record MissionResponse(
String mission, // README용
List<TestCase> testCases // ApplicationTest용
)장점:
- 한 번 파싱으로 두 가지 결과 획득
- Controller에서 용도별로 깔끔하게 분리
Red → Green → Refactor
테스트 구조:
- 단위 테스트: 각 클래스 메서드별 테스트
- 통합 테스트: Controller-Service-View 연동
- MockWebServer: HTTP 통신 시뮬레이션
@Test
void LM_Studio로_POST_요청_시_응답_본문_반환() {
// given
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody("{\"response\":\"성공\"}"));
// when
String result = sender.postToLmStudio("{}");
// then
assertThat(result).isEqualTo("{\"response\":\"성공\"}");
}- ✅ 한 메서드에 한 단계 들여쓰기
- ✅ else 예약어 사용 금지
- ✅ 원시값과 문자열 포장
- ✅ 3개 이하 인스턴스 변수
- ✅ getter/setter 없이 구현
- ✅ 메서드 인자 3개 이하
- ✅ 코드 한 줄에 점(.) 하나
하이브리드 구조의 장점:
- Java: 객체지향 설계, TDD 연습
- Python: ML/AI 생태계 활용
- FastAPI: 효율적인 브릿지
LLM 출력의 불확실성:
- 프롬프트로 100% 제어 불가
- 방어적 파싱 필수
- 정규식 + 후처리 조합
예시:
private String cleanTestValue(String value) {
value = value.replaceAll("```[a-z]*\\n?", ""); // 코드 블록 제거
value = value.replaceAll("\\([^)]+\\)", ""); // 괄호 제거
return value.split("\n")[0].trim(); // 첫 줄만
}Mock 전략:
- MockWebServer: HTTP 레이어 테스트
- Mockito: 비즈니스 로직 집중
결과:
- 안정적인 리팩토링
- 빠른 버그 발견
- 웹 인터페이스 추가 (React)
- 사용자 피드백 시스템
- 생성된 문제 공유 플랫폼
- 다양한 프로그래밍 언어 지원
정용태 (@jyt6640)
TeDDie와 함께 TDD를 연습하세요! 🧸