-
Notifications
You must be signed in to change notification settings - Fork 0
Labels
enhancementNew feature or requestNew feature or request
Description
1. Search API 도메인 통합
- 기존
/api/v1/books/search엔드포인트를/api/v1/search/books로 변경 - 검색 관련 API를 독립적인
search도메인으로 통합 - 작가 검색(
/search/authors), 추천 검색어(/search/recommendations) 등은 추후 구현 예정
2. 중복 요청 500 에러 해결
- 문제: GET 요청으로 도서 검색 시 DB에 저장 후, 동일한 검색을 다시 수행하면 500 내부 서버 오류 발생
- 원인: JPA orphan removal 타이밍 이슈로 인한 유니크 제약조건 위반(delete는 flush가 된 후에 작동되는 연산이라, 기존 orphan이 쓰레기 데이터로 남아있어서, insert 연산이 우선 실행되기에 중복이 발생함. 그래서 유니크 조건이 위배되는 경우가 생김)
- 해결: 명시적 2단계 처리 (기존 매핑 삭제 → 새 매핑 추가)
자세한 설명(위만 봐도 됩니다.)
# 첫 번째 요청
GET /api/v1/search/books?query=채식주의자&page=1&size=5
→ 200 OK ✅ (새 데이터 DB 저장)
# 두 번째 요청 (동일한 검색)
GET /api/v1/search/books?query=채식주의자&page=1&size=5
→ 500 Internal Server Error ❌근본 원인
1. DB 유니크 제약조건
BookAuthors 테이블에 다음과 같은 유니크 제약조건 존재:
UNIQUE KEY uk_book_authors_book_author_role (book_id, author_id, role)2. JPA Orphan Removal 타이밍 문제
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BookAuthors> bookAuthors = new ArrayList<>();orphanRemoval=true는 트랜잭션 커밋 시점에 실행됨replaceBookAuthors()메서드가clear()후 새 엔티티 추가 시, 기존 매핑이 아직 DB에서 삭제되지 않은 상태에서 같은 조합의 새 매핑 추가 시도- → 유니크 제약조건 위반 → 500 에러
3. 에러 발생 흐름
1. findOrCreateBook() → 기존 Books 조회 (book_id=1)
2. replaceBookAuthors(mappings)
→ bookAuthors.clear() (삭제 예약, DB 미반영)
→ 새 BookAuthors 추가 (메모리에만)
3. booksRepository.save(book)
→ INSERT INTO book_authors (book_id=1, author_id=한강, role=AUTHOR)
❌ DB에는 아직 기존 매핑이 존재
→ 유니크 제약조건 위반 (book_id, author_id, role)
→ 500 Internal Server Error
✅ 해결 방법
핵심 전략: 명시적 2단계 처리
수정 전 (BookImportService.java)
book.replaceBookAuthors(mappings);
Books saved = booksRepository.save(book);
items.add(bookSearchConverter.toResponse(saved, doc));수정 후 (BookImportService.java)
// ✅ 1단계: 기존 Books인 경우 매핑 삭제를 먼저 DB에 반영
if (book.getId() != null) {
book.getBookAuthors().clear();
booksRepository.save(book);
entityManager.flush(); // DELETE 즉시 실행
}
// ✅ 2단계: 새 매핑 추가
book.replaceBookAuthors(mappings);
Books saved = booksRepository.save(book);
items.add(bookSearchConverter.toResponse(saved, doc));해결 원리
수정 후 실행 순서
1. findOrCreateBook() → 기존 Books 조회 (book_id=1)
2. book.getId() != null 체크
→ bookAuthors.clear()
→ booksRepository.save(book)
→ entityManager.flush() ✅
→ DELETE FROM book_authors WHERE book_id=1 (즉시 실행)
3. replaceBookAuthors(mappings)
→ 새 BookAuthors 추가 (메모리)
4. booksRepository.save(book)
→ INSERT INTO book_authors (book_id=1, author_id=한강, role=AUTHOR) ✅
→ 성공! (기존 데이터는 이미 삭제됨)
추가 의존성
@Service
@RequiredArgsConstructor
public class BookImportService {
// ...기존 필드...
private final EntityManager entityManager; // ✅ 추가
}📁 변경된 파일 목록
🆕 신규 생성 파일 (4개)
Search 도메인
src/main/java/com/example/booklog/domain/search/
├── controller/
│ └── SearchController.java # 통합 검색 컨트롤러 (신규)
├── service/
│ └── BookSearchService.java # 도서 검색 서비스 (신규)
└── dto/
├── BookSearchResponse.java # 검색 응답 DTO (신규)
└── BookSearchItemResponse.java # 검색 항목 DTO (신규)
✏️ 수정된 파일 (2개)
1. BookImportService.java
// 위치: domain/library/books/service/BookImportService.java
// ✅ 추가된 의존성
private final EntityManager entityManager;
// ✅ 수정된 로직 (searchAndUpsert 메서드 내부)
if (book.getId() != null) {
book.getBookAuthors().clear();
booksRepository.save(book);
entityManager.flush();
}
book.replaceBookAuthors(mappings);
Books saved = booksRepository.save(book);2. BookSearchController.java 삭제
🏗️ 디렉토리 구조 변경
Before (변경 전)
src/main/java/com/example/booklog/
├── domain/
│ ├── library/
│ │ ├── books/
│ │ │ ├── controller/
│ │ │ │ └── BookSearchController.java # 검색 API
│ │ │ ├── service/
│ │ │ │ └── BookImportService.java # 검색 + 저장 로직
│ │ │ ├── dto/
│ │ │ │ ├── BookSearchResponse.java
│ │ │ │ └── BookSearchItemResponse.java
│ │ │ └── ...
│ │ └── shelves/
│ ├── tags/
│ └── users/
└── global/
After (변경 후)
src/main/java/com/example/booklog/
├── domain/
│ ├── search/ # 🆕 신규 도메인
│ │ ├── controller/
│ │ │ └── SearchController.java # 🆕 통합 검색 컨트롤러
│ │ ├── service/
│ │ │ └── BookSearchService.java # 🆕 도서 검색 서비스
│ │ └── dto/
│ │ ├── BookSearchResponse.java # 🆕 검색 응답 DTO
│ │ └── BookSearchItemResponse.java # 🆕 검색 항목 DTO
│ ├── library/
│ │ ├── books/
│ │ │ ├── controller/
│ │ │ │
│ │ │ ├── service/
│ │ │ │ └── BookImportService.java # 500 에러 수정
│ │ │ ├── dto/
│ │ │ │ ├── BookSearchResponse.java # 유지 (Legacy)
│ │ │ │ └── BookSearchItemResponse.java # 유지 (Legacy)
│ │ │ └── ...
│ │ └── shelves/
│ ├── tags/
│ └── users/
└── global/
🔄 API 엔드포인트 변경
엔드포인트 매핑
| 구분 | 엔드포인트 | 컨트롤러 | 상태 |
|---|---|---|---|
| 신규 | GET /api/v1/search/books |
SearchController |
도메인 경계 명확화
- 검색 기능이 독립적인
search도메인으로 분리 - Books 도메인은 도서 데이터 관리에만 집중
확장성 향상
- 작가 검색, 통합 검색 등 새로운 검색 기능 추가 용이
- RESTful API 명세와 구현 일치
안정성 개선
- 중복 요청 시 500 에러 해결
- 데이터 무결성 보장
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request