0. 들어가기 전에
RefreshToken을 처음 개발할 때 MySQL로 관리했는데, 시간적 여유가 생겨 Redis
를 적용하는 방향을 고려해봤습니다.
1. 기존 RefreshToken의 문제점
우리 바톤팀은 Jwt를 사용해서 로그인 기능을 구현했다. Jwt를 사용해서 서버의 부담을 줄이는 것은 좋았지만, AccessToken이 만료되었을 때 로그인이 풀리는 현상이 있었다. 이 문제를 해결하기 위해 RefreshToken을 도입했다. 그때는 기능 개발할 게 많아 MySQL을 통해 RefreshToken을 관리했었다.
이러다 보니 Jwt의 장점이 무색해졌다. 세션과 달리 Jwt는 서버가 무 상태이기 때문에 대량의 요청을 받았을 때 성능이 더 좋다. 하지만 로그인이 풀린다는 Jwt의 문제점을 해결하고자 도입한 RefreshToken 때문에 서버의 부담이 늘어났다.
2. 왜 Redis
인가?
Redis
는 속도가 빠르다.
Redis
는 메모리 기반 key-value 저장소이다. 그렇기 때문에 MySQL 보다 속도가 훨씬 빨라 서버 부하를 감소시킬 수 있다.
- TTL 기능이 있다.
RefreshToken은 유효기간이 존재하는데, MySQL을 사용해서 RefreshToken을 관리하면 서비스 로직에서 관리해줘야한다.
public Tokens reissueAccessToken(final AuthorizationHeader authHeader, final String refreshToken) {
//...
if (findRefreshToken.isExpired()) {
throw new OauthRequestException(ClientErrorCode.REFRESH_TOKEN_IS_ALREADY_EXPIRED);
}
//...
}
우리 서비스에서 위와 같이 RefreshToken 의 유효기간을 확인해 유효기간이 지났으면 예외를 발생하는 코드가 있다. 만약 Redis
의 TTL 기능을 사용한다면 이 문제가 없어질 것이다.
이러한 이유로 Redis
를 선택하게 되었다. Memcached
와 같은 기능만 사용하네? 라는 생각을 가질 수도 있다. 하지만 추후에 랭킹 기능이나 데이터 백업기능을 사용하기 위해 Redis
를 선택하게 되었다.
3. 개발
[build.gradle]
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Spring Data Redis를 사용할 예정이다. 추가로 기존 스프링부트의 버전이 3.1.1이었는데, 3.1.8로 버전 업했다. docker-compose
가 hostConfig()
관련 에러가 났었는데 이게 스프링 부트의 문제였다고한다. 3.1.6버전 부터 해결되었다고 하니 그 이후의 버전을 사용하면 된다.
[application.yml]
spring:
redis:
host: localhost
port: 16379
username: admin
password: admin
refresh_token:
expire_minutes: 10080
host, port을 설정해주고 local에서는 필요없지만, 운영서버에서의 일관적인 설정 파일 관리를 위해 username과 password를 설정해주었다. host와 port는 local에서 docker-compose
을 사용할 예정이기 때문에 이에 맞게 수정하면된다.
[docker-compose.yaml]
services:
redis:
image: 'redis:7.0-alpine'
container_name: 'baton-redis'
ports:
- '16379:6379'
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
- ./redis/users.acl:/etc/redis/users.acl
[redis.conf]
# spring 연결 허용
protected-mode no
# users.acl 사용 허용
aclfile /etc/redis/users.acl
redis.conf는 공식문서에서 다운받을 수 있고 변경된 사항은 위와 같다.
[users.acl]
user admin on +@all ~* >admin
redis를 연결할 때 사용하기 위해 admin 계정을 추가해 놓았다.
[redis config]
@Profile("!test")
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.username}")
private String username;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
final RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port);
redisStandaloneConfiguration.setDatabase(0);
redisStandaloneConfiguration.setUsername(username);
redisStandaloneConfiguration.setPassword(password);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
}
RedisTemplate은 사용하지 않고 CrudRepository를 사용할 예정이어서 Bean으로 등록안했고, 테스트에서는 ReidsTemplate을 사용할 예정이라 설정파일이 바껴 @Profile("!test")
를 추가했다.
Redis 클라이언트에는 Jedis
, Lettuce
, Redisson
과 같은 클라이언트를 사용하는데 나는 Lettuce
를 사용했다. 자세한 비교를 하지 않았지만, Jedis는 성능 문제가 존재하고 Redisson
은 추가적인 의존성이 필요하기 때문이다. 반면에 Lettuce
는 Spring Data Redis만 의존하면되고, 비동기가 지원이되고 NIO로 서버와 통신하기 때문에 성능이 Jedis
보다 더 낫다.
[Repository]
public interface RefreshTokenCommandRepository extends CrudRepository<RefreshToken, String> {
}
Crud 레포지토리를 구현하는 interface를 하나 만들고 사용하면 된다. JpaRepository와 마찬가지로 <T, Id>
를 입력해주면 된다.
[RefreshToken]
@RedisHash(value = "token:refresh")
public class RefreshToken {
@Id
private String socialId;
private Token token;
private Member member;
@TimeToLive(unit = TimeUnit.MINUTES)
private Long timeout;
// ...
}
@RedisHash
를 통해 RefreshToken을 저장을 해주었다. timeout은 어노테이션에도 넣을 수 있었지만 원활한 테스트를 위해 필드값으로 대체했다.
[Service]
서비스 코드에서는 기존에 있던 로직을 Redis를 사용하는 방향으로 변경해주면 된다. 이 과정에서 Facade 패턴을 도입했다. 왜냐하면 기존 OauthCommandService
는 크게 회원관리의 역할과 토큰관리의 역할을 가지고 있었다. 실제로 코드 절반 이상이 토큰과 관련된 코드였다. 내 생각으로는 토큰은 부가적인 역할이고 OauthCommandService
의 원래 역할을 회원관리를 하는게 맞다고 생각한다. 그래서 복잡한 토큰관련 코드는 TokenFacade
를 만들어서 역할을 분리하였다.
4. 예상되는 문제
4.1 Redis
가 다운되면?
Redis
가 다운이 되면 사용중인 사람들은 AccessToken이 만료되면 RefreshToken을 서버에서 확인해 줄 수 없으므로 로그인이 풀릴것이다. 또한 로그인할 때 AccessToken과 RefreshToken을 동시에 생성하기 때문에 로그인 자체도 안 될것이다.
이 문제를 해결하려면 Redis
의 가용성을 늘리면된다. Redis
는 센티넬이나 클러스터링을 제공해서 가용성을 높일 수 있다고 한다. 추가로 학습해야한다.
4.1.1 Redis
가 관리하던 RefreshToken은?
모두 사라진다. 좀 더 공부를 해봐야하지만 Redis
는 AOF나 RDS를 제공해서 백업을 할 수 있다. 하지만 이 기능을 사용하면 메모리를 추가로 사용하기 때문에 관리 포인트가 늘어날 수 있다고 한다. 또한 로그인이 안된다고해서서 우리 서비스의 주기능인 읽기가 안되는 건 아니고 현재 사용하는 이용자가 적으니 이런 해결책이 있다는 것만 인식하고 넘어가야겠다.
5. 추가 공부해야 할 것
- 센티넬, 클러스터링
- AOF, RDS
'개발 > [우테코]' 카테고리의 다른 글
확장을 고려해서 TaskScheduler로 자동 리뷰 완료 기능 구현하기 (0) | 2024.02.20 |
---|---|
Spring에서 Redis Test 하기 (0) | 2024.02.12 |
바톤의 DB Replication (0) | 2023.10.14 |
build 할 때 Rest Docs 파일이 생성이 안되는 문제 트러블 슈팅 (0) | 2023.08.20 |
[Nginx] 하나의 EC2 안에서 React와 Spring 통신하기 (2) | 2023.08.06 |