CodeRabbit AI로 코드리뷰 받기
JPA 상속 처리 리팩토링 및 리뷰 회고록
개인 프로젝트로 JPA 어노테이션을 분석해 SQL 스키마를 생성하는 'Jinx'라는 라이브러리를 개발하고 있다. 혼자 진행하는 프로젝트이다 보니 코드 리뷰를 받기 어려워 아쉬울 때가 많았는데, 최근 coderabbitai라는 AI 코드 리뷰어를 도입해 만족스러운 경험을 해서 그 후기를 공유해보고자 한다.
이번 글에서는 복잡한 JPA의 @Inheritance(strategy = JOINED) 상속 관계 처리 로직을 개선하는 과정에서 CodeRabbit AI와 어떻게 상호작용하며 더 나은 코드를 만들었는지에 대한 회고이다.
1. 도전 과제: 불안정한 JOINED 상속 처리
JPA의 JOINED 상속 전략은 부모와 자식 엔티티가 별도의 테이블로 나뉘고, 자식 테이블의 PK가 부모 테이블의 PK를 참조하는 FK가 되는 구조이다. 어노테이션 프로세서로 이를 처리하려면 몇 가지 까다로운 점이 있다.
- 컴파일 순서의 불확실성: 자식 엔티티를 처리하는 시점에 부모 엔티티가 아직 처리되지 않았을 수 있다.
- 다층 상속:
A -> B -> C와 같이 여러 단계로 상속이 이어질 경우, 가장 가까운 부모 엔티티를 정확히 찾아야 한다. - 정확한 FK 제약조건 생성: 자식 테이블의 PK 컬럼이 부모 테이블의 PK를 정확히 참조하도록 FK 제약조건을 만들어야 한다.
이 문제를 해결하기 위해 다음과 같은 기능을 담은 PR을 작성했다.
feat(processor): JOINED 상속 지연 처리(deferred) 추가 및 FK 생성 안정화
- 부모 엔티티가 아직 처리되지 않았을 경우를 대비해 작업을 잠시 미뤄두는 지연 처리 큐(
deferredEntities) 도입- 모든 엔티티의 1차 처리가 끝난 후, 큐에 쌓인 작업을 재시도하는 로직 추가
- 다층 상속 구조에 대응하기 위해 가장 가까운 JOINED 부모를 찾는
findNearestJoinedParentEntity헬퍼 메서드 구현- 테스트 커버리지 확대를 위한 다양한 시나리오의 유닛 테스트 추가
2. CodeRabbit AI: 단순 리뷰를 넘어선 분석 리포트
PR을 올리자마자 CodeRabbit AI가 리뷰를 시작했다. 결과는 기대 이상이었다.. 단순한 코드 지적을 넘어, PR의 의도를 완벽하게 파악한 종합 분석 리포트를 제공해준 것이다. CodeRabbit이 남겨준 리포트는 다음과 같다.
Walkthrough
JOINED 상속 처리에 대한 지연/재시도 메커니즘이 도입되었습니다. ProcessingContext에 지연 큐/이름 집합이 추가되고, EntityHandler에 지연 FK 처리 메서드가 생겼습니다. Processor는 다중 패스로 재시도하고 미해결 시 진단 에러를 출력합니다. 테스트가 광범위하게 재작성되었습니다. 코어 enum은 문법적 콤마만 추가되었습니다.
Changes
| Cohort / File(s) | Summary of changes |
|---|---|
Enum 문법 정리jinx-core/src/main/java/org/jinx/model/RelationshipType.java | enum 상수 뒤 트레일링 콤마 추가(의미 변화 없음). |
ProcessingContext 지연 구조 추가jinx-processor/src/main/java/org/jinx/context/ProcessingContext.java | Queue<EntityModel>와 Set<String> 필드 추가(각각 ArrayDeque, HashSet 초기화). java.util.* 와일드카드 임포트로 정리. 공개 API 시그니처 변화 없음. |
EntityHandler 지연 JOINED 처리/검증 강화jinx-processor/src/main/java/org/jinx/handler/EntityHandler.java | public void runDeferredJoinedFks() 추가. 중복 엔티티 시 더 이상 placeholder 생성하지 않음. SecondaryTable의 PK 유효성 검사 추가. JOINED 부모 탐색 로직(findNearestJoinedParentEntity) 도입 및 부모/PK 유효성 검증. processJoinTable이 boolean 반환 및 RelationshipType 인수 받도록 변경. IdClass 사용 시 엔티티 invalid 처리 추가. |
Processor 재시도 루프/테이블 제너레이터 연계jinx-processor/src/main/java/org/jinx/processor/JpaSqlGeneratorProcessor.java | public void processRetryTasks() 추가. 처리 시작 및 관계 해석 후 최대 5회 runDeferredJoinedFks() 재시도 루프 추가. 미해결 시 Diagnostic 에러 보고. 각 라운드 후 모델 JSON 저장. TableGeneratorHandler 초기화 및 주입. |
테스트 전면 확장(JUnit5/Mockito)jinx-processor/src/test/java/org/jinx/handler/EntityHandlerTest.java | Mockito 기반으로 전환, 지연 큐/집합 실제 컬렉션 사용. JOINED 상속, SecondaryTable, EmbeddedId/IdClass, 중복, 사이즈 불일치 등 다양한 시나리오 커버. 다수 헬퍼 메서드 및 상호작용 검증 추가. |
Sequence Diagram(s)

Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related issues
- yyubin/jinx#15 — JOINED 상속에 대한 지연 큐, EntityHandler의 지연 FK 처리, Processor 재시도 루프 추가가 이 이슈의 목표와 일치합니다.
이렇게 시퀀스 다이어그램까지 그려주며 구현한 지연 처리 로직의 흐름을 시각적으로 정리해 주었고, 변경 사항을 조목조목 분석한 Walkthrough까지 제공받았다.. 마치 숙련된 동료 개발자가 내 코드를 꼼꼼히 뜯어보고 정리해준 것 같았다.
기본적인 코드 스타일 제안(Nitpicks)도 당연히 포함되어 있다.
- 테스트 코드에 포함된
System.out.println제거 제안 - 재시도 로직의 매직 넘버
5를 상수로 만들 것을 권장 - 에러 메시지 가독성 개선 방안 제시
이런 사소하지만 중요한 피드백도 추가적으로 제안받을 수 있었다.
3. 핵심 로직에 대한 깊이 있는 토론
CodeRabbit AI는 핵심 로직에 대해서도 명쾌하게 리뷰해줬다. 내가 작성한 findNearestJoinedParentEntity 메서드에 대한 리팩토링을 다음과 같이 제안했다.
🤖 CodeRabbit AI의 첫 번째 제안
"현재 구현은 첫 번째 상위 엔티티가 JOINED 전략이 아니면 즉시 탐색을 중단합니다. 상속 체인을 끝까지 탐색해서 JOINED 전략을 사용하는 조상이 있는지 확인하고, 있다면 가장 가까운 @Entity 부모를 반환해야 합니다."
AI의 지적은 정확했다... 내 코드는 다층 상속의 특정 케이스를 놓치고 있던 것이다. 이를 바탕으로 추가적으로 고민해볼 수 있었다.
- 만약
A -> B -> A와 같이 상속 체인에 순환 참조가 있으면 무한 루프에 빠질 수 있지 않을까?- JPA 스펙상 상속 전략은 루트 엔티티에만 정의하는 것이 원칙인데, 만약 중간 엔티티가
SINGLE_TABLE같은 다른 전략을 명시하면 어떻게 처리해야 할까? 이건 명백한 모순이다.
이 고민들을 담아 AI의 제안을 개선한 코드를 다시 제시했다.

CodeRabbit AI는 추가로 제시한 개선안을 즉시 이해하고 다시 피드백을 주었다.

단순히 내 코드를 수용한 것을 넘어, 제 설계 결정(로컬 Set 사용)이 왜 더 좋은지 근거를 들어 설명해주기까지 했다... 마치 실제 동료와 기술적인 토론을 주고받으며 더 나은 해결책을 찾아가는 과정 같았다.
해당 개선점까지 취합하여 새로운 이슈로 할당하는 과정까지 거쳤다.
4. 제안받은 단위테스트에서 발견한 숨은 버그
CodeRabbit AI의 추가 기능으로, 단위 테스트를 추가로 요청할 수 있었는데, 이를 PR로 요청받고 실제로 확인해보며 숨겨져 있던 버그까지 찾을 수 있었다.
@Test @DisplayName("엔티티에 PK가 없으면 무효 처리되고 에러가 보고된다") void handle_NoPrimaryKey_ShouldInvalidateAndReportError() { // Arrange TypeElement e = mock(TypeElement.class); Name qn = mock(Name.class), sn = mock(Name.class); when(qn.toString()).thenReturn("com.example.NoPk"); when(sn.toString()).thenReturn("NoPk"); when(e.getQualifiedName()).thenReturn(qn); when(e.getSimpleName()).thenReturn(sn); lenient().when(e.getAnnotation(Entity.class)).thenReturn(mock(Entity.class)); TypeMirror none = mock(TypeMirror.class); when(e.getSuperclass()).thenReturn(none); when(none.getKind()).thenReturn(TypeKind.NONE); doReturn(List.of()).when(e).getEnclosedElements(); // Act entityHandler.handle(e); // Assert EntityModel em = schemaModel.getEntities().get("com.example.NoPk"); assertNotNull(em, "엔티티 모델이 생성되어야 합니다."); assertFalse(em.isValid(), "PK 부재 시 엔티티는 invalid여야 합니다."); verify(messager).printMessage(eq(Diagnostic.Kind.ERROR), contains("primary key"), eq(e)); }
생각해보면 정말 중요한걸 놓치고 있었다. JOINED 상속, @SecondaryTable 등 모든 처리가 끝난 후에도 엔티티에 PK가 없다면 그 엔티티는 유효하지 않는다. 하지만 이 사실을 놓치고 있었고 추가적인 로직 보완을 통해 이 최종 검증 단계를 추가하게 되었다.
// 최종 PK 검증 로직 추가 if (context.findAllPrimaryKeyColumns(entity).isEmpty()) { context.getMessager().printMessage( Diagnostic.Kind.ERROR, "Entity '" + entity.getEntityName() + "' must have a primary key.", typeElement ); entity.setValid(false); }
AI와의 리뷰 과정이 없었다면 그냥 지나쳤을지도 모르는 중요한 버그를 발견하고 수정할 수 있었던 것이다.
AI는 훌륭한 페어 프로그래밍 파트너다
사실 얻어간 건 그 이상이 맞긴함.. 이번 경험을 통해 AI 코드 리뷰어는 단순히 코드를 검사하는 도구를 넘어, 개발자의 사고를 확장시켜주는 훌륭한 파트너가 될 수 있음을 느꼈다. 한창 핫하던 클로드 소넷이나, MCP 등 사실 귀찮아서 세팅조차 해두지 않았는데, 앞으로는 코드 정적 분석 툴들, AI도 적극적으로 활용해볼 생각이 드는 경험이었다.. 특히 JPA.. 이런건 규칙위주고 문서나 자료도 많아서 AI가 특히나 잘할듯?? 비지니스랄게 없으니
사실 세팅도 아주 간단하다. 그냥 공홈에서 깃허브 연동하고 레포지토리 설정만 해주면, PR에 대한 리뷰를 자동으로 시작한다. 덤으로 한국어도 지원해줌.