1. 프로젝트 설명
이번 주 목표는 Redis를 이용해서 로그아웃을 관리하고, Refersh Token을 관리하려고 한다.
1.
2.
2. KPT 작성
[개인회고를 위한 Keep/ Problem/ Try]
- Keep: 프로젝트 완료 후에도 간직하고 싶은 잘했던 것 / 좋았던 것
*ex) "~기술 적용을 했더니 효율적으로 ~를 할 수 있었다." - Problem: 프로젝트 중 겪었던 어려움(기술, 소통, 협업, 에러 등 프로젝트 진행 관련된 그 어느것이든) / 프로젝트 완료 후에도 아쉬움으로 남는 것
*ex) "~기능 적용 중 ~이슈가 발생하였다." - Try: Problem 중 해결된 사항에 대한 해결 방법 / 해결되지 않은 사항에 대한 피드백
*ex) "~기능 적용 중 발생한 ~이슈 해결을 위해 ~를 하였다."
Keep: 처음에는 DB에서 관리를 했다가 찾아보니 Redis라는것이 있었다.
DB(하드디스크)는 관리할 때마다 IO작업 요청이 있기 때문에 서비스에 있어서 불필요한 IO작업은 안하는것이 좋겠다라고 생각했다.
In Memory DB인만큼 데이터가 날라갔을 때를 고민했는데 USER 정보처럼 중요한 데이터를 가지고 있는건 아니라서 사용해도 괜찮다고 생각했다.
Problem:
redis를 사용하기 위해서 config 클래스를 작성해주고
@Configuration
@EnableRedisRepositories // Redis Repository 활성화
public class RedisConfig {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(host, port);
// 패스워드가 있는경우
// lettuceConnectionFactory.setPassword("");
return lettuceConnectionFactory;
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
// redisTemplate를 받아와서 set, get, delete를 사용
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
/**
* setKeySerializer, setValueSerializer 설정
* redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지
*/
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
repository
public interface LogoutAccessTokenRedisRepository extends CrudRepository<LogoutAccessTokenFromRedis,String> {
// @Indexed 사용한 필드만 가능
Optional<LogoutAccessTokenFromRedis> findByEmail(String email);
}
Entity라고 해야될까..
@Getter
@RedisHash("logoutAccessToken")
@AllArgsConstructor
@Builder
/**
* RedisHash : Hash Collection 명시 -> Jpa의 Entity에 해당하는 애노테이션이라고 인지하면 됩니다.
* value 값은 Key를 만들 때 사용하는 것으로 Hash의 Key는 value + @Id로 형성됩니다.
* @Id : key를 식별할 떄 사용하는 고유한 값으로 @RedisHash와 결합해서 key를 생성하는 합니다.
* 해당 애노테이션이 붙은 변수명은 반드시 id여야 합니다.
* @Indexed : CRUD Repository를 사용할 때 jpa의 findBy필드명 처럼 사용하기 위해서 필요한 애노테이션입니다.
* @TimeToLive : 유효시간 값으로 초단위 입니다. 유효 시간이 지나면 자동으로 삭제됩니다.
* @TimeToLive(unit = TimeUnit.MILLISECONDS) 옵션으로 단위를 변경할 수 있습니다.
*/
public class LogoutAccessTokenFromRedis {
@Id
private String id;
@Indexed // 필드 값으로 데이터 찾을 수 있게 하는 어노테이션(findByAccessToken)
private String email;
@TimeToLive
private Long expiration; // seconds
public static LogoutAccessTokenFromRedis createLogoutAccessToken(String accessToken, String email,
Long remainingMilliSeconds){
return LogoutAccessTokenFromRedis.builder()
.id(accessToken)
.email(email)
.expiration(remainingMilliSeconds/1000)
.build();
}
}
지금 내가 관리하는 정도에서는 생각보다 사용하는법이 간단했다. 물론 캐시로 사용한다면 좀더 공부해서 사용해야 될거같은데 그래도 일주일동안 생각보다 많이 배운거같아서 기분은 좋았다.
이런식으로 RefreshToken도 관리했다.
@Getter
@RedisHash("refreshToken")
@AllArgsConstructor
@Builder
public class RefreshTokenFromRedis {
@Id
private String id;
@Indexed // 필드 값으로 데이터 찾을 수 있게 하는 어노테이션(findByAccessToken)
private String accessToken;
@Indexed // 필드 값으로 데이터 찾을 수 있게 하는 어노테이션(findByAccessToken)
private String email;
@TimeToLive
private Long expiration; // seconds
public static RefreshTokenFromRedis createRefreshToken(String refreshToken, String email, String accessToken,
Long remainingMilliSeconds){
return RefreshTokenFromRedis.builder()
.id(refreshToken)
.accessToken(accessToken)
.email(email)
.expiration(remainingMilliSeconds/1000)
.build();
}
public void updateAccessToken(String accessToken){
this.accessToken = accessToken;
}
}
AccessToken이 만료되면 RefreshToken을 이용해서 관리하려다가 만료된 AccessToken을 이용해서 유저정보를 빼서 DB에 있는 USER 랑 이메일을 비교하는 식으로 처리하려고 했는데,,, 하핳 예외때문에 그렇게는 안되는거 같아서 방법을 고민하다가
public String reissue(final ReissueDto reissueDto){
RefreshTokenFromRedis refreshToken = refreshTokenRedisRepository.findByAccessToken(reissueDto.getAccessToken())
.orElseThrow(() -> new UsernameNotFoundException("token이 일치하지 않습니다."));
if (!TokenUtils.isValidRefreshToken(refreshToken.getId()))
throw new UsernameNotFoundException("refresh token 에러");
User user = userRepository.findByEmail(refreshToken.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("token이 일치하지 않습니다."));
String accessToken = TokenUtils.generateJwtAccessToken(user);
refreshToken.updateAccessToken(accessToken);
refreshTokenRedisRepository.save(refreshToken);
return accessToken;
}
내가 받은 AccessToken으로 RefreshToken을 찾은 다음에 RefreshToken을 검증하고 그다음 User정보를 빼서 AccessToken을 재발급한 다음 RefreshToken을 수정해주었다.
Try:
1. RefreshToken에 AccessToken정보를 담고있으면 짧은 만료시간을 가지고 있는 AccessToken을 수시로 수정해줘야 되서 좋지않은 방법같아서 수정할 필요가 있을거 같다.
2. @Entity JPA를 사용하는게 아니라서 그런지 dirty checking이 적용되지 않아 생각보다 조금 버벅거렸다. 내가 만든 컨트롤러는 뭔가 repository에서 정보를 너무 찾고 수정하는거 같아서,, 좀더 공부하면서 좋은방법이 있으면 수정하려고 한다.
Embedded로 Redis를 설정해주고
/**
* RedisConfig와 다른점은 Embedd Redis를 띄운다는 것이고 해당 포트가 미사용중이라면 사용하고 사용중이랑 그외 다른 포트를 사용하도록 하는 설정입니다.
*/
@Slf4j
@Configuration
@Profile("local")
public class EmbeddedRedisConfig {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
private RedisServer redisServer;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(host, port);
// 패스워드가 있는경우
// lettuceConnectionFactory.setPassword("");
return lettuceConnectionFactory;
}
@PostConstruct
public void redisServer() throws IOException {
redisServer = new RedisServer(port);
System.out.println("port = " + port);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
}
테스트 코드도 작성해보았다. 하핳 수정하는 부분도 테스트코드 작성해야 현실적으로 맞는 테스트 코드인데 이전에 데이터만 저장되는지만 확인하고 Postman만 사용해서 확인한거 같다.
@RunWith(SpringRunner.class)
@SpringBootTest
public class LogoutAccessTokenRedisRepositoryTest {
@Autowired
LogoutAccessTokenRedisRepository logoutAccessTokenRedisRepository;
@BeforeEach
public void clear(){
logoutAccessTokenRedisRepository.deleteAll();
}
@DisplayName("save")
@Test
public void save() throws Exception{
//given
String accessToken = "accessToken";
String email = "email";
Long expiration = 3000L;
LogoutAccessTokenFromRedis logoutAccessToken = LogoutAccessTokenFromRedis.createLogoutAccessToken(accessToken, email, expiration);
//when
logoutAccessTokenRedisRepository.save(logoutAccessToken);
//then
LogoutAccessTokenFromRedis find = logoutAccessTokenRedisRepository.findById(accessToken).get();
assertAll(
() -> assertEquals(accessToken,find.getId()),
() -> assertEquals(email,find.getEmail()),
() -> assertEquals(expiration/1000,find.getExpiration())
);
}
}
3. 느낀점
많은 사람들이 테스트코드를 작성하는게 중요하다고 말하지만 아직까지는 크게 와닿지않는다.. 뭔가 내가만든 서비스가 좀 커지고 장애도 나서 더이상 고치기도 싫고 정도떨어지고 할때 쯤이면 테스트코드 작성해둘껄 하고 후회하지 않을까.. 뭔가 경험해보지 못해서 크게 와닿지는 않지만 그 때를 위해서 미리 조금씩 테스트코드 작성하는 방법도 연습해야 겠다.
추가적으로 Redis를 뭔가 캐시처럼 사용하는 방법으로
뭔가 나중에 임시저장 시스템을 만들면 어떨까 고민도 하면서 나름 재미있게 공부한거 같다.