Search

TADD: AI를 활용한 새로운 TDD 방법론

subtitle
TADD: Test + AI Driven Development
Tags
test
개발 일반
AI
Created
2025/03/11
2 more properties

TADD: Test AI Driven Development

“시간이 부족해서 테스트 코드 짤 시간이 없어요”는 이제 변명이 되지 못할 것 같다.
TDD에 AI가 결합하면 더 빠르고 안전하게 코드를 짤 수 있기 때문이다.
TDD가 보편화되어있지만, AI를 더해 TADD(Test + AI Driven Development)라는 용어를 만들어보았다.
기존의 TDD는 테스트를 먼저 짜고 개발하는 방식이지만, AI가 코드를 생성하면서 "버그가 없는 코드"를 짜도록 유도할 수 있음.
AI를 단순히 코드 생성용으로 쓰는 것이 아니라, "테스트를 기준으로 AI에게 코드 생성을 시키는 것" 이 핵심!
AI로 코드를 생성하는 일은 쉬워졌지만, 어떻게 명령을 내리느냐에 따라 AI가 생성하는 코드에도 버그가 있기 마련이다. AI를 통해 더 빠르고 안전하게 코드를 생성하려면, TDD로 테스트를 먼저 짠 다음 AI 에게 테스트케이스에 모두 부합하는 코드를 짜게 하면 된다.
1.
예시를 통해 TDD를 하지 않고 AI로 코드를 먼저 짜는 것과, TDD를 하고 AI로 코드를 짜는 것의 차이를 보고
2.
어떻게 테스트 코드와 AI로 빠르게 코드를 짤 수 있는지 알아보자.

TADD 방법

결론부터 얘기하면 TADD는 아래 세 단계로 이루어진다.
인터페이스를 먼저 정의한다.
마치 글을 쓰듯이 함수 시그니처만 작성하고, 내부 로직은 비워둔다.
테스트 케이스를 먼저 작성한다.
예상되는 동작을 테스트 케이스로 작성해서, AI가 엉뚱한 코드를 짜지 않도록 한다.
AI에게 테스트에 맞는 코드를 작성하게 한다.
AI가 테스트를 통과하는 코드를 짜게 하고, 이후 필요하면 수정한다.
장점
AI가 엉뚱한 로직을 짜는 걸 방지할 수 있다.
내가 원하는 스펙을 정확하게 반영한다.
디버깅 시간을 줄이고 더 안전하게 개발할 수 있다.
이제 내가 실무에서 TADD를 한 실제 예시를 통해 각 단계별 코딩 방법과
TADD를 했을 때와 안했을 때의 차이를 알아보자.

1단계: 인터페이스를 먼저 작성한다.

요구사항: 채팅 메시지 UI 구현

flat한 구조의 Message 배열 데이터를 다음과 같은 UI로 보여주어야 한다.

buildChatMessages

1.
메시지 사이에 날짜가 달라지면 날짜 구분선을 넣어주어야 한다.
2.
같은 사람이 보낸 메시지는 프로필 밑에 말풍선을 묶어서 보여준다. (내 메시지는 프로필을 보여주지 않는다.)
3.
같은 사람이 보낸 메시지 안에서 메시지 생성 날짜의 hh:mm 이 달라지면 분 단위로 나누어서 보여준다.

buildChatMessages spec

플랫한 구조의 데이터를 첫번째는 날짜로 묶고, 두번째는 보낸 사람으로 묶고, 세번째는 분 단위로 묶어야 한다.
하지만 플랫한 구조를 3번의 중첩된 구조로 가공하면 컴포넌트 단에서도 세 번의 중첩에 걸쳐 말풍선을 보여주어야 한다.
 Bad
const messageGroupsByDate = buildChatMessages(messages) messageGroupsByDate.map(messageGroupByDate => { return <> <DateDivider ... > {messageGroupByDate.messages.map(messageGroupBySender => { return <> <Profile ... /> {messageGroupBySender.messages.map(messageGroupByMinute => { // ... })} </> })} </> })
TypeScript
복사
따라서 message 데이터에 show… 필드를 추가해 메시지에 어떤 UI가 보여야하는지 마킹한다. 플랫한 데이터로 반환해서 마킹한 필드에 따라 날짜 구분선, 프로필, 보낸 시간을 보여주도록 한다.
 Good
const chatMessages = buildChatMessages(messages) return chatMessages.map(message => { return <> {message.showDateDivider && <DateDivider ... />} {message.showSenderProfile ? <MessageWithProfile ... /> : <Message ... /> } {message.showTrailingTime && <MessageTrailigTime ... />} </> })
TypeScript
복사

buildChatMessages 구현

처음부터 buildChatMessages의 로직을 작성하지 않는다.
글을 쓰듯이 인터페이스만 사용해서 코드를 작성한다.
const buildChatMessages = (messages: Message[]): Message[] => { // 메시지를 날짜 별로 묶은 후 첫번째 메시지에 showDateDivider를 true로 마킹한다. // 메시지를 sender 별로 묶은 후 첫번째 메시지에 showSenderProfile을 true로 마킹한다. // 단, 자신의 메시지(isMine === true)는 showSenderProfile이 false이다. // 메시지를 분 단위로 묶은 후 마지막 메시지에 showTrailingTime을 true로 마킹한다. return pipe(messages, markShowDateDivider, markShowSenderProfile, markShowTrailingTime) }
TypeScript
복사
markShowDateDivider, markShowSenderProfile, markShowTrailingTime 에 대한 코드는 작성하지 않는다. 이러한 함수들을 사용할 것이라는 모양새만 잡아놓는다.

인터페이스 작성

import { pipe } from '@fxts/core'; interface Message { id: string; content: string; createdAt: string; sender: { id: string; name: string; }; isMine: boolean; showDateDivider?: boolean; showSenderProfile?: boolean; showTrailingTime?: boolean; } export const buildChatMessages = (messages: Message[]): Message[] => { // 메시지를 날짜 별로 묶은 후 첫번째 메시지에 showDateDivider를 true로 마킹한다. // 메시지를 sender 별로 묶은 후 첫번째 메시지에 showSenderProfile을 true로 마킹한다. // 단, 자신의 메시지(isMine === true)는 showSenderProfile이 false이다. // 메시지를 분 단위로 묶은 후 마지막 메시지에 showTrailingTime을 true로 마킹한다. return pipe(messages, markShowDateDivider, markShowSenderProfile, markShowTrailingTime); }; const markShowDateDivider = (messages: Message[]): Message[] => {}; const markShowSenderProfile = (messages: Message[]): Message[][] => {}; const markShowTrailingTime = (messages: Message[][]): Message[] => {};
TypeScript
복사

2단계: 테스트 케이스를 작성한다.

describe('markShowDateDivider', () => { it('이전 메시지의 날짜와 현재 메시지의 날짜를 비교해, 날짜가 바뀌었으면 현재 메시지의 showDateDivider는 true이다.', () => {}) }) describe('markShowSenderProfile', () => { it('message를 연속된 같은 sender의 메시지끼리 묶는다.', () => {}) it('sender별로 묶인 메시지 배열의 첫번째 메시지의 showSenderProfile은 true이다.', () => {}) it('자신의 메시지인 경우(isMine이 true인 경우), showSenderProfile은 false이다.', () => {}) }) describe('markShowTrailingTime', () => { it('sender 별로 묶인 메시지 배열에서 같은 분(minute) 안에 포함된 메시지 중 마지막 메시지만 showTrailingTime이 true이다.') })
TypeScript
복사

3단계: AI에게 테스트 코드 및 로직을 작성하게 한다.

 AI: claude-3.7-sonnet

테스트 코드 작성

prompt
message type, buildChatMessages, markShowDateDivider, markShowSenderProfile, markShowTrailingTime 인터페이스 코드 … —- 스펙, 인터페이스와 테스트에 맞게 테스트 케이스 안에 테스트 코드를 작성해줘

AI가 작성한 테스트 코드

로직 작성

prompt
이제 이 테스트 코드에 맞게 markShowDateDivider, markShowSenderProfile, markShowTrailingTime 함수 내부 로직을 작성해줘

AI가 작성한 로직

TADD를 사용했을 때 vs TADD를 사용하지 않았을 때

둘 다 똑같은 클로드 소넷 3.7버전이다.

테스트 결과

TADD  
TADD  

TDD 없이 인터페이스 작성 단계에서 바로 내부 로직을 짜달라고 했을 때

인터페이스 단계 코드 —- 이 인터페이스와 스펙에 맞게 markShowDateDivider, markShowSenderProfile, markShowTrailingTime 함수 내부를 채워줘

테스트 없이 바로 작성한 AI 코드

같은 테스트 코드에 넣어서 테스트 돌려본 결과

테스트를 조작하지 않고 실제로 같은 테스트에 돌려본 결과이다.

왜 이런 결과가 나왔을까?

클로드 소넷의 설명을 봤는데 내가 스펙에 정의해두지 않은 부분까지 알아서 구현하고 있었다.
sender가 바뀔 때는 맞지만, 날짜가 바뀔 때 showSenderProfile을 true로 설정하라고 명시한 적 없음
showSenderProfile에서 Message[][]를 반환해야하는데 Message[]를 반환하게 해서 테스트가 맞지 않음

그럼 같은 테스트 코드가 아니라, 새롭게 테스트 코드를 생성해서 테스트하면?

버그는 계속 존재한다.
대부분의 테스트가 통과했고, 4개 중 3개는 showTrailingTime이 false가 아닌 undefined여서 생긴 fail이었지만, 하나의 테스트는 분명 잘못 동작하고 있었다.
it('마지막 메시지는 항상 showTrailingTime이 true여야 함', () => { const messages = [ createMessage('1', 'Hello', '2023-01-01T10:00:00Z', 'user1', 'User 1', false), createMessage('2', 'How are you?', '2023-01-01T10:00:30Z', 'user1', 'User 1', false), ]; const result = buildChatMessages(messages); expect(result[0].showTrailingTime).toBe(false); expect(result[1].showTrailingTime).toBe(true); });
TypeScript
복사

TDD 없이 바로 AI에게 코드를 짜게 하면?

내 의도와 다르게 동작하는 코드가 생성됨
테스트 없이 AI가 알아서 스펙을 "추측"해서 구현 → 원하지 않은 결과 발생
일부 테스트에서 undefinedfalse 처리 미흡

결론

결과적으로 테스트를 먼저 짜지 않고 로직을 먼저 채우라고 하면, 아래와 같은 일이 발생한다.
내가 원하지 않은 스펙까지 구현
내가 명시한 인터페이스를 무시하고 구현
버그가 있는 코드 생성
결국 테스트 코드를 짜지 않고 AI로 로직만 먼저 구현하면 나중에 버그를 발견하고, 왜 에러가 났는지 디버깅하는 시간이 또 생긴다.
이제는 AI가 테스트 코드는 물론, 로직을 알아서 생성해주니 똑똑하게 사용하면 된다.
TADD로 더 안전하고 빠르게 코딩해보자.