[Spring, Vue.js] [Spring, Vue.js] 카카오 간편 로그인 만들기③ - Spring으로 토큰 교환하기 (REST API)
[Spring, Vue.js] 카카오 간편 로그인 만들기② - Vue로 프론트엔드 작성하기 (REST API)[Spring,Vue.js] 카카오 간편 로그인 만들기 ① - 구조와 흐름 파악하고 카카오 디벨로퍼스 설정하기카카오 간편 로그
post-this.tistory.com
카카오에게 토큰을 받아왔다.
받아온 토큰으로 사용자의 정보를 읽어오자!
1. 화면
- 로그인 성공
- 메인화면(닉네임 O)
2. 전체 코드
package 여러분걸로 넣으세요 &_&
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
@RequestMapping("/oauth")
@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true")
public class KakaoController {
@Value("23558386d3608da96ec5a589aabe5a2b")
private String clientId;
@Value("${kakao.redirect-uri}")
private String redirectUri;
private final RestTemplate rest = new RestTemplate();
@PostMapping("/kakao")
public ResponseEntity<?> kakao(@RequestBody Map<String, String> body) {
// 1) 토큰 교환
String code = body.get("code");
if (!StringUtils.hasText(code)) {
return ResponseEntity.badRequest().body("code 누락");
}
try {
HttpHeaders tokenHeaders = new HttpHeaders();
tokenHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("grant_type", "authorization_code");
form.add("client_id", clientId);
form.add("redirect_uri", redirectUri);
form.add("code", code);
HttpEntity<MultiValueMap<String, String>> tokenReq = new HttpEntity<>(form, tokenHeaders);
ResponseEntity<Map> tokenRes = rest.postForEntity(
"https://kauth.kakao.com/oauth/token", tokenReq, Map.class);
if (!tokenRes.getStatusCode().is2xxSuccessful() || tokenRes.getBody() == null) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("토큰 교환 실패");
}
String accessToken = (String) tokenRes.getBody().get("access_token");
if (!StringUtils.hasText(accessToken)) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("access_token 없음");
}
// 2) 사용자 정보 조회 (닉네임만 필요)
HttpHeaders userHeaders = new HttpHeaders();
userHeaders.setBearerAuth(accessToken);
HttpEntity<Void> userReq = new HttpEntity<>(userHeaders);
ResponseEntity<Map> userRes = rest.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
userReq,
Map.class
);
if (!userRes.getStatusCode().is2xxSuccessful() || userRes.getBody() == null) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("사용자 정보 요청 실패");
}
// 3) JSON(Map)에서 nickname만 꺼냄
Map bodyMap = userRes.getBody();
Map kakaoAccount = (Map) bodyMap.get("kakao_account");
Map profile = kakaoAccount != null ? (Map) kakaoAccount.get("profile") : null;
String nickname = profile != null ? (String) profile.get("nickname") : null;
Map<String, Object> result = new HashMap<>();
String safeNickname = (nickname != null) ? nickname : "KakaoUser";
result.put("nickname", safeNickname);
return ResponseEntity.ok(result);
} catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("카카오 연동 중 오류");
}
}
}
3. KakaoController.java
// ①
HttpHeaders userHeaders = new HttpHeaders();
userHeaders.setBearerAuth(accessToken);
// ②
HttpEntity<Void> userReq = new HttpEntity<>(userHeaders);
①
카카오에서 정해둔 규칙을 보면 헤더에 Authorization: Bearer을 담으라고 되어있다. Authorization은 HTTP 요청에 클라이언트의 신원(권한 증명)을 붙이는 표준 헤더이다. 그리고 Authorization에 대표 스킴들 중 하나가 Bearer이다.
Bearer은 토큰을 그대로 들고가는 방식으로, 이 토큰을 들고 있는 자가 곧 권한자임을 뜻한다. 그래서 userHeaders에 setBearerAuth로 토큰을 넣고 카카오가 받으면 제대로 된 토큰인지 확인하고 요청을 처리한다.
②
다시 카카오가 정해둔 규칙을 살펴보자. 기본정보의 메서드를 보면 GET/POST라고 되어있다. 우리는 그 중 GET 방식으로 헤더에 토큰만 넣어 보낼 것이다. 즉, 바디는 필요하지 않다. 그러므로 Content-Type은 우리가 고려할 대상이 아니다.
토큰을 교환할 때, Content-Type이 폼형식인걸 고려하여 헤더에 MediaType.APPLICATION_FORM_URLENCODED 설정했다. 그리고 new HttpEntity<>(form, tokenHeaders);를 보면 HttpEntity에 바디와 헤더 객체를 넣어 보냈다.
사용자 정보 조회는 헤더에 토큰만 넣으면 되니까 그런거 다 필요없이 HttpEntity에 토큰이 들어있는 userHeader만 넣으면 된다.
Q. HttpEntity말고 RequestEntity를 사용해도도 되지않나?
A. 사용해도 된다. 그러나 둘 사이엔 차이가 존재한다. HttpEntity는 간단히 헤더/바디만 묶고 싶을 때 사용하고, RequestEntity는 요청 전체를 한줄로 표현하고 싶을 때 사용한다. 쉽게 말해서, HttpEntity는 헤더+바디를 담고 RequestEntity는 메서드+URL+헤더+바디인 요청 전부를 담는다. 또, RequestEntity는 exchange만 사용할 수 있다.
ResponseEntity, RequestEntity, HttpEntity
RequestEntity, ResponseEntity가 뭘까요? 일단 직접 코드를 열어봅시다. // ResponseEntity.java public class ResponseEntity extends HttpEntity { ... } // RequestEntity.java public class RequestEntity extends HttpEntity { ... } 두 객체 모두
dev-ws.tistory.com
// ①
ResponseEntity<Map> userRes = rest.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
userReq,
Map.class
);
①
exchange는 HTTP 요청 및 응답 처리로 RestTemplate에서 제공하는 메서드로, 헤더를 생성하고 모든 요청 방법을 허용한다. exchange는 exchange(url, HttpMethod, HttpEntity, responseType) 이렇게 생겨서 HTTP 메서드와 헤더/바디 모두 직접 지정 가능하기 때문에 모든 요청 방법이 허용되는 것이다.
아까 코드에서 HttpEntity에 토큰이 든 헤더를 담았다. 그걸 userReq라고 지었고, GET으로 보내기로 했다. 카카오가 보내라는 경로로 이 데이터를 보내면 된다.
그리고 토큰 교환 파트와 같이 Map으로 역직렬화하도록 설정했다. 그 이유는 키/값으로 데이터를 파싱하기 위해서인데, 원래는 DTO 클래스로 받는게 안전하지만 연습 코드로 빠르게 값을 뽑아 쓰기 위해 Map 타입으로 설정했다.
Q. 토큰을 교환했을 땐, postforEntity를 사용했다. 그러면 여기서도 getforEntity를 사용하면 되지 않을까?
A. 사용할 수 없다. 그 이유는 HttpEntity에 있다. 토큰은 HttpEntity에 담겨져 있어서 HttpEntity를 담을 수 있는 그릇이어야한다. exchange는 exchange(url, HttpMethod, HttpEntity, responseType)으로 담을 수 있는 공간이 있다. 하지만 getForEntity(url, responseType)으로 담을 수 있는 공간이 없어 토큰을 카카오 API 서버로 보낼 수 없다.
// ①
if (!userRes.getStatusCode().is2xxSuccessful() || userRes.getBody() == null) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("사용자 정보 요청 실패");
}
①
tokenRes의 받아온 상태 코드가 성공적이지 않거나, 받아온 바디가 null인 경우 오류를 반환한다.
// ①
Map bodyMap = userRes.getBody();
Map kakaoAccount = (Map) bodyMap.get("kakao_account");
①
응답으로 온 userRes에서 바디를 꺼낸다. 이때 Map 타입인 이유는 Map으로 역직렬화했기 때문이다. 그리고 bodyMap에서 kakao_account를 꺼내 kakaoAccount에 담는다.
// ①
Map profile = kakaoAccount != null ? (Map) kakaoAccount.get("profile") : null;
// ②
String nickname = profile != null ? (String) profile.get("nickname") : null;
①
profile 값을 꺼냈으면, Map으로 캐스팅하고 이름은 profile이다 = kakaoAccount가 null이 아니면 ? kakaoAccount에서 profile 키의 값을 꺼내고 : 만약 null이면 null을 넣어라.
쉽게 설명해서 kakaoAccount가 null이 아니면 profile을 꺼내 profile에 넣으라는 것이다. 이때 profile은 Map 타입이다.
②
nickname 값을 꺼냈으면, String으로 캐스팅하고 이름은 nickname이다 = profile이 null이 아니라면 ? profile에서 nickname 키의 값을 꺼내고 : 만약 null 이면 null을 넣어라.
profile에 nickname을 꺼내 nickname에 넣으라는 뜻이다. 이때 nickname은 String 타입이다.
// ①
Map<String, Object> result = new HashMap<>();
String safeNickname = (nickname != null) ? nickname : "KakaoUser";
result.put("nickname", safeNickname);
return ResponseEntity.ok(result);
①
result에 결과를 넣어 반환할 것이다. 닉네임 하나만 넣어 반환할거라 타입을 그냥 HashMap을 써줬다. 그리고 아까와 같이 nickname이 null 값인지 검사하고, null이면 safeNickname에 KakaoUser을 넣고, 값이 있으면 그 값을 넣었다. 마지막으로 성공을 의미하는 상태코드와 함께 result 값을 반환했다.
// ①
catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body("카카오 연동 중 오류");
}
①
try 부분은 끝났고, catch 부분만 작성하면 된다.
Exception은 광범위한 예외로, 대부분의 예외를 다 잡아버리는 광범위한 캐치이다. 사실 이렇게 예외를 잡는게 좋은 방법은 아니다. 세부적이게 예외를 잡는게 더 좋다. 카카오 디벨로퍼스에서도 상당히 구체적이게 예외에 대해 써놓은걸 확인할 수 있다. 시간이 있다면 한 번 해보는걸 추천한다!(하고 나도 알려줘잉~)
printStackTrace( )는 예외의 클래스명, 메시지, 호출 경로를 콘솔로 출력시킨다. 좋은 처리 방법은 절대 아니다. 로그를 수집하기도 어렵고 개인 정보 유출 위험도 있다.
이렇게 카카오 간편 로그인을 마무리했다. 전반적으로 회원가입 페이지를 마무리한 소감을 말해보자면 정말 어려웠다. 모든 부분이 어려운게 아니라, 카카오 간편 로그인이 진짜 너무 어려웠다. 대학생 시절부터 지금까지 나름대로 어려움이 있었지만, 그렇다고 포기를 생각할정도의 어려움은 없었다. 아무것도 모르는 팀원 3명을 데리고 졸업 프로젝트를 했을 때도, 답이 안보이진 않았다. 아무리 잘 모르겠어도 3일정도 고민하면 해결책이 보였고, GPT에게 슬쩍 물어봐서 해결 방법을 찾아내곤 했다.
일단 나는 GPT를 사용하지 않는다고 하면 완전 그으짓말이고, 사용은 하지만 의존하진 않으려 한다. 풀리지 않는 어려움이 있어도, 코드를 만들어달라기 보다 이런 상황에서 어떤 문법을 사용하여 나갈 수 있는지에 대해 묻는다. 그리고 그 안에서 최대한 생각해보고 사람들이 작성한 코드를 살펴보고 대충 어떤 흐름인지 알았으면 무작정 작성해보는 식으로 수행했다. 그런데 카카오 간편 로그인은 어떻게 해도 작동되지 않았다. 카카오 간편 로그인 화면은 분명 띄워졌지만, 토큰 교환에서 난항을 겪어 일주일동안 스트레스 받아가며 포기할까를 고민했다. 7일 뒤엔 GPT에게도 물었지만, 정말 어떤 흐름인지 하나도 이해가 가질 않아서 복붙하지도 못했다.
10일차엔 고민이 됐다. 어차피 자바스크립트랑 리액트 관련 프로젝트도 할 예정이라, 훨씬 예제 코드가 많을 것 같으니 거기서 해볼지 혹은 GPT 코드를 복붙해와서라도 완수할지... 그렇게 고민하던 찰나에 선배 개발자님들이 모두 GPT를 사용해서라도 풀어보라고 하셨다. 그리고 꼼꼼하게 공부하라고 덧붙였다. 그래서 그렇게 했다! 일단 작동되는 부분까지 들고가서 이어 만들어달라고 했다. 그리고 정말 꼼꼼하게 공부하려 노력했다. 처음엔 RestTemplate가 뭔지도 몰랐다. 그러니 처음 보든 간편 메서드들이 너무 많아서 머리가 아팠다. 코드 공부를 쭉 하다가 백엔드에서 사용하고 있는 것이 RestTemplate인지 알았다. 선언을 해놨어도 그게 뭔지 모르니 초반에는 그냥 넘겼던 것이다.
생각해보면 GPT 사용하길 참 잘한것 같다. 아마 그대로 넘어갔어도 성격상 계속 뒤를 돌아봤을 것이다. 그리고 블로그를 작성하면서 실제로 코드를 꽤 수정했다.(이건 내 힘으로!) 굳이 쓸 필요없는 메서드나 코드들을 지우고 그냥 간단하게 내가 아는 코드로 교체하기도 했다. 이번에 카카오 간편 로그인을 공부하면서 식견이 전보다 많이 넓어졌음을 깨달았다. 다음에 시간이 된다면 (얼른 취업해야함;;) 리액트나 자바스크립트로 네이버 간편 로그인도 수행해보고 싶어졌다. 왜냐면 그것도 어려울 것 같아서... 하는 도중은 꽤 열이 받겠지만 하고 나면 분명 더 발전할 것 같다.
그럼 모두들 꼭 취준하시고, 제 글이 도움이 됐기를 바랍니다!! 🍀
만약 다 하고 나서도 안 되는 부분이 있다면 댓글을 남겨주세요.
제가 빼먹은 부분이 있을지도 몰라요,,, 헷 🐥
! 화이팅 !