Contents
see ListSpring Boot 3.4에서 안정화된 RestClient는 기존 RestTemplate의 후속 동기 HTTP 클라이언트입니다. WebClient의 직관적인 빌더 패턴 API를 동기 환경에서 그대로 사용할 수 있어, 리액티브 스택이 필요 없는 프로젝트에서 훨씬 깔끔한 HTTP 통신 코드를 작성할 수 있습니다.
RestClient vs RestTemplate vs WebClient
| 특성 | RestTemplate | RestClient | WebClient |
|---|---|---|---|
| 방식 | 동기 | 동기 | 비동기/동기 |
| 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("홍길동", "hong@example.com");
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", "new@example.com"))
.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 스타일만 바꾸면 되므로 전환 비용도 낮습니다.