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)를 발견하는 등 테스트의 가치를 다시 실감했다.
'spring_2기[본캠프] > 과제' 카테고리의 다른 글
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 5 (1) | 2026.04.20 |
|---|---|
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 4 (1) | 2026.04.17 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 2 (0) | 2026.04.15 |
| [파이널 과제] NovelCraft 웹소설 창작 플랫폼 개발 프로젝트 Day 1 (0) | 2026.04.14 |
| [과제] Spring K사 서버 개발 프로젝트 Day 2 (1) | 2026.04.06 |