소셜 로그인을 작업하다보니 참고하는 블로그마다 WebClient와 RestTemplate, HttpUrlConnection 등 다양하게 존재했다. 각각 어떻게 사용하는지, 차이점, 장단점을 비교해서 적용해보고자 정리하기 시작했다😉
일단 HttpURLConnection은 코드가 복잡하고 권장되지 않는다 하여... 후순위로 간단하게 적어보려고 한다.
간단히 비교부터 하고 들어가면...
| 특징 | HttpURLConnection | RestTemplate | WebClient |
|---|---|---|---|
| 레벨 | 낮은 수준 API | 고수준 추상화 API | 반응형 고수준 API |
| 블로킹/논블로킹 | 블로킹 | 블로킹 | 논블로킹 |
| 비동기 지원 | X | X | O |
| Spring Integration | 직접 구현 필요 | Spring에서 기본 제공 | Spring 5+ 권장 |
| 권장 여부 | 권장되지 않음 | Spring 5 이후 비권장 | Spring 5+ 권장 |
| 코드 간결성 | 복잡함 | 간결함 | 간결함 |
📣
블로킹
- 요청을 보낸 후 서버의 응답이 도착할 때까지 해당 thread가 멈춘 상태로 대기하는 것이다.
- 동기식의 경우 블로킹관 연관되는데, 요청이 느리거나 네트워크 지원이 발생하면 리소스가 낭비될 수 있다.
📣논블로킹- 요청 보낸 후 즉시 thread가 반환된다.
- 응답이 완료되면 별도의 callback 또는 반응형 데이터 스트림으로 처리한다.
- 비동기와 연관되며, 자원을 효율적으로 사용할 수 있고 고성능이나 대규모 트래픽 처리 시 적합하다.
public class HttpUrlConnectionEx {
public static void main(String[] args) throws Exception {
// 호출하려는 API의 엔드포인트 URL 객체 생성
URL url = new URL("https://[그 외 url 주소]");
HttpURLConnection comm = (HttpURLConnection) url.openConnection();
// 요청 메서드 설정 - GET, POST 등
conn.setRequestMethod("[해당 설정 값]");
// header 부분 설정 (Content-Type이나 토큰 용 Authorization 등)
conn.setRequestProperty("Content-Type", "[해당 설정 값]");
conn.setRequestProperty("Authorization", "[해당 설정 값]"):
/* body에 내용을 담아 전송하는 경우, 이 부분에 넣기!!*/
// 서버로부터의 응답 코드 확인 (200: 성공, 404: 리소스 없음, 500: 서버 오류 등)
int responseCode = conn.getResponseCode();
if(responseCode == 200) {
// BufferedReader를 통해 받아온 데이터를 줄 단위로 읽기
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
// 한 줄씩 읽어와서 StringBuilder 객체에 추가
while((inputLine = br.readLine()) != null) {
response.append(inputLine);
}
System.out.println("Response : " + response.toString());
br.close();
} else {
System.out.println("Failed to fetch data. HTTP Response Code : " + responseCode);
}
conn.disconnection();
}
}
카카오 로그인 당시 사용한 부분을 예시로 들면,
body 내용을 String을 만들어서 OutputStream을 통해 전송하면 된다.
일단 conn.setDoOutput(true);는 반드시 설정해주어야 전송 데이터를 넣을 수 있다.
1. String 형태는 위의 Content-Type을 conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 이렇게 설정해야 한다!!!
String body = "grant_type=authorization_code"
+ "&client_id=" + "your_client_id"
+ "&redirect_uri=" + "your_redirect_uri"
+ "&code=" + "your_code"
+ "&client_secret=" + "your_client_secret";
2. JSON 타입은 conn.setRequestProperty("Content-Type", "application/json");
요청 부분
{
"grant_type": "authorization_code",
"client_id": "your_client_id",
"redirect_uri": "your_redirect_uri",
"code": "your_code",
"client_secret": "your_client_secret"
}
String body = response.toString();
3. 객체 타입은 conn.setRequestProperty("Content-Type", "application/json");
RequestDto requestDto = new RequestDto();
requestDto.setGrant_type("authorization_code");
requestDto.setClient_id("your_client_id");
requestDto.setRedirect_uri("your_redirect_uri");
requestDto.setCode("your_code");
requestDto.setClient_secret("your_client_secret");
위와 같이 객체를 코드 안에서 생성하거나, 파라미터로 들어온 경우에는
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(requestDto);
객체를 json 타입의 String으로 변환하여 넣어주면 된다!
2번의 json을 넣어주듯 작업하면 된다.
이렇게 설정된 String 타입의 body를 아래 내용에 담아 전송하면 된다!!
// OutputStream을 사용하여 Body 데이터 전송
try (OutputStream os = conn.getOutputStream()) {
byte[] input = body.getBytes("utf-8");
os.write(input, 0, input.length);
}
public class RestTemplateEx {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
String url = "https://[그 외 url 주소]";
String response = restTemplate.getForObject(url, String.class);
}
}
POST 는 Header를 설정하고, Body를 구성하는 작업 때문에 코드가 더 길어진다.
여기서도 body가 JSON 형태라면 Content-Type을 application/json으로 넣어주면 된다.
public class RestTemplateEx {
public static void main(String[] args) {
// Header 구성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// Body 구성
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", redirectUri);
body.add("code", code);
body.add("client_secret", clientSecret);
// HTTP 생성
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
// RestTemplate로 요청보내기
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
tokenUri,
HttpMethod.POST,
request,
String.class
);
String responseBody = response.getBody();
System.out.println("Response : " + responseBody);
}
}
🚨 RestTemplate 요청 보내는 부분은 2가지 방식으로 작성이 가능하다!
첫 번째, exchange()는 다양한 설정을 직접 할 수 있다. 그래서 응답을 수동으로 처리할 때 많이 사용한다.
ResponseEntity response = rt.exchange(
tokenUri,
HttpMethod.POST,
request,
String.class
);
두 번째, postForEntity()는 POST 요청만을 처리하며, 요청과 응답이 단순한 경우에 간결하게 사용한다.
ResponseEntity responseEntity = restTemplate.postForEntity(
tokenUri,
requestEntity,
String.class
);
위에서 사용한 LinkedMultiValueMap은 왜, 언제 쓰는지 해당 글에 간단히 적어두었다😉
public class WebClientEx {
public static void main(String[] args) {
WebClient webClient = WebClient.create("https://요청주소.com");
String response = webClient.get()
.uri("/posts/1") // 그 외 url 경로
.retrieve() // 응답을 받아오기 위한 메서드
.bodyToMono(String.class) // 응답을 String으로 처리
.block(); // 동기식으로 결과를 기다림
System.out.println("Response: " + response);
}
}
public class WebClientPostExample {
public static void main(String[] args) {
WebClient webClient = WebClient.create("https://요청주소.com");
// POST 요청에 필요한 Body
String requestBody = "{"
+ "\"grant_type\":\"authorization_code\","
+ "\"client_id\":\"your_client_id\","
+ "\"redirect_uri\":\"your_redirect_uri\","
+ "\"code\":\"your_code\","
+ "\"client_secret\":\"your_client_secret\""
+ "}";
String response = webClient.post()
.uri("/posts")
.header("Content-Type", "application/json")
.bodyValue(requestBody) // JSON 형식의 BODY 추가
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println("Response: " + response);
}
}
🚨 URL에 직접 파라미터를 넣어야 한다면?!?
카카오 로그인 작업을 하다보니... body로 보내지 않고 parameter로 직접 전송하는 경우가 있었다.
이럴 때는
.uri("/oauth/token?grant_type=authorization_code&client_id=" + clientId
+ "&redirect_uri=" + redirectUri
+ "&code=" + code
+ "&client_secret=" + clientSecret)
위와 같이 직접 해당 경로를 적어줘도 되지만, 유지보수가 어려울 수 있기에
UriBuilder를 적용할 것이다!
.uri(uriBuilder -> uriBuilder
.scheme("https")
.path("/oauth/token")
.queryParam("grant_type", authorizationGrantType)
.queryParam("client_id", clientId)
.queryParam("client_secret", clientSecret)
.queryParam("code", code)
.build(true))
이렇게 넣어주면 훨씬 깔끔하고 수정하기가 쉽다.
여기서 build 부분에 true는 특수문자 등을 인코딩 할지 여부이다.
띄어쓰기를 예로 들면 false는 띄어쓰기 그대로 적용되고, true는 %20으로 대체된다.
🚨 block()을 사용하는 이유?!?
🚨 비동기 방식으로 한다면..?!?
// 비동기 방식
Mono<String> responseMono = webClient.get()
.uri("/posts/1")
.retrieve()
.bodyToMono(String.class);
// 응답을 비동기적으로 기다림
responseMono.subscribe(response -> System.out.println("Response: " + response));
// 다른 작업도 동시에 수행 가능
System.out.println("Other tasks can be performed...");
WebClient 객체 생성 부분 이후로 위의 코드와 같이 수정하면 된다.
subscribe() : 비동기 작업 결과가 완료되었을 때, 비동기적으로 응답을 처리한다.