HTTP Clients in Spring Boot

๋จผ์ €, ์Šคํ”„๋ง์—์„œ ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ํ•˜๋‚˜์”ฉ ์•Œ์•„๋ณด์ž.

Java ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ HttpURLConnection ์€ ๋„˜์–ด๊ฐ€์ž.

๊ธฐ๋ณธ์ ์ธ ์š”์ฒญ/์‘๋‹ต ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ์‘๋‹ต ์ฒ˜๋ฆฌ๊ฐ€ ๋ณต์žกํ•˜๋ฉฐ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ๋‚ฎ๋‹ค๋Š” ๋‹จ์ ์œผ๋กœ ์ž˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

1. RestTemplate

๋งŒ์•ฝ RestTemplate์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ผ๋ฉด ์•„๋ž˜์˜ ๊ธ€์˜ ์ฃผ์˜์‚ฌํ•ญ์„ ์ฐธ๊ณ ํ•˜์ž. https://incheol-jung.gitbook.io/docs/q-and-a/spring/resttemplate

๋Œ€ํ‘œ์ ์ธ Spring์˜ HTTP Client ์ค‘ ํ•˜๋‚˜์ด์ง€๋งŒ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋Œ€์•ˆ์„ ๊ถŒ์žฅํ•˜๊ณ  ์žˆ๋‹ค.

image
  • ๋™๊ธฐ์‹ ์š”์ฒญ: RestClient

  • ๋น„๋™๊ธฐ/์ŠคํŠธ๋ฆฌ๋ฐ: WebClient

์ฃผ์š” ํŠน์ง•

  • ์˜ค๋žซ๋™์•ˆ Spring ์ง„์˜์—์„œ ์ง€์›๋˜๊ณ  ์žˆ์Œ

  • ๋™๊ธฐ์‹ Blocking ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘

  • RESTful ์›์น™ ์ค€์ˆ˜

๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•

Bean ๋“ฑ๋ก

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

์š”์ฒญ ์˜ˆ์‹œ

@Service
public class ExampleService {
    private final RestTemplate restTemplate;
    
    public String getData() {
        return restTemplate.getForObject(
            "http://example.com/api",
            String.class
        );
    }
}

RestTemplate์„ ์‚ฌ์šฉํ•˜์—ฌ ์™ธ๋ถ€ API๋ฅผ ํ˜ธ์ถœํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์ผ ๊ฒฝ์šฐ ํƒ€์ž„์•„์›ƒ(Timeout), ์žฌ์‹œ๋„(retry), ๋กœ๊น…(Logging), ์„œํ‚ท ๋ธŒ๋ ˆ์ด์ปค ๋“ฑ์„ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋‘๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

2. WebClient

Spring WebFlux์— ํฌํ•จ๋œ HTTP ํด๋ผ์ด์–ธํŠธ๋กœ, Non-Blocking I/O๋ฅผ ํ™œ์šฉํ•œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ๋‹ค.

์ฃผ์š” ํŠน์ง•

  • ๋น„๋™๊ธฐ/Non-Blocking ๋ฐฉ์‹ ์ง€์›

  • ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘

  • Reactor ๊ธฐ๋ฐ˜์˜ ์„ ์–ธ์  API ์ œ๊ณต

Reactor: JVM ์œ„์—์„œ ๋™์ž‘ํ•˜๋Š” ๋…ผ๋ธ”๋กœํ‚น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•

Bean ๋“ฑ๋ก

@Configuration
public class WebClientConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
            .baseUrl("http://api.example.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}

์š”์ฒญ ์˜ˆ์‹œ

@Service
public class ExampleService {
    private final WebClient webClient;
    
    public Mono<UserResponse> getUser(String userId) {
        return webClient.get()
            .uri("/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserResponse.class);
    }
}

๋งŒ์•ฝ ํ•™์Šต์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์•„๋ž˜์˜ ๊ณต์‹ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญํ•œ ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

3. OpenFeign (FeignClient)

Spring Cloud OpenFeign์€ Netflix์—์„œ ๊ฐœ๋ฐœํ•œ ์„ ์–ธ์  REST ํด๋ผ์ด์–ธํŠธ๋กœ, org.springframework.cloud ํŒจํ‚ค์ง€์— ํฌํ•จ์ด ๋˜์–ด์žˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

  • ์„ ์–ธ์  REST Client - ์ธํ„ฐํŽ˜์ด์Šค + ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

  • ๋™๊ธฐ์‹ HTTP ํ†ต์‹  ๋ฐฉ์‹ (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅ)

๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•

@FeignClient(name = "book-service", url = "${api.book.url}")
public interface BookClient {
    @GetMapping("/api/books")
    List<Book> searchBooks(
        @RequestParam String query,
        @RequestHeader(HttpHeaders.AUTHORIZATION) String authorization
    );
}

์œ„์ฒ˜๋Ÿผ Spring MVC์˜ ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐฉ์‹์œผ๋กœ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ, RestTemplate ์„ ์‚ฌ์šฉํ•œ๋‹ค ํ–ˆ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ ๊ตฌ์กฐ๋กœ ํ˜ธ์ถœ์„ ํ•ด์•ผํ•œ๋‹ค.

@Component
@RequiredArgsConstructor
public class BookRestTemplate {

    private final RestTemplate restTemplate;
    private final String apiUrl;
    private final String apiKey;

    public List<Book> search(String query) {
        String url = createSearchUrl(query);
        HttpHeaders headers = createHeaders();
        
        HttpEntity<?> entity = new HttpEntity<>(headers);
        
        ResponseEntity<List<Book>> response = restTemplate.exchange(
            url,
            HttpMethod.GET,
            entity,
            new ParameterizedTypeReference<List<Book>>() {}
        );
        return response.getBody();
    }

    private String createSearchUrl(String query) {
        return UriComponentsBuilder.fromUriString(apiUrl)
            .path("/api/books")
            .queryParam("query", query)
            .encode()
            .build()
            .toUriString();
    }

    private HttpHeaders createHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, "KEY " + apiKey);
        return headers;
    }
}

ํŠน์ง•

  1. ์ง๊ด€์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๊ณ„๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ์ตœ์†Œํ™” ๋ฉ๋‹ˆ๋‹ค.

  2. ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ ์ธก๋ฉด์—์„œ๋„ ๋›ฐ์–ด๋‚˜๋‹ค.

  3. ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ–ˆ์„ ๋•Œ Retry, Circuit Breaker ๋“ฑ์„ ๋„์ž…ํ•˜๊ธฐ ํŽธํ•˜๋‹ค.

ํ•˜์ง€๋งŒ, ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ํ™•์ธํ•ด์•ผํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค.

์ฝ์–ด๋ณผ๊ฑฐ๋ฆฌ

Last updated

Was this helpful?