오늘도 공부
AI 기반 개발의 세 가지 핵심 원칙 본문
GitHub Copilot이 VSCode에 처음 도입되었을 때만 해도, 많은 개발자들은 단순한 자동완성 기능 정도로 생각했습니다. 30% 정도의 생산성 향상을 기대했지만, 코드 작성 자체가 병목이 아니라는 점을 간과한 순진한 기대였죠.
하지만 AI 보조 개발 도구들이 진화하면서, 성공적인 AI 협업을 위한 명확한 패턴들이 드러나기 시작했습니다. 이 글에서는 AI와 함께 고품질 소프트웨어를 만들기 위한 세 가지 핵심 원칙을 소개합니다.
세 가지 핵심 원칙
성공적인 AI 기반 개발은 다음 세 가지 원칙 위에 세워집니다:
- 테스트 주도 개발(TDD) - AI의 작업을 검증하는 실행 가능한 명세
- 작은 단계로 진행하기 - AI가 잘못된 방향으로 빠지는 것을 방지
- 모듈러 아키텍처 - AI의 작업 범위를 명확하게 제한
각 원칙을 구체적인 예제와 함께 살펴보겠습니다.
1. 테스트 주도 개발: 실행 가능한 명세
최근 "스펙 주도 개발"이 주목받고 있지만, 텍스트 기반 스펙에는 치명적인 약점이 있습니다:
- 결정론적이지 않음: 같은 스펙을 줘도 매번 다른 결과가 나옴
- 실행 불가능: 자동으로 검증할 방법이 없음
반면, 테스트 코드는 이 두 문제를 모두 해결합니다.
실전 예제: 사용자 인증 기능
❌ 나쁜 접근법: 텍스트 스펙
"사용자가 이메일과 비밀번호로 로그인할 수 있어야 합니다.
비밀번호가 틀리면 에러를 반환해야 합니다."
✅ 좋은 접근법: 테스트 코드
# test_authentication.py
import pytest
from auth import AuthService
class TestAuthentication:
def test_successful_login_with_valid_credentials(self):
# Given: 등록된 사용자
auth = AuthService()
auth.register("user@example.com", "password123")
# When: 올바른 자격증명으로 로그인
result = auth.login("user@example.com", "password123")
# Then: 토큰을 받음
assert result.success is True
assert result.token is not None
def test_login_fails_with_wrong_password(self):
# Given: 등록된 사용자
auth = AuthService()
auth.register("user@example.com", "password123")
# When: 잘못된 비밀번호로 로그인 시도
result = auth.login("user@example.com", "wrong_password")
# Then: 실패하고 에러 메시지를 받음
assert result.success is False
assert result.error == "Invalid credentials"
워크플로우
- AI에게 테스트 작성 요청: "사용자 인증 기능에 대한 테스트를 작성해줘"
- 테스트 검증: 테스트가 올바른 이유로 실패하는지 확인
- 구현 요청: "이 테스트들이 통과하도록 최소한의 코드만 작성해줘"
- 리팩토링: 테스트가 통과하면 코드 개선
2. 작은 단계로 진행하기
AI에게 큰 기능을 한 번에 요청하면 다음과 같은 악순환에 빠집니다:
- AI가 대부분을 구현하지만 몇 가지가 이상함
- 수정을 요청하면 일부는 고쳐지지만 새로운 문제 발생
- 결국 처음부터 다시 시작
해결책: 아주 작게 쪼개기
실전 예제: REST API 엔드포인트 구축
사용자 정보를 수정하는 API를 만든다고 가정해봅시다.
❌ 한 번에 요청하기
"사용자 정보를 수정하는 PUT /users/:id 엔드포인트를 만들어줘.
이름, 이메일, 전화번호를 수정할 수 있고, 검증도 해야 해."
✅ 단계별로 진행하기
1단계: 엔드포인트만 존재하게
# test_user_update.py
def test_update_endpoint_exists():
response = client.put('/users/123')
assert response.status_code in [200, 400] # 404가 아니면 됨
2단계: 단일 필드 업데이트
def test_can_update_user_name():
# Given: 기존 사용자
user = create_user(name="홍길동")
# When: 이름 변경
response = client.put(f'/users/{user.id}', json={'name': '김철수'})
# Then: 이름이 변경됨
assert response.status_code == 200
updated_user = get_user(user.id)
assert updated_user.name == '김철수'
3단계: 수정 불가 필드 보호
def test_cannot_update_created_at():
user = create_user()
original_created_at = user.created_at
response = client.put(f'/users/{user.id}',
json={'created_at': '2024-01-01'})
user = get_user(user.id)
assert user.created_at == original_created_at
4단계: 검증 추가
def test_email_validation():
user = create_user()
response = client.put(f'/users/{user.id}',
json={'email': 'invalid-email'})
assert response.status_code == 400
assert 'email' in response.json()['errors']
각 단계마다 AI에게 테스트를 통과시키도록 요청하면, 작고 확실한 진전을 만들 수 있습니다.
3. 모듈러 아키텍처
AI 네이티브 엔지니어는 개별 모듈의 구현 방식보다는 모듈이 어디에 있어야 하는지에 집중합니다.
왜 모듈러 아키텍처인가?
- 집중된 컨텍스트: AI에게 전체 코드베이스 대신 특정 모듈만 가리킴
- 계층별 테스트: 비즈니스 로직과 통합 테스트를 분리
- 재생성 가능: 모듈이 잘못되면 테스트를 유지하면서 재생성
실전 예제: 헥사고날 아키텍처
잘못된 구조 (모든 것이 한 곳에)
app/
├── main.py # 비즈니스 로직 + DB + API 모두 섞임
└── tests.py
올바른 구조 (계층 분리)
app/
├── domain/ # 핵심 비즈니스 로직
│ ├── user.py
│ └── order.py
├── ports/ # 인터페이스 정의
│ ├── user_repository.py
│ └── payment_gateway.py
├── adapters/ # 구현체
│ ├── postgres_user_repository.py
│ └── stripe_payment_gateway.py
└── api/ # 외부 인터페이스
└── routes.py
AI와 함께 작업하기
도메인 로직 작성 시:
# domain/order.py 테스트
class TestOrderDomain:
def test_order_total_calculation(self):
# AI에게: "domain/order.py만 수정해서 이 테스트를 통과시켜줘"
order = Order()
order.add_item(Item(price=10000, quantity=2))
order.add_item(Item(price=5000, quantity=1))
assert order.calculate_total() == 25000
def test_discount_application(self):
order = Order()
order.add_item(Item(price=10000, quantity=1))
order.apply_discount(Discount(percentage=10))
assert order.calculate_total() == 9000
통합 테스트:
# tests/integration/test_order_api.py
class TestOrderAPI:
def test_create_order_end_to_end(self):
# AI에게: "통합 테스트가 통과하도록 필요한 어댑터들을 연결해줘"
response = client.post('/orders', json={
'items': [
{'product_id': 1, 'quantity': 2}
]
})
assert response.status_code == 201
assert 'order_id' in response.json()
AI에게 작업을 요청할 때:
- ✅ "domain/order.py에서 할인 로직을 추가해줘"
- ✅ "adapters/payment.py만 수정해서 테스트를 통과시켜줘"
- ❌ "주문 기능을 구현해줘" (너무 광범위함)
결론: 엔지니어링 원칙은 변하지 않는다
AI 보조 개발이 성공하려면 결국 좋은 소프트웨어 엔지니어링 관행으로 돌아갑니다:
- 테스트 주도 개발: 실행 가능한 명세로 AI 가이드
- 작은 단계: 한 번에 하나씩, 확실하게
- 모듈러 아키텍처: 명확한 경계와 책임
AI 없이 이런 원칙들을 따르면 고품질 시스템을 빠르게 만들 수 있습니다. AI를 추가하면 속도가 더 빨라집니다.
하지만 AI를 사용했는데 품질이 떨어진다면, 문제는 AI가 아니라 개발 방법론입니다. 한 걸음 물러서서 접근 방식을 재평가해야 할 시점입니다.
핵심 정리
- 텍스트 스펙 대신 테스트 코드로 소통하세요
- 큰 기능을 작은 단계로 쪼개세요 (더 작게, 더욱 더 작게!)
- 모듈 경계를 명확히 해서 AI의 작업 범위를 제한하세요
- AI가 품질을 떨어뜨린다면, 방법론을 점검하세요
CodeDeck - 개발자를 위한 코드 학습 카드 뉴스
프로그래밍 언어와 프레임워크를 카드 뉴스 형태로 쉽게 배우는 개발자 학습 플랫폼
www.codedeck.kr
'AI' 카테고리의 다른 글
| SDD 개발 도구 비교 분석 (0) | 2025.11.03 |
|---|---|
| Agent Lightning 완벽 가이드 (1) | 2025.10.31 |
| "Deep Research" Capability - 완벽 핸즈온 가이드 (0) | 2025.10.31 |
| Customer Support Chatbot 구축 - 완벽 핸즈온 가이드 #2 (0) | 2025.10.31 |
| LLM Playground 구축 - 완벽 핸즈온 가이드 #1 (0) | 2025.10.31 |
