기록은 기억의 연장선

더 많은 것을 기억하기 위해 기록합니다


  • Home

  • Tags

  • Categories

  • Archives

  • Search

[spring] @SessionAttributes, SessionStatus

Posted on 2018-03-07 | Edited on 2020-11-02 | In spring | Comments:

문제상황

아래는 기본적인 회원정보 수정 샘플 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class UserController{
// 수정 폼
@RequestMapping(value="/user/{uid}", method=RequestMethod.GET)
public String modifyForm(@PathVariable Long uid,
ModelMap modelMap){

User user = userService.getUser(uid);
modelMap.addAttribute("user", user);

return "user/modify"
}

// 수정
@RequestMapping(value="/user/{uid}", method=RequestMethod.POST)
public String modify(@PathVariable Long uid,
@ModelAttribute User user){

userService.modify(user);

return "user/modifySuccess"
}
}

위 코드는 기본적으로 문제가 있다.
수정 폼을 출력하는 메서드에서 uid로 회원을 조회하고, 결과인 User 오브젝트를 모델로 내린다.
여기까진 문제가 없다. 그러나 이후에 수정을 하는 부분에서 문제가 발생한다.
전달받은 User 오브젝트에서 실제로 사용자가 수정할 수 있는 필드는 제한적이라는 것이다.

회원 수정 폼에서는 수정이 필요한 부분만 input 태그로 입력을 받게되고,
이로 인해 최종적으로 modify 메서드를 호출 시 @ModelAttribute는 수정이 필요한 부분만 값이 찬, 불완전한 User 오브젝트를 받게 된다.
이 상태에서 modify 메서드를 실행할 경우 값이 차지 않은 프로퍼티들로 인해
실제 DB 데이터가 null이나 0으로 업데이트 되는 심각한 상황을 초래하게 된다.

이 상황에서 생각할 수 있는 해결법은 대표적으로 3가지가 있다.

해결법1 - hidden 필드

수정하면 안되는 데이터는 모두 hidden 필드에 넣어주는 것이다.
이럴 경우 서버로 모든 값이 전달될 수 있기 때문에 위의 문제점은 해결된다.

1
2
3
<input type="hidden" name="id" value="joont" />
<input type="hidden" name="level" value="3" />
...

하지만 이는 더 큰 문제를 초래할 수 있다.

첫째로 이 방식은 데이터 보안에 매우 심각한 문제를 초래한다.
hidden 필드의 값은 매우 간단하게 조작될 수 있기 떄문이다.
조작된 값은 서버에서 그대로 업데이트 되기 때문에 이는 매우 심각한 문제를 초래할 수 있다.

둘째로 도메인 오브젝트가 변경될 때 마다 매번 hidden 필드도 관리해줘야 한다는 점이다.
그리고 혹여나 실수로 hidden 필드를 하나라도 누락하게 된다면 또 데이터가 null이나 0으로 업데이트되는 상황이 발생할 것이다.

이는 해결법이라고 보기에는 무리가 있어 권장되지 않는 방식이다.

해결법2 - DB 조회

업데이트 전 DB에서 데이터를 한번 읽어와 도메인 오브젝트에 빈 프로퍼티가 없게 하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(value="/user/{uid}", method=RequestMethod.POST)
public String modify(@PathVariable Long uid,
@ModelAttribute User formUser){

User user = userService.getUser(uid);
// 수정할 오브젝트만 반영
user.setNickName(formUser.getNickName());
user.setEmail(formUser.getEmail());

userService.modify(originUser);

return "user/modifySuccess"
}

기능상으로만 보면 완벽하나, 매 submit 마다 DB에서 다시 데이터를 읽어와야 하는 부담이 있다.
또한 수정할 오브젝트를 일일히 세팅해줘야 하는데, 번거롭고 실수할 가능성이 있다.

해결법3 - 계층간 강한 결합

각 계층의 코드를 특정 작업을 중심으로 긴밀하게 결합시키는 방법이다.
간단하게 말해 기본정보 수정은 modifyDefault, 패스워드 수정은 modifyPassword, 프로필 사진 수정은 modifyProfile 과 같이 작성하여 필요한 부분만 업데이트 치게 하는 것이다.
이게 당장은 편리할지 모르나, 애플리케이션의 규모가 조금만 커져도 단점이 드러난다.
각 메서드는 각각 거의 하나의 화면에서만 사용되고, 각 메서드의 재사용성이 떨어진다.
게다가 메서드들을 각 계층마다 써줘야 한다. 이러면 코드의 중복이 발생하고 수정에도 취약하다.
이러다간 서비스 계층과 DAO 계층을 구분하는 것도 의미가 없어질지도 모른다.

그러므로 이 방식은 권장되지 않는 방법이다.
객체지향성이 떨어지고 데이터 중심으로 코드가 작성되기 때문이다.
각 계층이 서로 독립적이여야 재사용성이 높고, 확장이나 변경에도 유연하게 대응이 가능하다.


@SessionAttributes

스프링에선 위와 같이 수정 폼을 다루는 상황에서 깔끔한 해결법을 제공해주는데, 바로 세션을 이용하는 것이다.

수정폼 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
@SessionAttributes("user")
public class UserController{
// 수정 폼
@RequestMapping(value="/user/{uid}", method=RequestMethod.GET)
public String modifyForm(@PathVariable Long uid,
ModelMap modelMap){

User user = userService.getUser(uid);
modelMap.addAttribute("user", user);

return "user/form"
}

// 수정
@RequestMapping(value="/user/{uid}", method=RequestMethod.POST)
public String modify(@PathVariable Long uid,
@ModelAttribute User user){

userService.modify(user);

return "user/modifySuccess"
}
}

추가된건 달랑 @SessionAttributes 애노테이션 하나이다.
그러나 이 애노테이션으로 인해 앞의 모든 문제점이 해결되었다. 어떻게 동작하길래 그럴까?

@SessionAttributes가 제공해주는 기능은 2가지가 있다.

첫째로 컨트롤러의 메서드가 생성하는 모델정보 중에서 @SessionAttributes에 지정한 이름에 동일한 것이 있으면 이를 세션에 저장한다.
위의 상황에서는 수정폼(modifyForm)이 호출될 때 user 오브젝트가 모델에 저장되는 동시에 세션에도 저장된다. 지정한 이름이 동일하기 때문이다.

둘째로 파라미터에 @ModelAttribute 애노테이션이 있을 때 여기 전달할 오브젝트를 세션에서 가져오는 것이다.
원래는 파라미터에 @ModelAttribute가 있으면 새 오브젝트를 생성한 뒤 HTTP 요청 파라미터를 바인딩한다.
하지만 @SessionAttributes를 사용했을 경우는 다르다.
새 오브젝트를 생성하기 전 @SessionAttributes에 지정된 이름과 @ModelAttribute 파라미터의 이름을 비교하여 동일할 경우 오브젝트를 새로 생성하지 않고 세션에 있는 오브젝트를 사용하게 된다.
(참고로 비교하는 @ModelAttribute 파라미터의 이름은 타입의 이름이다. User 오브젝트일 경우 이름은 user 이다. 충돌이 발생하지 않도록 유의해야 한다!)

즉, 수정폼을 거친 뒤 수정을 하게되면 비어있는 프로퍼티가 하나도 없는 상태로 수정을 진행할 수 있게 되는 것이다!
이로인해 앞서 고민했던 문제점이 깔끔하게 해결되었다. 역시 대단하다.
아래는 @SessionAttributes를 사용했을때의 흐름을 그림으로 나타낸 것이다.
세션을 이용한 폼 모델 저장/복구
@SessionAttributes를 지정한 클래스내의 모든 메서드의 모델에 적용되며, 하나 이상의 모델을 세션에 저장할 수 있다.

등록폼 처리

@SessionAttributes은 수정폼외에 등록폼에서도 유용하게 사용할 수 있다.
보통 수정폼의 경우 DB에서 조회한 결과를 폼에 보여줘야 하므로 도메인 오브젝트를 유지해야 하지만,
등록폼의 경우 그럴 필요가 없다.
그래서 대부분, 사용자가 폼을 submit 할 때 도메인 오브젝트를 새로 만들게 한다.

1
2
3
4
5
6
7
@RequestMapping(value="/user", method=RequestMethod.POST)
public String save(@ModelAttribute User user){ // User 오브젝트가 새로 생성된다

userService.save(user);

return "user/saveSuccess"
}

하지만 이럴 경우 모델을 사용하지 않는 등록폼과, 모델을 사용하는 수정폼으로 폼을 총 2개를 만들어줘야 한다.
게다가 등록폼의 경우 모델 정보를 사용하지 않기 때문에 검증 로직에서 오류가 발생했을 경우 등록과 수정을 왔다갔다 거려야하는 꽤나… 아니 매우 번거로운 상황이 발생한다.
이럴바에는 처음부터 아예 모델을 사용하게 하고, 등록폼에는 빈(empty) 오브젝트라도 출력을 시켜주는 편이 낫다.
이렇게 하면 폼을 굳이 두개로 분리할 필요도 없어진다.
그리고 여기다가 @SessionAttributes를 이용하여 매번 폼이 새로 생성되지 않도록 세션에 저장해두는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@SessionAttributes("user")
public class UserController{
@RequestMapping(value="/user/add", method=RequestMethod.GET)
public String saveForm(ModelMap modelMap){

User user = new User();
// 이런식으로 기본값이 필요한 경우 지정해줄 수 있다
user.setJoinDt(new Date());

modelMap.addAttribute("user", user);

return "user/form"; // modifyForm과 동일한 폼 사용
}
}

SessionStatus

@SessionAttributes를 사용해 세션에 저장한 모델이 더 이상 필요없어질 경우 세션에서 제거해줘야 한다.
세션의 제거 시점은 스프링이 알수없으므로 이를 제거하는 책임은 컨트롤러에게 있다.
세션의 경우 서버의 메모리를 사용하므로 필요없는 시점에 제거해주지 않으면 메모리 누수가 발생할 수 있기때문에 항상 빼먹지 않고 제거해줘야 한다.
세션의 제거는 SessionStatus 오브젝트의 setComplete 메서드로 제거할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(value="/user/{uid}", method=RequestMethod.POST)
public String modify(@PathVariable Long uid,
@ModelAttribute User user,
SessionStatus sessionStatus){

userService.modify(user);

// 현재 컨트롤러 세션에 저장된 모든 정보를 제거해준다. 개별적으로 제거할 순 없다
sessionStatus.setComplete();

return "user/modifySuccess"
}

여기까지가 @SessionAttributes를 이용한 스프링의 전형적인 폼 처리 방식이다.
겉으로 보여주는 것 없이 애노테이션 하나로만 처리하기 때문에 동작방식을 정확히 이해하고 있어야 한다.

From
Read more »

[spring] @Controller

Posted on 2018-03-04 | Edited on 2020-11-02 | In spring | Comments:

여기서 말하는 @Controller란 빈 자동 스캔시 사용되는 스테레오 타입 애노테이션이 아니라,
애노테이션을 이용해 컨트롤러를 개발하는 방법을 말한다.
즉, AnnotationMethodHandlerAdapter가 실행하는 각 메서드들을 의미한다.


파라미터

개발자가 명시한 애노테이션과 파라미터 타입 등에 따라 AnnotationMethodHandlerAdapter가 적절히 변환하여 제공해줌

HttpServletRequest, HttpServletResponse, ServletRequest, ServletResponse

대게는 좀 더 상세한 파라미터 타입을 사용하면 되지만,
원한다면 직접 HttpServletRequest, HttpServletResponse 타입을 받을 수 있다.
ServletRequest, ServletResponse 타입도 가능하다.

HttpSession

HttpServletRequest에서 얻을 수 있는 HttpSession을 바로 받을 수 있다.
HttpSession은 멀티스레드 환경에서 안전성이 보장되지 않으므로
핸들러 어댑터의 synchronizeOnSession 프로퍼티를 true로 줘야한다.

Locale

java.util.Locale 타입으로 DispatcherServlet의 Locale Resolver가 결정한 Locale 오브젝트를 받을 수 있다.

InputStream, Reader

HttpServletRequest의 getInputStream()를 통해 받을 수 있는 InputStream과,
getReader()를 통해 받을 수 있는 Reader를 바로 받을 수 있다.

OutputStream, Writer

HttpServletResponse의 getOutputStream()를 통해 받을 수 있는 OutputStream과,
getWriter()를 통해 받을 수 있는 Writer를 바로 받을 수 있다.

@PathVariable

@RequestMapping url에 {}로 들어가는 패스 변수를 받는다.

1
2
3
4
@RequestMapping(value="/post/{postNo}")
public String detail(@PathVariable("postNo") Integer postNo){
// ...
}

속성으로 받을 패스 변수의 이름을 지정할 수 있으며,
전달받은 패스 변수는 선언한 파라미터 타입으로 형변환 된다.
즉, /post/10 이라고 요청하게 되면 postNo 변수에 Integer 타입으로 형 변환되어 담기게 된다.
만약 /post/notNumber 과 같은 형태로 요청하여 형변환이 불가능 할 경우
400 Bad Request 에러가 발생한다.

@RequestParam

HttpServletRequest의 getParameter()로 받을 수 있는 파라미터를 바로 받을 수 있다.
전달받은 파라미터는 선언한 파라미터 타입으로 자동 형 변환된다.
또한 필수여부, 디폴트 값 등을 설정할 수 있다.

1
2
3
4
5
// page라는 이름으로 전달된 파라미터를 받아 Integer 타입으로 변환한다
public String list(@RequestParam("page") Integer page)

// 필수 여부와 디폴트 값을 줄 수 있다. 필수 여부는 default가 true이다.
public String list(@RequestParam(value="page", required=false, defaultValue="1") Integer page)

파라미터 타입을 Map으로 선언하면 모든 파라미터를 맵으로 받을 수 있다.

1
public String list(@RequestParam Map<String, String> params)

@CookieValue

쿠키값을 받아올 수 있다. 속성으로 쿠키의 이름을 지정해주면 된다.

1
2
3
4
5
6
// 쿠키 name이 auth인 것을 가져온다
public String list(@CookieValue("auth") String auth)

// @RequestParam과 마찬가지로 필수 여부와 디폴트 값을 줄 수 있다.
// 필수 여부 default는 true이다.
public String list(@CookieValue(value="auth", required=false, defaultValue="NONE") String auth)

@RequestHeader

헤더값을 받아올 수 있다. 속성으로 헤더의 이름을 지정해주면 된다.
@RequestParam, @CookieValue와 마찬가지로 required, defaultValue를 설정해 줄 수 있다.

1
public String list(@RequestHeader("Host") String host)

Model, ModelMap, Map

모델 정보를 담을 수 있는 Model과 ModelMap 객체를 파라미터 레벨에서 바로 받을 수 있다.
Map도 앞에 특별한 애노테이션이 없다면 모델 정보를 담는데 사용할 수 있다. 하지만 갠적으로 좀 헷갈린다… 안써야지

1
2
3
4
5
6
public String list(ModelMap model){
model.addAttribute("key", "value");

// collection에 담긴 모든 오브젝트를 모델에 추가할 수 있다(자동 이름 생성 방식을 통해)
model.addAllAttribute(collection);
}

@ModelAttribute

이름에 Model이 들어가 있긴 하지만 우리가 일반적으로 사용하는 모델과는 조금 의미가 다르다.

컨트롤러가 받는 요청정보 중에서, 하나 이상의 값을 가진 오브젝트 형태로 만들 수 있는 정보를 @ModelAttribute 모델이라고 부른다.
@ModelAttribute라고 별다를 건 없다.
기존과 똑같이 파라미터를 받는데,
그걸 메서드에서 1:1로 받으면 @RequestParam인거고
도메인 오브젝트나 DTO에 바인딩해서 받으면 @ModelAttribute 인 것이다.

사용자가 리스트에서 검색할 떄 사용하는 파라미터를 한번 생각해 보자.
기본적으로 전달될 파라미터는 검색 키워드겠고, 그 외에도 검색 타입, 페이지 번호 등이 전달 될 수 있다.
이를 기존의 @RequestParam으로 표현하면 아래와 같이 된다.

1
2
3
4
5
6
7
public String search(
@RequestParam("q") String q,
@RequestParam("type") String type,
@RequestParam(value="page", required=false, defaultValue="1") Integer page){

service.search(q, type, page);
}

일단 서비스 메서드 부터 문제가 있다… 저런식으로 파라미터를 나열할 경우 변경에 매우 취약하게 되며,
같은 타입의 파라미터가 여러개면 실수할 가능성이 매우 높아진다.
이럴 경우에는 아래와 같이 오브젝트를 하나 만들어 전달하는 편이 낫다.

1
2
3
4
5
6
7
public class Search{
private String q;
private String type;
private Integer page;

// getter, setter
}

서비스 메서드는 이 오브젝트를 사용하며 해결이 가능한데, 오브젝트를 매번 초기화 해줘야 한다는 귀찮음이 따른다.
이럴떄 사용할 수 있는것이 @ModelAttribute이다!

1
2
3
4
public String search(@ModelAttribute Search search){

service.search(search);
}

이제 요청 파라미터들은 자동으로 Search 오브젝트에 바인딩 되어 들어오게 된다(타입 변환도 자동으로 된다).
코드가 매우 깔끔해지고 위에서 언급한 문제점 또한 단번에 해결할 수 있다.

@ModelAttribute는 위와 같이 사용할 수도 있지만 보통은 폼의 데이터를 받을 때 훨씬 유용하게 사용할 수 있다.
게다가 @ModelAttribute의 기능중에 하나가 전달받음과 동시에 컨트롤러가 리턴하는 모델에 자동으로 추가해준다는 점이다.
이로인해 사용자가 입력을 잘못했을 경우에도 입력한 모델을 다시 출력해주며
잘못 입력한 정보에 대해 재입력을 요구하는 기능을 쉽게 구현할 수 있게 된다.

Errors, BindingResult

@ModelAttribute와 같이 사용하는 파라미터 들이다.
기본적으로 @ModelAttribute는 파라미터를 처리할 떄 @RequestParam과는 달리 검증 작업이 추가적으로 진행된다.
검증작업이란 기본적으로 진행되는 타입 변환 외에도 필수 정보 입력 여부, 길이 제한, 값 허용 범위 등 다양한 기준이 적용될 수 있다.

BidingResult와 Errors는 이러한 검증작업의 결과가 담겨지는 곳이다.
컨트롤러에서는 이 두 오브젝트의 결과를 통해 사용자에게 적절한 조치를 취할 수 있게 되는 것이다(검증 에러가 난 부분에 대해 재입력 요구 등).
이러한 특성 때문에 @ModelAttribute는 바인딩에서 타입 변환이 실패하는 오류가 발생해도 400 에러를 발생시키지 않는다.
타입 변환 실패 또한 검증의 한 단계로 보고 BindingResult에 그에 해당하는 결과만을 담을 뿐이다.
BindingResult의 검증결과에서 오류가 없다고 나오면 그제서야 로직을 통과시키고, 오류가 있을 경우 사용자에게 계속 수정을 요구해야 한다.

1
2
3
4
5
6
7
public String add(@ModelAttribute User user, BindingResult result){
if(result.hasError()){
// 재입력 요구
} else{
userService.add(user);
}
}

BindingResult는 반드시 @ModelAttribute 뒤에 나와야 한다.
현재 위의 메서드로는 기본적인 검증인 타입 변환 검증만을 수행하는 상태이다.
모델 바인딩과 검증 바로가기

SessionStatus

@SessionAttributes를 통해 저장된 현재 세션을 다룰 수 있는 오브젝트이다.
@SessionAttributes, SessionStatus 바로가기

@RequestBody

이 애노테이션이 붙은 파라미터에는 HTTP 요청의 본문 부분이 그대로 전달된다.
XML이나 JSON 기반으로 요청하는 경우 매우 유용하게 사용된다.
AnnotationMethodHandlerAdapter에는 HttpMessageConverter타입의 메세지 변환기가 여러개 등록되어 있다.
@RequestBody가 붙은 파라미터가 있으면 요청의 미디어 타입을 먼저 확인한 후,
메세지 변환기들 중에서 이를 처리할수 있는것이 있다면 HTTP 요청 본문 부분을 통째로 변환하여 파라미터로 전달해준다.

1
2
3
4
5
public String test(@RequestBody String body){		
System.out.println(body); // http body가 그대로 출력

return "test";
}

StringHttpMessageConverter가 http body를 그대로 받고 있다.

@Value

시스템 프로퍼티나 다른 빈의 프로퍼티 값, SpEL등을 이용하는데 사용된다.
파라미터 변수 뿐 아니라 필드 변수에도 사용할 수 있다.

1
2
3
4
5
6
// 필드로 받기
@Value("#{'systemProperties['os.name']'}") String osName;
// 파라미터로 받기
public String hello(@Value("#{systemProperties['os.name']}") String osName){
// ...
}

상황에 따라 적절히 선택해서 사용하면 된다.

@Valid

@ModelAttribute 검증에 사용되는 애노테이션이다.
모델 바인딩과 검증 바로가기


리턴

파라미터 뿐만 아니라 리턴 타입도 다양하게 사용할 수 있다.
각 리턴 타입에 대해 다양한 결과를 얻어낼 수 있다.
참고로 어떤 방식으로 리턴하든 마지막에는 ModelAndView로 만들어져 DispatcherServlet에 전달된다.

모델에 자동으로 추가되는 오브젝트

리턴 타입을 알아보기 전에 굳이 명시하지 않아도 모델에 자동으로 추가되는 오브젝트들 부터 살펴보자.

  1. @ModelAttribute 파라미터
    파리미터에서 @ModelAttribute로 받은 오브젝트는 자동으로 모델에 추가된다.
    모델 오브젝트의 이름은 기본적으로 파라미터 타입 이름을 따른다.
    이름을 직접 지정하고 싶으면 @ModelAttribute("모델이름") 의 형태로 지정해주면 된다.

  2. Map, Model, ModelMap
    파라미터에 Map, Model, ModelMap 타입의 오브젝트를 사용하면 미리 생성된 모델 맵 오브젝트를 전달받을 수 있다.
    이후 추가하고 싶은 모델 오브젝트가 있으면 여기에 추가하면 된다.

  3. @ModelAttribute 메서드
    파라미터를 오브젝트로 받는 @ModelAttribute의 기능보단 공통적으로 사용되는 모델 오브젝트를 정의하기 위해 유용하게 사용되는 방식이다.

1
2
3
4
@ModelAttribute("countries")
public List<Country> countries(){
return commonService.getCountries();
}

이런식으로 클래스 내에 별도로 정의해놓으면 클래스 내의 다른 메서드들의 모델에 자동으로 추가된다.
같은 클래스 내의 메서드들의 모델에는 항상 "countries" 이름의 List<Country> 오브젝트가 추가되어있는 것이다.
<select> 태그를 써서 선택 가능한 목록을 보여주는 경우가 대표적이다.

  1. BidingResult
    @ModelAttribute와 같이 사용하는 BindingResult도 모델에 자동으로 추가된다.
    모델 맵에 추가될때의 키는 'org.springframework.validation.BindingResult.모델이름' 이다.

ModelAndView

컨트롤러가 리턴해야 할 정보를 담고 있는 가장 대표적인 클래스이다.
하지만 이것보다 편한 방법이 훨씬 많으므로 자주 사용되진 않는다.

1
2
3
4
5
6
7
public ModelAndView test(){
ModelAndView mv = new ModelAndView();
mv.addObject("key", "value");
mv.setViewName("test");

return mv;
}

참고로 ModelAndView를 리턴하더라도 Map, Model, ModelMap 파라미터는 모델에 자동 추가된다.

String

문자열을 리턴하면 이는 뷰 이름으로 사용된다.
모델은 Model, ModelMap 파라미터를 이용한다.

1
2
3
4
5
public String test(ModelMap modelMap){
modelMap.addAttribute("key", "value");

return "test";
}

void

아예 아무것도 리턴하지 않을경우 RequestToViewNameResolver에 의해 자동으로 뷰 이름이 생성된다.
뷰 이름을 일관되게 통일해야 하므로 규칙을 잘 정해야 한다.

모델 오브젝트

RequestToViewNameResolver를 사용해서 뷰 이름을 자동생성하고,
모델에 추가할 오브젝트가 하나뿐이라면 모델 오브젝트를 바로 반환해도 된다.
스프링은 리턴 타입이 단순 오브젝트이면 이를 모델 오브젝트로 인식해서 모델에 자동으로 추가해준다.
모델명은 모델 오브젝트의 타입 이름을 따른다.

1
2
3
4
public List<Post> getPostList(){

return postService.getPostList(); // return List<Post>
}

Map/Model/ModelMap

메서드에서 직접 Map/Model/ModelMap 오브젝트를 생성해서 리턴하면 모두 모델로 사용된다.
하지만 모델은 파라미터로 받을 수 있기 때문에 이 방식은 잘 사용되지 않는다.

여기서 한가지 주의해야 할 점이 있다. 바로 Map 오브젝트이다.
서비스 메서드에서 결과로 Map을 내려주는 경우가 있는데, 이를 모델 오브젝트라고 생각하고 바로 리턴했다가는 원치않은 결과를 얻게 된다.
Map은 모델 오브젝트가 아닌 모델 맵으로 인식되기 때문에, Map의 모든 속성이 모델에 추가되는 상황이 발생한다.

1
2
3
4
5
6
// 잘못된 코드!!
public Map getUser(){
Map user = userService.getUser();

return user; // user의 모든 속성이 모델에 추가되어 리턴된다.
}

View

뷰 이름대신 직접 View 오브젝트를 넘겨도 된다.

1
2
3
4
5
public View getPostListByExcel(ModelMap modelMap){
// model add..

return excelView; // excel view
}

@ResponseBody

@ReuqestBody와 비슷하게 동작한다.
메서드 레벨에 이 애노테이션이 붙어있으면, 리턴하는 오브젝트가 뷰를 통해 결과를 만들어내는데 사용되지 않고 메세지 컨터버를 통해 바로 HTTP 응답으로 반환된다.

1
2
3
4
@ResponseBody
public String test(){
return "succeed"; // 문자열 그대로 반환
}

@ResponseBody에 의해 succeed가 viewName으로 사용되지 않고 HTTP 응답으로 반환된다.

From
Read more »

[spring] @RequestMapping

Posted on 2018-03-03 | Edited on 2020-11-02 | In spring | Comments:

@RequestMapping은 DefaultAnnotationHandlerMapping에서 컨트롤러를 선택할 때 대표적으로 사용하는 애노테이션이다.
url당 하나의 컨트롤러에 매핑되던 다른 핸들러 매핑과 달리 메서드 단위까지 세분화하여 적용할 수 있으며,
url 뿐 아니라 파라미터, 헤더 등 더욱 넓은 범위를 적용할 수 있다.


속성

DefaultAnnotationHandlerMapping은 클래스와 메서드에 붙은 @RequestMapping 애노테이션 정보를 결합해 최종 매핑정보를 생성한다.
기본적인 결합 방법은 클래스 레벨의 @RequestMapping을 기준으로 삼고, 메서드 레벨의 @RequestMapping으로 세분화하는 방식으로 사용된다.
@RequestMapping에 사용할 수 있는 속성들은 아래와 같다.

String[] value

URL 패턴을 지정하는 속성이다.
String 배열로 여러개를 지정할 수 있으며, ANT 스타일의 와일드카드를 사용할 수 있다.

1
2
3
4
@RequestMapping(value="/post")
@RequestMapping(value="/post.*")
@RequestMapping(value="/post/**/comment")
@RequestMapping(value={"/post", "/P"})

{}를 사용하는 URI 템플릿을 사용할 수도 있다.

1
@RequestMapping(value="/post/{postId}")

{}를 패스 변수라고 부르며 컨트롤러에서 파라미터로 전달받을 수 있다.

참고로 URL 패턴은 디폴트 접미어 패턴이 적용되므로 아래 2개는 동일한 의미이다.

1
2
3
// 2개는 동일하다!
@RequestMapping(value="/post")
@RequestMapping(value={"/post", "/post/", "/post.*"})

RequestMethod[] method

RequestMethod는 HTTP 메서드를 정의한 ENUM이다.
GET, POST, PUT, DELETE, OPTIONS, TRACE로 총 7개의 HTTP 메서드가 정의되어 있다.
@RequestMapping에 method를 명시하면 똑같은 URL이라도 다른 메서드로 매핑해줄 수 있다.

1
2
3
4
5
// url이 /post인 요청 중 GET 메서드인 경우 호출됨
@RequestMapping(value="/post", method=RequestMethod.GET)

// url이 /post인 요청 중 POST 메서드인 경우 호출됨
@RequestMapping(value="/post", method=RequestMethod.POST)

이로인해 컨트롤러에서 번거롭게 HTTP 메서드를 확인할 필요가 없다.

String[] params

요청 파라미터와 값으로도 구분할 수 있다.
String 배열로 여러개를 지정할 수 있으며, 아래와 같이 사용 가능하다.

1
2
3
4
5
6
7
8
9
10
11
// /post?useYn=Y 일 경우 호출됨
@RequestMapping(value="/post", params="useYn=Y")

// not equal도 가능
@RequestMapping(value="/post", params="useYn!=Y")

// 값에 상관없이 파라미터에 useYn이 있을 경우 호출됨
@RequestMapping(value="/post", parmas="useYn")

// 파라미터에 useYn이 없어야 호출됨
@RequestMapping(value="/post", params="!useYn")

GET 파라미터만이 아닌 POST 파라미터 또한 비교 대상이다.
폼 내에서 input 태그 등으로 넘겨도 위의 매핑을 적용할 수 있다.

근데 만약 아래와 같은 상황이라면 어떻게 될까?

1
2
3
// 요청 : /post?useYn=Y
@RequestMapping(value="/post")
@RequestMapping(value="/post", params="useYn=Y")

요청이 2가지 매핑을 다 만족하지만 이럴 경우 구현이 상세한 쪽으로 선택된다.

String[] headers

헤더 값으로 구분할 수 있다.
방식은 위의 params와 비슷하다.

1
@RequestMapping(value="/post", headers="content-type=text/*")

매핑

핸들러 매핑이란 원래 오브젝트를 결정하는 전략이다.
@RequestMapping으로 메서드 레벨까지 세분화하여 작성하긴 했지만,
일관성을 위해 DefaultAnnotationHandlerMapping은 오브젝트까지만 찾아주고,
최종 실행할 메서드는 AnnotationHandlerAdapter가 결정한다.

타입 레벨 + 메서드 레벨 매핑

타입 레벨(클래스 레벨)에서 공통 조건을 지정하고, 메서드 레벨에서 이를 세분화한다.
그리고 URL 요청 시 이 둘을 조합하여 최종 조건이 결정된다.

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value="/post")
public class PostController{
// /post/add 에 매핑
@RequestMapping(value="/add")

// /post/modify 에 매핑
@RequestMapping(value="/modify")

// /post/remove 에 매핑
@RequestMapping(value="/remove")
}

타입 레벨에 ANT 패턴을 사용했을 경우에도 메서드 레벨과 조합될 수 있다.

1
2
3
4
5
@RequestMapping(value="/post/**")
public class PostController{
// /post/**/add 에 매핑
@RequestMapping(value="/add")
}

타입 레벨에 url을 주고 메서드 레벨엔 다른 매핑조건을 줄수도 있다.

1
2
3
4
5
@RequestMapping(value="/post")
public class PostController{
@RequestMapping(method=RequestMethod.GET)
@RequestMapping(method=RequestMethod.POST)
}

타입 레벨에도 메서드 레벨과 같이 method나 params등을 줄 수 있다.

1
2
3
4
5
@RequestMapping(value="/post", params="useYn=Y")
public class PostController{
// /post?useYn=Y&isForeign=Y 에 매핑됨
@RequestMapping(params="isForeign=Y")
}

타입 레벨에서 공통 조건을 지정하고 메서드 레벨에서 세분화 해준다는 개념만 지키면 어떤식의 조합도 가능하다.

메서드 레벨 단독 매핑

매핑 조건에 딱히 공통점이 없는 경우 메서드 레벨에서만 매핑정보를 지정할 수 있다.
하지만 위에서 얘기했듯이 DefaultAnnotationHandlerMapping은 오브젝트까지만 찾아주므로,
타입 레벨에 조건 없는 @RequestMapping을 선언해 매핑 대상으로 만들어야 한다.

1
2
3
4
5
6
@RequestMapping
public class HomeController{
@RequestMapping(value="/post")
@RequestMapping(value="/magazine")
@RequestMapping(value="/notice")
}

근데 만약 컨트롤러 클래스에 @Controller 애노테이션을 붙여 빈 자동 스캔으로 등록되게 했다면 타입 레벨 @RequestMapping은 생략할 수 있다.
@Controller 애노테이션을 보고 @RequestMapping을 사용한 클래스라고 판단하기 때문이다.

타입 레벨 단독 매핑

핸들러 매핑과 핸들러 어댑터는 서로 독립적인 전략이므로 아래와 같이 조합될 수 있다.

1
2
3
4
5
6
7
@RequestMapping(value="/post")
public class PostController implements Controller{
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ....
}
}

상속

@RequestMapping이 적용된 클래스를 다른 클래스에서 상속하여 사용할 수 있다.
상속 시 서브 클래스는 부모의 @RequestMapping을 상속받을 수 있게된다.
근데 만약 서브클래스 내에서 @RequestMapping을 재정의 할 경우 부모의 정보는 무시된다.

부모 @RequestMapping 상속

부모 클래스에 @RequestMapping이 정의되어있고,
자식에서 @RequestMapping을 재정의 하지 않을 경우 @RequestMapping은 그대로 상속된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping(value="/post")
public class PostController{
@RequestMapping(value="/list")
public String list(){
// ...
}
}

public class ChildPostController extends PostController{
@Override
public String list(){
// ...
}
}

ChildPostController의 list 메서드는 /post/list에 매핑된다.

부모 @RequestMapping + 자식 @RequestMapping

상속으로 조합도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value="/post")
public class PostController{

}

public class ChildPostController extends PostController{
@RequestMapping(value="/list")
public String list(){
// ...
}
}

부모와 자식의 @RequestMapping이 조합되어 /post/list에 매핑된다.

반대로도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PostController{
@RequestMapping(value="/list")
public String list(){
// ...
}
}

@RequestMapping(value="/post")
public class ChildPostController extends PostController{
@Override
public String list(){
// ...
}
}

자식 @RequestMapping 재정의

@RequestMapping을 재정의 할 경우 모든 조건이 다 재정의 된다는 점에 주의하여야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping(value="/post")
public class PostController{
@RequestMapping(value="/list", params="useYn=Y")
public String list(){
// ...
}
}

public class ChildPostController extends PostController{
@Override
@RequestMapping(value="/contents")
public String list(){
// ...
}
}

value만 재정의 한것 처럼 보이나 실제로는 부모 list 메서드의 @RequestMapping 자체를 모두 재정의 한것이 된다.
params 속성은 사라지게 된다.

상속과 제네릭스를 이용한 매핑 전략

이제 앞서 나열한 상속 성질들을 이용하면 공통 컨트롤러를 만들 수 있다.
대상으로는 도메인 오브젝트의 CRUD가 적합하다.
CRUD의 경우 대부분이 서비스를 호출하는 위임코드가 많다는 점을 이용하면 아래와 같이 코드를 대폭 줄일 수 있다.

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
public abstract class CrudController<T, K, S>{
S s;
@RequestMapping(value="/list")
public List<T> list(ModelMap map){
// ...
}

@RequestMapping(value="/detail")
public T detail(K id){
// ...
}

@RequestMapping(value="/add")
public void add(T entity){
// ...
}

@RequestMapping(value="/modify")
public void modify(T entity){
// ...
}

@RequestMapping(value="/remove")
public void remove(K id){
// ...
}
}

@RequestMapping(value="/post")
public class PostController extends CrudController<Post, Integer, PostService>{
@RequestMapping(value="/best") // 공통 외에 필요한것은 따로 정의하면 된다
public List<Post> best(){
// ...
}
}

상속받고 @RequestMapping만 정의해줬을 뿐인데 중복되는 코드를 대폭 줄일 수 있게 되었다!
만약 기본 메서드에 추가할 작업이 있다면 직접 구현해서 새로 작성하면 된다.
그리고 CRUD외에 추가할 메서드가 있으면 best 메서드 처럼 넣어주면 되므로
코드가 매우 간결해지고 개발 생산성이 대폭 상승된다.

From
Read more »

[db] 데이터베이스 무결성, 정합성

Posted on 2018-02-14 | Edited on 2020-11-02 | In db | Comments:

정합성에 어긋난다 == 데이터가 일치하지 않는다(같은 성격?의 데이터를 다루는 테이블들 간에)
무결성이 어긋난다 == 말이 안되는 데이터가 들어있다(말이 안되는 값, 부모가 없는 자식 등등)

무결성 > 정합성

참고 : https://dbaguru.tistory.com/432

Read more »

[spring] spring의 예외처리

Posted on 2018-02-13 | Edited on 2020-11-02 | In spring | Comments:

이번에는, 프로그램을 만들때 중요하지만 대부분의 개발자가 귀찮아 하는 예외처리에 대해 알아보겠습니다.
잘못된 예외처리는 찾기 힘든 버그를 낳을 수 있고, 더욱 난처한 상황을 만들 수 있으므로 항상 신경써줘야 합니다.

잘못된 예외처리

예외 블랙홀1

1
2
3
4
5
try {
// ....
} catch(Exception e){
// 아무 처리가 없음
}

예외발생을 무시하고 정상적인 상황인 것 처럼 다음으로 넘어가겠다는 분명한 의도가 있지 않은 이상 절대 하면 안됩니다.
위는 예외가 발생했는데 그것을 무시하고 진행하겠다는 의미인데, 이렇게 해봐야 최종적으로 프로그램에 오류가 발생할 것입니다.
하지만 그때는 이미 되돌리기엔 늦어버리고, 원인을 찾기도 힘듭니다.

예외 블랙홀2

1
2
3
4
5
6
7
try {
// ....
} catch(Exception e){
System.out.println(e.getMessage());
// 또는
e.printStackTrace();
}

예외 블랙홀1과 별다를게 없습니다. 이는 그냥 로그를 출력한 것이지 예외를 처리한 것이 아닙니다.
예외 블랙홀 처럼 굳이 예외를 잡아서 조치를 취할게 없으면 그냥 잡지를 말아야 합니다.
차리라 throws로 책임을 전가해버리는 것이 낫습니다.

무차별 throws

말했다고 바로 나오네요…

1
2
3
4
5
6
7
8
9
10
11
void method1() throws Exception{
method2();
}

void method2() throws Exception{
method3();
}

void method3() throws Exception{
// ...
}

method3을 개발한 개발자는, 예외를 잡아봤자 처리할 것도 없고 매번 발생가능한 예외를 찾아내서 throws로 선언하기 귀찮아서 그냥 최상위 예외인 Exception을 throws로 던져버리고 있습니다.
method2를 개발한 개발자도 똑같은 행위를 하고 있네요.

일단은, 이 처리 자체가 위의 예외 블랙홀 보다는 낫지만 결국 무책임한 처리이긴 합니다.
그리고 만약 method1을 개발하는 개발자가, 발생하는 예외를 처리하려 했다고 합시다.

그래서 method2를 봤더니… 왠열 걍 throws Exception이네요.
뭐지… 하고 method3까지 들어갔더니 또 throws Exception입니다.
이건 뭐… 왜 예외가 발생하는지 하나하나 다 까봐야겠네요 ㅡㅡ

이처럼 무차별 throws는 정확한 예외상황을 제공해주지 않는 무책임한 예외처리 기법입니다.

예외처리 방법

일단 예외를 처리하는 일반적인 방법은 아래와 같습니다.

예외 복구

예외 상황을 파악하고 문제를 해결하여 정상 상태로 돌려놓는 방법입니다.
말그대로 예외를 catch로 잡아서 적절한 처리를 진행하는 의미죠. 예외 블랙홀은 절대 예외 복구가 아닙니다.

예외 회피

예외를 직접 처리하지 않고 throws를 통해 호출한 쪽으로 던지거나,
catch로 잡아서 로그를 찍고 throw로 다시 던지는 방법입니다. 무차별 throws가 되지 않도록 주의해야 합니다.

예외 전환

예외 포장

예외 회피 처럼 호출한 쪽으로 예외를 던지는 것이긴한데, 그냥 던지는 것이 아니라 좀 더 의미있는 예외로 변환하여 던집니다.

1
2
3
4
5
@Test
public void add(){
dao.add(user1);
dao.add(user1);
}

예를 들어 위와 같은 테스트 코드는 분명 에러를 발생시킬 겁니다.
똑같은 데이터를 2번 넣고 있기 때문에 기본키 제약조건에 걸리거든요.

근데 중요한건, 여기서 발생하는 예외가 SQLException 이라는 겁니다.
SQLException은 SQL 관련해서 오류만 낫다하면 항상 발생하는 매우 모호한 Exception 입니다.

굳이 기본키 제약조건에 걸려 데이터가 중복되는 상황만이 아닌 모든 SQL 관련 오류에 발생하는 Exception 이라는 거죠.
(JDK 1.6부터 조금씩 변화하고 있지만 아직 대부분이 SQLException으로 처리되고 있습니다.)

이래서는 dao의 add를 호출한 쪽에서 예외를 처리하는것도 애매해집니다.
그냥 SQLException이라고 오면 이게 무슨 예외인지 어찌 알겠습니까… 무차별 throws 기억나시죠? 비슷합니다.

그래서 예외전환을 통해 DAO의 add를 아래와 같은 방식으로 전환해주는 겁니다.

1
2
3
4
5
6
7
8
9
10
11
public void add(User user) throws DuplicateUserIdException{
try{
// add 작업
} catch(SQLException e){
if(e.getErrorCode()==중복코드){
throw new DuplicateUserIdException(e);
} else{
throw e;
}
}
}

모호한 SQLException이 아니라 DuplicateUserIdException과 같은 좀 더 확실한 의미의 예외로 전환해서 던져주는 겁니다.
이로써 호출하는 쪽에서 예외처리 또한 수월해지게 됩니다.

getErrorCode
JDBC는 데이터 처리중에 발생하는 다양한 예외를 그냥 SQLException 하나에 담아버립니다.
getErrorCode는 SQLException에 정의된 메서드로써, 예외가 발생된 에러 코드를 반환해줍니다.
해당 코드를 통해 발생한 SQLException에 대해 적절한 처리가 가능해집니다.
하지만 에러코드라는게 DB 벤더마다 각각 다르므로 getErrorCode 메서드를 통해 DB에 독립적인 예외처리는 힘들다는 단점이 있습니다…
위에서 비교한 중복코드 라는 것이 DB 마다 다 제각각 이라는거죠.
SQLException에서 DB에 독립적인 예외처리를 위해 getSQLState라는 메서드를 제공하고는 있지만,
실상 각 DB 제조사들은 이 상태 코드를 제대로 작성해주지 않은 경우가 많습니다. ㅠ.ㅠ

그래서 DuplicateUserIdException을 전달할 때 SQLException을 넣어서 전달해주는 중첩 예외 방식을 사용하고 있습니다.
중첩예외로 발생시켜줄 경우, 호출하는 쪽에서 Throwable 클래스의 getCause() 메서드로 원인 예외를 확인할 수 있습니다.

RuntimeException 전환

위와 같은 포장(wrap)형 예외 전환 말고, 형태(?)를 바꿔주는 예외 전환 방법이 있습니다.
또 다시 SQLException으로 예를 들어보겠습니다.
SQLException은 체크 예외입니다. 체크 예외는, 해당 예외 발생 가능성이 있는 곳에서 예외의 처리를 문법적으로 강제합니다.

그런데 중요한점은, SQLException의 경우 대부분이 복구 불가능한 예외라는 점입니다.
즉 대부분이 예외를 잡아도 처리해줄수 있는게 없다는 뜻인데… 이럴경우 그냥 throws로 던져버려야 할까요?

호출하는 쪽에서라도 예외를 받아 처리하면 다행이겠지만, SQLException의 경우 호출하는 쪽에서도 딱히 처리할게 없습니다.
결국 무차별 throws와 같은 현상이 발생하게 됩니다… 이는 매우 무의미합니다.
아무리 예외 처리를 넘기고 넘겨봤자 처리가 불가능하기 때문입니다.
어쩌피 이렇게 처리가 안될 예외이면 가능한 빨리 이를 RuntimeException으로 포장하여, 호출하는 쪽에서 무차별 throws를 선언하지 않도록 해주는 것이 좋습니다.

1
2
3
4
5
6
7
8
9
10
try {
// DAO 코드
} catch (SQLException e) {
if(e.getErrorCode() 등을 이용해서 처리할 수 있는 경우){
// 처리 코드
} else{
// 로깅, 에러 내용 메일 전송 등의 로직
throw new RuntimeException(e);
}
}

위와 같이 처리 가능한 SQLExceptoin의 경우 처리해주고,
나머지 예외의 경우 RuntimeException으로 포장하여 호출 메서드들 쪽에서 신경쓰지 않도록 해주는 것이 좋습니다.
대신, 발생시에 로깅이나 메일 전송같은 로직을 발생시켜서 추후에 개발자가 그것을 보고 처리할 수 있도록 하는게 좋습니다.
무작정 throws를 선언하는 것은 올바른 예외처리 방법이 아닙니다.

애플리케이션 예외

이는 위의 RuntimeException 예외 전환과는 반대되는 방식입니다.
시스템 상의 예외가 아닌, 비즈니스 로직에서 발생하는 예외에 대해서는 사용자가 직접 예외를 정의해주기도 합니다.
예를 들자면 잔고가 0인 통장에서 출금을 하려는 경우 등이 있습니다.
적절한 리턴값으로 로직을 구성할 수도 있지만, 리턴값이라는게 표준도 없고 조건문이 많아져서 로직을 파악하기가 더 힘들어집니다.

이럴 경우, 비지니스적인 의미를 가진 예외를 정의하여 발생시켜 주는것이 상대적으로 코드가 더 깔끔해집니다.
(예외를 사용하는 경우 catch 블록에 모아 둘수 있기 때문. 리턴값 사용시 로직 안에서 체크하므로 보기 복잡합니다.)
그리고 이런 예외들은 체크 예외로 만들어서 예외 처리를 강제하도록 합니다.
이런 예외를 보통 애플리케이션 예외 라고 합니다.

여러가지 예외처리 방법에 대해 알아보았지만, 정답은 없습니다. 상황에 맞춰 사용해야 하죠.
잘못된 예외처리는 항상 피하고, 예외처리 방법의 특징을 항상 생각하며 프로그램을 작성해야겠네요…(다짐ㅋㅋ)

스프링 예외처리

스프링을 이용하여 DAO 코드를 작성하면, ex) JdbcTemplate
예외 발생 시 스프링에서 제공하는 예외를 던져줍니다.
스프링은 데이터 관련 작업에서 발생하는 수많은 예외들의 대부분을 추상화하고, 재정의하여 제공하고 있습니다.

일단 먼저 알고 가셔야할 것은, 스프링에서 제공하는 데이터 작업 예외들은 모두 RuntimeException 예외들입니다.
서버상에서 발생하는 SQL 관련 예외들은 대부분이 복구 불가능한 예외이기 때문에, 문법적인 불편함을 제거하기 위해 모두 RuntimeException으로 정의한 것입니다.

이제 추상화를 좀 더 상세하게 살펴보겠습니다.

  1. 스프링에서는 DB에 독립적인 예외처리가 가능하도록 예외들이 추상화 되어있습니다.
    JDBC로 DAO 작성했을 경우를 생각해보시면, 언제나 일관된 예외인 SQLException이 발생했었습니다.
    이럴 경우 예외만 가지고는 어떤 예외인지 판별이 안되므로, getErrorCode() 메서드를 호출하여 예외처리를 진행해야 했습니다.
    하지만 여기서 문제점은 getErrorCode 메서드 같은 경우, DB 제조사별로 제각각의 에러코드를 반환한다는 점 때문에 DB에 독립적인 프로그래밍이 거의 불가능했습니다.
    예외처리부분에서 각 DB 제조사별 에러 코드를 확인하고, 처리해야 했으니까요…

그러나 스프링의 경우 각 DB별로 예외클래스와 에러코드 매핑 파일을 두고, 일관된 예외 클래스로 반환하게 해줍니다.
스프링 라이브러리 파일 중에 jdbc 관련 jar 파일을 풀어서 보시면 안에 sql-error-codes.xml 이라는 파일이 보이실겁니다.

sql-error-codes1

해당 파일을 열어보시면 위와 같이 정의가 되어있습니다.

sql-error-codes2

MySQL도 있고…

sql-error-codes3

Oracle도 있습니다.

이런식으로 약 10여개 정도 DB의 에러 코드에 대해 모두 매핑이 되어있습니다.
그리고 보다시피, 각 에러코드에 대해 일관된 프로퍼티명으로 정의해뒀음을 보실 수 있습니다.
즉, 스프링을 사용하여 DAO를 작성할 경우, DB가 바뀌더라도 일관된 예외로 받을 수 있게 된다는 의미입니다.
이로 인해 DB에 독립적인 프로그래밍이 가능하게 됩니다.
스프링에서 제공하는 JdbcTemplate를 사용해보시면 DB를 바꾸더라도 SQL 에러 발생시 일관된 예외를 던져주는 것을 보실 수 있을 겁니다.

  1. 스프링에서는 DAO 구현 기술에 관해서도 독립적인 예외처리가 가능하도록 추상화 되어있습니다.
    보통 DAO 작성시, 인터페이스를 통한 전략패턴 형태로 작성합니다.
    DAO 작성 기술이 JDBC만 있는 것이 아니기 때문이죠. JDBC, Hibernate, JDO, JPA 등등 굉장히 많습니다.
    그리고 여기서 또 문제가 되는것은, 각각 기술에 따라 던져지는 예외 또한 다르다는 것입니다.
    JDBC는 SQLException, Hibernate는 HibernamteException, JPA는 PersistenceException… 이런식이죠.
    하지만 스프링은 이러한 것조차도 계층적으로 추상화 해두었다는 겁니다… ㄷㄷ

중복키 예외가 발생했을 떄를 예로 들어보면,

JDBC의 경우 DuplicateKeyException, 하이버네이트의 경우 ConstraintViolcationException을 발생시킵니다.

그리고 이 예외들은 공통된 상위 예외로 DataIntegrityViolationException를 가집니다.

즉, 이런식으로 대부분의 모든 예외가 추상화 되어 있기 떄문에, 스프링을 사용하면 DAO의 구현기술, DB의 종류에 독립적인 프로그래밍이 가능해집니다.

하지만… DAO의 구현기술에 관한 추상화는 DB 종류에 관한 추상화보다는 이용가치가 조금 떨어지는 것이 현실입니다.

DataIntegrityViolationException 예외의 경우 중복키 예외외에 다른 제약조건 위반상황에서도 발생하기 때문이죠 ㅠ.ㅠ

스프링의 추상화는 정말 대단하지만… 근본적인 한계 때문에 완벽하다고 기대할순 없습니다.

그래도 스프링은 참 대단하네요. 이정도만 해도 ㄷㄷ

간단히 DuplicateKeyException 계층을 보면 위와 같죠.

빨간색으로 표시한 DataAccessException이 스프링에서 제공하는 예외의 최상위 클래스입니다.

RuntimeException의 하위클래스인것도 볼 수 있네요.

여기까지 예외처리 방법과 스프링의 예외 추상화에 대해 알아보았습니다…

예외라는게 중요하지만 신경 잘 못쓰게되는 부분인 것 같습니다.

책을 통해 공부하고, 포스팅을 함으로써 다시 한번 중요성을 꺠닫는것 같네요…

다른 분들에게도 도움이 되길 바라겠습니다.

감사합니다.

Read more »

[db] mysql sample data

Posted on 2018-02-10 | Edited on 2020-11-02 | In db | Comments:

https://github.com/datacharmer/test_db

mysql < employees.sql
이거 외엔 딱히 할 필요 없어보이긴 함

image

[spring] WebApplicationInitializer

Posted on 2018-02-06 | Edited on 2020-11-02 | In spring | Comments:

서블릿은 3.0 이후부터 web.xml 없이도 서블릿 컨텍스트 초기화 작업이 가능해졌다.
프레임워크 레벨에서 직접 초기화할 수 있게 도와주는 ServletContainerInitializer API를 제공하기 때문이다.

서블릿 컨텍스트 초기화
web.xml에서 했던 서블릿 등록/매핑, 리스너 등록, 필터 등록 같은 작업들을 말한다.

스프링은 웹 모듈내에 이미 ServletContainerInitializer를 구현한 클래스가 포함되어 있고,
이는 WebApplicationInitializer 인터페이스를 구현한 클래스를 찾아 초기화 작업을 위임하는 역할을 수행한다.

1
2
3
public interface WebApplicationInitializer{
void onStartup(ServletContext servletContext) throws ServletException;
}

이 인터페이스를 구현한 클래스를 만들어두면 웹 어플리케이션이 시작할 때 자동으로 onStartup() 메서드가 실행된다.
여기서 초기화 작업을 수행하면 된다.

루트 웹 애플리케이션 컨텍스트 등록(root-context.xml)

기존에는 아래와 같이 web.xml에 리스너 형태로 등록했다.

1
2
3
4
<listener>
<!-- /WEB-INF/applicationContext.xml 생성-->
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

리스너의 역할
리스너는 서블릿 컨텍스트가 생성하는 이벤트를 전달받는 역할을 한다.
서블릿 컨텍스트가 만드는 이벤트는 컨텍스트 초기화 이벤트와 종료 이벤트이다.
즉 웹 어플리케이션이 시작되고 종료되는 시점에 이벤트가 발생하고, 리스너를 등록해두면 이를 받을 수 있는 것이다.

스프링은 왜 루트 웹 애플리케이션 컨텍스트를 서블릿 리스너 레벨에서 등록하게 했을까?
이는 위의 리스너의 역할에 보이듯이 루트 애플리케이션 컨텍스트의 생명주기가 서블릿 컨테이너와 일치하기 때문이다.

아래와 같이 리스너를 등록해준다.

1
2
ServletContextListener listener = new ContextLoaderListener();
servletContext.addListener(listener);

WebApplicationInitializer의 onStartup()은 서블릿 컨텍스트 초기화 시점에 실행된다고 했으니 리스너를 등록하지 않아도 된다고 생각할 수 있으나,
초기화 시점만 잡을 수 있지 종료 시점을 잡을수 없으므로 위와 같이 리스너는 등록해줘야 한다.
종료 시점을 잡아주지 않으면 애플리케이션이 종료되어도 리소스가 반환되지 못하므로 메모리 누수가 일어날 수 있다.

디폴트 루트 웹 애플리케이션 컨텍스트 클래스(XmlWebApplicationContext)나 디폴트 XML 설정파일 위치(/WEB-INF/applicationContext.xml)를 바꾸고 싶을때 web.xml에선 아래와 같이 했었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 애플리케이션 컨텍스트 클래스 변경 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationWebApplicationContext
</param-value>
</context-param>

<!-- 설정파일 위치 변경 -->
<context-param>
<param-name>contextConfigLoaction</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>

이는 서블릿 오브젝트의 setInitParameter() 메서드를 통해 지정할 수 있다.

1
2
3
4
// 애플리케이션 컨텍스트 클래스 변경 
servletContext.setInitParameter("contextClass", "org.springframework.web.context.support.AnnotationWebApplicationContext");
// 설정파일 위치 변경
servletContext.setInitParameter("contextConfigLoaction", "/WEB-INF/root-context.xml");

만약 Java Config를 사용한다면 더 깔끔하게 사용할 수 있다.

1
2
3
4
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(RootConfig.class); // RootConfig == root-context java config

servletContext.addListener(new ContextLoaderListener(rootContext));

register() 대신 scan() 메서드를 사용하여 패키지를 통째로 스캔할수도 있다.


서블릿 웹 애플리케이션 컨텍스트 등록(servlet-context.xml)

서블릿 웹 애플리케이션 컨텍스트는 서블릿 안에서 초기화되고 서블릿이 종료될 떄 같이 종료된다.
이때 사용되는 서블릿이 DispatcherServlet이다.
기존의 DispatcherServlet 등록은 아래와 같이 작성했었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<sevlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

이를 자바코드로 바꾸면

1
2
3
4
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("appServlet", new DispatcherServlet());
dispatcher.setInitParameter("contextConfigLocation", "/WEB-INF/spring/appServlet/servlet-context.xml");
dispatcher.setLoadOnStartUp(1);
dispatcher.addMapping("/");

이 또한 루트 애플리케이션 컨텍스트 처럼 Java Config를 사용할 수 있다.

1
2
3
4
5
6
AnnotationConfigWebApplicationContext sac = new AnnotationConfigWebApplicationContext();
sac.register(WebConfig.class);

ServletRegistration.Dynamic dispatcher = servletContext.addServlet("appServlet", new DispatcherServlet(sac));
dispatcher.setLoadOnStartUp(1);
dispatcher.addMapping("/");

여기까지가 루트 컨텍스트와 서블릿 컨텍스트를 자바 코드로 분리한 과정이다.
필터나 리스너들도 이런식으로 모두 등록할 수 있고, 그 이후엔 web.xml을 아예 제거할 수도 있다.

그 전까진 WebApplicationInitializer와 web.xml을 같이 사용할 수 있다.
대신 아래와 같이 web.xml에 version을 명시해줘야 한다.

1
2
3
4
5
<web-app version="3.0">
<!--
....
-->
</web-app>
From
Read more »

[spring] HandlerExceptionResolver, LocaleResolve, MulitpartResolver

Posted on 2018-02-04 | Edited on 2020-11-02 | In spring | Comments:

HandlerMapping, HandlerAdapter, ViewResolver 등 외에도 DispatcherServlet에는 다양한 확장 가능한 전략들이 존재한다.
아래는 남은 전략들 중 중요한 것들만을 나열한 것이다.

핸들러 예외 리졸버

HandlerExceptionResolver는 컨트롤러 작업 중 발생한 예외를 어떻게 처리할 지 결정하는 전략이다.
기본적으로 컨트롤러나 그 뒷 계층에서 발생한 예외는 일단 DispatcherServlet이 전달받은 다음 다시 서블릿으로 던져서 서블릿 컨테이너가 처리하게 된다.
별다른 처리를 하지 않았다면 500 Internal Server Error 같은 메시지가 출력될 것이다.
그런데 핸들러 예외 리졸버가 등록되어있다면 DispatcherServlet은 먼저 이 리졸버가 해당 예외를 처리할 수 있는지 확인한다.
만약 해당 예외를 처리할 수 있으면 예외는 DispatcherServlet 밖으로 던져지지 않고 해당 핸들러 예외 리졸버가 처리한다.

핸들러 예외 리졸버는 HandlerExceptionResolver 인터페이스를 구현해서 생성한다.

1
2
3
public interface HandlerExceptionResolver{
ModelAndView resolveException(HttpServletRequest reqeust, HttpServletResponse response, Object handler, Exception ex);
}

구현 메서드인 resolveException의 리턴 타입은 ModelAndView이다.
예외에 따라 사용할 뷰와 모델을 돌려주도록 되어있다.
만약 처리 불가능한 예외라면 null을 리턴한다.

스프링은 이미 4개의 HandlerExceptionResolver 구현 전략을 제공한다.

AnnotationMethodHandlerExceptionResolver

예외가 발생한 컨트롤러 내의 메서드 중에서 @ExceptionHandler 애노테이션이 붙은 메서드를 찾아 예외처리를 맡긴다.

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class HelloController{
@RequestMapping(value="/hello")
public void hello(){
// DataAccessException occured!
}

@ExceptionHandler(DataAccessException.class)
public ModelAndView dataAccessExceptionHandler(DataAccessException ex){
return new ModelAndView("error").addObject("msg", ex.getMessage());
}
}

특정 컨트롤러의 예외만을 처리하고 싶을때 유용하다.

ResponseStatusExceptionResolver

예외를 특정 HTTP 응답 상태코드로 전환해준다.
예외클래스에 @ResponseStatus 애노테이션을 붙이고 value(응답 상태 값)를 지정한 뒤 해당 예외를 발생시키면 ResponseStatusExceptionResolver가 HTTP 응답을 변환해준다.
단순한 HTTP 500 에러 대신 의미있는 응답 상태를 클라이언트에 전달하고자 할 떄 유용하게 사용된다.

1
2
3
4
@ResponseStatus(value=HttpStatus.SERVICE_UNVAILABLE, reason="이유도 지정 가능")
public class ServiceException extends RuntimeException {

}

위와 같이 @ResponseStatus가 붙은 Exception을 정의하고,

1
2
3
4
public void helloService(){
// ....
throw new ServiceException(); // exception 발생
}

이렇게 해당 Exception을 발생시키면 @ResponseStatus에 지정된 형태로 Response를 받을 수 있다.

만약 위처럼 정의할 수 없는 기존 예외가 발생했을 때는 위의 @ExceptionHandler를 사용하면 된다.

1
2
3
4
5
@ResposneStatus(value=HttpStatus.SERVICE_UNVAILABLE)
@ExceptionHandler(DataAccessException.class)
public ModelAndView dataAccessExceptionHandler(DataAccessException ex){
return new ModelAndView("error").addObject("msg", ex.getMessage());
}

DefaultHandlerExceptionResolver

위 두 가지 예외리졸버에서 처리하지 못한 예외를 다루는 예외 리졸버이다.
스프링에서 발생하는 주요 예외를 처리하는 표준 예외처리 로직을 담고 있다.
즉, 이 예외 리졸버는 신경쓰지 않아도 된다.

SimpleMappingExceptionResolver

web.xml의 <error-page>와 비슷하게 예외를 처리할 뷰를 지정할 수 있게 해준다.
(뷰를 찾을때는 뷰리졸버가 사용된다.)

1
2
3
4
5
6
7
8
9
10
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="mappedHandlers">
<props>
<!-- 클래스 이름에 패키지를 쓰지 않아도 됨 -->
<prop key="DataAccessException">error/dao</prop>
<prop key="BusinessLoginException">error/business</prop>
</props>
</property>
<property name="defaultErrorView">error/default</property>
</bean>

사실상 실제로 활용하기에 가장 편리한 예외 리졸버이다.
사용자에게 어려운 HTTP 상태코드를 보여주는 것보다는 문구가 있는 예외 페이지를 보여주는 편이 낫기 때문이다.
또한 모든 컨트롤러에서 발생하는 예외에 일괄 적용할 수 있다.

예외페이지를 보여주는 외에 로그를 남기거나 관리자에게 통보를 해야할 경우 SimpleMappingExceptionResolver 보다는 핸들러 인터셉터의 afterCompletion() 메서드를 사용하는 것이 좋다.
에러페이지에 에러로그를 남기는 것은 바람직하지 않기 때문이다.


지역정보 리졸버

LocaleResolver는 애플리케이션에서 사용하는 지역정보를 결정하는 전략이다.
디폴트인 AcceptHeaderLocaleResolver는 HTTP 헤더의 지역정보를 그대로 사용한다.
보통 HTTP 헤더의 지역정보는 브라우저의 기본 설정에 따라 보내진다.
브라우저 설정을 따르지 않고 사용자가 직접 변경하게 하려면 SessionLocaleResolver나 CookieLocaleResolver를 사용하는 것이 편리하다.
해당 리졸버를 사용하면 사용자가 국가 선택 시 쿠키나 세션의 locale 값을 변경하여 해당 지역의 리소스 파일이 사용되게 할 수 있다.
다국어 서비스에 유용하게 활용할 수 있다.
LocaleResolver 사용 예(http://yookeun.github.io/java/2015/08/12/spring-i18n/)


멀티파트 리졸버

멀티파트 포맷의 요청정보를 처리하는 전략이다.
멀티파트 리졸버 전략은 디폴트 전략이 없으므로 아래와 같이 빈을 등록해줘야 한다.

1
2
3
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000" />
</bean>

DispatcherServlet은 클라이언트로부터 멀티파트 요청을 받으면 멀티파트 리졸버에게 요청해서 HttpServletRequest의 확장 타입인 MultipartHttpServletRequest로 변환한다.
MultipartHttpServletRequest에는 멀티파트를 디코딩한 내용과 이를 참조하거나 조작할 수 있는 기능이 추가되어 있다.
이후 컨트롤러에서는 아래와 같이 MultipartHttpServletRequest로 캐스팅하여 사용할 수 있다.

1
2
3
4
5
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response){
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile multipartFile = multipartRequest.getFile("image");
// ....
}

RequestViewNameTranslator

컨트롤러에서 뷰 이름이나 뷰 오브젝트를 돌려주지 않을 경우 요청 URL 정보를 기준으로 해서 뷰 이름을 생성해준다.
디폴트로 DefaultRequestViewNameTranslator가 등록되어 있다.
잘 활용하면 매번 뷰 이름을 지정하는 수고를 덜어줄 수 있다.

From
Read more »

[spring] View, ViewResolver

Posted on 2018-02-04 | Edited on 2020-11-02 | In spring | Comments:

뷰는 모델이 가진 정보를 어떻게 표현해야 하는지에 대한 로직을 갖고 있는 컴포넌트이다.
일반적인 뷰의 결과물은 브라우저에서 볼 수 있는 HTML이다.
이 외에도 엑셀, PDF, RSS 등 다양한 결과를 생성할 수 있는 뷰 오브젝트들이 있다.

뷰

뷰는 View 인터페이스를 구현해서 생성한다.

1
2
3
public interface View{
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse resposne) throws Exception;
}

스프링에서 제공하는 뷰 목록은 아래와 같다.

InternalResourceView

RequestDispatcher의 forward(), include()를 이용하는 뷰다.

1
2
3
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/view/hello.jsp");
request.setAttribute("message", "This is message");
dispatcher.forward(request, response);

위는 RequestDispatcher를 사용했던 때의 방식이다.
InternalResourceView의 동작 방식도 이와 동일하다고 보면 된다.
아래는 사용 예제의 일부분이다.

1
2
3
View view = new InternalResourceView("/WEB-INF/view/hello.jsp");

return new ModelAndView(view, model);

JstlView
InternalResourceView의 서브 클래스이다.
JSP를 뷰 템플릿으로 사용할 때 JstlView를 사용하면 여러가지 추가 기능을 더 활용할 수 있다.(지역화 메세지 등)

RedirectView

HttpServletResponse의 sendRedirect()를 호출해주는 기능을 가진 뷰다.
실제 뷰를 생성하진 않고 URL만 만들어 다른 페이지로 리다이렉트 해준다.
모델정보가 있다면 URL 뒤에 파라미터로 추가된다.

1
2
3
4
// 뷰 사용시
return new ModelAndView(new RedirectView("/main"));
// 뷰 리졸버 사용시
return new ModelAndView("redirect:/main");

보다시피 redirect: 접두어를 사용하여 뷰 리졸버가 인식하게 할 수 있다.
redirect 경로는 절대경로(컨텍스트 패스, 서블릿 패스 포함)이어야 하는데,
contextRelative 옵션을 true로 주면 컨텍스트 패스는 생략하고 작성할 수 있다.

VelocityView, FreeMarkerView

JSP 대신 Velocity, FreeMarker 템플릿 엔진을 뷰로 사용할 수 있게 해준다.
둘은 JSP보다 훨씬 문법이 강력하고 속도도 빠른 장점이 있다.
개인적으로 둘은 써보지 않았고 Thymeleaf라는 템플릿 엔진을 써봤었는데,
이것도 추천합니다…

MarshallingView, MappingJacksonJsonView

모델의 정보를 xml, json으로 변환시킬 수 있는 뷰다.
MashallingView를 사용할 경우 xml, MappingJacksonJsonView를 사용할 경우 json으로 변환된다.
메세지 컨버터를 이용해서도 동일한 작업을 할 수 있다.

AbstractExcelView, AbstractJExcelView, AbstractPdfView

엑셀과 PDF문서를 만들어주는 뷰다.
Abstract가 붙어있으니 상속을 해서 구현해줘야 한다. 구현하는 부분은 문서를 생성하는 부분이다.
구현한 뷰 클래스는 빈으로 등록하여 컨트롤러에 DI해줘도 되고, 직접 생성해도 된다.

뷰 오브젝트는 멀티스레드 환경에서 공유해도 안전하다.
즉 싱글톤 빈으로 생성해도 문제되지 않는다.

AbstractAtomFeedView, AbstractRssFeedView

application/atom+xml과 application/rss+xml 타입의 피드 문서를 생성해주는 뷰다.
컨트롤러에서 사용하는 방법은 위의 AbstractExcelView 등과 동일하다.

뷰 리졸버

뷰를 선택하는 것도 컨트롤러의 역할이다.
하지만 위처럼 컨트롤러에서 매번 뷰를 생성하는 것은 비효율적이므로, 스프링에서는 이 작업을 적절히 분리하였다.
컨트롤러는 뷰의 논리적인 이름만을 리턴한 뒤 역할을 종료하고, 이를 DispatcherServlet의 뷰 리졸버가 받아 사용할 뷰 오브젝트를 찾고 생성하는 작업을 진행해준다.
게다가 뷰 리졸버는 보통 뷰 오브젝트를 캐싱하므로 같은 URL의 뷰가 반복적으로 만들어지지 않는 장점도 있다.
뷰 리졸버도 하나 이상 등록해서 사용할 수 있는데, 이때는 핸들러 매핑처럼 order 프로퍼티를 이용해 적용 순서를 적용해주는 것이 좋다.
뷰 리졸버는 ViewResolver 인터페이스를 구현해서 생성한다.

1
2
3
public interface ViewResolver{
View resolveViewName(String viewName, Locale locale) throws Exception;
}

InternalResourceViewResolver

주로 JSP를 사용할 때 쓰이는 뷰 리졸버이다.
뷰를 생성할 필요없이 논리적인 이름만을 리턴해주면 되는데, 그대로 사용할 경우 풀 패스를 써줘야 하므로 그대로 사용하는 것은 피해야 한다.
prefix, suffix 프로퍼티를 이용하면 앞뒤에 붙는 내용을 생략할 수 있다.

1
2
3
4
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view" />
<property name="suffix" value=".jsp" />
</bean>

컨트롤러에서는 hello 만을 리턴해주면 된다. 이는 나중에 변경에도 용이하다.

JSTL 라이브러리가 클래스패스에 존재할 경우 JstlView를 사용하고, 존재하지 않으면 InternalResourceView를 사용한다.

VelocityViewResolver, FreeMarkerViewResolver

Velocity와 FreeMarker를 사용하게 해주는 뷰 리졸버이다.
InternalResourceViewResolver와 같이 prefix, suffix를 사용가능하다.

ResourceBundleViewResolver

컨트롤러가 아닌 외부에서 뷰를 결정할 때, 한가지 뷰 만이 아니라 컨트롤러마다 뷰가 달라질 수 있을 때 사용하면 괜찮은 방식이다.
ResourceBundleViewResolver를 사용하면 클래스패스의 views.properties 파일에 논리적 이름과 뷰 정보를 정의하여 작성하고, 이를 사용하여 뷰를 선택하게 할 수 있다.
아래는 views.properties 파일 예시이다.

1
2
3
4
5
hello.(class)=org.springframework.web.servlet.view.JstlView
hello.url=/WEB-INF/view/hello.jsp

bye.(class)=org.springframework.web.servlet.view.velocity.VelocityView
bye.url=bye.vm

독립된 파일을 통해 뷰를 자유롭게 매핑할 수 있지만, 모든 뷰를 일일히 매핑해줘야 하는 불편도 뒤따른다.
그래서 단독으로 사용하는 것은 추천되지 않고, 다른 뷰 리졸버와 함께하면 유용하게 사용될 수 있다.

1
2
3
4
5
6
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="order" value="0" />
</bean>

<!-- order 기본값이 Integer.MAX 이므로 굳이 order 프로퍼티를 안줘도 됨 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" />

이렇게 사용하면 view.properties에 뷰가 없을 경우 아래 InternalResourceViewResolver를 사용하게 된다.
이로써 특별한 타입의 뷰가 필요할 때만 view.properties에 작성해주며 사용할 수 있다.

XmlViewResolver

ResourceBundleViewResolver와 용도는 동일하고, views.properties 대신 /WEB-INF/views.xml을 사용한다.
추가로 이 파일은 서블릿 컨텍스트를 부모로 가지므로 DI가 가능하다는 장점이 있다.

BeanNameViewResolver

뷰 이름과 동일한 이름을 가진 빈을 찾아서 뷰로 이용하게 해준다.

From
Read more »

Session 동작방식

Posted on 2018-02-03 | Edited on 2020-11-02 | In http | Comments:

개념

http 프로토콜은 매 접속마다 새로운 요청이 이뤄지는, 상태를 저장하지 않는 stateless 형식의 프로토콜이다.
그래서 서버쪽에서 클라이언트의 상태를 기억하기 사용하는 것이 session이다.
서버는 클라이언트의 정보를 내부에 저장하고, 이 저장된 정보에 접근하기 위해 세션 ID(JSESSIONID)를 이용한다.
여기에는 여러가지 정보(사용자 정보 등)를 담을 수 있고, 이 정보는 사용자가 일치하는 세션 ID를 가지고 들어오지 않는 한, 설정한 session timeout 만큼 유지된다.
쿠키와 달리 서버쪽에 저장되는 정보이므로 보안에 좀 더 안전하다.

FLOW

아래는 session의 요청 방식을 잘 보여주는 그림이다.
image

  1. 클라이언트가 처음 서버로 http요청을 시도한다.
  2. 서버쪽에서는 전달받은 세션 ID(JSESSIONID)가 없으므로 생성한다.
  3. 생성한 세션 ID를 쿠키에 넣어 클라이언트에게 전달된다. 여기서 만약 클라이언트가 쿠키를 사용하지 않을 경우에는 URL에 붙여서 전달한다. ex) http://joont92.github.io;JSESSIONID=~~

서버입장에서는 JSESSIONID를 전달받는 방식(request header에 넣어서 보내는지, URL에 붙여서 보내는지)에 따라 사용자가 쿠키를 사용하느냐 안하느냐의 여부를 알 수 있다.
첫 요청에는 JSESSIONID를 전달받지 않으므로 두 가지 방식으로 모두 보내본다.

  1. 클라이언트쪽에서는 받은 세션 ID를 쿠키에 저장한다.
    쿠키는 session 종료 시 같이 소멸되는 Memory Cookie로 저장한다.
  2. 같은 페이지에 2번째 요청할 때, 클라이언트는 세션 ID를 가지고 있으므로 이를 request header에 넣어 전달한다.
  3. 서버는 전달받은 세션 ID가 있으므로 새로 생성하지 않고 해당 세션 ID에 맞는 정보를 전달해준다.

이러한 FLOW를 통해 클라이언트의 상태를 유지할 수 있게 되는것이다.

Read more »
1…151617…19

JunYoung Park

182 posts
18 categories
344 tags
RSS
© 2020 JunYoung Park
Powered by Hexo v3.6.0
|
Theme – NexT.Muse v7.1.0