[ Redis 캐시 서버의 도입 ]
Redis 캐시를 도입하면서 겪은 디버깅 과정을 글로 적어볼까 한다. Voca-World 프로젝트에 최근 사용자 정보를 캐싱하는 기능을 도입했다. 캐싱을 한 이유는 다음과 같다.
- 토큰을 기반으로 사용자 정보를 찾는 일은 매 요청시마다 발생한다.
- 사용자 정보는 자주 사용되지만 자주 바뀌지 않는다.
사용자가 로그인할 때 Redis를 통해 캐싱하였다.
[ 문제 발생 ]
사용자 정보를 캐싱하고 나서 단어장이 보이지 않는 문제가 발생하였다.
[ 문제의 원인 ]
Redis에서 사용자 정보를 가져올 때 Vocabularies와 관련된 정보가 없는 것을 발견하였다.
[ 문제 해결 과정 ]
코드를 확인해보니 vocabularies 필드에 @JsonIgnore 어노테이션이 붙어있었다.
Redis는 기본적으로 객체를 저장할 때 문자열로 직렬화하여 저장한다. 이때 주로 사용되는 것이 JSON 또는 MessagePack이다. @JsonIgnore는 JSON 직렬화 및 역직렬화 중 특정 필드를 무시하고 싶을 때 사용된다. 따라서 @JsonIgnore를 제거해주어야 한다.
이후에 마주친 에러는 다음과 같다.
1. 무한루프
다음 User코드와 UserVocabulary코드에서는 User -> UserVocabulary -> User -> UserVocabulary와 같이 같은 객체를 계속 반복함으로써 무한루프가 돈다.
@Entity
public class User {
private Long id;
...
List<UserVocabulary> vocabularies = new ArrayList<>();
}
@Entity
public class UserVocabulary {
private Long id;
@ManyToOne
private User user;
@ManyToOne
private Vocabulary vocabulary;
}
이를 해결하기 위해 한 쪽 방향에 @JsonIgnore를 붙여주어야 한다.
@Entity
public class UserVocabulary {
private Long id;
@JsonIgnore // 추가됨
@ManyToOne
private User user;
}
2. 데이터 타입 에러
만약 아래와 같은 에러가 뜬다면 Java8에서는 LocalDateTime형식을 지원하지 않는다는 의미임으로 다음과 같은 작업이 필요하다.
Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
build.gradle에 다음 코드를 추가한다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'com.fasterxml.jackson.core:jackson-databind'
LocalDateTimed에 직렬화/역직렬화에 사용할 클래스를 지정해준다.
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createdAt;
3. 객체안에 boolean이 있는 경우
boolean을 직렬화할 때는 JSON은 'true' 또는 'false'를 문자열로 표현하지만, Java에서는 'boolean'으로 직렬화된다. 따라서 객체 안에 boolean이 있는 경우 다음과 같이 ObjectMapper를 활용하여 boolean 필드를 JSON 문자열로 직렬화해야 한다.
public void setUser(User user) {
String key = getKey(user.getUsername());
log.info("Set User to Redis {}:{}", key, user);
try {
String userData = objectMapper.writeValueAsString(user);
userRedisTemplate.opsForValue().set(key, userData, USER_CACHE_TTL);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public Optional<User> getUser(String username) {
String key = getKey(username);
String user = userRedisTemplate.opsForValue().get(key);
log.info("Get data from Redis {}:{}", key, user);
try {
return Optional.ofNullable(objectMapper.readValue(user, User.class));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}