spring_2기[본캠프]/과제

[파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 3

minwoo95 2026. 4. 16. 19:39

https://github.com/Hot6-NovelCraft/Hot6-NovelCraft

 

GitHub - Hot6-NovelCraft/Hot6-NovelCraft

Contribute to Hot6-NovelCraft/Hot6-NovelCraft development by creating an account on GitHub.

github.com

 

오늘 한 일

국립중앙도서관 Open API를 연동하여 도서 검색 및 내 서재 저장 기능을 구현했다.

 

기술 스택 선택 이유

1. RestTemplate

  • 국립중앙도서관 API는 외부 HTTP 호출이 필요한데, 프로젝트팀 내 표준으로 사용 중이었기 때문에 선택했다.
  • WebClient도 고려했지만 단순 동기 호출이고 별도 리액티브 환경이 아니기 때문에 RestTemplate이 더 적합했다.

2. Redis 캐싱 (RedisTemplate)

  • 국립중앙도서관 API는 외부 시스템이라 매 요청마다 호출하면 응답 속도가 느려지고 API 호출 횟수 제한 리스크가 있다.
  • 동일한 검색어에 대한 반복 호출이 많을 것으로 예상되어 검색 결과를 10분간 Redis에 캐싱하도록 설계했다.
  • Spring Cache 대신 RedisTemplate을 직접 사용한 이유는 캐시 키 전략과 TTL을 명시적으로 제어하기 위해서다.

3. 도메인 분리 (nationallibrary)

  • 국립중앙도서관 API는 외부 시스템 연동이라 관심사가 기존 library 도메인과 다르다.
  • 나중에 알라딘, 교보 같은 다른 도서 API로 교체하거나 추가할 때 영향 범위를 최소화하기 위해 별도 도메인으로 분리했다.
  • 기존 library 도메인은 소설 서재 관리에 집중하고, nationallibrary 도메인은 외부 도서 API 연동과 도서 저장을 담당하도록 책임을 명확히 분리했다.

4. 엔티티 연관관계 없이 PK 컬럼으로 관리

  • 프로젝트 컨벤션에 따라 Book과 UserBook 간에 @ManyToOne 같은 JPA 연관관계를 맺지 않고, bookId 컬럼으로만 관리했다.
  • 이를 통해 불필요한 즉시 로딩, N+1 문제 리스크를 원천 차단하고 도메인 간 결합도를 낮췄다.
  • 조회 시 bookRepository.findAllById(bookIds)로 일괄 조회하여 N+1을 방지했다.

5. XML → JSON 응답 전환

  • 국립중앙도서관 API는 기본적으로 XML을 반환한다.
  • apiType=json 파라미터를 명시하면 JSON으로 받을 수 있어, ObjectMapper로 간단하게 파싱할 수 있도록 설계했다.

트러블슈팅

1. API 키 파라미터명 오류

  • 문제 - cert_key, authKey 등 여러 파라미터명을 시도했으나 NO KEY VALUE 에러가 계속 발생했다.
  • 원인 - 국립중앙도서관 공식 명세를 확인하지 않고 임의로 파라미터명을 추측했다.
  • 해결 - 공식 명세 페이지(nl.go.kr/NL/contents/N31101030700.do)를 직접 확인한 결과 파라미터명은 key, 검색어는 kwd임을 확인하고 수정했다.
  • 교훈 - 외부 API 연동 시 반드시 공식 명세를 먼저 확인해야 한다.

2. XML 응답 파싱 오류

  • 문제 - format=json 파라미터를 넘겼으나 API가 XML을 반환하여 HttpMessageConverter 오류가 발생했다.
  • 원인 - format이라는 파라미터명은 이 API에서 지원하지 않는 파라미터였다.
  • 해결 - 공식 명세에서 apiType=json이 올바른 파라미터명임을 확인하고 수정했다.

3. Redis 캐시 히트로 인한 null 값 반환

  • 문제 - @JsonProperty 필드명을 수정했는데도 계속 null 값이 반환되었다.
  • 원인 - 이전에 null 값이었던 결과가 Redis에 캐싱된 상태였기 때문에 코드 수정이 반영되지 않았다.
  • 해결 - 다른 검색어로 테스트하여 캐시 미스 상태에서 새로 API를 호출하도록 했다.
  • 교훈 - 캐싱 환경에서 개발 시 코드 수정 후 반드시 캐시를 비우거나 다른 키로 테스트해야 한다.

4. JSON 필드명 불일치 (null 값 반환)

  • 문제 - API 호출은 성공했으나 title, author 등 모든 필드가 null로 반환되었다.
  • 원인 - 국립중앙도서관 API의 XML 응답 필드명(title_info)과 JSON 응답 필드명(titleInfo)이 달랐다. 처음에 XML 기준으로 @JsonProperty를 작성했기 때문에 JSON 파싱이 실패했다.
  • 해결 - 실제 JSON 응답을 로그로 찍어 확인한 결과 titleInfo, authorInfo, pubInfo, pubYearInfo, detailLink 등 camelCase임을 확인하고 @JsonProperty를 수정했다.

5. title 필드에 HTML 태그 포함

  • 문제 - title 값에 span class searching_txt 스프링 /span 형태의 HTML 태그가 섞여 반환되었다.
  • 원인 - 국립중앙도서관 API가 검색어 하이라이팅을 위해 HTML 태그를 title 필드에 포함시켜 내려준다.
  • 해결 - stripHtml 메서드를 만들어 replaceAll로 태그를 제거하고 순수 텍스트만 추출했다.
 
 
java
private String stripHtml(String html) {
    if (html == null) return "";
    return html.replaceAll("<[^>]*>", "").trim();
}

6. titleUrl 상대경로 문제

  • 문제 - detailLink 필드가 /NL/contents/search.do 형태의 상대경로로만 내려왔다.
  • 원인 - 국립중앙도서관 API가 절대경로가 아닌 상대경로를 반환한다.
  • 해결 - resolveUrl 메서드를 만들어 / 로 시작하는 경우 https://www.nl.go.kr 도메인을 앞에 붙여 전체 URL로 변환했다.

7. Missing request attribute userId 오류

  • 문제 - 내 서재 저장 API 호출 시 500 에러와 함께 Missing request attribute userId 메시지가 반환되었다.
  • 원인 - @RequestAttribute Long userId를 사용했는데, 이 방식은 request attribute에서 꺼내는 방식이다. 프로젝트는 @AuthenticationPrincipal UserDetailsImpl 방식으로 유저 정보를 꺼내는 패턴을 사용하고 있어서 충돌이 발생했다.
  • 해결 - @RequestAttribute Long userId를 @AuthenticationPrincipal UserDetailsImpl userDetails로 교체하고 userDetails.getUser().getId()로 userId를 추출했다.

최종 구현된 API

  • GET /api/v1/national-library/books/search - 국립중앙도서관 도서 검색 (Redis 캐싱 적용)
  • POST /api/v1/national-library/books/shelf - 내 서재 도서 저장
  • GET /api/libraries/me - 소설 서재 + 국립도서관 도서 서재 통합 조회

최종 데이터 흐름

 
 
1. 검색 요청
   - Redis 캐시 확인
   - 캐시 히트: 바로 반환
   - 캐시 미스: 국립중앙도서관 API 호출 → 결과 캐싱 후 반환

2. 내 서재 저장
   - books 테이블에 ISBN 존재 여부 확인
   - 없으면 저장, 있으면 재사용
   - user_books 테이블에 userId + bookId 저장

3. 내 서재 조회
   - library 테이블: 소설 서재 (페이지네이션)
   - user_books + books 조인 없이 일괄 조회: 국립도서관 도서 서재
   - 두 결과를 하나의 응답으로 통합하여 반환

느낀 점

  • 외부 API 연동 시 공식 명세를 반드시 먼저 확인해야 한다는 것을 뼈저리게 느꼈다. 파라미터명 하나 때문에 오랜 시간을 소비했다.
  • 캐싱 환경에서 개발할 때는 코드 수정 후 캐시 상태를 항상 의심해야 한다.
  • 도메인 분리의 중요성을 다시 한번 느꼈다. nationallibrary 도메인이 독립적이었기 때문에 기존 library 도메인에 영향 없이 개발할 수 있었다.
  • 테스트 코드를 작성하면서 실제 프로덕션 코드의 필드명 불일치(bookId vs id)를 발견하는 등 테스트의 가치를 다시 실감했다.