본문 바로가기

spring

Springboot Session에 대해

이번 포스팅은 세션에 대해 알아보도록 하자. 이전 포스팅과 같이 진행되며 세션에 대해 어느 정도 알고 있지만 다시 정리하고 세션을 진행하며 필자가 이해가지 않거나 이해가 부족한 것에 대해서는 중간중간 알아보도록 하겠다. 

 

세션이란?

세션(Session)은 서버와 클라이언트 간의 상태를 유지하는 데 사용된다. HTTP는 상태를 유지하지 않는(stateless) 프로토콜이므로, 클라이언트와 서버 간의 연속적인 요청을 하나의 세션으로 묶어 상태를 유지하기 위해 세션이 사용된다. 상태를 유지해야 하는 다양한 웹 애플리케이션 기능을 구현할 수 있다.

무상태로 서버로 전달되는 모든 request는 이전 request와 관계 없기 때문에 모든 request에 클라이언트 정보를 넘겨줘야 한다.

클라이언트의 상태를 유지하기 위해 사용된다. 세션은 서버에서 클라이언트의 상태를 유지하기 위해 사용된다. 이전 쿠키는 클라이언트 측에서 관리하게 되고 보안적으로 성능이 낮았지만 세션은 서버에서 관리하기 때문에 비교적 보안적 성능이 쿠키보단 높다.

클라이언트가 웹 애플리케이션에 처음 접근하면 서버는 새로운 세션을 생성하고, 세션 ID를 부여한다. 세션 ID는 클라이언트에게 전송되어 쿠키나 URL 파라미터 등을 통해 클라이언트 측에 저장된다.
이후 클라이언트가 서버에 요청할 때마다 세션 ID를 전송하여, 서버는 이 세션 ID를 통해 해당 클라이언트를 식별하고, 세션에 저장된 상태 정보를 사용한다. 세션 데이터는 서버에 저장되며, 클라이언트는 세션 ID만 유지한다.
세션은 일정 시간이 지나면 만료되거나, 사용자가 명시적으로 로그아웃하는 경우 종료된다.

만료된 세션 데이터는 서버에서 삭제된다.

 

세션 구성 요소는 세션 ID, 세션 데이터, 세션 저장소이다.

세션 ID는 세션을 식별하기 위한 고유 문자열이다. 클라이언트는 이 세션 ID를 통해 서버와의 세션을 유지한다.

세션 데이터는 세션 동안 유지해야 할 데이터로 사용자 로그인 정보나 장바구니의 데이터가 세션 데이터에 포함될 수 있다.

세션 저장소는 세션 데이터를 저장하는 위치로 메모리, DB, 파일 시스템 등이 있다.

 

세션 관리 방법을 알아보자. 일반적으로 필자와 같은 주니어 개발자라면 단일 서버에 관리 기법으로 쿠키, URL 파라미터, HTML Hidden form 등이 있을 것이다. 이는 이름 그대로 쿠키, URL, Form을 사용해서 세션을 검증해 사용하게 되는 것이고 이외 로드 밸런싱이나 다중 서버를 가지고 분산 시스템에서 서비스하게 된다면 세션을 관리하는 것도 중요해진다. 이때 세션을 관리하는 방법에 대해서도 알아보자.

Sticky Session, Session Clustering, Session Storage로 3가지 방법이 있다. 각각에 대해 알아보자.

 

Sticky Session - 고성 세션 방식으로 클라이언트마다 담당 처리 서버를 지정하는 방법으로 이는 특정 서버에 트래픽이 집중될 수 있고 장애가 발생하면 해당 서버에 고정된 클라이언트들이 로그인을 다시 해야 하는 가용성 문제가 발생할 수 있다.

 

Session Clustering - 서버끼리 실시간으로 싱크를 맞추는 방법으로 각 서버의 세션 저장소를 클러스터로 묶어 클러스터 내 저장소들 간에 세션을 실시간으로 동기화하는 방식이다. 이때 사용되는 전파 방식에는 모든 서버로 세션을 복제하는 All-to-All Repliction 방식과 Primart서버와 Secondary 서버에만 세션을 복제하고 나머지 서버에는 세션 ID만 복제하는 Primary-Secondary Session Replication방식이 있다. 두 방식 모두 전파 과정에 많은 네트워크 트래픽 사용과 복제로 메모리 낭비와 시간차로 인한 세션 불일치 문제가 있다.

 

Session Storage(Session Server) - 외부 저장소를 활용하는 방법이다. (ex: Redis)

세션 저장소를 외부 서버로 분리해 관리하는 방식이다. 저장소는 주로 In-Memory DB인 Redis를 사용한다 Disk-based DB인 Mysql, PostgreSQL, MongoDB등을 선택할 수도 있지만 I/O로 인한 오버헤드가 크며 세션은 소멸성 데이터이기 때문에 영속성을 보장할 필요가 없고 또한 메모리 휘발에 의해 세션 데이터가 소멸되더라도 피해가 상대적으로 적기 때문에 주로 Redis를 많이 사용한다. 위에서 확인한 Stiky Session의 트래픽 문제나 Session Clustering의 메모리 낭비, 세션 불일치 문제등을 해결할 수 있다.

하지만 단일 세션 서버로 구성한 경우 문제가 남는데 서버에 장애가 발생하면 모든 서버에 영향을 미칠 수 있기 때문에 일반적으로 1개의 Master 서버와 N개의 Slave 서버로 구성해 가용성을 향상 시키는 것이 권장된다. Master 서버는 Write 작업을 처리하고 Slave 서버는 Master 서버의 데이터를 복제해 Read 작업을 처리하는 역할을 한다.

 

여기까지 단일 서버와 다중 서버 구성에서 세션 관리에 대해 알아보았고 다음은 단점과 보안 요소를 확인하고 코드로 넘어가도록 하겠다.

 

단점으로는 요청 즉, request가 발생시마다 저장한 저장소에서 데이터를 검색해야하고 사용자가 많아질수록 그에 따른 세션 ID를 저장해야 할 세션 DB의 용량이 커지게 된다는 단점이 존재한다. 여기서 이 단점에 대한 필자의 생각은 이전에 확인한 Redis를 통해 세션을 관리할 서버를 둔다면 이 단점도 크게 단점으로 생각되지 않는다고 생각한다. 이유는 DB에 데이터를 검색해야 하지만 다중 세션 서버로 구성하게 되면 Read를 진행할 서버가 따로 존재하게 되고 사용자가 많다 하더라도 여러 Slave 서버를 둠으로 분산할 수 있으니 단점도 보완된다고 생각한다.

 

다음은 보안 요소이다.

세션 고정 공격(Session Fixation Attack): 공격자가 미리 세션 ID를 설정하고, 피해자가 이 세션 ID를 사용하도록 유도하는 공격이다.
세션 고정 공격의 대응으로 세션 생성 시 새로운 세션 ID를 부여하고, 로그인 후에도 새로운 세션 ID를 부여한다.

 

세션 하이재킹(Session Hijacking): 공격자가 세션 ID를 탈취하여 사용자의 세션을 가로채는 공격이다.
세션 하이재킹의 대응으로 세션 ID를 예측할 수 없도록 랜덤하게 생성하고, HTTPS를 사용하여 전송 중 세션 ID를 보호한다.

 

세션 만료:세션이 너무 오래 유지되면 보안 취약점이 발생할 수 있다.
세션 만료의 대응으로 세션 타임아웃을 설정하여 일정 시간 이후 세션을 자동으로 만료시킵니다.

 

여기까지 세션에 대해 기본적으로 알아보았다. 세션은 이전에 진행한 쿠키와 유사할 것이다. 우선 세션만 사용해서 구성할 것이고 이후에 쿠키와 세션을 같이 사용해서 서버가 가지는 부담을 줄여 보도록 구성할 수 있게끔 하겠다.

 

우선 이전처럼 controller 구성을 모두 확인해보고 테스트 코드로 넘어가도록 하겠다.

 

우선 세션 을 생성하고 속성 설정하는 것을 둘러보자.

웹 애플리케이션에서 세션은 사용자의 첫 요청이 들어올 때 자동으로 생성되고 명시적으로 세션을 생성할 시 HttpServletRequest 객체를 통해서 세션을 생성한다.

@RequestMapping("")
public String session(HttpServletRequest request){
	
    // request 에 대한 세션을 가져옴
    // true : 세션이 없다면 새로운 세션 생성해서 리턴
    // false : 세션이 없다면 null 리턴
   	HttpSession session = request.getSession(true);
    
 
}

 

세션 속성 설정은 setAttribute 메서드를 사용하는데 키와 값 쌍의 형태로 세션에 저장한다.

저장된 속성은 세션의 유효 기간 동안 유지하고 같은 사용자의 다른 요청에서도 사용이 가능하다.

// 키, 값 쌍으로 세션에 속성 추가
session.setAttribute("키", "값");

// 세션의 유효 기간 설정 1시간
session.setMaxInactiveInterval(60 * 60);

 

세션 데이터 읽기

@RequestMapping("")
public String getSessionAttribute(HttpSession session){
	
    // 세션에 담겨있는 속성 중 키에 값으로 "key"의 value 값을 가져온다.
    String value = session.getAttribute("key");
    

}
@RequestMapping("")
public String getSessionAttribute(@SessionAttribute(name = "키", required = false) String value){
    
    // @SessionAttribute 를 통해서 value 변수 안에 세션의 속성 중 "키" 이름을 가진 값이 매핑됨
    
}

 

세션 삭제는 크게 2가지 방법이 있다. 세션의 모든 속성을 제거하는 방법과 세션을 무효화하는 방법이 있다.

//키라는 key를 가진 세션 속성을 제거한다.
session.removeAttribute("키");
//세션 무효화: 세션에 저장된 모든 데이터 삭제, 세션 종료
session.inavlidate();

 

확인을 했으니 다음으로는 컨트롤러를 둘러보자. 쿠키와 코드 자체는 크게 다를게 없으니 컨트롤러 코드를 확인하고 바로 테스트로 넘어가도록 하겠다. 뷰를 구성하는 html은 이전에 사용하던 것과 뷰는 이전 포스팅을 참조하기 바란다.

@GetMapping("")
public String home(Model model,
                   @SessionAttribute(name = "memberId", required = false) Long memberId) {
    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    Member loginMember = memberService.getLoginMemberById(memberId);

    //로그인시 model에 이름 반환
    if (loginMember != null) {
        model.addAttribute("name", loginMember.getName());
    }
    return "session/home";
}

 

@SeesionAttribute를 활용해 세션의 memberId를 바인딩해서 사용하게끔 했다. 세션 값이 없다면 model.addAttribute를 생성하지 않고 있다면 값을 넣어서 뷰를 구성하게 된다.

@GetMapping("session-login/join")
public String joinPage(Model model) {
    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    model.addAttribute("joinRequest", new JoinRequest());
    return "cookie/join";
}

@PostMapping("session-login/join")
public String join(@Valid @ModelAttribute JoinRequest joinRequest,
                   BindingResult bindingResult, Model model) {
    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    if (memberService.checkLoginIdDuplicate(joinRequest.getLoginId())) {
        bindingResult.addError(new FieldError("joinRequest", "loginId", "ID가 존재합니다."));
    }


    // 비밀번호 = 비밀번호 체크 여부 확인
    if (!joinRequest.getPassword().equals(joinRequest.getPasswordCheck())) {
        bindingResult.addError(new FieldError("joinRequest", "passwordCheck", "비밀번호가 일치하지 않습니다."));
    }

    if (bindingResult.hasErrors()) {
        return "session/join";
    }
    memberService.join(joinRequest);

    return "redirect:/";
}

 

크게 바뀐게 없으니 스킵하겠다.

@GetMapping("session-login/login")
public String loginPage(Model model) {
    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    model.addAttribute("loginRequest", new LoginRequest());
    return "cookie/login";
}

@PostMapping("session-login/login")
public String login(@ModelAttribute LoginRequest loginRequest,
                    BindingResult bindingResult,
                    HttpServletRequest request, Model model) {
    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    Member member = memberService.login(loginRequest);
    Member admin = new Member("1","1","1",MemberRole.ADMIN);
    memberService.save(admin);

    if (member == null) {
        bindingResult.reject("loginFail","로그인 아이디 또는 비밀번호가 틀렸습니다.");
    }

    if (bindingResult.hasErrors()) {
        return "session/login";
    }
    // 기존의 세션을 무효화
    request.getSession().invalidate();

    //세션 생성
    HttpSession session = request.getSession(true);
    //세션에 memberId 속성 추가
    session.setAttribute("memberId", member.getId());
    //세션 유효기간 설정
    session.setMaxInactiveInterval(60 * 60);

    return "redirect:/";
}

 

쿠키와 달리 사용자 값을 쿠키에 담지 않고 세션에 담아 세션을 생성하고 속성을 추가해 유효기간을 설정해 세션을 바인딩할 수 있도록 구성했다.

@GetMapping("session-login/logout")
public String logout(HttpServletRequest request, Model model) {

    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션로그인");

    // request와 연관된 세션 불러옴 (없으면 null 반환)
    HttpSession session = request.getSession(false);

    // 세션이 존재
    if (session != null) {
        // 로그인 된 세션 무효화
        session.invalidate();
    }

    return "redirect:/";
}

 

로그아웃 시 모든 세션을 무효화하게끔 구성했다. 세션을 하나만 구성했으니 하나만 삭제하는 것보다 모든 세션을 무효화시키는 것으로 했다.

@GetMapping("session-login/info")
public String memberInfo(@SessionAttribute(name = "memberId", required = false) Long memberId, Model model) {

    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션로그인");

    Member loginMember = memberService.getLoginMemberById(memberId);

    if (loginMember == null) {
        return "redirect:/session-login/login";
    }

    model.addAttribute("member", loginMember);
    return "session/info";
}

@GetMapping("session-login/admin")
public String adminPage(@SessionAttribute(name = "memberId", required = false) Long memberId, Model model) {

    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    Member loginMember = memberService.getLoginMemberById(memberId);

    if(loginMember == null) {
        return "redirect:/session-login/login";
    }

    if(!loginMember.getRole().equals(MemberRole.ADMIN)) {
        return "redirect:/session-login";
    }

    return "session/admin";
}

이후 구성은 쿠키와 비슷하니 이 부분도 스킵하고 테스트 코드로 넘어가서 세션이 잘 저장이 되는지 확인해 보자.

 

package com.example.oauth2.controller;

import com.example.oauth2.domain.Member;
import com.example.oauth2.domain.MemberRole;
import com.example.oauth2.dto.LoginRequest;
import com.example.oauth2.service.MemberService;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;



import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@ExtendWith(SpringExtension.class)
// SessionController에 대한 Spring MVC 테스트를 구성
@WebMvcTest(SessionController.class)
@Slf4j
class SessionControllerTest {

    // MockMvc를 주입하여 HTTP 요청을 시뮬레이션
    @Autowired
    private MockMvc mockMvc;

    // MemberService를 목(mock) 처리하여 실제 서비스를 호출하지 않고 동작을 시뮬레이션
    @MockBean
    private MemberService memberService;

    // 테스트에서 사용할 Member와 LoginRequest 인스턴스 생성
    private Member member;
    private LoginRequest loginRequest;

    // 각 테스트 메서드가 실행되기 전에 테스트 데이터를 초기화
    @BeforeEach
    public void setup() {
        // Member 인스턴스 생성
        member = new Member(1L, "1", "password", "name", MemberRole.USER);
        // LoginRequest 인스턴스 생성
        loginRequest = new LoginRequest();
        loginRequest.setLoginId("1");
        loginRequest.setPassword("password");
    }


    @Test
    @DisplayName("로그인 시 세션 생성 여부를 검증하는 테스트")
    void testLogin_SessionCreate() throws Exception {
        // memberService의 login 메서드를 목 처리하여 member를 반환하도록 설정
        Mockito.when(memberService.login(Mockito.any(LoginRequest.class))).thenReturn(member);

        // loginRequest를 플래시 속성으로 포함하여 /session-login/login 경로로 POST 요청 수행
        MvcResult result = mockMvc.perform(post("/session-login/login")
                        .flashAttr("loginRequest", loginRequest))
                // 리다이렉션 상태 반환 확인
                .andExpect(status().is3xxRedirection())
                // "/"로 리다이렉션 확인
                .andExpect(redirectedUrl("/"))
                .andReturn();

        // 결과에서 세션을 가져옴
        HttpSession session = result.getRequest().getSession();

        log.info("LoginSessionId = {}", session.getAttribute("memberId"));
        log.info("LoginMemberSessionId = {}", member.getId());
        // 세션이 null이 아닌지 검증
        assertThat(session).isNotNull();
        // 세션에 올바른 memberId가 포함되어 있는지 검증
        assertThat(session.getAttribute("memberId")).isEqualTo(member.getId());
    }


    @Test
    @DisplayName("로그아웃 시 세션 무효화 여부를 검증하는 테스트")
    void testLogout_SessionInvalidation() throws Exception {
        // memberService의 login 메서드를 목 처리하여 member를 반환하도록 설정
        Mockito.when(memberService.login(Mockito.any(LoginRequest.class))).thenReturn(member);

        // loginRequest를 플래시 속성으로 포함하여 /session-login/login 경로로 POST 요청 수행
        MvcResult loginResult = mockMvc.perform(post("/session-login/login")
                        .flashAttr("loginRequest", loginRequest))
                // 리다이렉션 상태 반환 확인
                .andExpect(status().is3xxRedirection())
                // "/"로 리다이렉션 확인
                .andReturn();

        // 결과에서 세션을 가져옴
        HttpSession session = loginResult.getRequest().getSession();
        // 세션이 null이 아닌지 검증
        assertThat(session).isNotNull();

        log.info("LogoutSessionId = {}", session.getAttribute("memberId"));
        log.info("LogoutMemberSessionId = {}", member.getId());
        // 세션에 올바른 memberId가 포함되어 있는지 검증
        assertThat(session.getAttribute("memberId")).isEqualTo(member.getId());

        // memberId를 세션 속성으로 포함하여 /session-login/logout 경로로 GET 요청 수행
        MvcResult logoutResult = mockMvc.perform(get("/session-login/logout")
                        .sessionAttr("memberId", member.getId()))
                // 리다이렉션 상태 반환 확인
                .andExpect(status().is3xxRedirection())
                // "/"로 리다이렉션 확인
                .andReturn();

        // 로그아웃 후 세션을 가져옴
        HttpSession sessionAfterLogout = logoutResult.getRequest().getSession(false);

        log.info("sessionAfterLogout = {}", sessionAfterLogout);
        // 세션이 null인지 검증하여 무효화되었음을 확인
        assertThat(sessionAfterLogout).isNull();

        // /session-login/info 경로로 GET 요청 수행하여 로그아웃 후 리다이렉션을 검증
        MvcResult mvcResult = mockMvc.perform(get("/session-login/info")
                        .sessionAttr("memberId", member.getId()))
                // 리다이렉션 상태 반환 확인
                .andExpect(status().is3xxRedirection())
                // "/session-login/login"으로 리다이렉션 될 것을 확인
                .andExpect(redirectedUrl("/session-login/login"))
                .andReturn();

        // 리다이렉션된 URL 로그 출력으로 검증
        log.info("mvcResult={}", mvcResult.getResponse().getRedirectedUrl());
    }
}

 

위는 테스트 코드이다. 테스트에서 검증할 부분은 두 부분이다. 로그인 시 세션을 잘 생성하는지 세션 생성한 값이 의도한 결과와 같은지 검증하는 테스트 메서드와 로그아웃시 세션을 무효화하는지 검증하는 테스트 메서드이다. 로그아웃 테스트 아래쪽에 session-login/info로 get요청하는 코드가 있는데 이 부분은 로그아웃이 되었는지 엔드포인트에 맞는 컨트롤러 확인하면 if문으로 로그인되어 있으면 session/info로 로그인 되어 있지 않다면 redirection 해서 session-login/login인 로그인 화면으로 이동하게끔 되어 있기 때문에 따로 검증을 두진 않았지만 andExpect로 확인도 하고 로그로 출력해서 확인도 했다.

필자도 테스트 코드에 익숙하거나 잘 사용하지는 못해서 중간중간 알아보고 싶은 것은 많지만 더 깊게 알아보는 것은 따로 테스트만 다뤄보도록 하겠다. 이미 테스트에 대해서 간단히 mock이나 web에 대한 것 없이 단위 테스트 하는 테스트는 포스팅했지만 다음에 따로 테스트 포스팅은 깊게 해 보도록 하겠다.

간단히 몇 가지만 확인하고 진행해 보자.

@ExtendWith(SpringExtension.class)
// SessionController에 대한 Spring MVC 테스트를 구성
@WebMvcTest(SessionController.class)

@ExtendWith은 확장을 선언적으로 등록해 주는 역할을 한다. Extendtion 뒤에 인자로 확장할 Extension을 추가하여 사용할 수 있다.
Spring을 사용할 경우 @ExtendWith(SpringExtension.class)와 같이 사용한다.

@WebMvcTest는 Spring Boot 테스트 어노테이션 중 하나로, 주로 Spring MVC 웹 계층의 테스트에 사용된다. 이 어노테이션을 사용하면 웹 계층에 관련된 컴포넌트만을 로드하여 빠르게 테스트를 수행할 수 있다.
ExtendWith은 축소된 범위로 MVC 테스트에서 경량 돼서 사용하고 통합 테스트를 진행하려면 @SpringBootTest를 통해 진행할 수 있다.

중간에 .flashAttr과 .sessionAttr에 대해서 확인하고 실제 동작을 확인한 후 보통 세션과 쿠키는 같이 사용하게 되는데 같이 사용해서 쿠키로는 이전에 사용했듯 보안을 적용한 상태로 쿠키와 함께 세션을 사용해 보도록 하고 끝내도록 하겠다.

flashAttr("loginRequest", loginRequest): 키-값 형식을 담기로 설정하는 것이다. @ModelAttribute에 바인딩되는 값을 설정한다.

flashAttr의 키이름과 @ModelAttribute에서 바인딩하는 파라미터 값의 이름이 동일해야 한다.

sessionAttr은 세션의 값을 설정하기 위한 것으로 위와 같이 키-값 형식을 담아 사용하며 세션에 설정한 키 값과 동일하게 적용해야 한다.

이외에도 몇 가지 더 알아보고 싶지만 추후 테스트 포스팅에서 자세하게 다뤄보도록 하자.

 

 

 

 

이렇게 확인할 수 있다. 다른 부분은 이전 쿠키와 크게 다를 게 없어 확인할게 별로 없지만 마지막 세션 로그인을 진행한 후 쿠키에 세션 ID가 쿠키를 통해 전달됨을 확인할 수 있다.

 

다음으로는 쿠키와 세션을 함께 사용하는 것을 확인해 보자. 

일단 이전 쿠키 설명에서 JSESSIONID 쿠키에는 세션 ID를 서버에서 제공한다. 이를 서버에서 사용하게 되는데 궁금점이 든 부분이 있다. 세션은 쿠키를 기본적으로 사용한다. 위 JSESSIONID 쿠키를 토대로 세션 ID를 저장하고 서버 측의 세션 데이터를 가져와서 사용하게 되는데 쿠키를 생성해서 사용자 데이터를 담아서 사용하게 된다면 서버 측에 부담이 적어질까 생각하게 되었고 알아보니 서버측에 부담이 적어진다 했다. 그렇다면 디버그를 통해 확인을 해봐야 할 것 같다.

 

우선 위와 같이 브레이크 포인트를 3군데에 걸었다. 각 생성되고 속성 추가하고 유효기간이 설정되는지 그 안에 어떤 값이 들어가는지 확인하기 위해서이다.

아래에 디버그 콘솔을 보면 쿠키에 저장되어 있는 세션 아이디를 확인할 수 있다.

여기서는 세션 속 생성 정보 아이디, 접근 정보 데이터 사이즈 등 알 수 있다.

여기서 create = true로 새로 생성한 세션에 대해 알 수 있다.

다음 브레이크 포인트에서 조금 전에 생성한 세션에 대한 아이디를 토대로 연결된 것을 확인할 수 있고 

넘어가게 됐을 때 attributes에 memberId가 저장되는 것을 확인할 수 있다.

이렇게 세션에 값이 저장되는 것을 확인할 수 있고 세션에 저장된 정보들은 서버에서 관리하게 된다.

이렇게 세션에 저장되는 값을 확인해 보았으니 다음으로는 쿠키를 추가해서 사용자 데이터를 세션과 다른 데이터로 채워서 사용해 보도록 하자.

@PostMapping("session-login/login")
public String login(@ModelAttribute LoginRequest loginRequest,
                    BindingResult bindingResult,
                    HttpServletRequest request,
                    HttpServletResponse response,
                    Model model) {
    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션 로그인");

    Member member = memberService.login(loginRequest);
    Member admin = new Member("1","1","1",MemberRole.ADMIN);
    memberService.save(admin);

    if (member == null) {
        bindingResult.reject("loginFail","로그인 아이디 또는 비밀번호가 틀렸습니다.");
    }

    if (bindingResult.hasErrors()) {
        return "session/login";
    }
    // 기존의 세션을 무효화
    request.getSession().invalidate();

    //세션 생성
    HttpSession session = request.getSession(true);
    //세션에 memberId 속성 추가
    session.setAttribute("memberId", member.getId());
    //세션 유효기간 설정
    session.setMaxInactiveInterval(60 * 60);

    Cookie cookie = new Cookie("memberId", member.getLoginId());
    cookie.setMaxAge(60 * 60);
    cookie.setPath("/");
    response.addCookie(cookie);


    return "redirect:/";
}

@GetMapping("session-login/logout")
public String logout(HttpServletRequest request,HttpServletResponse response, Model model) {

    model.addAttribute("loginType", "session-login");
    model.addAttribute("pageName", "세션로그인");

    // request와 연관된 세션 불러옴 (없으면 null 반환)
    HttpSession session = request.getSession(false);

    // 세션이 존재
    if (session != null) {
        // 로그인 된 세션 무효화
        session.invalidate();
    }
    Cookie cookie = new Cookie("memberId", null);
    cookie.setMaxAge(0);
    cookie.setPath("/");
    response.addCookie(cookie);

    return "redirect:/";
}

 

로그인과 로그아웃은 위와 같이 바꿨으며, 확인을 해보자.

 

로그인을 진행하고 콘솔에서 생성한 쿠키와 세션을 확인할 수 있다.

저번 쿠키에는 암호화를 했는데 암호화에 대한 부분이 없는 이유는 다음 스프링 시큐리티를 활용해서 세션에 대해 보안을 추가할 수 있고 자바 클래스를 활용해서 저번 쿠키에 보안을 적용했지만 다음 시큐리티의 패키지를 사용해서 더 간단히 보안을 추가할 수 있기 때문에 보안은 건너뛰었다.

지금까지 세션에 대해서 사용해 보고 알아보았다. 세션에는 임시 저장소로 사용할 수도 있다. 필자의 생각으로는 세션을 임시 저장소로 사용하면 서버에 부담이 많이 가니 되도록 쿠키를 사용하도록 하고 보안요소를 잘 적용하는 편이 좋은 것 같다는 생각이 든다. 사실 세션에 대해 더 많이 알아볼 수 있지만 나중에 한번 더 알아봐야 할 것 같다 아직 다중 세션을 구성해보지 않았다. 세션에 대해서는 다중 세션을 다룰 때 지금보다 더 많이 자세하게 알아볼 예정이다.

여기까지 세션을 알아보았고 다음 포스팅은 스프링 시큐리티 적용과 로그인을 시큐리티를 통해 구현하도록 하겠다.

'spring' 카테고리의 다른 글

SpringBoot JWT에 대해서  (0) 2024.07.15
Spring Security에 대해  (0) 2024.07.13
Springboot Cookie에 대해  (1) 2024.07.11
멀티 모듈 kafka 추가하기  (1) 2024.06.19
단일 모듈 프로젝트 멀티 모듈 구성하기 -7  (1) 2024.06.18