개요
V1 쿠키 직접 사용
V2 커스텀 세션 매니저
V3 서블릿 HttpSession
V4 RedirectURL 처리
V5 @SessionAttribute
V6 ArgumentResolver
V7 Filter 인증 처리
V8 Interceptor 인증 처리 (최종)
V1 - 쿠키에 MemberId 저장
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form , BindingResult bindingResult, HttpServletResponse response) {
//로그인 성공 처리
Cookie cookie = new Cookie("memberId", String.valueOf((loginMember.getId())));
response.addCookie(cookie);
return "redirect:/";
}
초기에는 쿠키에 로그인 정보를 저장한 것부터 따라가 보자 한다
쿠키를 사용하기 이전에는 쿼리 파라미터를 계속 유지하면서 보냈다 하지만 쿼리파라미터의 종류가 많아지고 또 쿼리파라미터는 사용자에게 노출되기 때문에 쿠키를 이용한 방식이 도입되었다
쿠키를 이용하자
쿠키를 이용한 방식으로는 서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달하는 방식이다
코드를 살펴보면 로그인에 성공을 하면 ServeltResponse를 활용하여 response 객체에 쿠키를 추가하여 로그인을 유지하는 방식이다
쿠키의 종류?
쿠키의 종류는 크게 2가지로 나눠진다
- 영속 쿠키 : 만료 날짜를 입력한 쿠키로 해당 날짜까지 유지한다
- 세션 쿠키 : 만료 날짜를 생략하면 브라우저가 종료되기 전까지 쿠키를 유지한다
세션쿠키를 사용하면 브라우저를 종료하기 전까지 쿠키는 자동적으로 계속 보내 짐으로 구현도 매우 편리했다 하지만 이 방식은 치명적인 단점이 존재한다 바로 보안 문제 다
쿠키의 보안 문제
- 쿠키 값은 임의로 변경될 수 있다
- 클라이언트가 쿠키를 강제로 변경할 수 있다
- 쿠키에 보관된 정보는 훔쳐갈 수 있다
- 헤커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다
이러한 치명적인 오류 때문에 기존의 쿠키의 기능은 그대로 살리되 보안적인 측면을 개선시킨 Session 방식이 도입되었다
서버 세션 방식

세션 방식은 기존 쿠키 방식에서 사용자 데이터와 같은 민감한 데이터를 쿠키(클라이언트 측)가 아닌 서버(Session)에서 관리하는 것이 목적이다
기존의 쿠키의 특징인 브라우저를 끄기 전까지 계속 자동적으로 보내지는 장점을 살려 다음과 같은 방식으로 로그인을 지원한다
- 서버는 로그인이 성공하면 UUID (임의의 랜덤 값)을 만들어서 SessionID를 생성한다
- 해당 세션 아이디를 쿠키에 저장한다
- 서버는 SessionID 별로 Session Data를 저장한다
- 이후 클라이언트에 쿠키에 저장된 SessionID 를통해 서버로부터 Data를 가져온다
V2 - 직접 만든 SessionManager (서블릿 세션)
세션 방식을 이용하기 위해서 초기에 아무것도 없을 때는 직접 SessionManager를 만들어서 Session을 관리했다
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
public void createSession(Object value , HttpServletResponse response) {
//세션 아이디를 생성하고 값을 세션에 저장
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
//쿠키 생성
Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(cookie);
}
//세션 조회
public Object getSession(HttpServletRequest request) {
//쿠키 이름이 mySessionId 인 쿠키를 반환
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
//sessionCookie.getValue -> 세션아이디
return sessionStore.get(sessionCookie.getValue());
}
//세션 만료
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie != null) {
sessionStore.remove(sessionCookie.getValue());
}
}
}
Session 매니저는 다음과 같은 기능을 제공한다
- createSession을 통해서 세션을 만들고
- getSession을 통해 Session에 저장된 값을 가져온다
- expire을 통해 Session을 초기화시킨다
작동하는 기능은 크게 어렵지 않다 그래서 여기서는 세션의 create옵션에 대해 알아보자.
- request.getSession(true)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
- request.getSession(false)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성하지 않는다. null을 반환한다
V3 - HttpSession 사용 (표준)
사실 세션은 정말 많이 사용하는 기능이다 그래서 Spring은 세션에 대해서 편리한 기능을 제공한다
그걸 사용하기 위해선 HttpSession을 사용하면 된다 코드를 보자!
public String homeLoginV3(HttpServletRequest request, Model model) {
//세션 관리자에 저장된 회원 정보 조회
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember == null) {
return "home";
}
//세션이 있으면 로그인 유지
model.addAttribute("member", loginMember);
return "loginHome";
}
이전 단계에 우리가 만든 세션 매니저와 크게 달라진 점은 없어 보인다 SessionManger를 사용하는 것 대신에 HttpSession이라는 스프링이 제공하는 Class를 사용하고 있다
중요한 점은 꼭 Session을 request 객체에서 꺼내야 한다는 점이다!
reqeust.getSession을 사용하면 스프링이 알아서 JSESSION을 만들어서(임의의 UUID) 사용자 쿠키에 저장하고,
session.setAttribute를 사용해서 session에 데이터를 저장한다
하지만 여전히 불편한 점 이 있다.
session.getAttribute(세션키)
의 반환 타입이 Object 객체다..
그래서 우리가 원하는 객체로 꺼내려면 DownCasting을 하고 검증하는 코드가 꼭 필요하다
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember == null) {
return "home";
}
이 코드를 Session을 꺼낼 때마다 똑같은 코드가 계속 사용된다
그리고 이 객체를 Model에 추가해 주는 작업도 여전히 중복코드다..
V3 - @SessionAttribute
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER,required = false) Member loginMember, Model model) {
if (loginMember == null) {
return "home";
}
//세션이 있으면 로그인 유지
model.addAttribute("member", loginMember);
return "loginHome";
}
이전의 방식 정말 정말 편리했다 그런데 하나 불편한 점이 뷰에 데이터를 전달할 때는 Model에 담아야 하는데
여전히 세션에서 데이터를 꺼내서 직접 Model 에 Data를 담아야 하는 점이다
이를 편리하게 해 주기 위해 등장한 것이 @SessionAttribute 어노테이션이다
이 어노테이션은 Session에 데이터를 꺼내올 때를 편하게 하려고 만들어준 거다 (Session에 데이터를 넣는 게 아니다!)
이전에 불편한 점을 다시 생각해 보면
- Session에서 Data를 꺼내오고
- 꺼내온 data를 우리의 Member 객체로 다운 캐스팅한다
- 그 후 Model에 저장한다
이 과정 중에 @SessionAttribute 어노테이션은 Session에서 데이터를 꺼내와서 객체에 바인딩해주는 작업인 1번과 2번을 자동화해준다
사용법
@SessionAttribute(name = SessionConst.LOGIN_MEMBER,required = false) Member loginMember
사용법은 다음과 같다
name에 꺼내고 싶은 Session의 key를 입력해 주고
required 에는 Session의 필수 여부를 나타낸다
이렇게 되면 Session에서 꺼낸 타입과 Member 타입이 일치하면 loginMember로 자동 바인딩 해준다
하지만 이렇게 loginMember에 바인딩이 됐다 하더라도
model.addAttribute("loginMember",loginMember);
뷰에게 전달할 멤버에 담는 코드의 불편함은 아직도 남아있다
1. 편리하긴 한데 너무 길다
2. 여전히 모델에 자동으로 담기지 않는다
나만의 어노테이션 만들기 (강의 외 추가기능 추가)
나는 이제 이 모든 문제를 어노테이션 @Login을 만들어서 해결해 보고자 한다
@Login 이 붙으면 자동으로 Session에서 Data를 꺼내서 Member 객체로 바인딩해줌과 동시에 + Model에 도 객체를 넣어준다
즉 @ModelAttribute 기능과 @SessionAttribute 기능을 합친 기능을 만들어 보고자 한다!
파라미터의 바인딩과 관련되어 있음으로 ArgumentResolver와 관련이 있다
그래서!! 구상을 해보면
- @Login Annotation으로 검사 대상을 구분하고
- 나만의 ArgumentResolver를 만들어서 등록하면 될 것 같다
그럼 먼저 Annotaion을 정의해 보자
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
딱히 뭐 정의한 건 없는데
@Target을 통해서 PARAMETER 대상이라는 점과
@ Retention을 통해서 유지기간을 설정했다
이제 핵심인 ArgumentResolver를 만들어보자
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Nullable
@Override
public Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
log.info("resolverArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember != null && mavContainer != null) {
String name = ModelFactory.getNameForParameter(parameter);
mavContainer.addAttribute(name, loginMember);
}
return loginMember;
}
ArgumentResolver는 HandlerMethodArgumentResolver 인터페이스를 구현했다
여기서 HandlerMethod 란?
@RequestMapping 이 붙은 컨트롤러를 사용하면 @RequestMapping 이 붙은 컨트롤러의
메서드 + 그 메서드를 가진 객체(컨트롤러 빈)를 감싸는 객체다
구현해야 하는 Method는 크게 2가지다
근데 항상 느끼는 건데 스프링은 이런 거의 대다수의 인터페이스는 항상 2개의 Method는 꼭 있다
지난번 AdapterHandler를 까봤을 때도 , JDBC Driver 를 까봤을때도 구조는 비슷했다
1. supports (어떤 걸 담당(지원) 하냐)
2. 지원한다고 했을 때 어떤 동작을 할 거냐
supports 메서드
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
지금 내가 다루려는 Parameter 가 앞서 만든 @Login 이 붙어 있는지 나타내는 hasLoginAnntoation
그리고 지금 내가 다룰려는 Paramter 가 Member Class 타입을 가지는지 나타내는 hasMemberType
이 두 개가 모두 만족해야 support를 TRUE가 되게 했다
resolveArgument
@Nullable
@Override
public Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
log.info("resolverArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember != null && mavContainer != null) {
String name = ModelFactory.getNameForParameter(parameter);
mavContainer.addAttribute(name, loginMember);
}
return loginMember;
}
여기서는 단순하게
1. request 객체에서 session을 꺼내고
2. 꺼낸 Session에서 Member 객체를 꺼낸다 (이때 Object가 꺼내짐으로 다운케스팅 한다 )
3. 마지막으로 @ModelAttribute를 지원하는 ModelAttributeMethodProcessor
코드에서 파라미터 이름을 가져오는 코드를 참고해서 name 이란 이름으로 파라미터 이름을 꺼낸다
4. 그리고 인자 값에서 mavContrainer에서 addAttribute 해서 model을 만들어 넘겨준다
마지막으로 우리가 만든 ArgumentResolver 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
이러면 우리가 만든 ArgumentResolver 가 우선순위가 높아서 먼저 실행된다
잘 실행되는지 확인해 보기 위해보자
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member member, Model model) {
if (member == null) {
return "home";
}
//세션이 있으면 로그인 유지
log.info("member 객체 바인딩 확인 ={}", member);
log.info("model에 자동 바인딩 확인={}", model.getAttribute("member"));
return "loginHome";
}
이 컨트롤러를 호출할 거다
인자의 Model은 model에 data를 잘 저장하는지 로그를 찍어보기 위해서 넣어줬다
이제 컨트롤러를 호출하고 로그를 한번 확인해 보면

2026-02-15 00:36:54.225 INFO 249148 --- [nio-8080-exec-9] hello.login.web.HomeController : member 객체 바인딩 확인 =Member(id=1, loginId=test, name=테스터, password=test!)
2026-02-15 00:36:54.225 INFO 249148 --- [nio-8080-exec-9] hello.login.web.HomeController : model에 자동 바인딩 확인=Member(id=1, loginId=test, name=테스터, password=test!)
아주 잘 들어간다!!!
이제 컨트롤러에 필요 없는 Model을 지우고 화면을 잘 호출하는지 확인해 주면 끝!
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member member) {
if (member == null) {
return "home";
}
//세션이 있으면 로그인 유지
log.info("member 객체 바인딩 확인 ={}", member);
// log.info("model에 자동 바인딩 확인={}", model.getAttribute("member"));
return "loginHome";
}

이렇게 나만의 Anntoation 만들기 끝!!!!!!!!
TrackingMode
세션을 사용해서 로그인을 완전히 처음 시도하면 (세션을 처음 만들면) URL 뒤에 이상한 값이 붙는다
http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
이것은 만약에 쿠키를 지원 안 할 때를 대비해서 기존에 쿠키를 통해 JSESSION ID를 전달하는 방식이 아닌
URL에 SESSION을 유지하기 위해서 해당 URL을 구성하는 거다. 거의 사용하지 않는다
이걸 하기 싫으면 (URL 전달 방식을 끄고 항상 쿠키를 통해서만 쿠키를 전달하고 싶으면 )
application.properties에 옵션을 넣어주자
server.servlet.session.tracking-modes=cookie
세션의 편의 Method와 Timeout
log.info("sessionId={}", session.getId());
log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("creationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
세션의 종료 시점
사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도 유지해 준다
이렇게 설정된 이유는 대부분의 사용자는 로그아웃을 하지 않고 브라우저를 그냥 꺼버린다
근데 HTTP는 비연결성이기 때문에 서버 입장에서는 사용자가 웹브라우저를 종료한 건지 알 수 없다
(비연결성이면 그냥 요청 → 응답 이러고 끝나서) 그래서 세션은 최근에 요청한 시간을 기준으로 30분 정도 유지하고 접속하지 않으면 세션을 삭제한다.
Global Session 설정 & 개별 Session 설정
글로벌 세션 설정
application.properties 에 가서 server.servlet.session.timeout 을 설정하자
개별 Session 설정
특정 세션만 TTL을 설정하려면 session.setMaxInactiveInterval()을 활성화하자
TroubleShooting
내가 만든 어노테이션은 파라미터의 이름을 member로 해야 Model 이름도 memeber로 들어간다
그리고 타임리프는 member라는 이름의 객체를 받기 위해서 준비하고 있었다
맨 처음 어노테이션을 만들고 테스트해보는데 Paramter 이름을 loginMember라고 했었다
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
if (loginMember == null) {
return "home";
}
//세션이 있으면 로그인 유지
log.info("model={}",SessionConst.LoginMember);
return "loginHome";
}
이렇게 하고 로그를 찍어봤는데 model = {} 빈값이 출력 됐다
그런데 뷰템플릿은 잘 동작하고 있었다
즉 나는 Model에 loginmember를 키로 데이터를 넣어줬으니 로그는 안 찍혔다 근데 내가 생각하기에는 타임리프에서 member라는 객체 모델을 기대하는데 왜 타임리프는 오류 없이 정상 작동됐을까..?
알고 보니 타임리프가 굉장히 똑똑했다
1️⃣ "member"라는 key 없음
2️⃣ 그럼 Model 전체를 뒤짐
3️⃣ 타입이 Member 인 객체를 찾음
4️⃣ 발견함 (loginMember)
5️⃣ 그걸로 EL 평가함
우연히 우연히 성공한 거였다 ㅋㅋ;;
그래서 로그인 코드를 조금 바꾸고 다시 내가 생각한 정상 흐름으로 만들고 로그를 찍어보니 로그도 잘 맞고 타임리프도 당연히 잘 동작했다

'🌿 스프링 > 스프링 MVC 2편' 카테고리의 다른 글
| 서블릿 예외 처리와 오류 페이지 (0) | 2026.02.15 |
|---|---|
| 서블릿 필터와 스블릿 인터셉터 (0) | 2026.02.15 |
| Validation (0) | 2026.02.14 |
| 메시지 , 국제화 기능 (0) | 2026.02.14 |
| 타임리프 기본 기능 과 스프링 (0) | 2026.02.14 |