다중 서버 환경에서 세션 불일치

Mugeon Kim·2024년 1월 24일
0
post-thumbnail

서론


[ 글을 작성한 배경 ]

  • 최근 업무를 진행하면서 분산 환경에서 세션의 불일치를 해결하면서 학습한 내용을 기록하기 위해 작성했습니다.

[ 문제 상황 ]

  • 단일 서버에서 세션을 관리하면 WAS에서 세션이 관리가 가능하다. 하지만 서버가 2개가 된다면 WAS에서 세션을 공유하지 못하기 때문에 세션 불일치가 발생을 한다.

[ 해결 ]

  • 일반적으로 세션 스토리지 ( 영속성 )를 통하여 세션의 정합성을 해결할 수 있다. 보통 Redis를 사용하거나 RDBMS를 통해 세션을 영속하여 해결하지만 각 상황에 따라서 선택이 다르게 해야된다.

본론


  • 최근 웹 서비스는 대부분 스케일 아웃으로 서버를 확장하기 때문에 일반적으로 다중 서버를 구축합니다. 단일 서버에서는 세션 불일치의 문제가 생기지 않지만 다중 서버로 넘어가며 이 문제가 발생을 합니다.

  • 만약에 was가 총 3대가 있다고 가정하면 만약에 유저가 요청이 들어올 때 마다 1->2->3으로 순서대로 라운드 로빈 방식으로 분산한다면 was1에 로그인을 하여 세션을 만들고 was2에 들어가면 세션을 찾을 수 없다.
    이러한 문제를 세션 불일치라고 말하며 이를 해결하기 위해서는 크게 3가지 방식이 있다.

1. Sticky Session

1-1.Sticky Session이란

  • Sticky Session이란 클라이언트의 요청으로 생성된 was에 전달하는 방식을 의미를 한다. 세션 정보가 없는 유저가 요청을 한 경우 로드밸런서의 기본 알고리즘대로 요청을 전달한다.

  • Sticky Session경우 사용자에 대한 세션이 생성된 서버로 고정이 된다. 이때 이것을 판별하는 방식은 쿠키, IP를 기반으로 판단한다.

1-2.장점

  • 세션 정합성 , 캐싱
    기존의 세션 불일치를 특정 서버로 전송하며 세션 정합성을 맞추며 캐싱을 통하여 더 빠른 응답 속도를 가질 수 있다.

  • 고립성
    사용자가 특정 서버와의 세션을 유지하면서 애플리케이션에 영향을 주지 않는다. 이를 통하여 세션 간의 간섭이 줄어들어 애플리케이션의 안전성을 향상을 시킨다.

1-3.단점

  • 성능 저하, 로드 밸런서 목적 실패
    처음으로는 특정 서버에 트래픽이 집중될 경우 성능에 문제가 발생한다. 로드 밸런서의 목적은 부하를 적절하게 분산이다. 하지만 Sticky Session으로 인한 한 서버에 부하가 몰리면 로드 밸런서의 목적을 달성할 수 없다.

  • 에러 분산, 세션 유실
    하나의 서버에 장애가 발생하면, 해당 서버가 가지고 있는 세션 정보의 유실된다. 이러면 다시 로그인을 해야되는 가용성의 문제를 가지게 된다.


2. Session Clustering

2-1.Session Clustering이란

  • 위에 sticky session은 각 서버에 세션을 저장하고 사용자를 해당 was로 보내어 오히려 성능이 안좋아지는 결과를 만들 수 있다. 세션 클러스터링 방식은 세션을 각 서버에 저장이 아닌 데이터를 복사해 데이터를 전파해 가져다 쓸 수 있는 방식이다.

  • 클러스터 내의 모든 서버들이 세션을 공유할 수 있도록 하는 방식으로 방식에는 크게 all-to-all session replication, primary-secondary session replication방식이 있다.

2-2.장점

  • 이론적 무제한 확장
    이론적으로 제한없이 확장할 수 있다. 서비스 확장 가능하게 디자인된 경우 더 많은 노드를 추가하여 제한없이 증가할 수 있다. 쉽게 조정하고 리소스에 대한 비용만 지불하면 된다.

  • 아키텍처 동일성 유지
    소프트웨어 아키텍처가 동일하게 유지, 그에 따라서 프로그램 난이도가 스케일아웃에 비해서 상대적으로 단순하다.

2-3.단점

  • 세션 세팅의 어려움
    스케일 아웃 관점에서 새로운 서버를 만들 때 기존에 존재하는 was에 새로운 서버 ip/port를 입력해야되는 불편함이 있다.

  • 추가 메모리 사용
    서버마다 동일한 세션을 가지고 있어야 하기 때문에 서버가 확장될 수록 복제해야 할 세션 데이터가 늘어나 오버헤드로 이루어진다.

  • 시차로 인한 세션 불일치
    세션 전파 작업 중 모든 서버에 세션이 전파되기까지의 시간차로 인하여 세션 불일치의 문제가 발생할 수 있다.


3. 세션 스토리지 분리

3-1.세션 스토리지 분리란

  • 세션 스토리지 방식은 외부 서버에 세션을 저장하는 방식이다. 이때 일반적으로 영속성이 있는 DB에 사용한다. 주로 MySQL 또는 Redis에 저장한다. 자주 입출력이 잦은 세션 특성상 메모리에 위치한 DB를 사용한다.
    이 중에서 Redis, Memcached가 있다. 둘다 {key,value}로 데이터를 저장하고 빠른 응답속도를 자랑한다. 하지만 Redis는 다양한 자료구조 및 복제, 복구에 대한 다양한 서비스를 제공한다. 이러한 특성 때문에 Redis를 사용하는게 더 많이 사용한다.

업무에서는 어떤 방식을 사용하여 세션을 관리를 했는가

  • 물론 Redis를 사용하는게 성능적으로 좋은 방식인걸 알았지만 저는 RDBMS에 세션을 저장을 하였습니다. 이를 선택한 이유는 해당 프로젝트에서 Redis를 사용하는 기능이 없어 세션 분리를 위해서 Redis를 사용하는게 적절한 방식인가에 대해서 고민이 되었고 개발 인원이 부족하여 Redis를 모니터링 및 운영하기에 적절하지 않다고 판단하여 MySQL에서 세션을 관리를 하였습니다.

4. 스프링에서 MySQL에서 세션 관리하기

  • Spring에서는 jdbcSession을 사용하여 간단하게 세션을 관리할 수 있습니다. 일단 관련 의존성을 build.gradle에 등록을 합니다.
	implementation'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.session:spring-session-jdbc'
  • 세션을 사용하기 위해서는 테이블을 생성을 해야됩니다. 일단 mybatis에서는 해당 쿼리를 생성을 하거나 다음과 같은 옵션을 설정을 해야됩니다. 아래는 application.yml 코드입니다.
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
  • initalize를 설정을 하거나 직접 테이블을 생성을 해야됩니다.
CREATE TABLE SPRING_SESSION (
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
);

CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);

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

CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);
  • 만약에 JPA를 사용한다면 sql을 만들어 claassPath로 데이터를 넣으면 된다.
spring:
  datasource:
    data: classpath:schema-h2.sql # Spring Session 테이블 스키마 적용
  jpa:
    show-sql: true # JPA로 생성되는 쿼리 확인
    hibernate:
      ddl-auto: create # 프로젝트 시작시 테이블 생성
  h2:
    console:
      enabled: true
      path: /h2-console # h2 db 웹 클라이언트 접속 url
  devtools:
    livereload:
      enabled: true # 정적파일들의 실시간 갱신
  • 이후 스키마를 적용하고 JdbcSession옵션을 활성화를 시킨다. 이거는 config파일을 하나 만들어 EnableJdbcHttpSession어노테이션을 추가를 시킨다.
@EnableJdbcHttpSession
public class AppConfig {
}

  • 다음과 같이 설정하면 spring_session, spring_session_attribute를 생성을 합니다. 여기에 세션을 저장을 합니다. spring_session에는 세션을 판단하고 attribute에서는 세션의 정보를 저장을 합니다. 이때 정보를 byte[] 코드로 저장을 합니다.

  • db에 저장하면 세션 정합성을 생각할 수 있다. redis같은 경우에는 TTL을 통해서 맞출 수 있지만 mysql은 어떻게 맞추나 고민을 하였다.
  • jdbc-session을 사용하면 특정 시간동안 db에 select, delete를 특정 시간을 통하여 맞추기 때문에 정합성을 맞출 수 있다.

그런데 byte[]로 attribute_bytes를 저장하는데 어떻게 가져올 수 있니?

  • byte로 저장하기 때문에 나는 이것을 가져오기 위해서는 Serializable를 통해서 객체를 저장하면 쉽게 가져올 수 있다.

  • Serializable는 자바에서 지원되는 인터페이스 중 하나로 인스턴스를 직렬화할 수 있게 해준다. 이를 통해서 객체 상태를 바이트 스트림으로 변환하여 저장할 수 있다.

  • 따라서 해당 클래스는 객체를 역, 직렬화를 할 수 있게 된다. 이를 통하여 byte[]를 추가적인 설정을 할 필요없이 객체를 가져올 수 있습니다.

public class Member implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private int age;

    // 생성자, 게터, 세터 등 필요한 메서드들

    // 예시로 멤버 객체를 생성하고 세션에 저장하는 메서드
    public static Member createMember(String username, int age) {
        Member member = new Member();
        member.setUsername(username);
        member.setAge(age);
        return member;
    }
}



@RestController
public class SessionController {

    @Autowired
    private SessionRepository<?> sessionRepository;

    @GetMapping("/saveMemberToSession")
    public void saveMemberToSession() {
        // 멤버 객체 생성
        Member member = Member.createMember("김무건", 28);

        // 세션에 멤버 객체 저장
        Session session = sessionRepository.findById("sessionId"); // 세션 ID를 실제 세션의 ID로 대체해야 합니다.
        session.setAttribute("memberAttribute", member);
        sessionRepository.save(session);
    }

    @GetMapping("/getMemberFromSession")
    public Member getMemberFromSession() {
        // 세션에서 멤버 객체 가져오기
        Session session = sessionRepository.findById("sessionId"); // 세션 ID를 실제 세션의 ID로 대체해야 합니다.
        return (Member) session.getAttribute("memberAttribute");
    }
}

결론


  • 다중 서버에서 세션의 불일치를 해결하는 방법에는 여러가지가 있다. 이 중에서 세션 스토리지에서 Redis를 통해서 높은 성능을 가질 수 있지만 현재 서비스의 상황에 따라서 다양한 방식을 선택할 수 있다.

  • RDBMS에서 세션을 저장하면 지속적으로 세션을 탐색, 삭제를 통하여 세션 정합성을 맞추며 데이터를 byte기반으로 저장하여 보안성을 가져갈 수 있다.


참고


https://hyuntaeknote.tistory.com/6

https://jojoldu.tistory.com/170

https://itkjspo56.tistory.com/296

profile
빠르게 실패하고 자세하게 학습하기

0개의 댓글