Today I Learned
오늘은 인증/인가 관련 구현을 하면서 여러가지 설계에 대한 고민을 해봤다.
1. 회원가입 시 UserRole 설정 전략
회원가입 시 `UserRole`을 입력받는 방식은 보안상 위험할 수 있다고 생각했다.
그래서 아래와 같이 일반 사용자 가입 시에는 `ROLE_USER`를 코드에서 자동 설정하는 방식으로 구현했다.
public record SignupRequest(
@NotBlank(message = "회원이름은 필수입니다.")
String username,
@Email(message = "올바른 이메일 형식이 아닙니다.")
String email,
@NotBlank(message = "비밀번호는 필수입니다.")
@ValidPassword
String password
) {
}
User user = User.builder()
.username(request.username())
.email(request.email())
.password(encodedPassword)
.userRole(UserRole.ROLE_USER)
.build();
관리자 계정 설정은 일단 DB에서 직접 권한을 수정해 테스트를 진행했고,
이후 별도 관리자 생성 로직을 만들지 어떤 좋은 방법이 있을지 이 부분에 대해선 더 고민이 필요하다고 느꼈다.
2. User 도메인과 별도로 Auth 도메인 설계 시 에러코드 책임 분리
진행중인 프로젝트에선 `User`와 `Auth` 도메인으로 분리해서 설계하고 있다.
그 과정에서 예외 처리 시 책임을 어디에 둘 것인지에 대한 고민이 생겼다.
if (userRepository.existsByEmail((request.email()))) {
throw new AuthException(AuthErrorCode.ALREADY_EXIST_EMAIL);
}
위 코드처럼 중복 이메일 가입 방지를 위해 `UserRepository`를 통해 조회하고 만약 이미 존재하는 이메일로 예외를 던져줄 때,
해당 예외 처리는 `User`와 `Auth`중 어느 도메인에서 해야할지 말이다.
`AuthErrorCode`: 로그인, 회원가입, 인증 관련 에러
`UserErrorCode`: 사용자 정보 조회, 수정, 탈퇴 등 사용자 자체에 대한 에러
위처럼 기준을 나눠보니 이메일 중복 여부는 사용자에 대한 검증이므로 `User` 도메인의 책임이라는 결론을 내렸다.
개발자가 도메인별 책임을 명확히 나눠둬야 일관성 있고 명확한 코드를 짤 수 있음을 다시금 느꼈다.
3. 리프레시 토큰 Redis 저장 방식
JWT 기반 인증을 구현하면서 리프레시 토큰을 어디에 저장할지 고민했다.
지난 프로젝트에서는 쿠키에 리프레시 토큰을 저장했는데, 이번에는 Redis에 저장하는 방식을 선택했다.
쿠키에 저장하는 방식
쿠키 저장 방식은 클라이언트(브라우저)에 리프레시 토큰을 저장하는 구조다.
주로 `HttpOnly`, `Secure`, `SameSite` 옵션을 설정해서 보안을 강화한다.
장점
구현이 간단하다
클라이언트가 자동으로 쿠키를 전송하므로, 토큰 재발급 요청이 편리하다
Stateless 구조를 유지할 수 있다 (서버에 상태를 저장하지 않음)
단점
클라이언트에 저장되므로 보안 이슈에 더 민감하다
로그아웃 시 토큰을 무효화하기 어렵다 (쿠키 삭제는 클라이언트에 의존)
블랙리스트 처리나 강제 로그아웃이 어렵다
Redis에 저장하는 방식
Redis 저장 방식은 서버 측에서 리프레시 토큰을 Redis에 저장하고 관리하는 구조다.
보통 userId를 키로, 리프레시 토큰을 값으로 저장하며 만료 시간도 함께 설정한다.
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + user.getId(),
refreshToken,
Duration.ofDays(7)
);
장점
서버에서 토큰을 직접 관리할 수 있어 보안성이 높다
로그아웃 시 Redis에서 해당 토큰만 삭제하면 즉시 무효화 가능
블랙리스트 처리, 다중 디바이스 관리 등 확장성이 뛰어남
토큰 만료 시간도 Redis에서 제어 가능
단점
Redis 설정이 필요하고, 구현이 쿠키 방식보다 복잡하다
서버에 상태를 저장하므로 완전한 Stateless 구조는 아님
결론
이번 프로젝트는 베팅 기능을 포함한 서비스이기 때문에,
사용자의 자산과 관련된 민감한 정보가 오고 가며 보안성이 매우 중요하다고 생각했다.
베팅 내역, 포인트, 승패 결과 등 모두 사용자 인증 상태에 따라 접근이 제한되어야 하고, 로그아웃, 토큰 만료, 로그아웃 같은 인증 흐름도 명확하게 처리되어야 한다
위와 같은 이유로 리프레시 토큰을 쿠키에 저장하는 방식은 제어가 어렵다고 판단했고, Redis에 저장하는 방식을 선택했다.
로그아웃 시 즉시 토큰을 무효화할 수 있고, 향후 관리자 기능이나 베팅 제한 정책에도 유연하게 대응할 수 있다.
결국, 베팅 서비스처럼 보안과 상태 관리가 중요한 시스템에서는 Redis 기반 토큰 저장 방식이 더 적합하다고 생각했다.