본문 바로가기
Spring Framework

ModelAttribute, SessionAttribute(s)의 모든 것

by Jordy-torvalds 2020. 3. 11.
반응형

자주 사용하지 않아 익숙하지 않은 세 어노테이션에 대하여 심층적으로 분석해보는 시간을 가졌습니다. 읽어보시고 첨언 부탁드립니다.


Common Test Code

POJO, Niniz

public class Niniz {

    private String name;

    private long popularity;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public long getPopularity() {
        return popularity;
    }

    public void setPopularity(long popularity) {
        this.popularity = popularity;
    }

    @Override
    public String toString() {
        return "Niniz{" +
                "name='" + name + '\'' +
                ", popularity=" + popularity +
                '}';
    }
}

ModelAttribute

리퀘스트로 넘겨 받은 파라미터를 곧바로 모델에 추가해주는 어노테이션 입니다. 모델에 속성을 추가해주는 작업을 생략하게 해줌으로써 코드의 가독성을 높여줍니다.

코드와 함께 스프링 프로젝트에서 어떤 식으로 활용할 수 있을지 알아봅시다.

사용법 1. 파라미터를 즉시 모델에 받아주기.

아래는 테스트에 사용할 컨트롤러 코드 입니다.

@Controller
public class SampleController {

    @GetMapping("/modelAttr/null")
    public String modelAttrNull(@ModelAttribute Niniz niniz , Model model) {
        System.out.println(model.toString());

        return "nullPage";
    }

    @GetMapping("/modelAttr/name1")
    public String modelAttrName1(@ModelAttribute(name="jordy", binding = false) Niniz niniz , Model model) {
        System.out.println(model.toString());

        return "nullPage";
    }

    @GetMapping("/modelAttr/name2")
    public String modelAttrName2(@ModelAttribute("jordy") Niniz niniz , Model model) {
        System.out.println(model.toString());

        return "nullPage";
    }

    @GetMapping("/modelAttr/name3")
    public String modelAttrName3(@ModelAttribute("scappy") Niniz niniz , Model model) {
        System.out.println(model.toString());

        return "nullPage";
    }
}

/modelAttr/null 부터 /modelAttr/name3 까지 차례로 ?name=jordy&popularity=500 란 쿼리스트링을 붙여서 호출 했을 때 콘솔 창에 어떻게 출력 되는지 봅시다.


콘솔

  • 메서드 시그니처: public String modelAttrNull(@ModelAttribute Niniz niniz , Model model)
  • URL: /modelAttr/null?name=jordy&popularity=500
  • 결과: {niniz=Niniz{name='jordy', popularity=500}, org.springframework.validation.BindingResult.niniz=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

→ @ModelAttribute 에 별도의 name을 부여하지 않았습니다.

→ niniz를 키로하는 Niniz 객체에 쿼리스트링 값이 들어간 것을 알 수 있습니다.


2.

  • 메서드 시그니처: public String modelAttrName1(@ModelAttribute(name="jordy", binding = false) Niniz niniz , Model model)
  • URL: /modelAttr/name1?name=jordy&popularity=500
  • 결과: {jordy=Niniz{name='null', popularity=0}, org.springframework.validation.BindingResult.jordy=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

→ @ModelAttribute 에 name으로 jordy를 주고, binding 속성을 false로 했습니다.

→ jordy를 키로하는 Niniz 객체에 어떠한 쿼리스트링 값도 할당되지 않았습니다.


3.

  • 메서드 시그니처: public String modelAttrName2(@ModelAttribute("jordy") Niniz niniz , Model model)
  • URL: /modelAttr/name2?name=jordy&popularity=500
  • 결과: {jordy=Niniz{name='jordy', popularity=500}, org.springframework.validation.BindingResult.jordy=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

→ @ModelAttribute 에 name으로 jordy를 줬습니다.

→ jordy를 키로하는 Niniz 객체에 쿼리스트링 값이 할당되었습니다.


  • 메서드 시그니처: public String modelAttrName3(@ModelAttribute("scappy") Niniz niniz , Model model)
  • URL: /modelAttr/name3?name=jordy&popularity=500
  • 결과: {scappy=Niniz{name='jordy', popularity=500}, org.springframework.validation.BindingResult.scappy=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

→ @ModelAttribute 에 name으로 scappy를 줬습니다.

→ scappy를 키로하는 Niniz 객체에 어떠한 쿼리스트링 값도 할당되지 않았습니다.


사용법 2. 메소드에 @ModelAttribute 주기

@ModelAttribute를 메소드에 부여한 경우 해당 컨트롤러 내 모든 Request가 처리될 때 마다 호출되며, 호출 시점은 @RequestMapping 메소드가 실행되기 전 입니다.

이전에 보았던 컨트롤러의 코드의 하단에 아래와 같은 코드를 추가해주세요.

 ...

    @ModelAttribute("method1")
    public String modelMethod1() {
        System.out.println("modelMethod1");
        return "modelMethod1";
    }

    @ModelAttribute("method2")
    public String modelMethod2() {
        System.out.println("modelMethod2");
        return "modelMethod2";
    }

    @ModelAttribute("method3")
    public String modelMethod3() {
        System.out.println("modelMethod3");
        return "modelMethod3";
    }

...

/modelAttr/name2?name=jordy&popularity=500 를 호출하면 콘솔창에는 아래와 같이 출력됩니다.

modelMethod1
modelMethod3
modelMethod2
{method1=modelMethod1, method3=modelMethod3, method2=modelMethod2, scappy=Niniz{name='jordy', popularity=500}, org.springframework.validation.BindingResult.scappy=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

보시다시피 호출한 주소를 처리하는 메서드가 실행되기 이전에 @ModelAttribute 메소드가 먼저 호출되어 처리된 후 그 결과를 리턴을 통해 모델에 추가 합니다.

그래서 model.toString()한 문자열에 method1-3 까지 정상적으로 추가된 것을 확인 할 수 있습니다.

@ModelAttribute 메소드의 처리 순서는 여러 차례 재구동 해본 결과 매번 랜덤으로 정해졌으며, @Order 로도 그 순서를 제어하는 것도 불가능했습니다.

이와 관련하여 알고 계신 분은 댓글을 남겨주시기 바랍니다.


SessionAttributes

특정 키로 등록된 모델 내 value를 세션으로 저장해주는 어노테이션 입니다. 간단한 Login 기능 구현시 많이 사용됩니다.

해당 어노테이션의 활용법을 알아보겠습니다.

이전에 보았던 컨트롤러 클래스의 상단에 @SessionAttributes("jordy") 란 어노테이션을 추가해주세요.

@Controller
@SessionAttributes("jordy")
public class SampleController {

    @GetMapping("/modelAttr/null")
    public String modelAttrNull(@ModelAttribute Niniz niniz , Model model) {
        System.out.println(model.toString());

        return "nullPage";
    }

...

그 후에 /modelAttr/null 부터 /modelAttr/name3 까지 차례로 ?name=jordy&popularity=500 란 쿼리스트링을 붙여서 호출 해봅시다.

그러면 @ModelAttribute에 이름을 주지 않았거나 scappy를 준 /modelAttr/null/modelAttr/name3 는 정상적으로 처리되는 반면에, /modelAttr/name1/modelAttr/name2 는 아래와 같은 예외가 출력되는 것을 알 수 있습니다.

org.springframework.web.HttpSessionRequiredException: Expected session attribute 'jordy'

위와 같은 예외가 출력되는 이유는 @ModelAttribute가 아래와 같은 정보가 있는 경우 값을 가져오기 때문입니다.

  • 만약 모델에 벌써 추가된 정보가 있는 경우 모델로 부터
  • @SessionAttributes를 사용하고 있을 경우 HttpSession으로 부터
  • Converter를 통해 PathVariable로 부터
  • 기본 생성자로 부터
  • 서블릿 요청 매개변수와 일치하는 인수를 가진 "primary 생성자"의 호출로부터

현재의 예외의 경우 @SessionAttributes를 사용 중인데도 HttpSession 에서 값을 가져올 수 없기 때문에 발생한 예외라고 할 수 있습니다.

그래서 아래와 같이 @ModelAttribute("jordy") 어노테이션을 붙인 메소드를 만들어줌으로써 해당 예외를 해결할 수 있습니다.

@Controller
@SessionAttributes("jordy")
public class SampleController {

    @ModelAttribute("jordy")
    public Niniz initNiniz() {
        return new Niniz();
    }

    @GetMapping("/modelAttr/null")
    public String modelAttrNull(@ModelAttribute Niniz niniz , Model model) {

그 후에 서버 재기동 후 예외가 발생했던 주소를 재호출 하면 더이상 예외가 발생하지 않는 것을 알 수 있습니다.

참고로, initNiniz 메소드는 세션이 생성 전일 때 세션 생성을 위해 실행된 이후에는 더 이상 실행되지 않습니다.


세션 생성 실패 예외 처리 방법

@SessionAttributes를 사용하고, 아래와 같이 @ResponseBody 어노테이션이 부여된 요청을 호출할 경우 java.lang.IllegalStateException: Cannot create a session after the response has been committed 란 오류가 콘솔에 출력되는 것을 확인할 수 있습니다.

@GetMapping("/modelAttr/name2")
@ResponseBody
public String modelAttrName2(@ModelAttribute("jordy") Niniz niniz , Model model) {
    System.out.println(model.toString());

    return "nullPage";
}

@GetMapping("/modelAttr/name3")
@ResponseBody
public String modelAttrName3(@ModelAttribute("scappy") Niniz niniz , Model model) {
    System.out.println(model.toString());

    return "nullPage";
}

위 코드에서는 문자열을 반환하고 있지만, JSON을 반환하더라도 동일한 예외가 발생한다. 특이하게도 조금 전에 진행한 예시처럼 View를 반환하는 요청을 호출할 경우에는 예외가 발생하지 않습니다.

말그대로 세션을 생성하기도 전에 요청이 처리되어 발생한 예외 입니다.

해당 현상을 해결 하는 방법을 구글링해본 결과, 파라미터에 HttpSession 을 추가해주면 메소드 호출시 Session 처리되면서 문제가 해결됩니다.

아래 코드를 참조합시다.

@GetMapping("/modelAttr/name2")
@ResponseBody
public String modelAttrName2(@ModelAttribute("jordy") Niniz niniz , Model model, HttpSession httpSession) {
    System.out.println(model.toString());

    return "nullPage";
}

@GetMapping("/modelAttr/name3")
@ResponseBody
public String modelAttrName3(@ModelAttribute("scappy") Niniz niniz , Model model, HttpSession httpSession) {
    System.out.println(model.toString());

    return "nullPage";
}

SessionAttribute

세션으로 저장된 값을 키 값으로 읽어오는 어노테이션입니다. 해당 키로 등록된 세션이 없을 경우 400에러가 출력된다. 컨트롤러 하단에 아래와 같은 코드를 추가합시다.

...
@GetMapping("/getSession")
@ResponseBody
public String getSession(@SessionAttribute("jordy") Niniz niniz) {
    System.out.println(niniz.toString());

    return "nullPage";
}
...

위 코드를 실습하기 위해서는 우선 세션 생성이 필요하므로 세션을 생성할 수 있는 요청을 호출하여 세션을 생성하자. 그 후에 새로 추가한 요청을 호출 하면 콘솔 창에 저장된 세션의 정보가 출력되는 것을 확인할 수 있습니다.


지금까지 ModelAttribute, SessionAttributes, SessionAttribute 에 대하여 알아보았습니다. 다소 길어서 장황한 듯 합니다. 읽는 과정에서 이해가 안되거나 틀린 부분이 있으면 댓글을 남겨주시고, 괜찮으셨다면 공감 하트 버튼 한 번 씩 부탁드리겠습니다.

감사합니다.

반응형