이 과제는 3시간이라는 제한된 개발 시간과 지속적으로 확장 가능한 구조를 동시에 요구합니다. 또한 채점 기준에서 "구현의 양은 배점이 낮습니다."라고 명시하고 있습니다.
따라서 모든 기능을 동일한 수준으로 구현하기보다 핵심 사용자 흐름을 우선 완성하고, 이후 기능을 쉽게 확장할 수 있는 구조를 만드는 것을 목표로 했습니다.
요구사항을 분석한 결과 다음과 같은 설계 원칙을 세웠습니다.
요구사항에는 현재 OpenAI를 사용하지만, 향후 자사의 대외비 문서를 학습시키고 싶다는 내용이 포함되어 있습니다.
이는 특정 AI Provider에 종속되지 않는 구조가 필요하다는 의미로 해석했습니다.
따라서 AI 호출을 인터페이스로 추상화하여 구현체를 분리하고, 이후 다른 AI Provider나 자체 모델을 추가할 수 있도록 설계했습니다.
회원가입과 로그인을 제외한 모든 API는 JWT 인증이 필요하며, Member와 Admin은 접근 가능한 기능이 다릅니다.
인증과 권한은 모든 기능에서 공통으로 사용되는 정책이므로 먼저 설계하고, 이후 구현되는 API가 동일한 인증 및 권한 체계를 사용하도록 구성했습니다.
대화 기능은 단순히 질문과 답변을 저장하는 기능이 아니라 다음과 같은 비즈니스 규칙을 포함합니다.
- 첫 질문이면 새로운 Thread를 생성한다.
- 마지막 질문 이후 30분이 지나면 새로운 Thread를 생성한다.
- 30분 이내라면 기존 Thread를 사용한다.
- AI 요청 시 같은 Thread의 이전 대화를 함께 전달한다.
따라서 Thread를 단순한 데이터가 아닌 독립적인 도메인으로 설계했습니다.
요구사항을 분석한 결과 다음과 같이 우선순위를 결정했습니다.
| Priority | 내용 |
|---|---|
| P0 | AI Provider 추상화 |
| P1 | JWT 인증 및 권한 관리 |
| P2 | 대화 생성 및 Thread 관리 |
| P3 | 피드백, 통계, CSV 보고서 |
요구사항을 구현하기 전에 인수 테스트를 먼저 작성했습니다.
특히 Thread 생성 규칙은 경계 조건이 중요한 기능이므로 다음과 같은 시나리오를 테스트로 먼저 정의했습니다.
- 마지막 질문 후 29분 → 기존 Thread 유지
- 마지막 질문 후 31분 → 새로운 Thread 생성
- Member / Admin 권한 분리
핵심 사용자 흐름인 인증 → AI 대화 → Thread 관리에 시간을 집중했습니다.
피드백과 분석·보고 기능은 요구사항을 충족하는 수준으로 구현하고, 세부 예외 처리나 성능 최적화보다 핵심 흐름의 안정성과 확장 가능한 구조를 우선했습니다.
이번 과제에서는 AI를 요구사항 분석, 설계, 구현, 코드 리뷰, Git 관리의 다섯 단계에서 활용했습니다.
AI는 선택지를 제안하고 초안을 작성하는 역할을 수행했고, 구현 범위와 설계 방향, 개선안의 적용 여부는 직접 결정했습니다.
| 단계 | AI의 역할 | 직접 수행한 내용 |
|---|---|---|
| 요구사항 분석 | 요구사항 해석, 우선순위 및 설계 방향 제안 | 구현 범위 및 우선순위 결정 |
| 설계 | 인터페이스 구조, 테스트 전략, 리팩토링 방향 제안 | 도메인 모델 및 아키텍처 설계 |
| 구현 | 테스트 코드 및 구현 코드 초안 작성, 디버깅 아이디어 제안 | 코드 검토, 수정, 리팩토링 및 적용 여부 결정 |
| 코드 리뷰 | 아키텍처, 트랜잭션, 보안, 도메인 경계 관점에서 리뷰 | 개선안 적용 여부 판단 및 수정 |
| Git | Conventional Commit 형식의 커밋 메시지 초안 작성 | 커밋 단위 결정 및 최종 메시지 검토 |
요구사항을 받은 후 바로 구현하지 않았습니다.
먼저 AI와 함께 요구사항을 분석하고 엔티티와 패키지 구조를 검토한 뒤 구현 우선순위를 결정했습니다.
특히 "향후 자사 문서를 학습시키고 싶다."는 요구사항을 바탕으로 AI Provider를 인터페이스로 추상화하는 방향을 검토했습니다.
여러 설계안을 비교한 뒤, 확장 가능한 구조를 기준으로 AI Provider 인터페이스와 스트리밍·동기 호출을 분리하는 구조를 선택했습니다.
구현은 인수 테스트를 먼저 작성한 뒤 기능을 구현하는 방식으로 진행했습니다.
AI는 요구사항을 테스트 시나리오와 테스트 코드로 구체화하고 구현 코드 초안을 작성하는 데 활용했습니다.
생성된 코드는 그대로 사용하지 않고 요구사항을 기준으로 검토·수정한 뒤 적용했습니다.
예를 들어 다음과 같은 요구사항을 테스트로 먼저 정의했습니다.
- 이메일 중복 시 409 응답
- 30분 이후 새로운 Thread 생성
- 자신의 대화에만 피드백 생성 가능
이를 통해 리팩토링 과정에서도 기존 기능이 유지되는지 빠르게 검증할 수 있었습니다.
구현이 완료된 후에는 AI에게 아키텍처 규칙, 트랜잭션, 보안, 도메인 경계 관점에서 비판적으로 검토해 달라고 요청했습니다.
AI 리뷰를 통해 스트리밍 환경에서의 트랜잭션 처리, Aggregate 간 직접 의존, JWT Secret 관리와 같은 위험 요소를 다시 점검했고, 실제로 문제가 되는 항목만 수정했습니다.
UseCase 레이어 추가, Port/Adapter 전면 적용, Testcontainers 도입과 같은 제안은 현재 프로젝트 규모와 3시간이라는 제약을 고려했을 때 비용이 더 크다고 판단하여 제외했습니다.
Git 관리에서도 AI를 활용했습니다.
커밋 단위는 기능 단위로 직접 결정했고, 커밋 메시지는 Conventional Commit 형식의 초안을 AI에게 작성하도록 한 뒤 검토하여 사용했습니다.
가장 어려웠던 점은 AI의 제안을 선별하는 것보다, 어떤 기준으로 선별할 것인지를 정하는 일이었습니다.
AI는 이론적으로 개선 가능한 사항을 폭넓게 제안하지만, 모든 제안을 적용하는 것이 항상 좋은 결과로 이어지지는 않았습니다.
이번 과제에서는 현재 코드의 안정성과 유지보수성에 실제 영향을 주는가를 기준으로 적용 여부를 판단했습니다.
또한 최신 프레임워크에서는 AI의 답변이 항상 정확하지는 않았습니다.
Spring Boot 4.x와 Jackson 3.x를 사용하는 과정에서 AI가 제시한 해결 방법이 실제 환경과 맞지 않아 공식 문서와 빌드 결과를 바탕으로 다시 검증한 사례도 있었습니다.
이번 과제를 통해 AI는 구현을 대신하는 도구가 아니라 다양한 선택지를 제시하고 검토를 보완하는 도구라는 점을 확인했습니다.
이번 과제에서 가장 어려웠던 기능은 SSE 스트리밍 환경에서 대화를 저장하는 트랜잭션을 관리하는 것이었습니다.
일반적인 대화 생성 API는 @Transactional 메서드 안에서 AI를 호출하고 응답을 저장하면 충분했습니다.
하지만 스트리밍 API는 Flux<String>을 반환하기 때문에 동일한 방식으로 구현할 수 없었습니다.
Flux를 반환하는 시점에 메서드 실행이 종료되면서 @Transactional의 트랜잭션도 함께 종료됩니다.
이후 AI 응답이 모두 완료되어 doOnComplete에서 대화를 저장하려는 시점에는 이미 트랜잭션이 종료된 상태였습니다.
초기 구현에서는 이 점을 고려하지 못해 doOnComplete에서 저장 로직을 수행했고, 스트림 종료 시점에는 트랜잭션이 보장되지 않는다는 문제를 확인했습니다.
이를 해결하기 위해 응답 저장 시점에 TransactionTemplate으로 새로운 트랜잭션을 시작하도록 변경했습니다.
.doOnComplete {
transactionTemplate.executeWithoutResult {
chatRepository.save(Chat(...))
}
}또한 Thread 생성은 AI 응답이 시작되기 전에 완료되어야 하므로 별도의 @Transactional 메서드에서 처리하고, 대화 저장은 스트림 종료 시점에 별도의 트랜잭션으로 처리하도록 분리했습니다.
이 문제는 일반적인 CRUD에서는 발생하지 않고 스트리밍과 비동기 실행 환경에서만 드러났습니다.
이번 구현을 통해 트랜잭션의 적용 범위보다 실제 코드가 실행되는 시점이 더 중요할 수 있다는 점을 확인했고, 이번 과제에서 가장 많은 시간을 사용한 기능이었습니다.
구현 과정에서는 Aggregate 간 의존성도 함께 고민했습니다.
Feedback 생성 시 Chat 정보를 조회해야 했지만 FeedbackService가 ChatRepository를 직접 참조하지 않도록 ChatReader 인터페이스를 도입했습니다.
Before
FeedbackService
└── ChatRepository
After
FeedbackService
└── ChatReader
└── ChatService
Repository를 직접 사용하는 것이 구현은 더 단순했지만, 과제의 "지속적으로 확장 가능한 구조"라는 요구사항을 고려하여 도메인 간 의존성을 인터페이스로 분리했습니다.
구현 비용은 크지 않았지만 저장 방식이 변경되더라도 Feedback 도메인의 변경 범위를 최소화할 수 있도록 설계했습니다.