HTTP X SpringBoot - RestTemplate & WebClient
HTTP μμ² λ©μμ§μ μλ° μΉκ°λ°
β’
HTTP λ©μμ§μ ν€λ μ 보μλ μμ²νλ 리μμ€μ νμ
, κΈΈμ΄, μΈμ½λ© λ°©μ λ±μ μ λ³΄κ° ν¬ν¨λ μ μμ΅λλ€.
β’
Javaμμλ HttpURLConnection ν΄λμ€λ₯Ό μ¬μ©νμ¬ HTTP μμ²μ λ³΄λΌ μ μμ΅λλ€. μμ μ½λμμλ GET μμ²μ 보λ΄λ λ°©λ²μ 보μ¬μ€λλ€.
β’
λν, Spring Bootμμλ RestTemplateκ³Ό WebClientλ₯Ό μ¬μ©νμ¬ HTTP μμ²μ λ³΄λΌ μ μμ΅λλ€.
Java μμ HTTP μμ² λ³΄λ΄κΈ°
: HttpURLConnection ν΄λμ€λ₯Ό μ¬μ©νμ¬, μμ²μ 보λ΄κΈ°
μμμ½λ
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class HttpURLConnectionExample {
public static void main(String[] args) {
try {
URL url = new URL("https://api.example.com/data");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
System.out.println("Response: " + response.toString());
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Java
볡μ¬
Spring Boot μμ HTTP μμ² λ³΄λ΄κΈ°
1. RestTemplateμ μ¬μ©ν HTTP μμ²
RestTemplateμ Springμμ μ 곡νλ κ°λ¨ν HTTP ν΅μ μ μν ν΄λμ€μ
λλ€. λ€μν HTTP λ©μλ(GET, POST, PUT, DELETE λ±)λ₯Ό μ¬μ©νμ¬ μμ²μ λ³΄λΌ μ μμ΅λλ€.
β’
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
XML
볡μ¬
β’
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
Plain Text
볡μ¬
μμμ½λ
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
// RestTemplate κ°μ²΄ μμ±
RestTemplate restTemplate = new RestTemplate();
// GET μμ² λ³΄λ΄κΈ°
String url = "https://api.example.com/data";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
// POST μμ² λ³΄λ΄κΈ°
String postUrl = "https://api.example.com/data";
String requestData = "{ \"key\": \"value\" }";
ResponseEntity<String> postResponse = restTemplate.postForEntity(postUrl, requestData, String.class);
Java
볡μ¬
2. WebClientμ μ¬μ©ν HTTP μμ²
WebClientλ λΉλκΈ°μ μ΄κ³ 리μ‘ν°λΈν λ°©μμΌλ‘ HTTP μμ²μ λ³΄λΌ μ μλ λΌμ΄λΈλ¬λ¦¬μ
λλ€.
β’
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
XML
볡μ¬
β’
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Plain Text
볡μ¬
μμμ½λ
import org.springframework.web.reactive.function.client.WebClient;
// WebClient κ°μ²΄ μμ±
WebClient webClient = WebClient.create();
// GET μμ² λ³΄λ΄κΈ°
webClient.get()
.uri("https://api.example.com/data")
.retrieve()
.bodyToMono(String.class)
.subscribe(response -> {
// μμ² ν μ²λ¦¬ν λ΄μ©
});
// POST μμ² λ³΄λ΄κΈ°
webClient.post()
.uri("https://api.example.com/data")
.bodyValue("{ \"key\": \"value\" }")
.retrieve()
.bodyToMono(String.class)
.subscribe(response -> {
// μμ² ν μ²λ¦¬ν λ΄μ©
});
Java
볡μ¬
RestTemplate vs WebClient λΉκ΅
RestTemplateμ Spring μ ν리μΌμ΄μ
μμ μΌλ°μ μΌλ‘ μ¬μ©λλ λκΈ°μ HTTP ν΄λΌμ΄μΈνΈλ‘, HTTP μμ²μ 보λ΄κ³ μλ΅μ λ°λ κ°λ¨νκ³ νΈλ¦¬ν λ°©λ²μ μ 곡ν©λλ€.
λ°λ©΄μ WebClientλ Spring 5μμ μκ°λ λΉλκΈ°μ, 리μ‘ν°λΈ HTTP ν΄λΌμ΄μΈνΈλ‘, 리μ‘ν°λΈ νλ‘κ·Έλλ° λͺ¨λΈκ³Ό ν¨κ» μλνλλ‘ μ€κ³λμ΄ μμΌλ©° RestTemplateμ λΉν΄ λ λμ μ±λ₯μ μ 곡ν©λλ€.
RestTemplateμ μ¬μ ν λ리 μ¬μ©λκ³ μμ§λ§, μλ‘μ΄ νλ‘μ νΈμμλ νλμ μΈ μΉ μ ν리μΌμ΄μ
μ 리μ‘ν°λΈ λ° λΉλκΈ°μ μΈ νΉμ±κ³Ό λμ± μΌμΉνλ WebClientλ₯Ό μ¬μ©νλ κ²μ΄ κΆμ₯λ©λλ€.
κ·Έλ¬λ, RestTemplateκ³Ό WebClient λͺ¨λ Spring Boot μ ν리μΌμ΄μ
μμ HTTP μμ²μ 보λ΄κ³ μλ΅μ μ²λ¦¬νλ λ° ν¨κ³Όμ μΌλ‘ μ¬μ©λ μ μμ΅λλ€. λ λ°©λ² μ€ μ νμ μ ν리μΌμ΄μ
μ νΉμ μꡬμ¬νκ³Ό λμμΈμ λ°λΌ λ€λ₯Ό μ μμ΅λλ€.
RestTemplateμ Spring 5μμλ μ£Όλ‘ WebClientλ‘ λ체λκ³ μμ΅λλ€. WebClientλ 리μ‘ν°λΈ(non-blocking) λ°©μμΌλ‘ λμνμ¬ μ±λ₯μ μ΄μ μ κ°μ§λλ€.
HTTP μμ² ν
μ€νΈ μ¬μ΄νΈ
httpbin
HTTP μμ²μ ν
μ€νΈνκ³ λλ²κΉ
νλ λ° μ¬μ©λλ λ¬΄λ£ μ¨λΌμΈ μλΉμ€μ
λλ€. μ΄ μ¬μ΄νΈλ₯Ό μ¬μ©νλ©΄ λ€μν μ νμ HTTP μμ²μ λ³΄λΌ μ μμΌλ©°, ν΄λΉ μμ²μ λν λ€μν μ νμ μλ΅μ λ°μ μ μμ΅λλ€.
1.
GET μμ² νμΈ
β’
https://httpbin.org/get: κΈ°λ³Έ GET μμ²μ 보λ
λλ€.
β’
https://httpbin.org/anything: μ μ‘λ λͺ¨λ κ²μ λ°νν©λλ€.
2.
POST μμ² νμΈ
β’
https://httpbin.org/post: μ μ‘λ λ°μ΄ν°λ₯Ό λ°νν©λλ€.
3.
κΈ°ν μ μ©ν κΈ°λ₯
β’
https://httpbin.org/status/{μν μ½λ}: μ§μ λ HTTP μν μ½λλ₯Ό λ°νν©λλ€.
β’
https://httpbin.org/headers: ν΄λΌμ΄μΈνΈλ‘λΆν° μ μ‘λ ν€λλ₯Ό λ°νν©λλ€.
β’
https://httpbin.org/cookies: ν΄λΌμ΄μΈνΈλ‘λΆν° μμ λ μΏ ν€λ₯Ό λ°νν©λλ€.
β’
https://httpbin.org/ip: ν΄λΌμ΄μΈνΈμ IP μ£Όμλ₯Ό λ°νν©λλ€.
RestTemplate, WebClient μ μ΄μ©ν HTTP μμ² μ€μ΅
β’
νλ‘μ νΈ κ΅¬μ‘°
β’
νλ‘μ νΈ νκ²½
β’
μμ
νμΌ
β’
μ€ν κ²°κ³Ό
νλ‘μ νΈ κ΅¬μ‘°
νλ‘μ νΈ νκ²½
β’
Spring boot version : 3.3.5
β’
Group Id : com.aloha
β’
Artifact Id : spring-http
β’
Java version : JDK 17
β’
Packaging : WAR
β’
Dependency
β¦
Spring Web
β¦
Spring Reactive Web
β¦
Spring Dev Tools
β¦
Lombok
μμ νμΌ
β’
build.gradle
β’
HttpResponse.java
β’
SpringHttpApplication.java
β’
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
SQL
볡μ¬
β’
HttpResponse.java
package com.aloha.springhttp.dto;
import java.util.Map;
import lombok.Data;
@Data
public class HttpResponse {
private Map<String, String> args;
private Map<String, String> headers;
private String origin;
private String url;
}
Java
볡μ¬
β’
SpringHttpApplication.java
package com.aloha.springhttp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.aloha.springhttp.dto.HttpResponse;
@SpringBootApplication
public class SpringHttpApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(SpringHttpApplication.class, args);
restTemplate();
restTemplateToObject();
webClient();
webClientToObject();
}
/**
* RestTemplate μ μ¬μ©ν HTTP μμ²
*/
public static void restTemplate() {
System.out.println("########### RestTemplate ###########\n");
RestTemplate restTemplate = new RestTemplate();
// GET μμ²
String getUrl = "https://httpbin.org/get";
String response = restTemplate.getForObject(getUrl, String.class);
System.out.println("########### GET Response ########### \n" + response);
// POST μμ²
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String postUrl = "https://httpbin.org/post";
String requestData = "{key: value}";
HttpEntity<String> request = new HttpEntity<>(requestData, headers);
String postResponse = restTemplate.postForObject(postUrl, request, String.class);
System.out.println("########### POST Response ########### \n" + postResponse);
}
/**
* RestTemplate μμ² -> μλ΅ -> κ°μ²΄λ‘ λ³ννκΈ°
* @throws JsonProcessingException
*/
public static void restTemplateToObject() throws JsonMappingException, JsonProcessingException {
RestTemplate restTemplate = new RestTemplate();
ObjectMapper objectMapper = new ObjectMapper();
// GET μμ²
String getUrl = "https://httpbin.org/get";
ResponseEntity<String> getResponseEntity = restTemplate.getForEntity(getUrl, String.class);
String getResponseBody = getResponseEntity.getBody();
HttpResponse httpResponse = objectMapper.readValue(getResponseBody, HttpResponse.class);
System.out.println("########### Response to Object (RestTemplate) ###########");
System.out.println(httpResponse);
System.out.println();
}
/**
* WebClient λ₯Ό μ¬μ©ν HTTP μμ²
*/
public static void webClient() {
System.out.println("########### WebClient ###########\n");
WebClient webClient = WebClient.create();
// GET μμ²
String getUrl = "https://httpbin.org/get";
String response = webClient.get()
.uri(getUrl)
.retrieve()
.bodyToMono(String.class)
.block(); // block()μ μμ² μλ£λ₯Ό λκΈ°νκ³ κ²°κ³Όλ₯Ό κ°μ Έμ΅λλ€.
System.out.println("########### GET Response ########### \n" + response);
// POST μμ²
String postUrl = "https://httpbin.org/post";
String requestData = "{ key : value }";
String postResponse = webClient.post() // POST μμ²
.uri(postUrl) // uri μ§μ
.contentType(MediaType.APPLICATION_JSON) // contentType μμ² ν€λ μ€μ
.body(BodyInserters.fromValue(requestData)) // μμ² λ³Έλ¬Έ(body)μ μ€μ
.retrieve() // μμ²
.bodyToMono(String.class) // μ€νΈλ¦Ό ννλ‘ λ°μ μλ΅μ λͺ¨λ
Έλ‘ λ³ν
.block(); // block()μ μμ² μλ£λ₯Ό λκΈ°νκ³ κ²°κ³Ό λ°μ
System.out.println("########### POST Response ########### \n" + postResponse);
}
/**
* WebClient μμ² -> μλ΅ -> κ°μ²΄λ‘ λ³ννκΈ°
* @throws JsonProcessingException
*/
private static void webClientToObject() throws JsonProcessingException {
WebClient webClient = WebClient.create();
String getUrl = "https://httpbin.org/get";
HttpResponse httpResponse = webClient.get()
.uri(getUrl)
.retrieve()
.bodyToMono(HttpResponse.class)
.block();
// GetResponseλ₯Ό JSON λ¬Έμμ΄λ‘ λ³ν
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(httpResponse);
// JSON λ¬Έμμ΄μ User κ°μ²΄λ‘ λ³ν
HttpResponse response = objectMapper.readValue(jsonResponse, HttpResponse.class);
System.out.println("########### Response to Object (WebClient) ###########");
System.out.println(response.toString());
System.out.println();
}
}
Java
볡μ¬
μ€ν κ²°κ³Ό
########### RestTemplate ###########
########### GET Response ###########
{
"args": {},
"headers": {
"Accept": "text/plain, application/json, application/*+json, */*",
"Host": "httpbin.org",
"User-Agent": "Java/1.8.0_202",
"X-Amzn-Trace-Id": "Root=1-65409ad8-4108dc922304952c08309ec7"
},
"origin": "14.37.8.141",
"url": "https://httpbin.org/get"
}
########### POST Response ###########
{
"args": {},
"data": "{key: value}",
"files": {},
"form": {},
"headers": {
"Accept": "text/plain, application/json, application/*+json, */*",
"Content-Length": "12",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "Java/1.8.0_202",
"X-Amzn-Trace-Id": "Root=1-65409ad8-19f9017679f786530b296b38"
},
"json": null,
"origin": "14.37.8.141",
"url": "https://httpbin.org/post"
}
########### Response to Object (RestTemplate) ###########
HttpResponse(args={}, headers={Accept=text/plain, application/json, application/*+json, */*, Host=httpbin.org, User-Agent=Java/1.8.0_202, X-Amzn-Trace-Id=Root=1-65409ad9-09b29f6449dab4f70e43154a}, origin=14.37.8.141, url=https://httpbin.org/get)
########### WebClient ###########
########### GET Response ###########
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "ReactorNetty/1.0.38",
"X-Amzn-Trace-Id": "Root=1-65409ada-1f78a58f3cd9db604c2ffe84"
},
"origin": "14.37.8.141",
"url": "https://httpbin.org/get"
}
########### POST Response ###########
{
"args": {},
"data": "{ key : value }",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Content-Length": "15",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "ReactorNetty/1.0.38",
"X-Amzn-Trace-Id": "Root=1-65409ada-442b88ff2f1d5e7b21311f38"
},
"json": null,
"origin": "14.37.8.141",
"url": "https://httpbin.org/post"
}
########### Response to Object (WebClient) ###########
HttpResponse(args={}, headers={Accept=*/*, Accept-Encoding=gzip, Host=httpbin.org, User-Agent=ReactorNetty/1.0.38, X-Amzn-Trace-Id=Root=1-65409adb-4efe9c325a44763652475ff6}, origin=14.37.8.141, url=https://httpbin.org/get)
Plain Text
볡μ¬
λκΈ°&λΉλκΈ°, λΈλ‘νΉ&λ ΌλΈλ‘νΉ
λΈλ‘νΉ (Blocking) | λ
ΌλΈλ‘νΉ (Non-Blocking) | |
λκΈ°μ | μμ
μ΄ μλ£λ λκΉμ§ κΈ°λ€λ¦¬λ©°
λ€λ₯Έ μμ
μ νμ§ μμ | μμ
μ΄ μλ£λ λκΉμ§ κΈ°λ€λ¦¬μ§λ§
λ€λ₯Έ μμ
μ ν μ μμ |
λΉλκΈ°μ | μμ
μ μμ²ν ν λ€λ₯Έ μμ
μ νμ§ μμ | μμ
μ μμ²ν ν λ€λ₯Έ μμ
μ ν μ μμ |
λΉλκΈ°λ?
μμ
μ μμ²ν ν κ·Έ μμ
μ΄ μλ£λ λκΉμ§ κΈ°λ€λ¦¬μ§ μκ³ ,
λ€λ₯Έ μμ
μ κ³μν μ μλ λ°©μμ
λλ€.
μμ
μ΄ μλ£λλ©΄ μ½λ°± ν¨μλ μ΄λ²€νΈλ₯Ό ν΅ν΄ κ²°κ³Όλ₯Ό μλ €μ€λλ€.
λ ΌλΈλ‘νΉμ΄λ?
μμ
μ μμ²ν λ, κ·Έ μμ
μ΄ μ¦μ μλ£λμ§ μλλΌλ νμ¬ μμ
μ λ©μΆμ§ μκ³
λ€λ₯Έ μμ
μ κ³μν μ μλ λ°©μμ
λλ€.
μ¦, μμ
μ΄ μλ£λ λκΉμ§ κΈ°λ€λ¦¬μ§ μκ³ λ°λ‘ λ€μ μμ
μ μνν μ μμ΅λλ€.
λκΈ°μ λ ΌλΈλ‘νΉμ΄ κ°λ₯νκ°?
μΌλ°μ μΌλ‘λ λκΈ°μ λ
ΌλΈλ‘νΉ μ‘°ν©μ μ€λ¬΄μμ κ±°μ μ¬μ©λμ§ μμ΅λλ€. λκΈ°μ νΈμΆμ΄ λ
Όλ¦¬μ μΌλ‘ λΈλ‘νΉμ μλ°νλ νΉμ±μ΄ μκΈ° λλ¬Έμ
λλ€. κ·Έλ¬λ μ΄λ‘ μ μΌλ‘λ λ€μκ³Ό κ°μ λ°©λ²μΌλ‘ λκΈ°μ λ
ΌλΈλ‘νΉμ ꡬνν μ μμ΅λλ€
β’
ν΄λ§(Polling): μΌμ κ°κ²©μΌλ‘ μμ
μνλ₯Ό 체ν¬νμ¬ μλ£λμλμ§ νμΈνλ λ°©λ²μ
λλ€. μ΄λ νΉμ μ£ΌκΈ°λ‘ μμ
μλ£ μ¬λΆλ₯Ό νμΈνκ³ , μλ£λμ§ μμλ€λ©΄ λ°λ‘ μ μ΄κΆμ λ°ννλ λ°©μμ΄λ―λ‘ λ
ΌλΈλ‘νΉμ²λΌ λμν μ μμ΅λλ€.
β’
λ°μ λκΈ°(Busy-waiting): CPU μμμ νμ©ν΄ 무ν 루νλ₯Ό λλ©° κ²°κ³Όλ₯Ό κΈ°λ€λ¦¬λ, μμ
μ΄ μλ£λμ§ μμΌλ©΄ λ€λ₯Έ λ‘μ§μΌλ‘ μ μ΄λ₯Ό λκΈ°μ§ μλ λ°©μμ
λλ€.
νμ§λ§, μ΄λ¬ν λ°©λ²μ CPU 리μμ€λ₯Ό λλΉν μ μμΌλ―λ‘ ν¨μ¨μ μ΄μ§ μμΌλ©°, λκΈ°μ λ
ΌλΈλ‘νΉμ νμμ±μ΄ ν° κ²½μ°μλ λΉλκΈ° λ
ΌλΈλ‘νΉ λ°©μμ λ μ νΈνκ² λ©λλ€.