본문 바로가기
빌드노트 검색

빌드노트

RestController에서 redirect를 반환했는데 문자열만 보인다

Spring MVC에서 redirect:/done이 본문에 보인다면 뷰 이름과 ResponseBody 처리를 구분한다. 격리 MockMvc에서 Controller의 302, RestController의 200 본문, ResponseEntity의 명시적 303·Location을 비교하고 브라우저 이동은 검증 범위에서 분리한다.

Spring MVC의 @RestController 메서드에서 "redirect:/done"을 반환했는데 응답 본문에 그 글자만 보인다면, 먼저 클래스의 어노테이션을 확인한다. redirect:는 뷰 이름을 해석할 때 쓰는 접두사다. 응답 본문으로 처리되는 String에 붙인다고 이동 응답이 되지는 않는다. REST 컨트롤러를 유지하면서 리다이렉트를 보내려면 상태 코드와 Location 헤더를 명시한다.

@RestController에는 @Controller와 @ResponseBody가 함께 붙어 있다. @ResponseBody가 적용된 반환값은 메시지 컨버터를 거쳐 HTTP 본문으로 쓰인다. 클래스가 @Controller여도 해당 메서드에 @ResponseBody가 있으면 같은 구분이 필요하다. Spring의 ResponseBody 설명 (새 탭에서 열림)

먼저 상태·Location·본문을 함께 읽는다

기존 MockMvc 테스트가 있다면 문제가 되는 메서드에 요청을 보내 다음 세 값을 확인한다. 아래의 mvc는 그 컨트롤러를 등록한 MockMvc이고, /submit은 예제의 POST 매핑이다. 자신의 실제 경로와 메서드에 맞춘다. post는 org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post를 static import하며, 테스트 메서드는 throws Exception을 선언할 수 있다. 저장을 실행하는 메서드라면 테스트용 데이터와 의존성을 사용한다.

var response = mvc.perform(post("/submit")).andReturn().getResponse();
System.out.printf("status=%d Location=%s body=\"%s\"%n",
        response.getStatus(), response.getHeader("Location"),
        response.getContentAsString());

@RestController가 String 값 redirect:/done을 반환하도록 별도로 실행했을 때, 위 세 응답 필드는 다음과 같았다. 여기서 null은 Location 헤더가 없다는 표시이며, 본문 양옆의 따옴표는 출력 형식이다.

status=200 Location=null body="redirect:/done"

이 결과에서는 리다이렉트 응답 자체를 보내지 않았다. 목적지 /done의 매핑을 고치기 전에 반환값의 처리 방식을 바꿔야 한다. 반대로 첫 응답에 이미 의도한 3xx와 Location이 있다면 이 문자열 문제와는 다르다. 브라우저에서는 최종 응답만 보지 말고 Network에서 최초 요청의 상태와 응답 헤더를 확인한다.

같은 문자열을 뷰 이름과 본문으로 각각 반환했다

비교는 Windows, OpenJDK 17.0.2, Spring Framework 6.2.11, Jakarta Servlet API 6.0.0에서 수행했다. 각 컨트롤러를 별도 standaloneSetup으로 구성하고, 모두 같은 POST /submit에서 "redirect:/done"을 반환하게 했다. String 본문에는 UTF-8 StringHttpMessageConverter, 뷰 해석에는 InternalResourceViewResolver를 명시했다. Spring Boot 자동 설정이나 실제 서버는 사용하지 않았다.

응답 필드만 추리면 다음과 같다. 첫 번째 조건에는 클래스나 메서드의 @ResponseBody가 없다.

@Controller                 → 302, Location=/done, body=""
@RestController             → 200, Location 없음, body="redirect:/done"
@Controller + @ResponseBody → 200, Location 없음, body="redirect:/done"

첫 번째에서는 반환 문자열이 뷰 이름으로 남았다. InternalResourceViewResolver의 상위 클래스인 UrlBasedViewResolver가 redirect:를 읽어 RedirectView를 만든다. 뒤의 두 조건에서는 뷰 이름이 생기지 않았고 문자열이 본문으로 쓰였다. 같은 Java 반환형 String이어도 @ResponseBody 적용 여부에 따라 역할이 달라진다. 반환형별 처리 (새 탭에서 열림), 뷰 해석의 redirect 규칙 (새 탭에서 열림)

화면을 반환하는 MVC 메서드라면 @Controller에서 @ResponseBody 없이 return "redirect:/done";을 사용할 수 있다. 다만 기존 REST 컨트롤러 전체를 @Controller로 바꾸면 다른 String 반환 메서드도 영향을 받는다. 한 메서드의 이동 응답을 고치려면 그 메서드에서 응답을 명시하는 편이 변경 범위를 좁힌다.

또한 위의 302는 이번 뷰 설정의 실제 결과다. redirect:라는 글자가 언제나 302를 고정하는 것은 아니다. 실험 버전의 RedirectView는 기본 호환 경로에서 sendRedirect를 호출하며, 명시한 상태나 호환 설정에 따라 다른 상태를 사용할 수 있다. Spring 6.2.11 RedirectView 구현 (새 탭에서 열림)

REST 컨트롤러에서는 303과 목적지를 명시한다

POST 처리를 마친 뒤 별도 결과 주소를 조회하게 하려는 경우라면 303 See Other로 의도를 나타낼 수 있다. 다음은 기존 @RestController 안의 메서드를 바꾸는 예다. URI는 java.net, ResponseEntity와 HttpStatus는 org.springframework.http, PostMapping은 org.springframework.web.bind.annotation에서 가져온다. 예제는 이동 응답만 만들며 저장 로직은 포함하지 않는다. 실제 코드에서는 처리가 성공한 뒤 반환하는 부분에 적용한다.

@PostMapping("/submit")
public ResponseEntity<Void> submit() {
    return ResponseEntity.status(HttpStatus.SEE_OTHER)
            .location(URI.create("/done"))
            .build();
}

같은 MockMvc 조건에서 이 메서드를 실행하자 상태는 303, Location은 /done, 본문은 빈 문자열이었다. ResponseEntity는 뷰 이름 대신 응답 상태와 헤더를 지정한다. 따라서 이 Location에는 redirect:를 붙이지 않는다. build()로 끝내는 것은 이 예제에서 별도 응답 본문을 만들지 않겠다는 뜻이다. ResponseEntity API (새 탭에서 열림)

303은 단순히 302보다 새로운 코드라서 고른 것이 아니다. RFC 9110에서 303은 원래 요청의 결과를 다른 리소스의 GET 또는 HEAD 조회로 확인하는 의미다. 반면 302는 임시로 다른 URI를 가리키며, 후속 요청에서 POST를 GET으로 바꾸는 것을 허용한다. 이 글의 수정안은 POST 후 결과 조회를 명확히 표현하려고 303을 선택했다. 같은 POST를 다른 곳에서 계속 처리하려는 요구라면 별도의 메서드 보존 정책을 검토해야 한다. RFC 9110의 302·303 설명 (새 탭에서 열림)

목적지는 요청 파라미터를 받지 않는 고정 경로다. 또 /done은 사이트 루트 기준이다. 앱이 /app 아래 배포되어 실제 목적지가 /app/done이라면 그 경로를 Location에 반영해야 한다. ResponseEntity는 이 문자열에 컨텍스트 경로를 자동으로 덧붙이지 않는다. 이번 실행은 컨텍스트 경로가 없는 조건만 확인했다.

303 응답 직후 /done의 호출 횟수는 0이었다. 이어서 실험 코드가 Location을 읽어 GET을 직접 보내자 200과 본문 done을 받았고 호출 횟수가 1이 됐다. 이는 목적지 GET을 별도로 확인한 것이며, MockMvc가 리다이렉트를 자동으로 따라간 결과가 아니다. MockMvc 공식 설명 (새 탭에서 열림)도 실제 서버 없이 DispatcherServlet을 실행하는 서버 측 검사로 범위를 설명한다.

이번에 확인한 것은 MVC 반환값 처리와 응답의 상태·Location·본문까지다. 실제 브라우저의 자동 추적과 주소창 변경, Servlet 컨테이너·보안 필터·프록시를 거친 응답은 실행하지 않았다. 수정 뒤 303과 올바른 Location이 나왔는데도 원하는 화면 이동이 없다면, 페이지 탐색으로 호출했는지 자바스크립트가 응답을 받는 호출인지까지 구분해 확인한다. 6.2 참조 문서는 확인 당시 6.2.19를 표시했고, 핵심 어노테이션과 redirect 처리는 실험 버전인 6.2.11의 공식 태그 소스와 대조했다.

← 전체 글 보기