Redis 세션 관리 시스템 구축

yedi·2025년 9월 1일
post-thumbnail

Redis를 이용한 로그인/로그아웃 과정에서의 세션관리를 구현해보도록 하겠습니다.


시스템 개요

Redis 도입 배경

세션을 서버 메모리에 두면 서버가 여러 대일 때 공유가 되지 않고, 재시작 시 모두 사라지는 문제가 있습니다.
DB에 저장하면 조회·갱신이 잦아 불필요하게 부하가 커집니다.

이런 문제를 해결하기 위해 Redis를 세션 저장소로 사용했으며, 아래와 같은 특징을 가지고 있습니다.

  • 확장성 : 여러 서버 인스턴스 간 세션 공유 가능 → 분산 환경에서도 로그인 상태 유지
  • 성능 : 메모리 기반 저장소이므로 세션 조회 속도가 매우 빠름
  • 안정성 : TTL 기반 자동 만료 지원, 필요 시 영속성 옵션으로 데이터 보존 가능
  • 분리 : 세션을 애플리케이션 서버와 분리하여 비즈니스 로직과 세션 관리 책임을 분리

아키텍처 구성

EC2 Server (Public IP)
├── Web Server (8080)   : 로그인/메인 화면
├── API Server (60818)  : 인증 API
└── Redis (6379)        : 세션 저장소
  • Web Server (8080) : 로그인 및 메인 화면 제공
  • API Server (60818) : 인증 API 처리
  • Redis (6379) : 세션 ID 관리

세션 처리 흐름

하나의 EC2 서버에서 Web Server와 API Server가 함께 동작합니다.
사용자가 로그인에 성공하면 세션 ID가 쿠키에 저장되고 동시에 Redis에도 저장됩니다.
로그아웃 시에는 쿠키와 Redis에서 해당 세션 ID가 모두 삭제됩니다.


Redis 설치 및 설정

Redis 설치

Amazon Linux 2023 환경의 EC2 인스턴스에 Redis를 설치해보겠습니다.

  1. 패키지 업데이트
    최신 패키지 정보를 가져옵니다.
    sudo dnf update -y
  2. Redis 패키지 검색
    설치 가능한 Redis 버전을 확인합니다.
    sudo dnf search redis
    현재 EC2에서는 redis6 버전을 사용할 수 있습니다.

  3. Redis 설치
    sudo dnf install redis6


Redis 설정 파일 수정

Redis를 제대로 사용하기 위해 설정 파일을 수정해야 합니다.

sudo vi /etc/redis6/redis6.conf

주요 설정 변경사항

  1. bind 설정 (외부 접근 허용)
    참고로, 운영 환경에서는 특정 IP만 허용하도록 설정하세요.

    # 기본값: bind 127.0.0.1 -::1
    bind 0.0.0.0  # 모든 IP에서 접근 허용

  2. 데몬 실행 설정
    systemd가 직접 Redis를 관리하므로 daemonize는 no로 둬야 합니다.

    daemonize no  # systemd 사용 시 no로 유지

  3. 보안 모드

    protected-mode yes  # 보안을 위해 유지

  1. 비밀번호 설정
    Redis 접속 시 사용할 비밀번호를 입력합니다.
    requirepass <비밀번호>


서비스 파일 확인

Redis 서비스가 올바른 설정 파일을 사용하는지 확인합니다.

sudo vi /lib/systemd/system/redis6.service

ExecStart 부분에서 올바른 설정 파일이 지정되어 있는지 확인합니다.

--daemonize no : systemd가 Redis를 직접 관리하기 때문에 꼭 필요하지만, 이미 redis.conf에 설정해두면 서비스 파일에서는 생략해도 됩니다.
--supervised systemd : 없어도 동작하지만, 명시해두면 systemd와의 연동이 더 확실해집니다.

ExecStart=/usr/bin/redis-server /etc/redis6/redis6.conf

따라서 ExecStart=/usr/bin/redis6-server /etc/redis6/redis6.conf 만 있어도 정상 실행됩니다.


EC2 보안 그룹 설정

Redis는 기본적으로 6379 포트를 사용하므로, 외부에서 접속하려면 EC2 보안 그룹에서 해당 포트를 열어야 합니다.

  1. AWS 콘솔에서 해당 EC2 인스턴스의 보안 그룹으로 이동합니다.
  2. 인바운드 규칙 탭에서 규칙 편집을 클릭합니다.
  3. 규칙을 추가하고 아래와 같이 설정합니다.
    • 유형: 사용자 지정 TCP
    • 포트 범위: 6379
    • 소스: 내 IP (개발 편의상 0.0.0.0/0도 가능하지만, 운영 환경에서는 특정 IP만 허용하는 것이 안전합니다.)
  4. 규칙을 저장합니다.


Redis 서비스 시작

  1. 부팅 시 자동 시작 설정
    이 명령어는 부팅 시 Redis가 자동 실행되도록 등록하며, 한 번만 실행하면 됩니다.

    sudo systemctl enable redis6
  2. 서비스 시작

    sudo systemctl start redis6
  3. 상태 확인

    sudo systemctl status redis6

  4. 설정 변경 후 재시작
    설정 변경 후에는 반드시 재시작해야 적용됩니다.

    sudo systemctl daemon-reload
    sudo systemctl restart redis6



로그인/로그아웃 세션 관리 구현

이제 실제 세션 관리 기능을 구현해보겠습니다.

API 요청 정리

  • 브라우저(JavaScript) → API 서버
    요청 URL: http://<EC2 Public IP>:60818
    이유: 브라우저는 EC2 서버 외부에서 접근하기 때문에 Public IP를 사용해야 함
  • 서버(Controller) → API 서버
    요청 URL: http://localhost:60818
    이유: 같은 EC2 내부 통신이므로 localhost가 더 빠르고 효율적임

Web Server (8080 포트)

  1. login.jsp (로그인 페이지)
    사용자 인증을 위한 로그인 폼과 AJAX 로그인 처리 로직을 포함합니다.
<script src="/resources/js/jquery-1.12.4.min.js"></script>
<script src="/resources/js/jquery-ui.min.js"></script>
<script src="/resources/js/common.js"></script>

<div class="login-container">
  <input id="username" placeholder="ID">
  <input id="password" type="password" placeholder="PW">
  <button onclick="login()">로그인</button>
</div>

<script>
function login() {
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;

    $.ajax({
        type: "POST",
        url: "http://<Public IP>:60818/common/auth/login",  // EC2 API 서버
        contentType: "application/json",
        data: JSON.stringify({ username, password }),
        xhrFields: { withCredentials: true }, // 쿠키 저장/전송 허용
        success: function() {
            window.location.href = '/common/main';
        },
        error: function(xhr, status, error) {
            console.error('로그인 실패:', error);
            alert('로그인 실패');
        }
    });
}
</script>
  1. main.jsp (메인 페이지)
    로그인 성공 후 표시되는 메인 화면으로, 로그아웃 기능을 제공합니다.
<div id="container">
    <section id="content">
        <button type="button" class="btn_ty_s_c3">popup</button>
        <button class="btn_ty_s_c3" onclick="logout()">로그아웃</button>
    </section>
</div>

<script>
function logout() {
    $.ajax({
        type: "POST",
        url: "http://<Public IP>:60818/common/auth/logout",  // API 서버 로그아웃
        xhrFields: { withCredentials: true },                   // 쿠키 포함 전송
        success: function() {
            window.location.href = "/common/login";             // 로그인 페이지로 이동
        },
        error: function(xhr, status, error) {
            console.error('로그아웃 실패:', error);
            alert('로그아웃 실패');
        }
    });
}
</script>

API Server (60818 포트)

  1. application.properties (Redis 연결 설정)
    API Server에서 Redis에 연결하기 위한 필수 설정입니다.
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=1111
  1. AuthApiController.java (인증 컨트롤러)
    로그인/로그아웃 REST API를 제공하며, 세션 ID 생성 및 Redis 저장을 담당합니다.
@RestController
@RequestMapping("/common/auth")
public class AuthApiController {

    @Resource(name = "AuthApiService")
    private AuthApiService authApiService;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostMapping("/login")
    public ResponseEntity<Void> login(@RequestBody Map<String, String> loginRequest) {
        
        // DB 조회 (아이디+비밀번호 일치 확인)
        boolean isAuthenticated = authApiService.getAccount(loginRequest);
        
        if (isAuthenticated) {
            String username = loginRequest.get("username");
            
            // 세션 ID 생성
            String sessionId = UUID.randomUUID().toString();
            
            // Redis에 세션 저장 (30분 TTL)
            redisTemplate.opsForValue().set(sessionId, username, 30, TimeUnit.MINUTES);

            // 쿠키 생성
            ResponseCookie sessionCookie = ResponseCookie.from("sessionId", sessionId)
                    .httpOnly(true)
                    .path("/")
                    .maxAge(Duration.ofMinutes(30))
                    .sameSite("Strict")
                    .build();

            return ResponseEntity.ok()
                    .header(HttpHeaders.SET_COOKIE, sessionCookie.toString())
                    .build();
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(@CookieValue(value = "sessionId", required = false) String sessionId) {
        if (sessionId != null) {
            redisTemplate.delete(sessionId); // Redis에서 세션 삭제
        }

        // 세션 쿠키 만료 처리
        ResponseCookie deleteCookie = ResponseCookie.from("sessionId", "")
                .httpOnly(true)
                .path("/")
                .maxAge(0) // 즉시 만료
                .sameSite("Strict")
                .build();

        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, deleteCookie.toString())
                .build();
    }
    
    @PostMapping("/secure-endpoint")
    public ResponseEntity<String> secured(@CookieValue(value = "sessionId", required = false) String sessionId) {
        if (sessionId == null || redisTemplate.opsForValue().get(sessionId) == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("세션 만료");
        }

        String username = redisTemplate.opsForValue().get(sessionId);
        return ResponseEntity.ok("Hello " + username);
    }
}
  1. AuthApiService.java (서비스 인터페이스)
public interface AuthApiService {
    boolean getAccount(Map<String, String> loginRequest);
}
  1. AuthApiServiceImpl.java (서비스 구현체)
@Service("AuthApiService")
public class AuthApiServiceImpl implements AuthApiService{
    
    @Resource
    private AuthApiMapper authApiMapper;

    @Override
    public boolean getAccount(Map<String, String> loginRequest) {
        int count = authApiMapper.checkLogin(loginRequest);
        return count > 0;
    }
}
  1. AuthApiMapper.java (매퍼 인터페이스)
@Mapper
public interface AuthApiMapper {
    int checkLogin(Map<String, String> param);
}
  1. auth_SQL.xml (SQL 매핑 파일)
<mapper namespace="common.auth.service.mapper.AuthApiMapper">
    <select id="checkLogin" parameterType="map" resultType="int">
    /* AuthApiMapper.checkLogin */
        SELECT COUNT(1)
          FROM user
         WHERE username = #{username}
           AND password = #{password}
    </select>
</mapper>
  1. CorsConfig.java (CORS 설정)
    서버 간 도메인이 달라 발생하는 CORS 문제를 해결하기 위해, 로컬 개발 환경과 운영 서버에서 API 서버로의 요청과 쿠키 전송을 허용합니다.
@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins(
                            "http://localhost:8080",           // 로컬 개발용
                            "http://<Public IP>:8080"       // EC2 서버용
                        )
                        .allowedMethods("*")
                        .allowCredentials(true); // 쿠키 허용
            }
        };
    }
}

전체 세션 흐름

  1. 로그인 요청
    → Web Server가 API Server로 로그인 요청을 보냅니다.
  2. DB 인증
    → API Server에서 AuthApiService → AuthApiMapper → auth_SQL.xml 순으로 사용자 정보를 확인합니다.
  3. 세션 생성
    → 인증이 성공하면 UUID 기반 세션 ID를 새로 생성합니다.
  4. Redis 저장
    → 세션 ID를 키로, 사용자명을 값으로 Redis에 저장하며 TTL은 30분으로 설정합니다.
  5. 쿠키 응답
    → 생성된 세션 ID를 httpOnly 쿠키로 클라이언트에 전달합니다.
  6. 로그아웃 처리
    → 사용자가 로그아웃하면 Redis에서 해당 세션 키를 삭제하고, 쿠키도 만료시킵니다.

로그인/로그아웃 실행 결과

실제로 동작한 모습은 아래와 같습니다.

  1. 로그인 전
    로그인 페이지 화면이 보이고, 개발자 도구에서 확인했을 때 쿠키는 존재하지 않습니다.또한 Redis에 접속하여 KEYS * 명령어를 실행해보면 아무 키도 존재하지 않는 것을 확인할 수 있습니다.

  2. 로그인 성공
    로그인에 성공하면 메인 페이지로 이동하며, 개발자 도구에서 세션 쿠키가 새로 생성된 것을 확인할 수 있습니다.Redis에 접속해 KEYS * 명령어를 입력하면 방금 로그인한 사용자의 세션 키가 생성된 것을 확인할 수 있습니다.

  3. 로그아웃
    로그아웃을 하면 다시 로그인 페이지로 돌아오고, 개발자 도구에서는 세션 쿠키가 삭제된 것을 확인할 수 있습니다.또한 Redis에서 KEYS *를 실행하면 기존에 생성되었던 세션 키가 사라진 것을 볼 수 있습니다.


Redis Insight를 통한 세션 확인

Redis Insight 설치

Redis에 저장된 세션 데이터를 시각적으로 확인하기 위해 Redis Insight를 설치합니다.

Homebrew로 설치

brew install --cask redisinsight



Redis Insight 설정

  1. Redis Insight 실행

  2. 새 연결 추가

    연결 방식
    Connect to a Redis Database: 직접 연결
    Connect via Redis Sentinel: Sentinel 통해 연결
    Connect to Redis Cluster: 클러스터 연결

    “Add Redis Database”를 클릭한 후 연결 방식을 선택하고, 아래와 같이 연결 정보를 입력합니다.

    Host: <EC2 Public IP>
    Port: 6379
    Password: <설정한 비밀번호>

    연결 정보를 입력한 뒤 "Test Connection"을 눌러 성공하면, "Add Redis Database"를 선택합니다.

세션 모니터링

이번에는 Redis Insight를 통해 로그인/로그아웃 세션 과정을 확인해보겠습니다.

  1. 로그인 성공
    로그인에 성공하면 Redis에서 KEYS * 명령어로 세션 키가 생성된 것을 확인할 수 있습니다.
    동일한 세션 키가 Redis Insight에 생성된 것을 확인할 수 있습니다.
  1. 로그아웃
    로그아웃하면 Redis에서 KEYS * 실행 시 세션 키가 사라진 것을 확인할 수 있습니다.존재하던 해당 세션 키가 Redis Insight에서 삭제된 것을 확인할 수 있습니다.

이처럼 Redis Insight를 활용하면 세션값을 실시간으로 모니터링하고 시각적으로 쉽게 확인할 수 있습니다.

0개의 댓글