본문 바로가기
💻 뚝딱뚝딱/북북클럽

[개발일지 #029] 책(Book) - Spring WebClient로 외부 API 연동: KakaoBookClient 구현

by 뚜루리 2025. 5. 1.
728x90
320x100

🎯 오늘의 목표

  • 책(Book) - Spring WebClient로 외부 API 연동: KakaoBookClient 구현

⚙️ 진행한 작업

  • 책(Book) - Spring WebClient로 외부 API 연동: KakaoBookClient 구현

📌  build.gradle : 의존성 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
  • spring-boot-starter-web만으로는 WebClient가 포함되지 않아서, WebClient를 쓰려면 반드시 spring-boot-starter-webflux 의존성을 추가해야 함.

 

 Spring WebClient?

  • WebClient는 Spring에서 제공하는 최신 HTTP 클라이언트
  • 내 서버에서 카카오 API 같은 외부 서버로 비동기 요청을 보낼 때 사용
  • RestTemplate의 후계자이자 고성능 서비스를 위한 선택
  • 예:
  • 내 서버 → Kakao API로 GET 요청 → 응답 받아오기
  • 내 서버 → 다른 마이크로서비스로 POST 요청 → 결과 처리

 

  WebClient RestTemplate
동기/비동기 동기 (blocking) 비동기 (non-blocking)
스레드 효율 요청당 스레드 필요 요청당 스레드 불필요, 효율적
성능 단순 앱은 OK, 대규모에서 부하 큼 대규모, 고성능 서비스에 적합
지원 상태 Spring 5 이후 Deprecated 예정 Spring 5 이후 공식 후계자

 

✅ WebClient를 선택하게 된 이유

외부 API 요청을 백엔드에서 직접 처리하려고 WebClient를 선택한 거고, RestTemplate보다 최신 방식이라 WebClient로 간거임.

 

 

 

📌  도서 API 고르기 : 카카오 도서 API 선택

예전에 다른 사이드 프로젝트에서도 도서 API가 필요해서 카카오 도서 API를 선택했던적이 있고 그 과정을 아래의 포스팅에 정리해두었음.

 

[개발일지#014] 도서 API 적용하기 (Feat. 카카오 도서 API)

tmi.정말 오랜만에 다시 시작한 사이드 프로젝트작년 9월에 Ver1을 마무리 지었으니 5개월만에 다시 꺼내본다...!5개월만에 코드를 다시보니 너무나 새로워...!Ver1을 마치면서 앞으로 추가될 기능으

ddururiiiiiii.tistory.com

추가로 API 키 받는건 이 포스팅에서는 생략하였음.

 

📌  KakaoBookClient 생성

package ddururi.bookbookclub.global.external.kakao;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
@RequiredArgsConstructor
public class KakaoBookClient {

    private final WebClient webClient = WebClient.builder()
            .baseUrl("https://dapi.kakao.com")
            .defaultHeader("Authorization", "KakaoAK {REST_API_KEY}") // REST_API_KEY 자리에 실제 키 넣기
            .build(); // REST_API_KEY 자리에 실제 키 넣기

    public KakaoBookSearchResponse searchBooks(String query) {
        return webClient.get()
                .uri(uriBuilder -> uriBuilder
                        .path("/v3/search/book")
                        .queryParam("query", query)
                        .build())
                .retrieve()
                .bodyToMono(KakaoBookSearchResponse.class)
                .block(); // 비동기 → 동기로 받음
    }
}

📌 KakaoBookClient를 따로 만든 이유

1️⃣ 역할 분리

  • KakaoBookClient는 백엔드에서 카카오 책 API만 전담해서 호출하는 컴포넌트.
  • 컨트롤러, 서비스 로직에서 외부 API 호출을 직접 다루지 않고 → 별도의 클라이언트로 캡슐화.
  • 코드 재사용성 높이고, 유지보수 쉽게 만들기 위해 분리.

2️⃣ 서버-서버 통신 필요

  • 일부 서비스에서는 프론트에서 외부 API 호출이 아니라, 백엔드에서 외부 API로 직접 요청하고 → 가공된 데이터를 프론트에 내려주는 방식이 필요.
  • 예: 프론트에서 /api/books/search 호출 → 백엔드에서 KakaoBookClient가 카카오 API 호출 → 결과를 가공해 프론트로 반환.

3️⃣ API 키 보안

  • 카카오 API 키는 노출되면 안 되는 민감 정보.
  • 프론트에서 직접 카카오 API를 호출하면 키가 노출되기 때문에, 백엔드에서 대신 호출하면 보안을 강화할 수 있음.

 

📌  카카오 응답 DTO 생성, 수정

@Getter
public class KakaoBookSearchResponse {
    private List<KakaoBookDocument> documents;
}
package ddururi.bookbookclub.global.external.kakao;

import lombok.Getter;
import java.util.List;

@Getter
public class KakaoBookDocument {
    private String title;
    private List<String> authors;
    private String publisher;
    private String isbn;
    private String thumbnail;
}
/**
 * 책 등록 요청 DTO
 */
@Getter
@NoArgsConstructor
public class BookRequest {

    @NotBlank(message = "제목은 비어 있을 수 없습니다.")
    private String title;

    private List<String> authors; //수정

    private String publisher;
    private String isbn;
    private String thumbnailUrl;
}
package ddururi.bookbookclub.domain.book.service;

import ddururi.bookbookclub.domain.book.dto.BookRequest;
import ddururi.bookbookclub.domain.book.dto.BookResponse;
import ddururi.bookbookclub.domain.book.entity.Book;
import ddururi.bookbookclub.domain.book.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 책 관련 비즈니스 로직 처리
 */
@Service
@RequiredArgsConstructor
@Transactional
public class BookService {

    private final BookRepository bookRepository;

    /**
     * 책 등록
     * @param request 책 등록 요청 데이터
     * @return 등록된 책 정보
     */
    public BookResponse createBook(BookRequest request) {
        String author = (request.getAuthors() != null && !request.getAuthors().isEmpty())
                ? String.join(", ", request.getAuthors())
                : "알 수 없음"; //추가

        Book book = Book.create(
                request.getTitle(),
                author, //수정
                request.getPublisher(),
                request.getIsbn(),
                request.getThumbnailUrl()
        );
        Book savedBook = bookRepository.save(book);
        return new BookResponse(savedBook);
    }
}

 

 

📌  BookController 수정

package ddururi.bookbookclub.domain.book.controller;

* 책 관련 API 컨트롤러
 * - 등록
 */
@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;
    private final KakaoBookClient kakaoBookClient;

    @GetMapping("/search")
    public ResponseEntity<ApiResponse<KakaoBookSearchResponse>> searchBooksFromKakao(
            @RequestParam String query
    ) {
        KakaoBookSearchResponse response = kakaoBookClient.searchBooks(query);
        return ResponseEntity.ok(ApiResponse.success(response));
    }

	//코드생략//
}

 

 


[포스트맨으로 테스트하기]

GET http://localhost:8080/api/books/search?query=클린코드

 

[테스트 결과 : 성공]

728x90
320x100