카카오 로그인 ec2 배포 에러

앱 id: 1031569

안녕하세요.
로컬에서 카카오 로그인 api를 사용할 때는 사용자 정보까지 정상적으로 받아오는 것을 확인하였습니다.
하지만 aws ec2에 배포를 한 이후에는 로그인 이후 token을 받아오는 과정에서 에러가 발생하는데 원인을 모르겠습니다.

에러와 현재 작성된 로직은 아래와 같습니다.

감사합니다.

2024-02-12T04:24:52.940Z DEBUG 1865 --- [nio-8080-exec-4] h.server.controller.LoginController      : [LoginController.requestLogin]
2024-02-12T04:24:52.941Z DEBUG 1865 --- [nio-8080-exec-4] hibuy.server.service.KakaoService        : [KakaoService.getAccessToken]
2024-02-12T04:24:53.497Z ERROR 1865 --- [or-http-epoll-3] hibuy.server.service.KakaoService        : do on error: org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from POST https://kauth.kakao.com/oauth/token
2024-02-12T04:24:53.498Z ERROR 1865 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from POST https://kauth.kakao.com/oauth/token] with root cause

org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from POST https://kauth.kakao.com/oauth/token
        at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:307) ~[spring-webflux-6.1.2.jar!/:6.1.2]
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
        *__checkpoint ⇢ 400 BAD_REQUEST from POST https://kauth.kakao.com/oauth/token [DefaultWebClient]
Original Stack Trace:
                at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:307) ~[spring-webflux-6.1.2.jar!/:6.1.2]
                at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:214) ~[spring-webflux-6.1.2.jar!/:6.1.2]
                at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onNext(FluxOnErrorReturn.java:162) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:122) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:415) ~[reactor-netty-core-1.1.14.jar!/:1.1.14]
                at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:446) ~[reactor-netty-core-1.1.14.jar!/:1.1.14]
                at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:500) ~[reactor-netty-core-1.1.14.jar!/:1.1.14]
                at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:780) ~[reactor-netty-http-1.1.14.jar!/:1.1.14]
                at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:114) ~[reactor-netty-core-1.1.14.jar!/:1.1.14]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103) ~[netty-codec-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346) ~[netty-codec-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318) ~[netty-codec-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1475) ~[netty-handler-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1338) ~[netty-handler-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1387) ~[netty-handler-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) ~[netty-codec-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468) ~[netty-codec-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[netty-codec-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final]
r.LoginController.requestKakaoLogin             at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:509) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:407) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.104.Final.jar!/:4.1.104.Final]
                at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.104.Final.jar!/:4.1.104.Final]
                at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]
        Suppressed: java.lang.Exception: #block terminated with an error
                at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:103) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at reactor.core.publisher.Mono.block(Mono.java:1728) ~[reactor-core-3.6.1.jar!/:3.6.1]
                at hibuy.server.service.KakaoService.getAccessToken(KakaoService.java:45) ~[!/:na]
                at hibuy.server.controller.LoginController.requestKakaoLogin(LoginController.java:30) ~[!/:na]
                at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
                at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
                at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
                at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
                at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:262) ~[spring-web-6.1.2.jar!/:6.1.2]
                at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190) ~[spring-web-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.2.jar!/:6.1.2]
                at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.2.jar!/:6.1.2]

컨트롤러

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/login")
public class LoginController {

    private final KakaoService kakaoService;

    @Value("${kakao.oauth2.client_id}") private String clientId;
    @Value("${kakao.oauth2.redirect_uri}") private String redirectUri;

    @GetMapping("/oauth2/code/kakao")
    public ResponseEntity<LoginResponse> requestKakaoLogin(@RequestParam String code) {

        log.debug("[LoginController.requestLogin]");

        String accessToken = kakaoService.getAccessToken(code);
        LoginResponse userInfo = kakaoService.getUserInfo(accessToken);

        System.out.println("userInfo.getName() = " + userInfo.getName());
        System.out.println("userInfo.getEmail() = " + userInfo.getEmail());

        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(URI.create("/result"));

        return new ResponseEntity<>(userInfo, headers, HttpStatus.MOVED_PERMANENTLY);
    }

}

서비스

package hibuy.server.service;

import hibuy.server.domain.User;
import hibuy.server.dto.oauth2.KakaoTokenResponse;
import hibuy.server.dto.oauth2.KakaoUserInfoResponse;
import hibuy.server.dto.oauth2.LoginResponse;
import hibuy.server.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoService {

    private final UserRepository userRepository;

    @Value("${kakao.oauth2.client_id}") private String clientId;
    @Value("${kakao.oauth2.redirect_uri}") private String redirectUri;
    @Value("${kakao.oauth2.client_secret}") private String clientSecret;

    public String getAccessToken(String code) {

        log.debug("[KakaoService.getAccessToken]");

        WebClient webClient = WebClient.builder().build();
        String requestUrl = "https://kauth.kakao.com/oauth/token";

        KakaoTokenResponse responseBody = webClient.post()
                .uri(requestUrl)
                .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8")
                .bodyValue(buildAccessTokenRequestBody(code))
                .retrieve()
                .bodyToMono(KakaoTokenResponse.class)
                .doOnError(error -> {
                    log.error("do on error: " + error);
                    log.error("error code: " + error.getMessage());
                }).block();

        return responseBody.getAccess_token();
    }

    public LoginResponse getUserInfo(String accessToken) {

        log.debug("[KakaoService.getUserInfo]");

        WebClient webClient = WebClient.builder().build();
        String requestUrl = "https://kapi.kakao.com/v2/user/me";

        KakaoUserInfoResponse kakaoUserInfoResponse = webClient.get()
                .uri(requestUrl)
                .headers(httpHeaders -> {
                    httpHeaders.set("Authorization", "Bearer " + accessToken);
                    httpHeaders.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
                })
                .retrieve()
                .bodyToMono(KakaoUserInfoResponse.class)
                .doOnError(error -> {
                    log.error("do on error: " + error);
                })
                .block();

        Optional<User> user = userRepository.findByKakaoUserId(kakaoUserInfoResponse.getId());

        if(user.isEmpty()) {
            userRepository.save(new User(
                    kakaoUserInfoResponse.getId(),
                    kakaoUserInfoResponse.getKakao_account().getName(),
                    kakaoUserInfoResponse.getKakao_account().getEmail(),
                    kakaoUserInfoResponse.getKakao_account().getPhone_number()));
        }

        return new LoginResponse(
                kakaoUserInfoResponse.getId(),
                kakaoUserInfoResponse.getKakao_account().getName(),
                kakaoUserInfoResponse.getKakao_account().getEmail(),
                kakaoUserInfoResponse.getKakao_account().getPhone_number());

    }


    private MultiValueMap<String, String> buildAccessTokenRequestBody(String code) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("client_secret", clientSecret);
        params.add("redirect_uri", URLEncoder.encode(redirectUri, StandardCharsets.UTF_8));
        params.add("code", code);
        return params;
    }
}

안녕하세요.

인가코드와 접근토큰 발급 시 사용된 redirect_uri 는 서로 같아야만 합니다.
해당 파라미터 값을 확인 부탁드립니다.

aws 배포 시, spring 변수 값이 정상 배포되었는지 확인해 보시면 좋을것 같습니다.