들어가기 앞서 . . .
OAuth에 대한 지식이 부족하고 전체적인 흐름을 잘 이해하지 못한 채 개발을 시작하면서, 유독 더 많은 에러를 마주하고 많은 시간을 소요했던 것 같습니다. 자체 로그인은 여러 번 구현해본 경험이 있어서 JWT 토큰 발급까지는 어렵지 않았지만, 소셜 로그인에서는 인가 코드 등 익숙하지 않은 개념과 용어들이 많아 처음부터 난관이었습니다.
보통 코드는 구글링하면서 직접 타이핑해보며 이해하는 게 빠르다고들 하죠. 저 역시 그런 방식이 익숙했고 실제로 많은 도움이 되곤 했습니다. 하지만 이번엔 다양한 블로그 포스팅을 참고하며 적용해봤지만, 생각보다 잘못된 정보가 많았고 그로 인해 같은 에러를 오랫동안 해결하지 못했습니다.
결국 천천히 OAuth의 이론과 흐름을 먼저 이해한 뒤 다시 코드를 보게 되었고, 그제서야 문제의 본질이 보이기 시작했습니다. 당연히 작동하지 않는 코드였습니다...😭 이 글을 보시는 분들이라면 저처럼 ‘맨땅에 헤딩’하는 방식도 좋지만, 기본 개념과 흐름을 먼저 익힌 뒤 실습하시는 걸 추천드립니다.
Jwt 적용한 카카오 소셜로그인 Postman으로 테스트하기
소셜 로그인을 구현한 뒤, 프론트엔드 연동 전 단계에서 Postman으로 직접 API를 호출하면서 정상적으로 JWT가 발급되고 처리되는지 테스트해보는 것이 중요했습니다. 이 과정을 통해 로그인 흐름과 토큰 발급, 쿠키 저장까지 전체 백엔드 로직이 문제 없이 동작하는지 빠르게 검증할 수 있습니다.
카카오 소셜로그인
우선 사용한 소스코드는 아래와 같습니다. jwt 관련 소스코드는 따로 업로드하지않았습니다.
OauthController.java
@Operation(summary = "카카오 소셜 로그인 콜백 컨트롤러 입니다.")
@GetMapping("/callback/kakao")
public ResponseEntity<?> getKaKaoAuthorizeCode(@RequestParam(value = "code", required = false) String code,
HttpServletResponse response) {
if (code == null) return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("code 파라미터가 없습니다.");
try {
String accessToken = oauthService.getKakaoAccessToken(code);
ResultResponse<?> resultResponse = oauthService.kakaoLogin(accessToken, response);
return ResponseEntity.status(HttpStatus.OK).body(resultResponse);
} catch (BusinessException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("카카오 로그인 처리 중 오류 발생");
}
}
OauthService.java
@Override
public String getKakaoAccessToken(String code) {
log.info("[getKakaoAccessToken] 카카오 액세스 토큰 요청: code={}", code);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", kakaoClientId);
body.add("client_secret", kakaoClientSecret);
body.add("redirect_uri", kakaoRedirectUri);
body.add("code", code);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.postForEntity(kakaoTokenUri, request, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(response.getBody());
log.info("[getKakaoAccessToken] 카카오 액세스 토큰 발급 완료");
return element.getAsJsonObject().get("access_token").getAsString();
} else {
log.error("[getKakaoAccessToken] 카카오 액세스 토큰 요청 실패: {}", response.getStatusCode());
throw new BusinessException(ErrorCode.OAUTH_ACCESS_TOKEN_ERROR);
}
}
@Override
public HashMap<String, Object> getUserKakaoInfo(String accessToken) {
log.info("[getUserKakaoInfo] 카카오 사용자 정보 요청 시작");
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(userInfoReqUri, HttpMethod.GET, entity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(response.getBody());
HashMap<String, Object> userInfo = new HashMap<>();
userInfo.put("id", element.getAsJsonObject().get("id").getAsString());
JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
userInfo.put("nickname", properties.get("nickname").getAsString());
if (element.getAsJsonObject().get("kakao_account").getAsJsonObject().has("email")) {
userInfo.put("email", element.getAsJsonObject().get("kakao_account").getAsJsonObject().get("email").getAsString());
}
log.info("[getUserKakaoInfo] 카카오 사용자 정보 조회 성공: email={}", userInfo.get("email"));
return userInfo;
} else {
log.error("[getUserKakaoInfo] 카카오 사용자 정보 요청 실패: {}", response.getStatusCode());
throw new BusinessException(ErrorCode.OAUTH_USER_INFO_ERROR);
}
}
@Override
public ResultResponse<?> kakaoLogin(String accessToken, HttpServletResponse response) {
log.info("[kakaoLogin] 카카오 로그인 요청 시작");
ParentSignUpRequest requestDto = getUserKakaoSignupRequestDto(getUserKakaoInfo(accessToken));
ParentResponse parentResponse = findByUserKakaoIdentifier(requestDto.id());
if (parentResponse == null) {
log.info("[kakaoLogin] 신규 사용자, 회원가입 진행: email={}", requestDto.email());
signUp(requestDto);
parentResponse = findByUserKakaoIdentifier(requestDto.id());
if (parentResponse == null) {
log.error("[kakaoLogin] 회원가입 후 사용자 정보 조회 실패: email={}", requestDto.email());
throw new BusinessException(ErrorCode.USER_REGISTRATION_FAILED);
}
}
String newAccessToken = jwtProvider.createAccessToken(parentResponse.email(), parentResponse.roles(), "SOCIAL_KAKAO");
String newRefreshToken = jwtProvider.createRefreshToken(parentResponse.email());
tokenRedisRepository.save(new TokenRedis(parentResponse.email(), newAccessToken, newRefreshToken));
jwtProvider.saveAccessTokenToCookie(response, newAccessToken);
log.info("[kakaoLogin] 카카오 로그인 성공: email={}, accessToken 저장 완료", parentResponse.email());
ParentLoginResponseDto dto = new ParentLoginResponseDto(newAccessToken, parentResponse.email());
return new ResultResponse<>(ResultCode.PARENT_LOGIN_SUCCESS, dto);
}
@Override
public ParentResponse findByUserKakaoIdentifier(String kakaoIdentifier) {
log.info("[findByUserKakaoIdentifier] 카카오 ID로 정보 조회: kakao_id={}", kakaoIdentifier);
List<Parent> parents = parentRepository.findParentByProviderId(kakaoIdentifier).orElse(List.of());
if (parents.isEmpty()) {
log.warn("[findByUserKakaoIdentifier] 정보 없음: kakao_id={}", kakaoIdentifier);
return null;
}
return new ParentResponse(parents.get(0));
}
@Override
@Transactional
public Long signUp(ParentSignUpRequest requestDto) {
try {
log.info("[signUp] 회원가입 요청: email={}", requestDto.email());
Long parentId = parentRepository.save(requestDto.toEntity(requestDto.email(), requestDto.nickname(), requestDto.id())).getId();
log.info("[signUp] 부모 회원가입 완료: parent_id={}", parentId);
return parentId;
} catch (Exception e) {
log.error("[signUp] 회원가입 중 오류 발생: {}", e.getMessage());
throw new BusinessException(ErrorCode.FAILED_TO_SAVE_USER);
}
}
private ParentSignUpRequest getUserKakaoSignupRequestDto(HashMap<String, Object> userInfo) {
return new ParentSignUpRequest(
(String) userInfo.get("email"),
(String) userInfo.get("nickname"),
(String) userInfo.get("id")
);
}
카카오 인가 코드 받기
카카오 로그인 URL에 접속하여 인가 코드를 발급받습니다.
https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code
여기서 말하는 client_id와 redirect_uri는 https://developers.kakao.com 에서 설정 & 확인할 수 있습니다.
client_id : 내 애플리케이션 -> 앱 설정 -> 앱 키 -> REST API 키
redirect_uri: 내 애플리케이션 -> 제품 설정 -> 카카오 로그인 -> Redirect URI (직접 설정해주셔야합니다.)
(Redirect URI는 비즈니스 인증 카테고리에도 존재하는데 이것과 혼동하시면 안됩니다ㅠ.ㅠ)
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
위의 url을 브라우저에 입력하면 아래와 같이 kakao login 화면이 뜨게 되고, 로그인을 진행하면 리다이렉트로 되면서 url을 통해서 인가 코드를 발급받을 수 있습니다. 여기서 http://localhost:8080/callback/kakao는 제가 설정한 Redirect URI입니다.
인가코드를 통해서 token을 성공적으로 발급받은걸 확인할 수 있었습니다.
http://localhost:8080/callback/kakao?code=abcd1234e.....


Postman으로 테스트하기
이제 postman으로 로그인이 잘 되는지 확인해보겠습니다.
요청을 보내기전에 설정을 해야합니다. Authorization에서 아래와 같이 설정을 바꿔주시면 됩니다.
Auth Type: OAuth 2.0
Add authorization data to: Request Headers
Current Token
Header Prefix: Bearer
Configure New Token
Token name: [자유롭게 설정해주세요.]
Grant Type: Authorization Code
Callback URL: [본인의 Redirect URL 입력해주세요.]
Auth URL: https://kauth.kakao.com/oauth/authorize
Access Token URL: https://kauth.kakao.com/oauth/token
Client ID: [본인의 REST API 키를 입력해주세요.]
Client Secret: [본인의 Client Secret 를 입력해주세요.]
Client Secret은 https://developers.kakao.com 확인할 수 있습니다. (내 애플리케이션 -> 제품 설정 -> 카카오 로그인 ->
보안 -> Client Secret 코드 복사)
Client Authentication: Send client credentials in body


입력후 아래에 Get New Access Token 버튼을 누르면 아까 브라우저에서 직접 URL을 입력했던 것 처럼 kakao 로그인 화면창이 나오게 됩니다. 그대로 로그인을 진행하면 아래와 같이 Authentication complete라는 창이 뜨게 됩니다. Proceed를 눌러주세요.


Proceed를 누르면 아래와 같이 token 정보가 뜨게 됩니다.

USE TOKEN을 눌러줍니다.
여기서 제가 블로그 포스팅들을 확인하며 가장 많이 겪은 오류 KOE320가 등장합니다.
이 게시글은 POSTMAN으로 테스트하는 방법을 주로 다루고 있기 때문에 오류 내용을 하나하나 작성하지 않았습니다.
혹시나 싶어서 접은글에 간단하게 오류가 발생한 이유를 적어놓았고, 따로 포스팅을 다뤘으니 궁금하신분들은 참고 부탁드립니다.
http://localhost:8080/callback/kakao?code=abcd1234e.....
일부 포스팅에서 위와 같은 {redirect_url}?code=의 code값에 Access Token을 넣은 뒤, 로그인을 성공적으로 수행하는 테스트 코드를 다루고 있습니다. 오류를 해결한 뒤 혹시나 소스코드가 달라서 안되는건가 싶어서 Kakao Dev talk에 문의를 남겼습니다만, code에 Accesstoken 값을 넣는 테스트 방식은 사용 불가하다는 답변을 받았습니다.
Access Token은 말 그대로 "액세스 토큰"이지, 인가코드가 아닙니다. 인가코드는 접근토큰을 받기 위한 임시 토큰으로 카카오 로그인 과정중 최종적으로 카카오가 전달하는 값이고, postman에서는 접근토큰 발급까지 자동으로 이루어지기에 인가코드가 즉시 소비 되므로 postman에서 인가코드로 발급 받은 Access Token을 리다이렉트 code값으로 전달하면 안됩니다.

이후 Headers를 확인해보면 Authorization에 발급받은 token이 자동으로 들어가게 됩니다.

Headers에 토큰이 들어간 걸 확인했으면 아래와 같이 GET 요청을 보내봅시다!
https://kapi.kakao.com/v2/user/me
https://kapi.kakao.com/v2/user/me는 카카오 사용자 정보를 가져오는 공식 API 엔드포인트입니다.
이 API는 카카오 소셜 로그인 후, access token을 이용해 사용자 정보를 조회하는 데 사용됩니다.

로그인 한 사용자의 정보가 잘 뜨는 것을 확인할 수 있었습니다.
이렇게 소셜로그인이 성공적으로 수행되는 것이 확인되면, 본인의 프로젝트에 맞게 소스코드를 조금씩 고쳐가며 완성하시면 될 것 같습니다.
마무리하며
이번 Postman을 통한 소셜로그인 테스트과정에서 가장 크게 느낀 건, 단순히 블로그를 따라 하기보다는 이론적인 흐름을 먼저 이해하는 게 훨씬 중요하다는 점이었습니다. 처음엔 다양한 블로그 포스팅을 참고하며 코드를 적용했지만, 구조를 제대로 이해하지 못한 상태에서는 오히려 더 많은 시행착오를 겪었습니다.
결국, 블로그는 참고자료일 뿐 정답지는 아니라는 걸 다시금 느꼈고, 앞으로는 이론 -> 구조 이해 -> 코드 적용이라는 흐름을 가지고 학습해야겠다는 다짐도 하게 되었습니다.
이 글이 소셜 로그인 구현 중 막막함을 느끼는 분들께 조금이나마 도움이 되기를 바랍니다! 긴 글 읽어주셔서 감사합니다.
'Spring > Security' 카테고리의 다른 글
| [Spring Security] Security 용어와 동작 원리 이해하기 (0) | 2025.02.25 |
|---|