본문 바로가기

Spring

[Spring] Session (feat. 프로젝트 경험)

Session

Http 프로토콜은 기본적으로 요청에 대한 상태를 저장하지 않는 Stateless한 성질을 가지고 있습니다.

이를 해결해주기 위해 Session을 활용하는데요. 세션의 기본적인 특징을 알아보겠습니다.

 

우선 Session은 앞서 말씀드린대로 Stateful하게 하기위한 개념입니다.

사용자 A가 사이트에 로그인을 하게되면 서버에서는 사용자 A에게 고유한 값을 쿠키로 전달해줍니다.

 

 

실제로 제가 하는 프로젝트의 한 상황을 캡쳐한것인데요.

이런식으로 F12를 누르고 해당 옵션을 선택하면 어떤 쿠키가 왔는지 볼수있습니다.

그리고 추후에 클라이언트는 이 쿠키를 서버에 요청을 할때 같이 보내기만 하면 인증이되는 구조입니다.

 

세션의 특징으로는 쿠키와 비교를 할 수 있는데요.

  • 1. 쿠키는 클라이언트에 정보가 저장되지만, 세션은 서버에 정보가 저장이됩니다. 따라서 서버에 부담을 줄 수 있습니다.
  • 2. 쿠키는 클라이언트에 저장되기에, 스니핑이나 정보의 변질 우려가 있다는 것에서 세션이 쿠키보다 보안성이 높습니다.
  • 3. 세션과 쿠키 모두 만료시간을 정해줄 수 있습니다.
  • 4. 세션은 정보가 서버에 있기에 처리속도가 느릴 수 있습니다.
  • 5. 다중 서버일때 세션을 관리하기 힘들어집니다. 이때는 JWT를 사용해서 쉽게 구조를 설계할 수 있습니다.

 

세션과 쿠키는 완전히 다른 기술이라기 보단 세션이 쿠키를 활용한 기술이라고 표현할 수 있습니다!

그럼 이제 간단하게나마 세션에 대해 이해를 했다고 가정하고 실제 구현에대해 설명하겠습니다. 

 

세션의 구현방법(feat. 설정)

세션의 구현방법에는 여러가지가 존재합니다. 

톰캣 세션, RDB를 저장소로 사용, Nosql을 저장소로 사용 등이있습니다.

 

당연히 각각의 장단점이 존재합니다. 그중 저는 RDB를 저장소로 사용하는 방식을 선택했는데요.

RDB를 저장소로 사용하면 로그인이나 로그아웃 요청마다 DB에 I/O가 발생한다는 단점이 존재하지만 다중서버일때 편하게 사용할 수 있는 방식입니다.

 

먼저 세션관련 정보를 RDB에서 관리하려면 설정이 필요한데 아래 코드는 Spring 공식문서에서 가져온 코드입니다.

 

@Configuration(proxyBeanMethods = false)
@EnableJdbcHttpSession
public class JdbcHttpSessionConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("org/springframework/session/jdbc/schema-h2.sql")
                .build();
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

}

 

해당 2개의 메서드에 대해 설명하자면, 먼저 dataSource() 메서드의 schema-h2.sql을 타고들어가면 아래와 같이 Session에 대한 테이블을 생성해주는 SQL문이 존재하는걸 알 수 있습니다.

말 그대로 세션을 관리해주는 테이블을 RDB에 생성하기 위함입니다.

 

CREATE TABLE SPRING_SESSION (
   PRIMARY_ID CHAR(36) NOT NULL,
   SESSION_ID CHAR(36) NOT NULL,
   CREATION_TIME BIGINT NOT NULL,
   LAST_ACCESS_TIME BIGINT NOT NULL,
   MAX_INACTIVE_INTERVAL INT NOT NULL,
   EXPIRY_TIME BIGINT NOT NULL,
   PRINCIPAL_NAME VARCHAR(100),
   CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
   SESSION_PRIMARY_ID CHAR(36) NOT NULL,
   ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
   ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
   CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
   CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);

 

두번째 메서드인 transactionManager에서는 트랜잭션 매니저를 명시해줬는데, 해당 코드를 그대로 사용하신다면 문제가 될 수 있습니다.

잠깐 PlatformTransactionManager에 대해 설명을 하고갈게요.

 

스프링은 PlatformTransactionManager 인터페이스를 위와 같은 방식으로 Bean으로 등록하고 DI받아 사용합니다.

여기서 여러가지 상황에 맞게 TransactionManager를 주입해줄 수 있는데 JDBC 기반의 라이브러리로 RDB에 접근하는 경우엔 공식문서에 나온대로 DataSourceTransactionManager를 주입하면 됩니다. 하지만 JPA 까지 사용하신다고 하면 JpaTransactionManager를 주입해주어야합니다. 

그렇지않고 JPA를 사용하신다면 트랜잭션을 잡아줘도 TransactionRequiredException 이 발생할 것입니다.

 

이제 다시 돌아와서 그러면 RDB에 다음과 같이 테이블이 만들어졌습니다.

 

 

SPRING_SESSION 과 SPRING_SESSION_ATTRIBUTES 테이블의 이름이 마음에 안드신다면 변경하는 방법도 구글에있으니 찾아보시길 바랍니다!

 

위에 사진에서 이미 테이블에 무언가 들어가있네요? 제가 실험하면서 넣어본 데이터입니다 ㅎㅎ

그러면 어떻게 하면 저렇게 다룰수 있는지 알아보러 가볼게요.

 

세션 사용방법(feat. 코드)

다들 각자만의 로그인 로직이 존재할겁니다. 

Spring이 제공하는 OAuth-Client, JWT를 활용한 처리등등 이 있을텐데요.

저는 OAuth 라이브러리는 사용하지 않고, Rest-Api로 OAuth 기반의 동작을 구현하였습니다!

그 내용은 다른 페이지에서 다루기로하고, 각자 로그인 로직을 무사히 통과 했다고 가정하고 시작하겠습니다.

 

먼저 로그인 관련 컨트롤러에 다음과 같이 HttpSession을 받아오게 파라미터를 설정합니다.

 

@GetMapping("/kakao")
public ResponseEntity<?> studyLogin(@RequestParam String code, HttpSession httpSession){
    Long avatarID = accountService.kakaoLogin(code);
    sessionUtils.createSession(avatarID, httpSession);

    return ResponseEntity.ok(null);
}

 

그후 로그인이 성공적으로 되었다면 createSession을 통해 다음과 같이 httpSession에 setAttribute를 하게됩니다. 

 

public void createSession(Long sessionValue , HttpSession httpSession){
    httpSession.setAttribute("session", sessionValue);
    httpSession.setMaxInactiveInterval(3600);
}

 

이렇게 setAttribute를 통해 값을 저장했다면 아까 RDB에서 보신것과 같이 데이터가 추가되었을겁니다!

그리고 response에 SESSION_ID가 저장되있는 쿠키를 담아보냅니다.

 

그 후 이제 권한이 있어야하는 요청에 대하여 보겠습니다.

저는 Resolver를 하나 만들었습니다. 보통 ArgumentResolver는 컨트롤러에 들어오는 공통로직을 처리하기위해 많이 사용하시는데요.  

 

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
    HttpSession httpSession = request.getSession(false);


    return (httpSession != null) ? httpSession.getAttribute("session") : null;
}

 

위와 같이 리졸버에서 HttpServletRequest를 받아와서 session이 존재하는지 파악합니다.

request.getSession의 경우 true라면 Session이 존재하지 않으면 새로 하나 만들어주고, false 이라면 존재하지 않을 때 null을 보내주는 옵션입니다.

따라서 위와 같이 매핑하여 컨트롤러에 보내주게 됩니다. (컨트롤러에 보내주는 값은 취향입니다.)

 

여기서 잠깐!!

해당 코드에 대하여 많이들 질문하시는 문제가 있습니다.

Q : setAttribute를 할때 보면 Key값이 "session" 으로 고정되어있는데 방금과 같이 getAttribute를 할 때 사용자 A, B를 어떻게 구분해서 가져와요?

 

정말 유명한 질문이라 질문에 대한 대답을 알고있어야 하는데요.

일반적인 방식으로 HttpSession이 그냥 Map과 완전히 같은 구조라고 생각하시면 안됩니다.

Map과 같은 구조이긴하지만 그전에 이미 구분이 된 Session이 사용자에게 도착을 합니다.

 

무슨소리냐면 아까 로그인이 잘 된 사용자에 response에 쿠키를 담아보낸다고 했죠?

그게 클라이언트의 요청헤더에 담아져오면 그것으로 이미 Request를 구분이 되어 각자 요청에 맞는 HttpSession을 전달하게 됩니다.

그렇기에 "session"이라는 고정된 키값으로 구분할 수 있는 것이지요.

그럼 그 쿠키는 어디서 설정하냐는 질문에는 스프링형님께서 자동으로 보내주시기에 걱정하지 않으셔도됩니다!!

 

보안 문제있을거같은데?

해당 내용을 보면 아시겠지만 악의적인 사용자가 쿠키를 탈취한다면 너무나 쉽게 보안문제가 생깁니다. 그래서 보통은 Http 프로토콜만 사용하는 것이아닌 Https 를 사용하여 SSL 에 의존합니다.

비대칭키를 이용한 암/복호화를 통해 해당 쿠키안의 세션 아이디를 숨겨 사용하곤 합니다.

 

 

이제 모든과정을 설명드린 것 같은데 세션의 구현방법은 정말 많습니다. 또한 현업에서는 Stateful하게 사용하기 위해 JWT보다 세션의 활용을 훨씬 많이 한다고합니다. 그만큼 세션에대한 중요성을 느끼는 경험이었습니다.

'Spring' 카테고리의 다른 글

[Spring] AOP  (0) 2023.06.08
[Spring] JPA? Hibernate? Persistence?  (0) 2023.04.30
[Spring] application-{환경}.yml 과 @Value  (0) 2023.04.17
[Spring] 단위 테스트  (0) 2023.03.24
[Spring] Bean  (0) 2023.02.17