SOFTMOA TECHNOLOGY

Spring Boot 3.4 RestClient 완벽 가이드 - WebClient를 대체하는 동기 HTTP 클라이언트

소프트모아가 정리한 기술 기록입니다.

Contents
작성일 2026. 04. 07.

Spring Boot 3.4에서 안정화된 RestClient는 기존 RestTemplate의 후속 동기 HTTP 클라이언트입니다. WebClient의 직관적인 빌더 패턴 API를 동기 환경에서 그대로 사용할 수 있어, 리액티브 스택이 필요 없는 프로젝트에서 훨씬 깔끔한 HTTP 통신 코드를 작성할 수 있습니다.

RestClient vs RestTemplate vs WebClient

특성RestTemplateRestClientWebClient
방식동기동기비동기/동기
API 스타일메서드 기반빌더 패턴빌더 패턴
상태유지보수 모드권장리액티브 환경 권장
Spring Boot 버전전 버전3.2+전 버전

RestClient 생성

import org.springframework.web.client.RestClient;

@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        return builder
            .baseUrl("https://api.example.com")
            .defaultHeader("Accept", "application/json")
            .defaultHeader("User-Agent", "MyApp/1.0")
            .requestInterceptor((request, body, execution) -> {
                log.debug("요청: {} {}", request.getMethod(), request.getURI());
                var response = execution.execute(request, body);
                log.debug("응답: {}", response.getStatusCode());
                return response;
            })
            .build();
    }
}

기본 CRUD 작업

GET 요청

// 단일 객체 조회
User user = restClient.get()
    .uri("/users/{id}", userId)
    .retrieve()
    .body(User.class);

// 리스트 조회 (ParameterizedTypeReference 사용)
List<User> users = restClient.get()
    .uri("/users?page={page}&size={size}", 0, 20)
    .retrieve()
    .body(new ParameterizedTypeReference<List<User>>() {});

// 쿼리 파라미터 빌더 사용
List<User> filtered = restClient.get()
    .uri(uriBuilder -> uriBuilder
        .path("/users")
        .queryParam("status", "active")
        .queryParam("role", "admin")
        .queryParam("sort", "name,asc")
        .build())
    .retrieve()
    .body(new ParameterizedTypeReference<>() {});

POST 요청

// JSON 바디 전송
User newUser = new User("홍길동", "[email protected]");

User created = restClient.post()
    .uri("/users")
    .contentType(MediaType.APPLICATION_JSON)
    .body(newUser)
    .retrieve()
    .body(User.class);

// ResponseEntity로 헤더 포함 응답 받기
ResponseEntity<User> response = restClient.post()
    .uri("/users")
    .body(newUser)
    .retrieve()
    .toEntity(User.class);

URI location = response.getHeaders().getLocation();
HttpStatusCode status = response.getStatusCode();

PUT / PATCH / DELETE

// PUT - 전체 수정
restClient.put()
    .uri("/users/{id}", userId)
    .body(updatedUser)
    .retrieve()
    .toBodilessEntity();

// PATCH - 부분 수정
restClient.patch()
    .uri("/users/{id}", userId)
    .body(Map.of("email", "[email protected]"))
    .retrieve()
    .body(User.class);

// DELETE
restClient.delete()
    .uri("/users/{id}", userId)
    .retrieve()
    .toBodilessEntity();

에러 처리

User user = restClient.get()
    .uri("/users/{id}", userId)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
            throw new UserNotFoundException("사용자를 찾을 수 없습니다: " + userId);
        }
        throw new ClientException("클라이언트 오류: " + response.getStatusCode());
    })
    .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
        throw new ServerException("서버 오류 발생");
    })
    .body(User.class);

// 전역 에러 핸들러 설정
@Bean
public RestClient restClient(RestClient.Builder builder) {
    return builder
        .baseUrl("https://api.example.com")
        .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
            String body = new String(response.getBody().readAllBytes());
            log.error("API 오류 - {} {}: {}",
                response.getStatusCode(), request.getURI(), body);
            throw new ApiException(response.getStatusCode(), body);
        })
        .build();
}

Exchange 메서드로 세밀한 제어

// exchange로 응답을 직접 처리
Optional<User> user = restClient.get()
    .uri("/users/{id}", userId)
    .exchange((request, response) -> {
        if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
            return Optional.empty();
        }
        if (response.getStatusCode().is2xxSuccessful()) {
            ObjectMapper mapper = new ObjectMapper();
            User u = mapper.readValue(response.getBody(), User.class);
            return Optional.of(u);
        }
        throw new ApiException("예상치 못한 응답: " + response.getStatusCode());
    });

파일 업로드

MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", new FileSystemResource("/path/to/file.pdf"));
body.add("description", "첨부 문서");

UploadResult result = restClient.post()
    .uri("/files/upload")
    .contentType(MediaType.MULTIPART_FORM_DATA)
    .body(body)
    .retrieve()
    .body(UploadResult.class);

인증 헤더 자동 주입

@Bean
public RestClient restClient(RestClient.Builder builder, TokenProvider tokenProvider) {
    return builder
        .baseUrl("https://api.example.com")
        .requestInitializer(request -> {
            String token = tokenProvider.getAccessToken();
            request.getHeaders().setBearerAuth(token);
        })
        .build();
}

HTTP Interface와 결합

Spring 6의 HTTP Interface를 사용하면 선언적 방식으로 API를 정의할 수 있습니다.

// 인터페이스 정의
public interface UserApi {

    @GetExchange("/users/{id}")
    User getUser(@PathVariable Long id);

    @GetExchange("/users")
    List<User> listUsers(@RequestParam String status);

    @PostExchange("/users")
    User createUser(@RequestBody User user);

    @DeleteExchange("/users/{id}")
    void deleteUser(@PathVariable Long id);
}

// 프록시 생성
@Bean
public UserApi userApi(RestClient restClient) {
    RestClientAdapter adapter = RestClientAdapter.create(restClient);
    HttpServiceProxyFactory factory = HttpServiceProxyFactory
        .builderFor(adapter)
        .build();
    return factory.createClient(UserApi.class);
}

// 사용
@Service
public class UserService {
    private final UserApi userApi;

    public User getUser(Long id) {
        return userApi.getUser(id); // 인터페이스 메서드 호출
    }
}

테스트 지원

@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    private MockRestServiceServer mockServer;

    @BeforeEach
    void setup(@Autowired RestClient.Builder builder) {
        mockServer = MockRestServiceServer.bindTo(builder).build();
    }

    @Test
    void shouldGetUser() {
        mockServer.expect(requestTo("/users/1"))
            .andRespond(withSuccess(
                "{\"id\":1,\"name\":\"홍길동\"}",
                MediaType.APPLICATION_JSON));

        User user = userService.getUser(1L);
        assertThat(user.getName()).isEqualTo("홍길동");
        mockServer.verify();
    }
}

RestClient는 Spring Boot 3.4 이상의 프로젝트에서 동기 HTTP 통신의 표준으로 자리 잡았습니다. RestTemplate에서 마이그레이션할 때 API 스타일만 바꾸면 되므로 전환 비용도 낮습니다.

Share this

Spring

Have a questions?

견적 및 기술문의

mobile : 010-7931-4813

Contact Form