• spring
  • spring-mvc
  • servlet

세션 직접 만들어 보기와 HttpSession

세션 저장소를 직접 구현해 동작 원리를 이해하고, HttpSession과 @SessionAttribute 사용법을 정리한다.

시리즈 · Spring7 / 14
  1. SOLID 원칙과 OCP·DIP를 지키는 방법
  2. 의존성 주입(DI)과 스프링 컨테이너
  3. 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
  4. FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
  5. @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
  6. System.out 대신 SLF4J로 로그 남기기
  7. 세션 직접 만들어 보기와 HttpSession
  8. 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
  9. 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
  10. JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
  11. JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
  12. JPA 변경 감지(dirty checking)와 병합(merge)
  13. 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
  14. 싱글톤 빈의 동시성 문제와 ThreadLocal

쿠키에 회원 정보를 직접 담아 로그인 상태를 유지하면 심각한 보안 문제가 생긴다. 그래서 로그인 처리에는 세션을 사용한다.

세션이란

로그인 요청을 받은 서버가 회원을 조회하고, sessionId와 value를 보관하는 세션 저장소를 두는 구조

서버는 클라이언트에 세션 ID만 쿠키에 담아 전달하고, 세션 ID에 해당하는 실제 정보는 서버에 저장한다. 쿠키 방식의 보안 문제는 다음과 같이 해결된다.

  • 쿠키 값 변조 → 예측 불가능한 복잡한 세션 ID를 사용한다.
  • 쿠키에 보관한 정보 탈취 → 세션 ID가 노출되어도 그 안에는 중요한 정보가 없다.
  • 쿠키 탈취 후 사용 → 서버에서 세션 만료 시간을 짧게 유지한다.

세션 직접 구현

서블릿이 세션 기능을 제공하지만, 원리를 이해하기 위해 직접 만들어 보았다.

생성

UUID로 추정 불가능한 세션 ID를 만들어 저장소에 값을 보관하고, 세션 ID를 쿠키에 담아 응답한다.

public void createSession(Object value, HttpServletResponse response) {
    // 세션 ID를 생성하고 값을 세션에 저장
    String sessionId = UUID.randomUUID().toString();
    sessionStore.put(sessionId, value);

    // 쿠키 생성
    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
    response.addCookie(mySessionCookie);
}

조회

요청에 세션 쿠키가 없으면 null을, 있으면 쿠키 값(세션 ID)으로 저장소를 조회해 반환한다.

public Object getSession(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if (sessionCookie == null) {
        return null;
    }
    return sessionStore.get(sessionCookie.getValue());
}

public Cookie findCookie(HttpServletRequest request, String cookieName) {
    if (request.getCookies() == null) {
        return null;
    }
    return Arrays.stream(request.getCookies())
            .filter(cookie -> cookie.getName().equals(cookieName))
            .findAny()
            .orElse(null);
}

만료

쿠키의 세션 ID를 찾아 저장소에서 해당 세션을 제거한다.

public void expire(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if (sessionCookie != null) {
        sessionStore.remove(sessionCookie.getValue());
    }
}

테스트

테스트에서는 실제 HttpServletRequest, HttpServletResponse를 쓸 수 없으므로 스프링이 제공하는 MockHttpServletRequest, MockHttpServletResponse를 사용한다.

class SessionManagerTest {

    SessionManager sessionManager = new SessionManager();

    @Test
    void sessionTest() {
        // 세션 생성
        MockHttpServletResponse response = new MockHttpServletResponse();
        Member member = new Member();
        sessionManager.createSession(member, response);

        // 요청에 응답 쿠키 저장
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setCookies(response.getCookies());

        // 세션 조회
        Object result = sessionManager.getSession(request);
        Assertions.assertThat(result).isEqualTo(member);

        // 세션 만료
        sessionManager.expire(request);
        Object expired = sessionManager.getSession(request);
        Assertions.assertThat(expired).isNull();
    }
}

서블릿이 제공하는 HttpSession

public HttpSession getSession(boolean create);
  • request.getSession(true) (기본값): 세션이 있으면 기존 세션을 반환하고, 없으면 새로 생성해 반환한다.
  • request.getSession(false): 세션이 있으면 기존 세션을 반환하고, 없으면 새로 만들지 않고 null을 반환한다.

값 보관은 session.setAttribute(name, value), 세션 제거는 session.invalidate()를 사용한다.

스프링의 @SessionAttribute

@SessionAttribute를 사용하면 세션에서 값을 꺼내는 코드를 줄일 수 있다.

@GetMapping("/")
public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
        Model model) {

    // 세션에 회원 데이터가 없으면 home
    if (loginMember == null) {
        return "home";
    }

    // 세션이 유지되면 로그인 홈으로 이동
    model.addAttribute("member", loginMember);
    return "loginHome";
}

URL의 jsessionid 끄기

브라우저가 쿠키를 지원하지 않을 수 있기 때문에, 서버는 URL에 세션 ID를 포함해 전달하는 방식도 지원한다.

localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872

URL 전달 방식을 끄고 항상 쿠키로만 세션을 유지하려면 application.properties에 다음 옵션을 넣는다.

server.servlet.session.tracking-modes=cookie

참고

  • 김영한, 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (인프런)