Redis를 이용한 로그인/로그아웃 과정에서의 세션관리를 구현해보도록 하겠습니다.
세션을 서버 메모리에 두면 서버가 여러 대일 때 공유가 되지 않고, 재시작 시 모두 사라지는 문제가 있습니다.
DB에 저장하면 조회·갱신이 잦아 불필요하게 부하가 커집니다.
이런 문제를 해결하기 위해 Redis를 세션 저장소로 사용했으며, 아래와 같은 특징을 가지고 있습니다.
EC2 Server (Public IP)
├── Web Server (8080) : 로그인/메인 화면
├── API Server (60818) : 인증 API
└── Redis (6379) : 세션 저장소
하나의 EC2 서버에서 Web Server와 API Server가 함께 동작합니다.
사용자가 로그인에 성공하면 세션 ID가 쿠키에 저장되고 동시에 Redis에도 저장됩니다.
로그아웃 시에는 쿠키와 Redis에서 해당 세션 ID가 모두 삭제됩니다.
Amazon Linux 2023 환경의 EC2 인스턴스에 Redis를 설치해보겠습니다.
sudo dnf update -ysudo dnf search redis 현재 EC2에서는 redis6 버전을 사용할 수 있습니다.
sudo dnf install redis6 
Redis를 제대로 사용하기 위해 설정 파일을 수정해야 합니다.
sudo vi /etc/redis6/redis6.conf
주요 설정 변경사항
bind 설정 (외부 접근 허용)
참고로, 운영 환경에서는 특정 IP만 허용하도록 설정하세요.
# 기본값: bind 127.0.0.1 -::1
bind 0.0.0.0 # 모든 IP에서 접근 허용

데몬 실행 설정
systemd가 직접 Redis를 관리하므로 daemonize는 no로 둬야 합니다.
daemonize no # systemd 사용 시 no로 유지

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

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 만 있어도 정상 실행됩니다.

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

부팅 시 자동 시작 설정
이 명령어는 부팅 시 Redis가 자동 실행되도록 등록하며, 한 번만 실행하면 됩니다.
sudo systemctl enable redis6
서비스 시작
sudo systemctl start redis6
상태 확인
sudo systemctl status redis6

설정 변경 후 재시작
설정 변경 후에는 반드시 재시작해야 적용됩니다.
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가 더 빠르고 효율적임
login.jsp (로그인 페이지)<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>
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>
application.properties (Redis 연결 설정)spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=1111
AuthApiController.java (인증 컨트롤러)@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);
}
}
AuthApiService.java (서비스 인터페이스)public interface AuthApiService {
boolean getAccount(Map<String, String> loginRequest);
}
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;
}
}
AuthApiMapper.java (매퍼 인터페이스)@Mapper
public interface AuthApiMapper {
int checkLogin(Map<String, String> param);
}
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>
CorsConfig.java (CORS 설정)@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); // 쿠키 허용
}
};
}
}
AuthApiService → AuthApiMapper → auth_SQL.xml 순으로 사용자 정보를 확인합니다.실제로 동작한 모습은 아래와 같습니다.
로그인 전
로그인 페이지 화면이 보이고, 개발자 도구에서 확인했을 때 쿠키는 존재하지 않습니다.
또한 Redis에 접속하여 KEYS * 명령어를 실행해보면 아무 키도 존재하지 않는 것을 확인할 수 있습니다.
로그인 성공
로그인에 성공하면 메인 페이지로 이동하며, 개발자 도구에서 세션 쿠키가 새로 생성된 것을 확인할 수 있습니다.
Redis에 접속해 KEYS * 명령어를 입력하면 방금 로그인한 사용자의 세션 키가 생성된 것을 확인할 수 있습니다.
로그아웃
로그아웃을 하면 다시 로그인 페이지로 돌아오고, 개발자 도구에서는 세션 쿠키가 삭제된 것을 확인할 수 있습니다.
또한 Redis에서 KEYS *를 실행하면 기존에 생성되었던 세션 키가 사라진 것을 볼 수 있습니다.
Redis에 저장된 세션 데이터를 시각적으로 확인하기 위해 Redis Insight를 설치합니다.
Homebrew로 설치
brew install --cask redisinsight

Redis Insight 실행

새 연결 추가
연결 방식
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를 통해 로그인/로그아웃 세션 과정을 확인해보겠습니다.
KEYS * 명령어로 세션 키가 생성된 것을 확인할 수 있습니다.
동일한 세션 키가 Redis Insight에 생성된 것을 확인할 수 있습니다.
KEYS * 실행 시 세션 키가 사라진 것을 확인할 수 있습니다.
존재하던 해당 세션 키가 Redis Insight에서 삭제된 것을 확인할 수 있습니다.
이처럼 Redis Insight를 활용하면 세션값을 실시간으로 모니터링하고 시각적으로 쉽게 확인할 수 있습니다.