Skip to content

[FEAT] Search API 리팩터링 및 중복 요청 500 에러 해결 #7

@millkk04

Description

@millkk04

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 에러 해결
  • 데이터 무결성 보장

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions