2_TeamBuilder.jpg

<aside>

한 줄 요약

자연어로 원하는 팀원을 설명하면 AI가 맞는 프로젝트 · 팀원을 추천하는 개발자 매칭 플랫폼입니다.

4인 캡스톤에서 팀장 · 백엔드 리드를 맡아, LLM이 DB에 없는 사람을 추천하는 문제와 동시 수락으로 모집 정원이 넘치는 문제를 가장 먼저 막았습니다.

</aside>

<aside>

4건 중 1건

동시 수락 중 성공

남은 자리 1개에 4명이 동시에 수락 (MySQL 통합 테스트)

</aside>

<aside>

0건

후보 밖 AI 추천

반환 ID를 후보 집합 · DB 재조회로 대조

</aside>

<aside>

7 → 0

깨진 테스트

CI를 붙이자 드러난 기존 실패를 모두 복구

</aside>

<aside>

개발 기간

2026.03 ~ 06

캡스톤디자인

</aside>

<aside>

팀 구성

4명 팀

기여도 40%, PR 18건 직접 리뷰 · 머지

</aside>

<aside>

내 역할

팀장 · 백엔드 리드

ERD · AI 추천 · 실행 환경 · 형상관리

</aside>

<aside>

담당 업무

기술 스택 · Java 21, Spring Boot 3.5, JPA, MySQL 8, Gemini API, Docker Compose, AWS EC2, GitHub Actions

</aside>

<aside>

Review 남은 자리 1개에 4명이 동시에 수락해도 1건만 성공


프로젝트 진행 과정

<aside>

</aside>

01. 화면

메인 화면 — 모집글 목록과 AI 추천

메인 화면 — 모집글 목록과 AI 추천

02. 추천 흐름

<aside>

사용자 요청 → 요청에서 기술 스택 추출 → DB에서 후보 최대 50건 조회(트랜잭션 1) → Gemini 호출(트랜잭션 없음) → 후보 ID 집합으로 거르고 DB 재조회(트랜잭션 2) → 최대 3건 응답. 제목 같은 표시 값은 AI가 아니라 DB 값을 씁니다.

</aside>

03. 기술 선택 이유

기술 고른 이유 포기한 것 (트레이드오프)
조건부 UPDATE 확인과 증가를 한 문장으로 처리합니다. InnoDB가 행 잠금 후 최신 값으로 WHERE를 다시 평가해 정원을 넘지 않습니다. 엔티티의 currentCount가 영속성 컨텍스트와 어긋날 수 있어 수락 흐름에서는 그 값을 다시 쓰지 않습니다.
낙관적 · 비관적 락 대신 낙관적 락은 기존 행 version이 NULL이라 마이그레이션과 재시도가 필요하고, 비관적 락은 잠금 시간이 더 깁니다. 같은 포지션 수락은 여전히 직렬화됩니다.
OSIV 끄기 Gemini 응답(최대 15초)을 기다리는 동안 커넥션을 반환해, AI 요청이 몰려도 다른 API가 막히지 않습니다. 컨트롤러에서 지연 로딩을 쓸 수 없어, 서비스가 DTO까지 만들어 반환해야 합니다.

04. 트러블슈팅

<aside>

1. 정원 체크 메서드가 어디서도 호출되지 않았다

문제 README에는 정원 초과를 막는다고 적혀 있었지만, 수락을 몇 번 해도 현재 인원이 그대로였습니다.

원인 지원서와 포지션이 문자열로만 연결돼 호출되지 않았고, 엔티티 메서드는 "읽고 → 비교하고 → 쓰는" 방식이라 호출했더라도 동시 수락 두 건이 같은 값을 읽어 정원을 넘길 수 있었습니다.

해결 current_count < required_count 조건부 UPDATE로 바꾸고, 영향 행이 0이면 409와 함께 지원서 상태 변경까지 롤백합니다.

<aside>

결과 정원 1명 포지션에 4건 동시 수락 → 1건만 성공

</aside>

증거: PositionCapacityIntegrationTest PositionSeatsTest

</aside>

<aside>

2. AI가 후보가 아니었던 ID를 돌려줘도 통과했다

문제 AI가 이번 요청과 무관한 실제 프로젝트 ID를 지어내면 그대로 추천됐습니다.

원인 환각 방지가 "DB에 존재하는가"만 확인했습니다.

해결 이번 요청의 후보 ID 집합으로 먼저 거른 뒤 DB를 재조회하고, 중복 제거 · 최대 3건 · 점수 0~100 보정을 서버에서 강제합니다.

<aside>

결과 존재하지 않거나 후보 밖인 ID는 응답에서 제외

</aside>

증거: AiCandidateReaderTest AiRecommendationIntegrationTest

</aside>

<aside>

3. Gemini 응답을 기다리는 동안 DB 커넥션을 잡고 있었다

문제 AI 요청 10건이 동시에 대기하면 AI와 무관한 API까지 커넥션을 기다릴 수 있었습니다(HikariCP 기본 풀 10개).

원인 메서드 전체가 @Transactional이었고, OSIV가 켜져 있어 트랜잭션을 나눠도 요청이 끝날 때까지 커넥션이 반환되지 않았습니다.

해결 DB 구간을 별도 빈으로 분리해 Gemini 호출을 트랜잭션 밖으로 빼고, open-in-view: false로 바꾼 뒤 지연 로딩 의존 코드를 전수 점검했습니다.

<aside>

결과 Gemini 대기 중 트랜잭션 · EntityManager가 바인딩되지 않음을 테스트로 확인

</aside>

증거: AiServiceTest TeamFlowIntegrationTest

</aside>

05. GitHub