📂 목차
📚 본문
Cookie
웹 브라우저와 서버 사이에서 정보를 작고 단순한 문자열 형태로 저장하고 주고 받는 방법이며, 서버가 브라우저에 데이터를 잠깐 기억해달라고 요청하면 브라우저는 이를 로컬에 저장하고 같은 도메인으로 요청할 때마다 서버에 다시 전달하게 된다.
용도
- 로그인 상태 유지(Session ID 저장)
- 사용자 선호 설정 저장(언어, 테마)
- 트래킹 및 분석(방문 기록, 장바구니 정보)
서버측에서는 응답으로 보낼때 쿠키를 저장해달라고 요청할 수 있고, 그때 HTTP 에는 Set-Cookie
헤더가 포함되게 된다. 이후에 브라우저가 요청을 서버에 한다면 자동으로 이 쿠키들이 전송되게 된다.
서버 -> 브라우저 Set-Cookie 헤더
Set-Cookie: userId=park123; Path=/; Max-Age=3600; HttpOnly; Secure
브라우저 -> 서버 Cookie
Cookie: userId=park123
HTTP 상태를 보존하는 가장 낮은 레벨의 메커니즘이 바로 Cookie 이다.
Cookie 의 구성요소
Name
: 이름Value
: 값Domain
: 쿠키가 적용될 도메인Path
: 쿠키가 적용될 경로Max-Age
: 쿠키 유효기간(-1 이면 창을 껏다 키면 사라지는 옵션)Secure
: HTTPS 에서만 전송HttpOnly
: JS 에서 접근 불가, XSS 방어용SameSite
: CSRF 공격 방지(Strict
,Lax
,None
)
Spring 에서 Cookie 다루기
@CookieValue 어노테이션을 통해 읽어오기
@GetMapping("/viewCookie")
public String viewCookie(@CookieValue(value = "userId", required = false) String userId,
Model model) {
model.addAttribute("userId", userId);
return "cookie_view";
}
브라우저에 쿠키가 저장되어 있고
userId
라는 이름의 쿠키가 있다면 들고옴 없으면null
쿠키 생성 및 저장
@PostMapping("/addCookie")
public String addCookie(CookieRequest cookieRequest, /* @ModelAttribute 생략 가능 */
HttpServletResponse response) {
Cookie cookie = new Cookie(cookieRequest.cookieName(), cookieRequest.cookieValue());
cookie.setPath("/");
cookie.setMaxAge(-1);
response.addCookie(cookie);
return "redirect:/viewCookie";
}
@ModelAttribute
는 하나의 object 만이 요청으로 오게 되면 생략이 가능하다.
쿠키 삭제
@GetMapping("/delCookie")
public String deleteCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("userId", null);
cookie.setMaxAge(0); // 만료
cookie.setPath("/");
response.addCookie(cookie);
return "cookie_deleted";
}
브라우저에서 쿠키를 제거하려면 같은 이름, 같은
Path
,MaxAge=0
으로 다시 보내야 함
setHttpOnly
자바스크립트에서 쿠키에 접근하는 것을 차단한다. 즉, document.cookie
로 접근할 수 없게 된다.
XSS
공격 방지용SessionID
가 드러나지 않도록
setSecure
HTTPS 연결에서만 쿠키를 전송하도록 제한을하며, HTTP 요청에서는 이 쿠키가 전송되지 않는다.
- 네트워크 상에서 쿠키가 노출되는 것을 방지하고
- HTTP 의 암호화 부재로, 누군가 트래픽 감청 시 세션ID 가 노출되면 안된다
setSameSite
CSRF
공격 방지를 위해 쿠키가 어떤 요청 상황에서 전송될 수 있는지를 제한하는 옵션이다. 기본값은 Lax
이다.
CSRF
는 요청을 보낸 페이지의 도메인 과 요청을 받는 서버의 도메인이 다를 수 있는데 이럴때 크로스 사이트 요청이라고 한다. 기본적으로 다른 사이트의 요청은 쿠키를 전송하지 않는다.
Session
웹에서 HTTP
는 stateless
프로토콜이다. 즉, 요청 -> 응답 사이에 서버는 사용자를 기억하지 못하는데, 대부분의 웹 어플리케이션은 사용자 상태 유지가 필요하다.
여기서 등장하는 것이 session
이며, 세션은 서버가 클라이언트 별로 유지하는 일시적 데이터 저장 공간이라고 볼 수 있다.
Session 동작 원리
- 사용자가 브라우저로 요청을 보냄
- 서버는 세션 객체를 생성하고, 고유한 Session ID 를 발급 받음
- 세션 ID 는 주로 쿠키(JSESSIONID) 를 통해 클라이언트에 전달됨
- 클라이언트는 이후 요청마다 세션 ID 를 서버에 보내고, 서버는 이를 사용하여 세션 저장소를 조회하게 된다.
Session 생성
Spring MVC 에서 Session 은 HttpSession
인터페이스를 통해 제공되지만, 실제로는 Servlet
컨테이너(Tomcat, Jetty 등)에서 관리한다. 즉, Spring 이 직접 세션 객체를 만드는 것이 아니라 요청 시 Servlet
컨테이너가 세션을 정하고 Spring 은 그걸 활용한다.
따라서 DispatcherServlet
에서 받아진 HttpServletRequest
안에 Session
이 들어있게 된다.
request.getSession()
호출- 요청 쿠키에
JSESSIONID
를 확인 - 없으면 새로운 세션 객체 새성
- 고유한 세션 ID 를 생성(
UUID
또는 랜덤 문자열) - 서버 메모리(기본은
HashMap
등)에 세션 저장 - 클라이언트에게
Set-Cookie: JSESSIONID=랜덤값
전송
- 요청 쿠키에
- 이후 요청에서 세션 사용
- 브라우저가
JSESSIONID
쿠키를 보내면 컨테이너가 세션 객체를 조회해서 반환 - Spring
Controller
에서 바로session.getAttribute("user")
등으로 접근 가능
- 브라우저가
Session 구조
HttpSession
: 인터페이스StandardSession
(Tomcat 구현)id
: 세션 식별자creationTime
: 세션 생성 시간lastAccessedTime
: 마지막 접근 시간attributes
: 세션에 저장된 key-valueMap
Servlet 이 생성함을 알고 있자
Session 활용 예시
@GetMapping("/login")
public String login(HttpSession session) {
// 로그인 성공 후 사용자 정보를 세션에 저장
session.setAttribute("user", "park");
session.setAttribute("role", "admin");
return "home";
}
이때 String,
Object
로 들어가는데,Object
는Serializable
해야 한다.
SessionAttribute
@ModelAttribute
처럼 여기서도 @SessionAttribute
의 name
옵션은 생략 가능하다.
@GetMapping("/dashboard")
public String dashboard(@SessionAttribute(name="user", required=false) String user) {
if (user == null) return "redirect:/login";
return "dashboard";
}
SessionAttributes
이 어노테이션은 살짝 헷갈릴 수 있는데, 새롭게 접속하는 클라이언트는 쓸려는 key
가 Session
에 없기 때문에 @ModelAttribute
로 key
가 생성되면 @SessionAttributes
덕분에 이 값이 세션에도 저장되게 된다. 즉, HTTP Session 이 생성될 때마다 자동으로 저장하도록 지정한다.
@SessionAttributes("visitCount")
public class SessionController {
// HTTP Request 에서 모델에 값이 없을 때만 호출되는 애너테이션이다.
// 하지만, 위에서 SessionAttributes 로 HTTP Session 범위로 만들었기 때문에 해당 key 는 세션이 끝날때까지 계속 유지
@ModelAttribute("visitCount")
public Integer initVisitCount() {
System.out.println("initVisitCount");
return 0;
}
...
}
만약 클라이언트가 같은 컨트롤러의 다른 요청을 다시 보내게 되면 세션에 이미
key
가 존재해서, 이 값을 자동으로Model
에 적용하고, 이 어노테이션은Controller
단위로 적용되므로 다른 컨틀롤러에서는 해당key-value
를 못쓴다. 다만, 다른 컨트롤러에서 동일 key 를 수동으로 꺼내서 쓰는건 가능하다. 세션은 공유 영역이기 때문
Exception Handling
보통 자바에서 에러를 만나게 되면 내부적으로 try-catch 를 사용하여 처리를 하게 된다. 하지만 이를 컨트롤러에서 메서드 하나하나 마다 생성시킨다고 한다면 유지보수가 어려워진다.
이때 사용할 수 있는게 @ExceptionHandler
이다. 컨트롤러 단위에서 예외 처리를 해주고 예외를 한 곳에서 처리할 수 있다. 만약 하나의 컨트롤러에 대해서 Exception Handling 을 하고 싶다면 그 컨트롤러 내부에 @ExceptionHandler
가 붙은 메서드를 정의하면 된다.
@Controller
public class UserController {
@GetMapping("/user")
public String getUser(@RequestParam(required = false) String name) {
if (name == null) throw new IllegalArgumentException("이름이 없습니다!");
return "user";
}
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgument(IllegalArgumentException e, Model model) {
model.addAttribute("error", e.getMessage());
return "error"; // error.html 로 포워딩
}
}
모든 컨트롤러 즉, 전역적인 예외 처리를 하고 싶다면 클래스를 따로 만들어 @ControllerAdvice
를 클래스에 붙여주면 된다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgument(IllegalArgumentException e, Model model) {
model.addAttribute("error", e.getMessage());
return "error";
}
@ExceptionHandler(Exception.class)
public String handleGeneral(Exception e, Model model) {
model.addAttribute("error", "서버 오류가 발생했습니다.");
return "error";
}
}
ControllerAdvice
는 다음 옵션을 제공한다.
basePackages
: 특정 패키지 하위의 컨트롤러들에만 예외 처리를 적용할 때 사용
@ControllerAdvice(basePackages = "com.example.user")
public class UserExceptionHandler { ... }
basePackageClasses
: 클래스 기준으로 패키지를 지정할 수 있다. 해당 클래스가 속한 패키지가 자동 인식된다. 아래와 같이 쓰면UserController
가 속한 패키지의 모든 컨트롤러에 적용됨
@ControllerAdvice(basePackageClasses = UserController.class)
public class UserExceptionHandler { ... }
assignableTypes
: 특정 클래스나 타입(컨트롤러 클래스 또는 인터페이스)에만 예외 처리를 적용,basePackageClasses
랑 다른 점은 속한 클래스의 적용 유무이다.
@ControllerAdvice(assignableTypes = {AdminController.class, UserController.class})
public class SpecificControllerAdvice { ... }
annotations
: 특정 애너테이션이 붙은 컨트롤러들에만 적용
@ControllerAdvice(annotations = RestController.class)
public class RestApiExceptionHandler { ... }
RestControllerAdvice
는 REST API 용 전역 처리
ExceptionHandler 규칙
모든 편리한 것에는 규칙이 있음을 잊지 말고 여기서도 규칙을 지키도록 어떤 규칙이 있는지 보자.
보통 @ExceptionHandler
메서드가 자동으로 주입 받을 수 있는 인자는 컨트롤러의 일반 핸들러 메서드와 동일한 규칙으로 인자를 해결한다. 즉 HandlerMethodArgumentResolver
들이 지원하는 모든 타입이 사용 가능하다.
- 예외 타입
- HTTP 관련 표준 타입
HttpServletRequest
HttpServletResponse
HttpSession
ServletRequest
/ServletResponse
WebRequest
- Spring MVC 의 웹레벨 도우미
HttpEntity
Locale
Principal
InputStream
/Reader
등등
- 모델 관련 타입(보통 key-value 를 나타내는거-Map 면 다 된다)
Model
ModelMap
ModelAndView
Map<String, Object>
- 요청 바인딩 관련
@RequestParam
@RequestHeader
@RequestBody
@PathVariable
@ModelAttribute
@CookieValue
@SessionValue
- …
위 타입들은 자동으로 주입 받는다. 따라서 보통 첫 인자로 Exception
하위 클래스를 인자를 정의해주면 내부에서 이를 사용할 수 있다. 그리고 보통 Error 처리를 할 때, Status 코드와 에러 메시지를 열거형으로 선언하고 이를 사용해주는게 깔끔할 것 같다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ModelAndView handleNotFound(ResourceNotFoundException e) {
ModelAndView mav = new ModelAndView("error/404");
mav.addObject("message", e.getMessage());
return mav;
}
@ExceptionHandler(ValidationException.class)
public String handleValidation(ValidationException e, Model model) {
model.addAttribute("errors", e.getErrors());
return "error/validation";
}
@ExceptionHandler(DataAccessException.class)
public String handleDatabaseError(DataAccessException e,
Model model) {
logger.error("Database error", e);
model.addAttribute("error", "Database error occurred");
return "error/500";
}
@ExceptionHandler(Exception.class)
public String handleGeneral(Exception e, Model model) {
logger.error("Unexpected error", e);
model.addAttribute("error", "An unexpected error occurred");
return "error/general";
}
}
// Custom Exception
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}