반응형
레디스는 값을 처리하는 두 가지 방식이 있다
✅ 문자열 방식
"user:test@example.com" =>
"{\"email\":\"test@example.com\", \"name\":\"홍길동\", \"service\":\"helpdesk\"}"
- 값을 하나의 문자열(JSON 또는 직렬화된 객체)로 처리한다.
- 단순하고 빠르다.
- 한 번에 저장하고 조회를 하는 것이 가능하다.
- 하지만 내부 필드별 접근을 하려면 파싱 처리를 추가적으로 해야 함
- 정렬, 조건별 필터링 어려움
✅ 해시 방식
HMSET user:test@example.com
email test@example.com
name 홍길동
service helpdesk
- 필드 단위로 접근이 가능하다.
- 그렇기 때문에 특정 필드 기준으로 정렬도 가능함
- 많은 유저 데이터를 관리할 때 메모리 효율 면에서 유리하다.
- 하나의 필드만 수정하거나 조회 가능하다.
- 전체 데이터를 직렬화, 역직렬화하려면 따로 매핑 작업이 필요한데, 이 작업은 RedisTemplate이 자동으로 처리해 준다.
설정 파일
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
- 해당 템플릿은 키, 값의 직렬화와 역직렬화, 외부 시스템으로의 데이터 전송을 담당한다.
- 레디스 서버와의 연결을 관리하는 LettuceConnectionFactory 구현체를 커넥션 팩토리로 설정하였다.
컨트롤러
@PostMapping
public ResponseEntity<Void> saveUser(@RequestBody @Validated UserDto user) {
service.setUser(user);
return ResponseEntity.ok().build();
}
@GetMapping("/all")
public ResponseEntity<Page<UserDto>> getAllUserPage(
@PageableDefault(
size = 10, sort = "service", direction = Sort.Direction.DESC
) Pageable pageable
) {
Page<UserDto> allUserListPage = service.getAllUserListPage(pageable);
return ResponseEntity.ok(allUserListPage);
}
- 본인은 사용자 전체 목록 조회 시 service 필드를 기준으로 정렬하기 위해 해시 방식으로 값을 처리할 예정이다.
서비스
@Service
@RequiredArgsConstructor
public class UserService {
private final RedisTemplate<String, Object> template;
private static final String USER_KEY_PREFIX = "user:";
public void setUser(UserDto dto) {
String key = "user:" + dto.getEmail();
template.opsForHash().put(key, "email", dto.getEmail());
template.opsForHash().put(key, "name", dto.getName());
template.opsForHash().put(key, "description", dto.getDescription());
template.opsForHash().put(key, "phoneNumber", dto.getPhoneNumber());
template.opsForHash().put(key, "service", dto.getService());
template.opsForSet().add("user:keys", key);
}
public Page<UserDto> getAllUserListPage(Pageable pageable) {
// 1. 모든 키 목록 가져오기
Set<Object> keySet = template.opsForSet().members("user:keys");
// 2. 가져온 키 목록이 비었다면 빈 페이지 반환
if (keySet == null || keySet.isEmpty()) return Page.empty(pageable);
// 3. 정렬 처리
List<String> allKeys = keySet.stream()
.map(Object::toString)
.map(key -> Map.entry(key, template.opsForHash().get(key, "service")))
.sorted(
Comparator.comparing(
entry -> (String) entry.getValue(),
Comparator.nullsLast(String::compareTo)
)
)
.map(Map.Entry::getKey)
.toList();
// 4. 페이징 처리
int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), allKeys.size());
List<String> pagedKeys = allKeys.subList(start, end);
// 5. 반환 사용자 목록 생성
List<UserDto> userList = pagedKeys.stream()
.map(key -> template.opsForHash().entries(key))
.map(map -> {
UserDto dto = new UserDto();
dto.setEmail((String) map.get("email"));
dto.setName((String) map.get("name"));
dto.setDescription((String) map.get("description"));
dto.setPhoneNumber((String) map.get("phoneNumber"));
dto.setService((String) map.get("service"));
return dto;
})
.toList();
// 6. 반환
return new PageImpl<>(userList, pageable, allKeys.size());
}
}
- 등록 메서드에서 opsForHash() 메서드를 까보면 다음과 같다.
/* RedisTemplate.class */
private final HashOperations<K, ?, ?> hashOps = new DefaultHashOperations(this);
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return this.hashOps;
}
/* DefaultHashOperations.class */
public void put(K key, HK hashKey, HV value) {
byte[] rawKey = this.rawKey(key);
byte[] rawHashKey = this.rawHashKey(hashKey);
byte[] rawHashValue = this.rawHashValue(value);
this.execute((connection) -> {
connection.hSet(rawKey, rawHashKey, rawHashValue);
return null;
});
}
- RedisTemplate이 키, 해시키, 해시값을 내부적으로 직렬화한다.
- 직렬화한 값을 execute() 메서드를 통해 레디스로 전송하는 것이다.
// 1. 모든 키 목록 가져오기
Set<Object> keySet = template.opsForSet().members("user:keys");
// 2. 가져온 키 목록이 비었다면 빈 페이지 반환
if (keySet == null || keySet.isEmpty()) return Page.empty(pageable);
// 3. 정렬 처리
List<String> allKeys = keySet.stream()
.map(Object::toString)
.map(key -> Map.entry(key, template.opsForHash().get(key, "service")))
// ...
- 위 코드는 모든 키 목록을 가져와 정렬 처리를 하는 코드의 일부분이다.
- 1번에서 Set<Object>로 키 목록을 선언 및 초기화했기 때문에 .map(Object::toString) 메서드를 통해 스트림 내부에서 Object를 String으로 변환한다.
- .map(key -> Map.entry(key, template.opsForHash().get(key, "service"))) 라인을 보면 스트림 내부에서 키와 해시키를 통해 값을 추출하는 부분임을 알 수 있다.
- 내부 코드를 보면 다음과 같다.
/* RedisTemplate.class */
private final HashOperations<K, ?, ?> hashOps = new DefaultHashOperations(this);
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return this.hashOps;
}
/* DefaultHashOperations.class */
public HV get(K key, Object hashKey) {
byte[] rawKey = this.rawKey(key);
byte[] rawHashKey = this.rawHashKey(hashKey);
byte[] rawHashValue = (byte[])this.execute((connection) -> connection.hGet(rawKey, rawHashKey));
return (HV)(rawHashValue != null ? this.deserializeHashValue(rawHashValue) : null);
}
- return (HV)(rawHashValue != null ? this.deserializeHashValue(rawHashValue) : null); 이 부분이 역직렬화가 일어나는 라인이다.
- rowHashValue는 레디스에서 가져온 byte[] 타입의 데이터로, 제네릭 타입인 HV로 바꾸려면 역직렬화가 필요하다.
페이지 직렬화 경고
Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.
- Spring에서 PageImpl 객체를 직접 JSON으로 직렬화할 경우 위 경고가 뜬다.
- 이건 설정 파일에 다음 어노테이션을 추가하면 된다.
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
- 이 설정을 하면 PageImpl을 자동으로 DTO 구조로 직렬화해서 안정적인 JSON 데이터를 만들어 준다고 한다.
마치며
- 일단 테스트를 통해서는 등록과 조회가 잘 된다.
- 이제 프로젝트를 라즈베리파이에 배포하고, 다른 서비스에서 API 요청이 정상적으로 동작하는지 확인하려고 한다.
- 멋사 부트캠프 강사님께 "코드를 까보는 습관을 갖는 것이 좋다"는 내용을 배웠는데, 이번에 로직을 이해하는 측면에서 내부 코드를 보는 것이 필요한 작업임을 실감했다.
이미지 출처
레디스 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 레디스(Redis)는 Remote Dictionary Server의 약자로서[4], "키-값" 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터베이스 관리 시스
ko.wikipedia.org
'개발 > 스프링' 카테고리의 다른 글
API 게이트웨이 및 ELK를 통해 간단한 중앙 관리자 서버를 만들어보자 (2) | 2025.04.17 |
---|---|
Websocket-STOMP 및 DB 통합 테스트해보자 (0) | 2025.04.05 |
[Trouble-shooting] 클라이언트에서 Websocket-STOMP 연결 요청 시 FALLBACK이 사용되는 현상 (2) | 2025.03.28 |
[Trouble-shooting] 중첩 클래스명 중복으로 Swagger가 고장났다 (2) | 2025.03.21 |
Faker가 만들어 주는 테스트 데이터, 또 나만 몰랐지 (3) | 2025.03.19 |