카카오 소셜 로그인 서비스를 가져와서 내가 만든 프로젝트에 카카오 로그인 기능을 적용시켰다.
평소에 접하던 카카오, 네이버 같은 로그인 기능이 이렇게 구현되는 것이란걸 알 수 있었다.
그리고 가끔 소셜 로그인을 했는데도 또 사이트의 로그인이나 회원가입이 나오는 이유가 궁금했었는데, 오늘 직접 구현해보면서 왜 그렇게 되었는지 이해는 해줄 수 있었다! (근데 굳이 그렇게 했어야했니...)
kakao developers > 로그인 REST API
카카오는 친절하게 그림까지 보여주며 이해하기 쉽게 로그인 과정을 설명해주지만, 더 간단히 나타낸 그림은 다음과 같다.

(프론트)앱에서 사용자가 카카오 로그인 ID, PW를 입력하면 인증 코드 요청이 된다.
그러면 카카오 서버는 인증 코드 전달을 주며, 이것을 Controller로 받아오면 된다.
    @GetMapping("/user/kakao/callback")   // kakao developers의 어플리케이션 등록 할 때 넣어줬던 path
    public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException { 
        String token = kakaoService.kakaoLogin(code);
        
        Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7));  // Cookie에는 띄어쓰기 못넣는다. -> "Bearer "로 시작하기 때문에 오류 -> substring해줬음
        cookie.setPath("/");
        response.addCookie(cookie); //successfulAuthentication과 같은 역할
        
        return "redirect:/";
    }



    public String kakaoLogin(String code) throws JsonProcessingException {
        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getToken(code);
        // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
        KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
         				
               ... // 회원가입, JWT 토큰 반환 등 -> 필요한 로직으로 변경
                        
        return createToken;
        }
    private String getToken(String code) throws JsonProcessingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://kauth.kakao.com")
                .path("/oauth/token")  // REST API 문서에 따라 작성
                .encode()
                .build()
                .toUri();
        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        // HTTP Body 생성
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", "**rest API key 복붙**");
        body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback");
        body.add("code", code);
        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity   // '이런 형태로 request'라고 고정됨.
                .post(uri)   //post로 고정되어있다.
                .headers(headers)
                .body(body);
        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(   
                requestEntity,
                String.class
                
                ...
        );
    }
    
		private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
	       		...
	}
RequestEntity<MultiValueMap<String, String>>라고 request를 보낸 것.restTemplate.exchange: restTemplate를 호출하여 카카오 서버를 호출하는 것이다! -> 이게 실행되면 인증코드로 토큰 요청을 하는 것.private String getToken(String code) throws JsonProcessingException {
     ...
     
     // HTTP 응답 (JSON) -> 액세스 토큰 파싱
     JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
     return jsonNode.get("access_token").asText();
     }


    private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://kapi.kakao.com")
                .path("/v2/user/me")
                .encode()
                .build()
                .toUri();
        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
                .post(uri)
                .headers(headers)
                .body(new LinkedMultiValueMap<>());  //body는 보낼 필요가 없어서 그냥 생성만 해서 보냄
        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(
                requestEntity,
                String.class
        );
        
        ...
}

    private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
        Long id = jsonNode.get("id").asLong();
        String nickname = jsonNode.get("properties")
                .get("nickname").asText();
        String email = jsonNode.get("kakao_account")
                .get("email").asText();
        return new KakaoUserInfoDto(id, nickname, email);
}
        // DB 에 중복된 Kakao Id 가 있는지 확인
        Long kakaoId = kakaoUserInfo.getId();
        User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);
        if (kakaoUser == null) {
            // 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
            String kakaoEmail = kakaoUserInfo.getEmail();
            User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
            if (sameEmailUser != null) {
                kakaoUser = sameEmailUser;
                // 기존 회원정보에 카카오 Id 추가
                kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId); 
            } else {
                // 신규 회원가입
                // password: random UUID
                String password = UUID.randomUUID().toString();
                String encodedPassword = passwordEncoder.encode(password);
                // email: kakao email
                String email = kakaoUserInfo.getEmail();
                kakaoUser = new User(kakaoUserInfo.getNickname(), encodedPassword, email, UserRoleEnum.USER, kakaoId);
            }
            
            userRepository.save(kakaoUser);  // 영속성
        }
        return kakaoUser;
kakaoUser.kakaoIdUpdate(kakaoId);: Transactional이 필요 없게 하려고 return을 kakaoUser라는 객체를 반환했다.외부 API 받아오는 것도 처음엔 뭐가뭔지 도통 몰랐는데, 이렇게 하나씩 뜯어가며 공부해보니 받아와서 사용만 하면 되겠구나 싶다. 물론 막상 스스로 해보면 잘 될지는 모르겠지만... 아무튼 이번 기회에 그 구조에 대해 이해 할 수 있었다.
다른 API들도 받아와서 활용해보고싶다!!!