새로고침과 데이터 중복 문제, Redirect로 해결해보자

심현민·2025년 1월 25일
1

Spring

목록 보기
10/18

스프링 공부를 하면서 리다이렉션에 대해 이전에 배운 적이 있었는데

이걸 어디에 사용하는지 처음에는 몰랐다가 최근에 데이터가 중복 등록되는 문제에서 유용하게 쓸 수 있다는걸 알게 되어서 관련해서 내용을 정리하고자 포스팅을 하게 되었다.

새로고침 문제와 데이터 중복의 심각성

보통 우리가 웹에서 화면을 구성하거나 데이터를 클아이언트로 받거나, 전송할 때 GET, POST 요청 및 응답을 처리하는 로직을 많이 만들 것이다.

다음과 같은 등록 폼에서 전송하는 데이터 또한 스프링에서 지원하는 @PostMapping을 통해 간단하게 객체에 데이터를 저장하는 로직을 짤 수 있다.

@PostMapping("/add")
    public String addItem(Item item) {
        itemRepository.save(item);
        return "basic/item";
    }

근데 이러한 설계가 옳은 설계인지 잘 생각해볼 필요가 있다.

클라이언트로부터 넘겨받은 데이터를 객체에 저장하고 객체를 itemRepository에 넘겨주고 있는 모습이다.

그런데, 만약 웹에서 이 페이지를 새로고침하면 어떤 일이 일어날까?

결론부터 말하자면, 데이터를 중복으로 계속 등록하게 된다.



웹에서 새로고침을 처리하는 방식

웹에서는 새로고침을 다음과 같은 행위로 정의한다.

"클라이언트에서 같은 URL로 다시 요청을 보내는 행위"

  • 브라우저가 사용자가 요청한 URL로 다시 GET 또는 POST 요청을 서버에 보낸다. (같은 URL에 대해)
  • 서버는 동일한 경로에 대해 다시 해당 컨트롤러를 호출하고 응답을 반환

간단한 예시로 설명하자면 수강신청을 할 때 페이지를 계속 새로고침하는 과정을 생각하면 이해하기 쉽다.

우리가 수강 신청 사이트에서 새로고침을 하는 이유는 보통 계속해서 바뀌는 여석(데이터)을 확인하려고 새로고침을 하게 된다. 여기서 계속해서 바뀐 데이터가 화면에 출력될 수 있는 이유를 생각해보자

학생 A, 학생 B가 있을 때
A가 만약 수강신청 폼 안에 button을 클릭을 하게 되면 서버로 POST 요청을 보내게 된다. 그러면 서버는 전송된 데이터를 통해 서버의 상태를 변경하게 된다.

이후, 서버는 요청했던 A에게 변경된 서버 상태를 포함하는 페이지를 응답으로 주게 된다.

B도 비슷한 원리로 생각해보면 웹 페이지에 접속하는 시점에 A가 아직 데이터를 서버로 보내기 직전이면 B는 기존 서버를 포함하는 페이지를 응답으로 받을 것이다.

하지만, 이후에 A가 데이터를 서버로 보낸 이후에 새로고침을 함으로써 GET요청을 다시 서버로 보내게 되면 응답으로는 바뀐 서버 상태를 포함하는 페이지를 받게 된다.



POST 요청 새로고침 : 중복된 데이터 문제

이런 원리를 머릿속에 집어 넣고

포스팅 초반에 설명했던 폼 안에 데이터를 POST 요청으로 서버로 보내고 이후에 새로고침을 하게 되면 어떻게 될까?

아마, 계속해서 폼 안에 있는 데이터를 서버로 보내게 될 것이다.
그렇게 되면 결과는 뻔하게 결국 서버에 중복된 데이터가 쌓이게 될 것이다.

따라서, 똑똑하게도 웹에서 이런 위험이 있을 때 다음과 같은 경고문구를 띄우게 된다.

그런데 사용자가 과연 저런 문구를 고려할까? 웹 개발자라면 모르겠지만 일반 사용자는 이러한 문구를 그냥 지나치고 새로고침을 계속해서 반복할 수도 있다.

따라서, 웹 개발자는 사전에 이런 문제를 방지하기 위해서 경로 리다이렉트를 통한 PRG 패턴을 도입할 수 있다.



PRG 패턴이란?

Post/Redirect/Get의 약자로 다음과 같은 과정을 통해 중복 데이터를 방지할 수 있다.

  1. POST 요청, 서버는 데이터를 처리하고 상태 변경
  2. 그 후 서버가 리다이렉션 응답을 보내며, 클라이언트는 이를 따라 GET 요청 전송
  3. GET 요청은 페이지의 자원을 가져오기만 하며, 상태를 변경하지 않으므로 새로고침을 해도 중복 처리가 일어나지 않게 된다.

이 과정을 쉽게 이해하기 위해서 GET 요청에 대해서 자세히 알 필요가 있다.

GET 요청의 멱등성

GET 요청은 POST 요청과 다르게 멱등성을 가진다.

여기서 말하는 멱등성은 수학적 개념에서 유래했는데,
쉽게 말해서 5 + 0처럼 수행한 연산의 결과가 그 연산을 여러 번 반복하더라도 변하지 않는 것을 멱등적이라고 부른다.

GET 메서드는 보통 자원을 조회할 때 사용된다.
중요한 점은 GET 요청을 여러 번 보내도 자원의 상태가 변하지 않기 때문에 GET 요청은 위 정의에 의해서 멱등성을 가진다고 할 수 있다.
(또한, POST는 상태 변경이 있어서 멱등성이 없다.)

그래서 이러한 GET 요청의 특성을 이용해서 POST 요청에 대해서 새로고침을 했을 때 GET 요청으로 리다이렉션을 발생시켜서 중복 데이터가 POST 요청으로 넘어가지 않게끔 사전에 방지할 수 있는 것이다.

이를 코드로 다음과 같이 구현할 수 있다.

@PostMapping("/add")
public String addItemV5(Item item) {
        itemRepository.save(item);
        // "/basic/items/{itemId}"
        return "redirect:/basic/items/" + item.getId();
    }

현재 예제에서는 /basic/items/add 페이지에서
POST 요청이 발생하면 basic/items/{아이템 번호}로 리다이렉션 되게끔 구현한 경우이다.

Network 검사

이제 PRG패턴을 적용한 코드가 실제 웹에서 요청을 어떻게 처리하는 지 보면 다음과 같다.

/add URL에 대해 POST 요청 이후에 basic/items/3으로 리다이렉션 한 GET 요청을 보내는 모습을 볼 수 있다.


redirect 사용법

보통 return redirect:/path 이런 식으로 구현한다.

이게 뜻하는 의미는 그냥 간단하게 생각해서 POST 요청을 받은 후 서버에서 처리를 완료하고,

응답을 할 때 클라이언트에게 특정 경로(GET 요청)로 이동하도록 지시하는 문장이라고 생각하면 된다.



RedirectAttributes로 동적 파라미터 처리

또한, 이러한 리다이렉션이 중복 데이터를 해결해주는 것 뿐만 아니라 동적 파라미터 처리도 지원해준다.

간단하게 얘기해서 redirectAttributes을 사용하면 리다이렉션할 URL에 동적으로 파라미터를 추가할 수 있다.

예시로 살펴보면 쉽게 이해할 수 있다.

 @PostMapping("/add")
 public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/basic/items/{itemId}";
}

위 코드는 아까 살펴봤던 /basic/items/add 메서드를 개선한 버전이다.

하나 하나 뜯어서 살펴보자
먼저 redirectAttributes.addAttribute("itemId", savedItem.getId());을 통해서는 저장한 Item 객체의 Id를 넘겨주고 있다는 사실을 알 수 있다.

근데 어디로 넘겨주는 걸까?

return "redirect:/basic/items/{itemId}";

이 부분을 주목해보자 현재 {itemId} 부분에 경로 변수가 들어가 있는데 매핑해주는 부분이 아무것도 없다.

@GetMapping("/{itemId}")이라던지 @PathVariable long itemId 이런 코드가 있어야 경로 변수에 매핑을 할 수 있을텐데 어떻게 된걸까?

이제 여기서 짐작가는 부분이 있을 것이다.

바로, redirectAttributes.addAttribute("itemId", savedItem.getId()); 이 문장을 통해 경로 변수에 실제 값을 매핑시킬 수 있는 것이다.

그리고 경로변수가 아닌 값을 넣을 경우에

redirectAttributes.addAttribute("status", true);

다음과 같은 문장이 하는 역할은 URL 주소창에 쿼리 파라미터 형식으로 실제 값을 넘겨준다.

예를 들어 다음과 같다.

basic/items/3?status=true

이렇게 리다이렉션을 활용함으로써 다음과 같은 장점을 얻을 수 있다.

장점

  • 리다이렉션 후의 요청을 더 유연하게 처리하고 코드 중복을 줄일 수 있다.
    = > 상품 상세 페이지, 등록 완료 상품 상세 페이지를 따로 만들 필요 없이 하나의 페이지 안에 포함시킬 수 있음
//item.html 일부
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
  • 클라이언트는 URL에 직접 파라미터 값을 삽입하지 않고, 자동으로 처리된 값을 통해 보다 직관적이고 효율적인 요청 흐름을 경험할 수 있다.
profile
혼자 성장하는 것보다 함께 성장하는 것을 선호합니다.

0개의 댓글