AT: 유효기간 짧음, DB 저장X→ 얘만 쓰면 폐기처분이 안됨
RT : 유효기간 김, DB 저장O
물론 RT 써도 폐기처분 안됨 하지만 AT의 유효기간을 짧게 가져가서 검증하는 로직을 추가해주고, RT는 DB에 적재시켜 필요 시 제거하여 토큰 탈취를 대응하는 전략
FE에서는 RT로 AT를 재발급 받는 로직을 짜야한다.

예시로 Axios 인터셉터를 쓴다면 “요청에 AT 자동 첨부”, “응답 401 시 refresh 단 한 번만 시도”, “refresh 실패 시 전역 로그아웃” 같은 규칙으로 묶는 식으로 구현
Redis는 인메모리 DB(램)라서 성능이 좋다가 핵심
key-value 의 해시테이블의 구조로 보통 조회/저장이 O(1) 수준으로 빠르다
그리고 애초에 성능이 매우 빨라서 멀티스레드 구조로 설계될 필요가 없어 싱글 스레드 구조로 설계된다
게시글 상세조회 캐싱은 아래와 같은 흐름이다
select * from post where id=10 조회 post:10 -> {id:10, title:..., contents:...} 이런 식으로 저장(보통 JSON 문자열) 목록은 select * from post 같은 형태로 결과가 자주 바뀌고(페이지/정렬/필터까지 걸리면 경우의 수가 더 폭발함), 캐시 키를 어떻게 잡아도 무효화가 너무 빡세서 유지가 어렵다.
반면 상세 조회는 키도 명확하고(post:10처럼), 변경 범위도 그 글 하나라 캐싱 효율이 좋다.
캐싱의 함정은 데이터가 바뀔 때다. 예를 들어 게시글 수정이 발생하면 Redis에 있던 값은 구버전이 된다. 이때는 전략을 정해야 한다.
Redis의 value는 문자열만 되는 게 아니라 “문자열 + 다양한 자료구조”를 지원한다는 게 강점이다.
Redis value는 문자열만 되는 게 아니라 자료구조를 여러 개 지원함. 상황에 맞게 고르면 됨.
좋아요 예시로 보면:
sadd likes:posting:1 h1 sadd likes:posting:1 h2 likes:posting:1 -> {h1,h2,...} (중복도 자동 방지)시나리오별 자료구조 감 잡기:
그러면 아예 Redis랑 RDB를 같이 쓰면 됨.
이렇게 하면 Redis가 날아가도 RDB가 백업 역할을 해줌.
이 때 RT 검증 방법으로는 두가지가 있고, req dto는 RT가 res dto에는 AT가 된다.
Redis는 리눅스 기반이기 때문에 윈도우에서는 사용되지 않고, 이 때 이미지를 사용하여(설치파일) 도커허브에서의 이미지를 찾아서 다운로드 후 실행시킴
docker run -d --name my-redis -p 6379:6379 redis : -d 백그라운드로 실행되도록 하는 옵션, -p 포트:포트, 프로그램 이름(redis)
docker exec -it 컨테이너ID redis-cli : 레디스 접속
select 0(디비번호) : 레디스 DB 선택
keys * : key값 확인
redis는 0번부터 9번까지의 (10개)DB가 시작되고, 이 때 모두 key:value 형식으로 저장되어있다
redis 관련된 의존성 추가 필요 : implementation 'org.springframework.boot:spring-boot-starter-data-redis'
redis에 연결할 싱글톤 객체(빈 객체)를 만들어야함
package com.beyond.order_system.common.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
// 연결 빈객체 (DB 0)
@Bean
@Qualifier("rtInventory")
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
configuration.setDatabase(0);
return new LettuceConnectionFactory(configuration);
}
// 템플릿 빈객체 (DB 0)
@Bean
@Qualifier("rtInventory")
public RedisTemplate<String, String> redisTemplate(
@Qualifier("rtInventory") RedisConnectionFactory redisConnectionFactory
) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
// 연결 빈객체 (DB 1)
@Bean
@Qualifier("stockInventory")
public RedisConnectionFactory redisStockConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
configuration.setDatabase(1);
return new LettuceConnectionFactory(configuration);
}
// 템플릿 빈객체 (DB 1)
@Bean
@Qualifier("stockInventory")
public RedisTemplate<String, String> redisStockTemplate(
@Qualifier("stockInventory") RedisConnectionFactory redisConnectionFactory
) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
RedisTemplate은 key/value serializer를 설정할 수 있다.
package com.beyond.order_system.common.auth;
import com.beyond.order_system.member.domain.Member;
import com.beyond.order_system.member.repository.MemberRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class JwtTokenProvider {
/* *********************** JWT 설정 *********************** */
@Value("${jwt.secretKey}")
private String st_secret_key;
@Value("${jwt.expiration}")
private int exp_minuet;
private Key secret_key;
@Value("${jwt.secretKeyRt}")
private String st_secret_key_rt;
@Value("${jwt.expirationRt}")
private int exp_minuet_rt;
private Key secret_key_rt;
/* *********************** DI 주입 *********************** */
private final RedisTemplate<String, String> redisTemplate;
private final MemberRepository memberRepository;
@Autowired
public JwtTokenProvider(
@Qualifier("rtInventory") RedisTemplate<String, String> redisTemplate,
MemberRepository memberRepository
) {
this.redisTemplate = redisTemplate;
this.memberRepository = memberRepository;
}
@PostConstruct
public void init() {
secret_key = new SecretKeySpec(
Base64.getDecoder().decode(st_secret_key),
SignatureAlgorithm.HS512.getJcaName()
);
secret_key_rt = new SecretKeySpec(
Base64.getDecoder().decode(st_secret_key_rt),
SignatureAlgorithm.HS512.getJcaName()
);
}
public String createAtToken(Member member) {
Claims claims = Jwts.claims().setSubject(String.valueOf(member.getId())); // email에서 id로 개선
claims.put("role", member.getRole().toString());
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + exp_minuet * 60 * 1000L))
.signWith(secret_key)
.compact();
return token;
}
public String createRtToken(Member member) {
// 1. 유효 기간이 긴 RT 토큰 생성
Claims claims = Jwts.claims().setSubject(String.valueOf(member.getId()));
claims.put("role", member.getRole().toString());
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + exp_minuet_rt * 60 * 1000L))
.signWith(secret_key_rt)
.compact();
// 2. 생성한 RT토큰 Redis에 저장
// opsForSet, opsForZset, opsForList 등 value에 들어올 수 있는 자료구조들이 있다.
// opsForValue는 일반 String 자료구조를 저장할 때 사용
redisTemplate.opsForValue().set(
String.valueOf(member.getId()),
token,
exp_minuet_rt,
TimeUnit.MINUTES
);
return token;
}
public Member validateRt(String refreshToken) {
Claims claims;
try {
claims = Jwts.parserBuilder()
.setSigningKey(secret_key_rt)
.build()
.parseClaimsJws(refreshToken)
.getBody();
} catch (Exception e) {
throw new IllegalArgumentException("잘못된 토큰입니다.");
}
Long id = Long.valueOf(claims.getSubject());
Member member = memberRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("entity not found"));
String redisRt = (String) redisTemplate.opsForValue().get(String.valueOf(id));
if (redisRt != null && !redisRt.equals(refreshToken)) {
throw new IllegalArgumentException("잘못된 토큰입니다.");
}
return member;
}
}
parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt) 형태로 파싱/검증이 가능하다.
@PostMapping("/doLogin")
public ResponseEntity<?> login(@RequestBody @Valid MemberLoginReqDto dto) {
Member member = memberService.login(dto);
String accessToken = jwtTokenProvider.createAtToken(member);
// refresh token 생성 및 저장
String refreshToken = jwtTokenProvider.createRtToken(member);
MemberLoginResDto tokenDto = MemberLoginResDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
return ResponseEntity.status(HttpStatus.OK).body(tokenDto);
}
@PostMapping("/refresh-at")
public ResponseEntity<?> refreshAt(@RequestBody RefreshTokenReqDto dto){
// 1. RT 검증 (validateRt: 토큰 자체검증 -> Redis 조회 검증)
// - 현재 Redis에서 TTL 작업을 수행하지 않았기 때문에 토큰 자체 검증까지 수행하는것이다. (물론 TTL 설정도 할것이다)
// - 이 때 member 객체를 만들기 위해 claims 를 꺼내면서 토큰은 자체검증이 됨
Member member = jwtTokenProvider.validateRt(dto.getRefreshToken());
// 2. AT 신규 생성
String accessToken = jwtTokenProvider.createAtToken(member);
MemberLoginResDto tokenDto = MemberLoginResDto.builder()
.accessToken(accessToken)
.refreshToken(dto.getRefreshToken()) // null로 둬도 됨
.build();
return ResponseEntity.status(HttpStatus.OK).body(tokenDto);
}