spring로 검색한 결과 :: 시소커뮤니티[SSISO Community]
 
SSISO 카페 SSISO Source SSISO 구직 SSISO 쇼핑몰 SSISO 맛집
추천검색어 : JUnit   Log4j   ajax   spring   struts   struts-config.xml   Synchronized   책정보   Ajax 마스터하기   우측부분

회원가입 I 비밀번호 찾기


SSISO Community검색
SSISO Community메뉴
[카페목록보기]
[블로그등록하기]  
[블로그리스트]  
SSISO Community카페
블로그 카테고리
정치 경제
문화 칼럼
비디오게임 스포츠
핫이슈 TV
포토 온라인게임
PC게임 에뮬게임
라이프 사람들
유머 만화애니
방송 1
1 1
1 1
1 1
1 1
1

spring로 검색한 결과
등록일:2015-05-21 13:42:34
작성자:
제목:[Spring] Annotation - @sessionAttributes, SessionStatus


HTTP 요청에 의해 동작하는 서블릿은 기본적으로 상태를 유지하지 않는다. 따라서 매 요청이 독립적으로 처리된다.

하지만 애플리케이션은 기본적으로 상태를 유지할 필요가 있다. 예를 들어 사용자가 로그인하면 그 로그인 정보는 계속 

유지되어야 할 것이고, 여러 페이지에 걸쳐 단계적으로 정보를 입력하는 위자드 폼 같은 경우에도 폼 정보가 하나의 요청을 

넘어서 유지되어야 한다. spring Framework에서는 Annotation을 통해 이러한 문제를 쉽게 해결 할 수 있다.


회원 정보 수정 페이지를 만든다고 예를 들어보자.

회원 정보 수정 페이지로 들어가면 기존 회원(User)의 정보가 그대로 나타나야 할 것이다.

이 뜻은 Controller가 View를 반환하기 전에 DB로부터 데이터를 가져온 다음 User 오브젝트를 생성하여 넘겨줘야한다는 것이다.

그리고 회원 정보를 수정한 후 "저장" 버튼을 누르면 다시 Controller가 호출되며 갱신된 정보가 DB에 입력되어야 한다.


① Client가 회원 정보 수정 페이지 Request

② 해당 Controller가 호출되어 DB에 접근한 다음 회원 정보를 가져와서 User 오브젝트를 생성한다.

③ Controller는 User 오브젝트와 View를 Client에게 반환한다.

④ Client는 몇몇 정보를 수정한 후 "저장" 버튼을 누른다.

⑤ 해당 Controller가 호출되어 User 오브젝트의 내용을 DB에 갱신한다.


최소한 두 번의 요청이 서버로 전달되어야 한다.

어떻게 생각하면 수정 작업은 수정 전 페이지(View)와 수정 후 페이지(View) 2개가 있으면 간단히 해결될 것처럼 보인다.

하지만 좀 더 생각해보면 수정 작업은 생각보다 복잡해 질 수 있다. 사용자가 수정한 폼의 필드 값에 오류가 있는 경우에

에러 메시지와 함께 수정 화면을 다시 보여줘야 하기 때문이다. (또 상태를 유지하지 않고 폼 수정 기능을 만들려면 도메인

오브젝트 중심 방법보다는 계층 간의 결합도가 높은 데이터 중심방식을 사용하기 쉬워진다.)

수정 페이지를 반환하는 Controller는 아마 아래와 같을 것이다.
1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/form.do", method = RequestMethod.GET)
public String form(Model model) {
    User user = new User();     // DB로부터 회원 데이터를 가져온다.
                    // 여기서는 setter가 DB에서 가져온 데이터라고 생각하자.   
    user.setId("이지형"); 
    user.setPw("110"); 
     
    model.addAttribute("user", user);  
 
    return "form";
}

회원 정보 수정 폼에는 사용자 테이블에 담긴 대부분의 정보를 출력해줄 필요가 있다.

그런데 문제는 여기서부터다. 회원 정보 수정 화면이라고 모든 정보를 다 수정하는 것이 아니다!

사용자 스스로 자신의 정보를 수정할 수 없는 경우라면 수정 할 수 있는 필드는 제한되어 있다. 로그인 ID나 중요한 가입정보

(주민번호, 캐시점수 등등) 이러한 정보는 아예 수정 폼에 나타나지 않거나, 나타난다고 하더라도 읽기 전용으로 출력해줘

야한다. 따라서 User 도메인 오브젝트에 담겨서 전달된 내용 중에서 수정 가능한 일부 정보만 폼의 <input>이나 <selecte> 등을

이용한 수정용 필드로 출력된다.


그렇기 때문에 폼을 수정하고 저장 버튼을 눌렀을 때 원래 User에 담겨 있던 내용 중에서 수정 가능한 필드에 출력했던 

일부만 서버로 전송된다는 문제가 발생한다! 폼의 Submit을 받는 Controller 메소드에서 만약 다음과 같이 User 오브젝트로

폼의 내용을 바인딩하게 했다면 어떻게 될까?

1
2
3
4
5
@RequestMapping(value = "/form.do", method = RequestMethod.POST)
public String submit(@ModelAttribute User user) {
    userService.updateUser(user);
    return "result";
}
 
@ModelAttribute가 붙은 User 타입 파라미터를 선언했으니 폼에서 POST를 통해 전달되는 정보는 User 오브젝트의 프로퍼티

로 바인딩돼서 들어갈 것이다. 문제는 폼에서 <input>이나 <select> 로 정의한 필드의 정보만 들어간다는 점이다.

아예 출력도 안했던 주민번호나 캐시 점수 같은 정보는 폼에서 전달되지 않았으므로 submit() 메소드의 파라미터로 전달되는

user 오브젝트에는 이런 프로퍼티 정보가 모두 비어 있을 것이다. 이 상태로 DB에 갱신(update)하게 되면 치명적인 오류가

발생할 지도 모른다. 그렇다면 이 문제를 해결 할 수 있는 방법을 한번 생각해보자.


Hidden Field(히든필드)

수정을 위한 폼에 User의 모든 프로퍼티가 다 들어가지 않기 때문에 이러한 문제가 발생했으니 모든 User 오브젝트의 프로퍼티를

폼에 다 넣어 주는 방법을 생각해 볼 수 있다. 물론 사용자가 수정하면 안되는 정보가 있으니 이런 정보는 히든 필드에 넣어줘야

한다. 히든 필드를 사용하면 화면에서는 보이지 않지만 폼을 Submit하면 다시 서버로 전송된다. 결국 Controller가 받는 User

타입의 오브젝트에는 모든 프로퍼티의 값이 채워져 있을 것이다.


간단히 문제를 해결한 듯 보이지만 사실 두 가지 심각한 문제가 있다. 첫째, 데이터 보안에 심각한 문제를 일으킨다.

폼의 히든 필드는 브라우저 화면에는 보이지 않지만 HTML 소스를 열어보면 그 내용과 필드 이름까지 쉽게 알아낼 수 있다.

폼을 통해 다시 서버로 전송되는 정보는 간단히 조작될 수 있기 때문에 상당히 위험하고 보안에 취약하다.

두번째 문제는 사용자 정보에 새로운 필드가 추가됐다고 해보자. 그런데 깜빡하기 폼에 이 정보에 대한 히든 필드를

추가하지 않았다면, 추가된 필드의 값은 수정을 거치고 난 후 null로 바뀌는 현상이 발생할 것이다. 따라서 히든 필드

방식은 매우 유치한 해결 방법이고 공개된 서비스에서는 사용을 권장할 수 없다.



DB 재조회

두 번째 해결책은 기능적으로 보자면 완벽하다. 폼으로부터 수정된 정보를 받아 User 오브젝트에 넣어줄 때 User 오브젝트

대신 DB에서 다시 읽어온 User 오브젝트를 이용하는 것이다.

1
2
3
4
5
6
7
8
9
10
@RequestMapping(value="/form.do", method=RequestMethod.POST)
public String submit(@ModelAttribute User formUser, @RequestParam int id) {
    User user = userService.getUser(id);
    user.setName(formUser.getName());   
    user.setPassword(formUser.getPassword());   
    user.setEmail(formUser.getEmail());
         ...        
    userService.updateUser(user);   
    return "user/editsuccess";
}

이 방법은 업데이트를 위해 서비스 계층으로 전달할 User 오브젝트를 DB에서 새로 읽어온 것으로 사용한다. DB에서 새로 읽어
왔기때문에 User 오브젝트에는 모든 프로퍼티의 내용이 다 들어 있으므로 폼에서 전달되는 정보를 담은 formUser 의 프로퍼티를
DB에서 읽어온 user에 적용 시킨 후 Update 한다.

완벽해 보이긴 하지만 이 방법에는 몇 가지 단점이 있다. 일단 폼을 Submit 할 때마다 DB에서 사용자 정보를 다시 읽는 부담이
있다. 성능에 큰 영향을 줄 가능성이 높진 않더라도 분명 DB의 부담을 증가시키는 것은 사실이다. 성능은 그렇다치더라도
폼에서 전달되는 필드가 어떤 것인지 정확히 알고 이를 복제해줘야 한다.

얼핏 보면 간단히 문제를 해결한 듯 보이지만 여전히 불편하며 새로운 문제를 초래하는 방법이다.



계층 사이의 강한 결합

세 번째로 생각해볼 수 있는 방법은 계층 사이에 강한 결합을 주는 것이다. 강한 결합이라는 의미는 각 계층의 코드가 다른 

계층에서 어떤 일이 일어나고 어떤 작업을 하는지를 자세히 알고 그에 대응해서 동작하도록 만든다는 뜻이다.


이 방식은 앞에서 지적한 폼 수정 문제의 전제를 바꿔서 문제 자체를 제거한다. 기본 전제는 서비스 계층의 updateUser() 메소드

가 User라는 파라미터를 받으면 그 User는 getUser()로 가져오는 User와 동등하다고 본다는 것이다. User라는 오브젝트는

한 사용자 정보를 완전히 담고 있고, 그것을 전달 받으면 원하는 원하는 모든 필드를 참조하고 조작할 수 있다고 여긴다는 말이다.

이렇게 각 계층이 도메인 모델을 따라 만든 도메인 오브젝트에만 의존하도록 만들면 각 계층 사이에 의존성과 결합도를 대폭

줄일 수 있다.

 결합도를 줄인다는 의미는 한 계층의 구현 코드를 수정해도 기본적인 전체인 도메인 오브젝트가 바뀌지 않으면 다른 계층의

 코드에 영향을 주지 않는다는 뜻이다. UserService의 updateUser() 메소드는 사용자 레벨을 업그레이드하는 로직에서 수정하

 는 화면을 처리하는 UserController에서 호출되든 상관없이 동일하게 만들 수 있다. UserDao의 update() 메소드도 마찬가지

 다. 전달되는 User 오브젝트가 폼을 통해 수정된 User인지 상관하지 않고 자신의 기능에만 충실하게 만들 수 있다. 결국 

 든 계층의 코드가 서로 영향을 주지 않고 독립적으로 확장하거나 변경할 수 있고, 여러 개의 로직에서 공유할 수

 있다.

 

 반면에 계층 사이에 강한 결합을 준다는 건 각 계층의 코드가 특정 작업을 중심으로 긴밀하게 연결되어 있고, 자신을

 사용하는 다른 계층의 코드가 어떤 작업을 하는지 구체적으로 알고 있다는 뜻이다. 예를 들어 사용자 자신의 정보를 수정하는

 폼을 통해 전달되는 User 오브젝트에는 name, password, email 세 개의 필드만 들어온다는 사실을 UserService의

 updateUserForm() 메소드가 알고 있게 해주면 문제는 간단해진다. 폼에서 세 개의 필드만 수정할 수 있음을 알고 있으니

 그 세개의 필드만 담긴 User 오브젝트가 컨트롤러로 전달되는 것을 알 수 있다. 그에 따라 세 개의 필드 외에는 참조하지 않고

 무시하도록 코드를 만들면 된다. DAO도 마찬가지다. User의 모든 필드를 업데이트 하는 대신 updateForm() 이라는 메소드를

 하나 따로 만들어서 name, password, email 세 개의 필드만 수정하도록 SQL을 작성하게 해주면 된다.


 관리자 메뉴의 사용자 정보 수정 기능이 있고 거기에는 더 많은 정보를 수정하도록 만들어진 폼이 있다면, 이 폼을 처리하는

 Controller는 관리자 사용자 정보 수정을 전담하는 UserService의 updateAdminUserForm()을 호출하게 된다. updateAdmin

 UserForm() 은 폼에 어떤 필드가 수정 가능하도록 출력되는지를 알고 있는 메소드다. 따라서 해당 필드만 참조해서 비즈니스

 로직을 처리하고, 역시 해당 필드만 DB에 반영해주는 UserDao의 updateAdminForm() 메소드를 사용한다. 아예 Controller

 에서 폼의 정보를 받는 모델 오브젝트를 폼에서 수정 가능한 파라미터만 가잔 UserForm, UserAdminForm 등을 독립적으로

 만들어서 사용하는 방법도 있다. 결국 개별 작업을 전담하는 코드를 각 계층마다 만들어야 한다.

이런식으로 계층 간에 강한 의존성을 주고 결합도를 높이면 처음에는 만들기 쉽다. 대부분의 코드는 화면을 기준으로 해서 각

계층별로 하나씩 독립적으로 만들어질 것이다. DAO의 메소드와 서비스 계층의 메소드는 각각 하나의 화면을 위해서만 사용된다.

결국 화면을 중심으로 거기에 사용되는 SQL을 정의하고 하나씩 메소드를 새롭게 추가하는 것을 선호하는 개발자가

애용하는 방식이다.

 

하지만 이렇게 결합도가 높은 코드를 만들 경우 애플리케이션이 복합해지기 시작하면 단점이 드러난다. 일단 코드의 중복

늘어난다. 코드를 재사용하기가 힘들기 때문이다. 수정할 필드가 조금 달라도 다른 메소드를 만들어야 한다. 코드는 자꾸

중복되고 그 때문에 기능을 변경할 때 수정해야 할 곳도 늘어난다. 또한 계층 사이에 강한 결합이 있기 대문에 한 쪽을

수정하면 연관된 코드도 함께 수정해줘야 한다. 코드의 중복은 많아지고 결합이 강하므로 그만큼 테스트하기도 힘들다.

아무튼 이렇게 계층 사이에 강한 결합을 만들고 수정할 필요가 있는 필드가 어떤 것인지 모든 계층의 메소드가 다 알고 있게

하면 문제는 해결 할 수 있다. 이럴 땐 User와 같은 도메인 오브젝트보다는 차라리 파라미터 Map을 쓰는 것이 편리할 수 있다.

다음과 같이 Controller 메소드의 @RequestParam 이 붙는 Map<String, String> 타입 파라미터를 사용하면 모든 요청

파라미터를 Map 에 담은 것을 전달 받을 수 있다.

1
2
3
4
5
@RequestMapping(value="/form.do", method=RequestMethod.POST)
public String submit(@RequestParam Map<String, String> userMap) {
    userService.updateUserByUserMap(user);
    return "user/editsuccess";
}




자, 이제 마지막으로 spring이 제공하는 편리한 기능에 대해서 알아보자

@SessionAttributes

수정 폼을 다루는 Controller 작성 시 spring의 접근 방법은 바로 Session을 이용하는 것이다. 아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.appdi.join;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
 
import com.appdi.user.User;
 
@Controller
@SessionAttributes("user")      // Controller 메소드가 생성하는 Model 정보 중
                                // "user" 이름을 가진 것이 있다면 그것을 세션이 저장한다.
public class ValidateUserInfoController {
 
    @RequestMapping(value = "/form.do", method = RequestMethod.GET)
    public String form(Model model) {
        User user = new User();
         
        // DB로부터 회원 데이터를 가져온다.
        // 여기서는 setter가 DB에서 가져온 데이터라고 생각하자.
        user.setId("이지형");
        user.setPw("110");
 
        // 이 user 오브젝트는 "user" 이름을 가진채로
        // Model에 추가되기 때문에 Session에 저장되게 된다.
        model.addAttribute("user", user);      
 
        return "form";
    }
 
    @RequestMapping(value = "/form.do", method = RequestMethod.POST)
    // 이전 view에서 form으로 전송된 값들을 파라미터로 선언된
    // user 오브젝에 자동으로 바인딩 시킨 후 "user" 이름을 부여한다.
    // 이렇게 파라미터 앞에 @ModelAttribute가 붙게 되면 자동으로
    // 메소드 return 시 해당 파라미터가 Model에 추가된다.
    // 또한 "user"라는 이름을 붙여줬으므로 Session에 저장되게 된다.
    public String submit(@ModelAttribute("user") User user) {  
        System.out.println("id : " + user.getId());
        System.out.println("pw : " + user.getPw());
         
        return "result";
    }
 
}

기존에 만든 form()과 submit() 메소드는 전혀 손댈 것 없이 단지 @SessionAttributes에 "user"라는 이름을 넣어서

클래스에 부여해주는 것만으로 앞에서 살펴봤던 모든 문제가 해결된다. @SessionAttributes 가 해주는 기능은 두 가지이다.

첫째, Controller 메소드가 생성하는 모델 정보 중에서 @SessionAttributes에 지정한 이름과 동일한 것이 있다면

이를 Session에 저장한다. 두번째로 @ModelAttribute가 지정된 파라미터가 있을 때 이 파라미터에 전달해줄 오브젝

트를 세션에서 가져오는 것이다. 원래 파라미터에 @ModelAttribute가 있으면 해당 타입의 새 오브젝트를 생성한 후에 요청 파

라미터 값을 프로퍼티에 바인딩해준다. 그런데 @SessionAttributes에 선언된 이름과 @ModelAttribute의 모델 이름이 동일하면

그 때는 먼저 Session에 같은 이름의 오브젝트가 존재하는지 확인한다. 만약 존재한다면 모델 오브젝트를 새로 만드는 대신

Session에 있는 오브젝트를 가져와 @ModelAttribute 파라미터로 전달해줄 오브젝트로 사용한다. @ModelAttribute는 폼에서

전달된 필드 정보를 모델 오브젝트의 프로퍼티에 넣어준다. 폼을 출력하기 위한 컨트롤러인 form() 메소드를 먼저 거쳤다면 DB

에서 가져온 User 오브젝트를 가져와 폼에서 전송해준 파라미터만 바인딩한 뒤에 Controller의 user 파라미터로 넘겨준다.

DB에서 처음 가져왔던 오브젝트를 그대로 사용해서 변경된 프로퍼티만 바인딩해줬으니 폼에 출력하지 않았던 프로퍼티 값도

그대로 유지 된다.

form.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

DB 정보 <br>
ID : ${ user.id } <br>
PW : ${ user.pw } <br><br>

세션 정보 <br>
ID : ${ sessionScope.user.id } <br>
PW :${ sessionScope.user.pw } <br><br>

정보 수정 <br>
<form action="form.do" method="post">
 ID : <input type="text" name="id"> <br>
 <input type="submit" value="저장">
</form>
</body>
</html>


     =>    

* ID만 수정 된 것을 확인 할 수 있다.

 

@SessionAttributes는 하나 이상의 모델을 Session에 저장하도록 지정할 수 있다. @SessionAttributes 의 설정은 클래스의

모든 메소드에 적용된다. Controller 메소드에 의해 생성되는 모든 종류의 모델 오브젝트는 @SessionAttributes에 저장될 이름을

갖고 있는지 확인된다. 따라서 Model 파라미터를 이용해 저장한 모델이든, 단일 모델 오브젝트의 리턴을 통해 만들어지는 모델이

든, @ModelAttribute 로 정의된 모델이든 상관없이 모두 @SessionAttributes의 적용 후보가 된다.

 

단, @SessionAttributes의 기본 구현인 HTTP Session을 이용한 Session 저장소는 모델 이름을 Session에 저장할 애트리뷰트

이름으로 사용한다는 점을 주의하자. 위의 코드에서 user라는 Session 애트리뷰트에 User 오브젝트가 저장된다. 따라서

@SessionAttributes에 사용하는 모델 이름에 충돌이 발생하지 않도록 주의해야 한다.

 

 

SessionStatus

@SessionAttributes를 사용할 때는 더 이상 필요없는 Session 애트리뷰트를 코드로 제거해 줘야 한다는 점을 잊지말자.

1
2
3
4
5
6
7
8
public String submit(@ModelAttribute("user") User user, SessionStatus session) {   
    System.out.println("id : " + user.getId());
    System.out.println("pw : " + user.getPw());
         
    session.setComplete();
         
    return "result";
}