Spring

[SpringBoot]GET API route 중복 issue 에러 해결 & SpringBoot와 React는 어떻게 통신하나?(Feat. Tomcat)

Woonys 2022. 7. 29. 02:27
반응형

Abstract

  • localhost:8080/ 페이지 요청 시 스프링부트에서 리액트 화면을 불러오는 과정에서 Whitelabel Error Page 에러 발생
  • 서버 단 에러 로그 확인 시 MethodArgumentTypeMismatchException와 NumberFormatException이 발생 → 프론트 단의 index.html을 불러오는 과정에서 이슈가 발생
    • MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long';
    • nested exception is java.lang.NumberFormatException: For input string: "index.html"
  • 확인 결과, Controller의 특정 GET API의 url이 (”/{entityId}”) 로 설정되어 있었음. 즉, 루트 화면 페이지 주소와 특정 GET API 호출 주소가 중복되어 발생한 이슈임.
  • url 상에서 “/” 호출 시 톰캣이 React 단으로 index.html를 호출한 다음 받아서 화면에 그려준다. 그런데 특정 API의 url을 “/” 으로 해놨으니 서버는 자기가 받은 index.html 을 entityId를 호출하는 API에다 넘겨준다. 이 때문에 이 string(index.html)을 Long으로 치환하려는 과정에서 이슈가 터진 것.
  • 스프링부트에 내장된 톰캣 컨테이너와 React, 브라우저는 각각 어떻게 통신하는가?

Introduction

Get method 관련 백엔드 단 작업을 마무리한 뒤, 프론트엔드에서 데이터를 받아올 준비를 마치고 서버를 띄웠다.

어라? 근데 뭔가 이상하다. Whitelabel Error Page가 뜬다. 이는 index.html 페이지를 따로 설정하지 않았을 때 생기는 이슈이다.

에러메시지를 확인해보면 아래와 같다.

Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; nested exception is java.lang.NumberFormatException: For input string: "index.html"]

Issue: MethodArgumentTypeMismatchException & NumberFormatException

구글에 검색해봤지만 뚜렷한 원인을 알 수 없었다. 그래서 에러 로그에 좀 더 주목했다.  

 

  1. MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long';
  2. nested exception is java.lang.NumberFormatException: For input string: "index.html"

첫번째 로그를 보면 클라가 요청한 것은 Long 타입인데, String 타입을 Long으로 전환할 수 없어 TypeMisMatchException이 발생했다고 나온다.  

 

뒤이어 중첩 exception에서는 input string에 대해 NumberFormatException이 발생하는데, “index.html”이라고 나온다.  

 

자, 원래 나와야 했을 페이지는 스프링부트에 같이 빌드된 리액트 UI이다. 그런데 해당 페이지를 불러오는 과정에서 이슈가 생겼다는 건데..

관련해서 좀 더 찾다가 재밌는 글을 하나 발견했다. 위의 whitelabel Error Page에 대한 이슈 원인 및 해결에 대한 글이다.  

에러 원인 Component Scan을 하지 못해서

해결방법 Application main() 클래스 하위 디렉토리 안에 Component(controller)가 있어야 한다. 또는 @ComponentScan({”com.woony”})와 같이 스캔할 페이지를 지정해준다.

패키지의 최상단이란 의미는 다음과 같습니다. 컨트롤러 클래스가 com.web.controller 패키지에 있다면 메인 메소드가 있는 클래스는 com.web이나 com 패키지에 있어야 합니다. com.web.project에 있거나 com.woony에 있으면 안됩니다. 같은 계층 구조 안에 있어야 합니다.

@Restcontroller는 잘 설정되어 있는 것을 확인했기에 저것이 원인은 아니었다. 하지만 컨트롤러 단에서 문제가 발생했다는 건 짐작할 수 있었다.  

 

컨트롤러에 등록된 GET API를 하나씩 살펴보다가 뭔가 이상한 놈을 하나 찾았다. 하나의 특정 엔티티를 불러오는 GET API였다.  

@GetMapping("/{entityId}")
public EntityDTO getOneEntity(@PathVariable("entityId") Long entityId) {
    return entityQueryService.getentityDTO(entityId);
}

url을 보면 “/” 다음에 바로 엔티티 ID가 온다. 이는 프론트엔드 화면을 불러오는 url과 동일하다. 만약 localhost:8080/로 접속한다고 하자. 원래는 이를 서버가 받아서 같이 빌드된 리액트로 요청을 넣어 index.html 페이지를 받아 클라이언트에 반환한다. 그런데 지금은 리액트가 준 index.html 을 위의 getOneEntity 가 input으로 받고 있는 상황이다. 즉, index.html 을 entity의 ID로 인식하고 이에 맞는 엔티티를 가져오려고 한다. 그로 인해 java.lang.NumberFormatException: For input string: "index.html" 라는 Exception 메시지가 발생한 것.  

How to solve?

간단하다. API 경로를 루트와 겹치지 않게 수정하면 된다.

@GetMapping("/getId/{entityId}")
public EntityDTO getOneEntity(@PathVariable("entityId") Long entityId) {
    return entityQueryService.getentityDTO(entityId);
}

이제 이 문제가 왜 발생하게 된 건지 근본 원리를 파헤쳐보자.  

SpringBoot과 React는 어떻게 통신하는 거지?(Feat.Tomcat)

위의 이미지는 서버를 띄웠을 때 브라우저와 웹 서버가 어떻게 동작하는지를 도식화한 그림이다. 브라우저에서 url을 통해 웹페이지를 요청하면, 스프링부트에 내장된 톰캣 컨테이너가 이를 받아 그에 맞는 웹페이지를 동적으로 뿌려준다. 맨 처음에 해당 웹페이지에 접속하려고 요청하면 톰캣은 메인 페이지를 그리기 위해 리액트로부터index.html을 가장 먼저 불러온다. React가 index.html을 건네주면 톰캣이 받아 localhost:8080/으로 서빙하는 것.  

 

다시 문제로 돌아가보자. 브라우저에서 localhost:8080/에 접속하면 스프링부트에 내장된 톰캣 컨테이너는 이를 받아 같이 빌드된 리액트로부터 index.html을 요청한다. 이를 다시 톰캣 컨테이너가 받는데, 원래대로라면 이 index.html 을 브라우저에 뿌려줘야 하나 위의 문제에서는 이를 GET API(getOneEntity())가 input으로 받아버려 생긴 이슈였다. 따라서 API의 경로를 수정해 index.html을 input으로 받지 않도록 함으로써 해결했다.  

 

이쯤에서 Tomcat을 잘 알아야 할 필요가 있다. 간단히 정리하고 넘어가자.  

Further Study 1: Apache Tomcat이란?

아파치 톰캣(Apache Tomcat)은 아파치 소프트웨어 재단에서 개발한 WAS(Web Application Server)로, 서블릿 컨테이너(또는 웹 컨테이너)만 갖고 있다.

컨테이너: JSP, Servlet을 실행시킬 수 있는 소프트웨어. 

 

서블릿(Servlet): 클라이언트의 요청을 받고 요청을 처리해 결과를 클라이언트에게 제공하는 자바 인터페이스. 

 

톰캣은 웹 서버와 연동해 실행할 수 있는 자바 환경을 제공하여 자바 서버 페이지(JSP)와 자바 서블릿이 실행할 수 있는 환경을 제공하고 있다. DB 처리와 같은 동적인 기능을 가공하여 HTML 파일로 만들어 클라이언트에게 제공한다(8080 포트).

기본적으로 Apache와 Tomcat은 나뉘어져 있지만 Tomcat 5.5버전부터 정적 콘텐츠 처리기능이 추가되고 Tomcat이 Apache 기능을 포함하기 때문에 Apache Tomcat이라고 부른다. 아래 그림을 통으로 아파치 톰캣이 한다고 보면 된다.

Further study 2: Servlet은 뭐지?

서블릿(Servlet)이란 동적 웹 페이지를 만들 때 사용되는 자바 기반 웹 어플리케이션 프로그래밍 기술(결국 자바 코드를 말한다!). 서블릿은 웹 요청과 응답 흐름을 간단한 메소드 호출만으로 체계적으로 다룰 수 있게 해준다.

 

톰캣을 통해 스프링부트와 React가 API 통신을 주고받으며 그 결과를 동적으로 웹페이지로 뿌려주는 만큼, 이 과정에 대한 이해도가 높아야 할 필요가 있음을 느낀 이슈였다.

Reference

반응형