진짜 개발자
본문 바로가기

FrameWork/Spring MVC

Spring MVC - 수정폼 작성방법(@SessionAttributes, SessionStatus란)

728x90

Spring MVC에서 수정폼을 작성하는 효율적인 방법과 @SessionAttributes에 대해 알아보도록 하겠습니다.


User 정보 수정

1. 예제 어플리케이션

5

완성된 프로젝트 구조는 위와 같습니다.



build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

꼭 필요한 의존성은 위와 같습니다.



UserModel.class

@Getter
@Setter
public class UserModel {
    private String userId;
    private String name;
    private int age;
}

한 User를 표현하는 Model 클래스입니다.



UserService.class

@Service
public class UserService {
    public UserModel getUser() {
        UserModel userModel = new UserModel();
        userModel.setUserId("galid");
        userModel.setAge(10);
        userModel.setName("JJY");

        return userModel;
    }
}

이 클래스는 데이터베이스로부터 한 유저의 정보를 가져오는 서비스를 임시로 구현한 것입니다. (실제로 데이터베이스에서 조회하지 않고, 직접 간단한 정보를 만들어 반환합니다.)



TestController.class

@Controller
@RequiredArgsConstructor
public class TestController {
    private final UserService userService;

    @GetMapping("/users")
    public String getUserPage(Model model) {
        UserModel user = userService.getUser();
        model.addAttribute("user", user);
        return "modifyUserInfo";
    }

    @PostMapping("/users")
    @ResponseBody
    public void modifyUserInfo(@ModelAttribute UserModel userModel) {
        System.out.println("아이디 : " + userModel.getUserId());
        System.out.println("이름 : " + userModel.getName());
        System.out.println("나이 : " + userModel.getAge());
    }
}

정보 수정폼 출력 요청을 처리하는 핸들러, 수정 요청시 응답 핸들러를 가지고 있는 Controller 클래스입니다.


getUserModifyPage(Model model)

수정폼 출력 요청처리 핸들러는 UserService클래스를 이용해 유저정보를 가져와 model에 매핑하고, modifyUserInfo.html를 반환합니다.


modifyUserInfo(@ModelAttribute UserModel userModel)

수정 요청 응답핸들러는 실제 수정 작업을 처리하지는 않지만 이를 시뮬레이션할 수 있도록 만든 임시 핸들러 입니다.



modifyUserInfo.html

<body>
    <form th:action="@{/users}" th:method="post" th:object="${user}">
        <label th:for="name">이름</label>
        <input type="text" th:field="*{name}">

        <label th:for="age">나이</label>
        <input type="number" th:field="*{age}">

        <button type="submit">수정</button>
    </form>
</body>

사용자정보 수정 폼입니다.





2. 수정 폼의 문제점

수정폼을 작성할때의 절차는 아래와 같습니다.

  1. 사용자가 수정 폼을 씌워 달라는 요청을 보낸다. (수정할 사용자 ID와 함께)
  2. 서버는 ID에 해당하는 사용자 정보를 읽어와 수정 폼을 띄워준다.
  3. 사용자가 정보를 수정하고 폼을 전송한다.
  4. 서버는 수정정보를 받아 db에 저장하고 수정완료 메시지를 띄워준다.

간단히 표현하면, 수정폼을 띄우고 수정 요청을 처리한다로 볼수 있지만, 사실 이렇게 간단하지만은 않습니다.


모든 정보가 수정가능 하지 않다

중요한 점은, 항상 모든 정보가 수정가능하지 않다는 것입니다.

예를들어, 회원의 아이디 같은 경우, 특별한 권한이 없다면 수정이 불가능합니다. 따라서 이러한 정보들은, 애초에 수정폼에 띄우지 않다거나, 읽기전용으로 출력되도록 해야합니다.

또한 마지막 로그인 시간등 처럼 사용자의 수정 요청폼에 보여질 필요없는 데이터들도 존재합니다.

따라서, 사용자에게는 모든 정보가아니라, 수정에 필요한 일부 정보들만이 출력됩니다. 이렇게되면, 모델의 일부정보만 서버에 전송되어, 일부 필드들은 null, 0의 값으로 전달이 되어지며, 이 값들이 DB에 저장이 되어져, 심각한 정보 손실 문제가 발생할 수도 있습니다.


7

예를들어 앞서 작성한 예제에서 회원정보에는 userId, name, age라는 데이터가 존재하는데요, 이중 userId는 특별한 권한이 없는 경우 수정이 불가능하므로, 수정폼에는 name과 age만이 출력됩니다.


8

이 때문에, 당연히 전달받지 못한 userId의 경우 null값이 서버로 전달되어지게 되며, 이 값을 저장하게 되면 userId를 null로 저장하려는 시도가 발생할것입니다.

이제 이 문제를 해결하는 방법들을 알아보도록 하겠습니다.





3. 완벽하지 않은 해결방법

지금부터 살펴볼 방법들은, 완벽하지 않지만(결함이 있지만) 임시로 해결이 가능한 방법들입니다.


3.1 히든 필드

서버로 수정에 필요한 모든 정보가 전달되지 않기 때문에 발생하는 문제이므로, 모든 값을 전달하면 문제가 해결되지 않을까요? 첫번째 방법은 html의 hidden 속성을 이용하는 것입니다.

<form th:action="@{/users}" th:method="post" th:object="${user}">
    <input type="hidden" th:field="*{userId}">  <!-- 사용자에게 보이지 않음 -->

    <label th:for="name">이름</label>
    <input type="text" th:field="*{name}">

    <label th:for="age">나이</label>
    <input type="number" th:field="*{age}">

    <button type="submit">수정</button>
</form>

수정할 정보들만을 사용자에게 보이게 하며, 수정이 불가능하거나 필요하지 않은 필드는 hidden을 통해 숨기는 것입니다.


9

잘 전송이 되어지는것을 볼 수 있지만, 문제가 있습니다.


첫번째 문제

10

첫번째 문제는, hidden필드의 경우 사용자의 브라우저에서는 보이지 않지만, html 소스를 확인하면 해당 필드를 확인할 수 있고, 손쉽게 수정이 가능하다는 것입니다.


11

해당 필드를 더블클릭하면 수정할 수 있게 변하는데 임의의 값을 입력한뒤 수정요청을 보내보겠습니다.


12

해당값이 변경된것을 확인할 수 있습니다. (이렇게 되면 안되겠죠?)


두번째 문제

두번째 문제로는, user 모델에 변경이 생겼을 때입니다. 예를들어, 주소가 추가가 되었는데, html의 수정폼에는 이 정보를 추가하지 않았을 경우 마찬가지로 해당 정보가 null로 서버로 전달이 되어집니다.



3.2 DB 재조회

@PostMapping("/users")
@ResponseBody
public void modifyUserInfo(@ModelAttribute UserModel userModel) {
    UserModel findUser = userService.getUser();
    findUser.setName(userModel.getName());
    findUser.setAge(userModel.getAge());
   ...
}

두번째 해결방법은 DB를 재조회하는 방법입니다. 사용자가 수정 요청을 보내었을때, 빈 오브젝트에 값을 설정하는 것이 아닌, DB에서 완전한 오브젝트를 가져와 필요한 값만을 설정하는 것 입니다.


이 방법은 완벽해보이지만, 수정 페이지를 보여줄때 한번, 수정을 처리할때 DB를 다시한번 조회한다는 성능상의 문제가 있습니다.





4. Spring MVC에서의 수정요청 처리방법 (@SessionAttributes)

4.1 @SessionAttributes의 기능

우선 @SessionAttributes를 이용해 수정요청을 처리하기 전 이 어노테이션이 어떤일을 수행하는지 알아보아야 할 것 같습니다.


1. Model에 저장된것을 Session에 저장해준다

13

첫번째 기능은, 핸들러에서 생성하는 model과 @SessionAttributes에서 지정한 값이 같은 경우, 이를 세션에 저장해줍니다.


2. @ModelAttribute가 지정된 파라미터를 세션에서 가져와 매핑

14

두번째 기능은 @ModelAttribute가 지정된 파라미터에, Session에 저장된 값을 가져와 매핑해주는 것 입니다.



4.2 @SessionAttributes를 이용한 수정폼 문제 해결

앞서 살펴본 @SessionAttributes의 기능을 이용해, 수정폼의 문제를 해결해볼 수 있을것 같습니다.

@Controller
@RequiredArgsConstructor
@SessionAttributes("user")
public class TestController {
    private final UserService userService;

    @GetMapping("/users")
    public String getUserModifyPage(Model model) {
        UserModel user = userService.getUser();
        model.addAttribute("user", user);
        return "modifyUserInfo";
    }

    @PostMapping("/users")
    @ResponseBody
    public void modifyUserInfo(@ModelAttribute("user") UserModel userModel) {
        System.out.println("아이디 : " + userModel.getUserId());
        System.out.println("이름 : " + userModel.getName());
        System.out.println("나이 : " + userModel.getAge());
    }
}

핸들러를 위와 같이 수정합니다.


9

수정 요청결과 제대로된 값이 전달되어지는 것을 볼 수 있습니다.



4.3 과정 설명

@SessionAttributes("user")
public class TestController {
    private final UserService userService;

    @GetMapping("/users")
    public String getUserModifyPage(Model model) {
        UserModel user = userService.getUser();
        model.addAttribute("user", user);
        return "modifyUserInfo";
    }
    ...

우선 첫번째 기능에 의해, model.addAttribute("user", user) 코드가 실행되고, 세션에 DB에서 조회해온 완전한 정보를 가지고 있는 User 객체가 저장됩니다.


@PostMapping("/users")
@ResponseBody
public void modifyUserInfo(@ModelAttribute("user") UserModel userModel) {
    System.out.println("아이디 : " + userModel.getUserId());
    System.out.println("이름 : " + userModel.getName());
    System.out.println("나이 : " + userModel.getAge());
}

이후, 사용자가 수정 요청을 보내면, @ModelAttribute("user") UserModel userModel에 사용자가 전달한 값을 매핑하기전, session에 저장된 값을 먼저 매핑합니다.


이후, 사용자가 전달한 값을 set합니다.

이렇게 되면, DB재조회방법을 사용할때와 마찬가지로, 빈 객체가아닌 완전한 객체에 변경할 정보만을 set해주게 되지만, DB를 재조회 하지 않고 세션에 있는 정보를 사용하기 때문에 성능저하 문제도 발생하지 않습니다.




4.4 @SessionAtributes의 중요사항과 SessionStatus

@SessionAttributes는 사용후, 더 이상 사용하지 않는다면 코드로 직접 제거를 해주어야 합니다.



왜 자동으로 제거하지 않을까?

수정요청시, 사용자가 잘못된 값을 전달하는 경우, 이를 다시 요청하도록 해야 하며, 세션의 값은 그대로 유지되어야 합니다. 때문에, 자동으로 제거 되지 않고 직접 코드를 통해서 사용하지 않는 session을 제거해주어야 메모리 누수가 발생하지 않습니다.



SessionStatus
@PostMapping("/users")
@ResponseBody
public void modifyUserInfo(@ModelAttribute("user") UserModel userModel, SessionStatus ) {
    ...
    sessionStatus.setComplete();
}
sessionStatus의 setComplete()는 @SessionAttributes에 저장된 객체들을 제거합니다.