-
Notifications
You must be signed in to change notification settings - Fork 0
[COZY-683] fix : 레디스 커스텀 인덱스 수정/보완 #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Walkthrough이 변경사항은 멤버 통계 데이터의 라이프사이클을 이벤트 기반 아키텍처로 리팩토링합니다. MemberStat 생성/수정/삭제 시 각각의 도메인 이벤트를 발행하며, MemberStatEventListener가 이를 구독하여 Redis 캐시와 라이프스타일 매치율 데이터를 자동으로 관리합니다. Redis 캐시 연산을 Lua 스크립트 기반의 원자적 연산으로 개선하고, 멤버 탈퇴 시 관련 데이터를 일괄 정리하는 프로세스를 체계화합니다. Sequence Diagram(s)sequenceDiagram
participant Client
participant CommandService as MemberStatCommandService
participant Repository as Repository
participant EventPublisher as ApplicationEventPublisher
participant EventListener as MemberStatEventListener
participant CacheService as MemberStatCacheService
participant MatchRateService as LifestyleMatchRateService
Client->>CommandService: createMemberStat(...)
CommandService->>Repository: save(memberStat)
Repository-->>CommandService: savedMemberStat
CommandService->>EventPublisher: publishEvent(MemberStatCreatedEvent)
EventPublisher->>EventListener: onCreated(event)
EventListener->>CacheService: saveByArgs(SaveCommand)
CacheService-->>EventListener: ✓
EventListener->>MatchRateService: saveLifestyleMatchRate(...)
MatchRateService-->>EventListener: ✓
EventListener-->>Client: ✓
Client->>CommandService: modifyMemberStat(...)
CommandService->>Repository: update(memberStat)
Repository-->>CommandService: updatedMemberStat
CommandService->>EventPublisher: publishEvent(MemberStatModifiedEvent)
EventPublisher->>EventListener: onModified(event)
EventListener->>CacheService: updateByArgs(UpdateCommand)
CacheService-->>EventListener: ✓
EventListener->>MatchRateService: updateLifestyleMatchRate(...)
MatchRateService-->>EventListener: ✓
EventListener-->>Client: ✓
Client->>CommandService: deleteRelatedWithMember(memberId)
CommandService->>Repository: deleteMemberStat(memberId)
Repository-->>CommandService: Optional<MemberStat>
alt MemberStat 존재
CommandService->>EventPublisher: publishEvent(MemberStatDeleteEvent)
EventPublisher->>EventListener: onDelete(event)
EventListener->>CacheService: deleteByArgs(DeleteCommand)
CacheService-->>EventListener: ✓
EventListener->>MatchRateService: deleteAllMatchRateByMemberId(...)
MatchRateService-->>EventListener: ✓
end
EventListener-->>Client: ✓
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45-60 minutes 주의가 필요한 영역:
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (7)
src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatDeleteEvent.java (1)
5-10: LGTM!이벤트 레코드 구조가 적절합니다.
memberId가Long타입인 것은 도메인 엔티티 ID를 나타내므로 올바른 설계입니다.참고: Line 9의
Map<String,String>에서 쉼표 뒤 공백이 없는데, 다른 파일들(SaveCommand, UpdateCommand 등)에서는Map<String, String>로 공백을 포함하고 있어 미묘한 스타일 불일치가 있습니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatCreatedEvent.java (1)
5-10: LGTM!이벤트 레코드 구조가 적절하며,
memberId를Long타입으로 사용하는 것이 올바릅니다.참고: 필드 순서가
MemberStatDeleteEvent(universityId가 첫 번째)와 다릅니다(memberId가 첫 번째). 레코드이므로 기능적 문제는 없지만, 일관성을 위해 이벤트 레코드들의 필드 순서를 통일하는 것을 고려해보세요.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatEventListener.java (1)
25-49: runWithRetry 동작 방식에 대한 의도 확인 제안현재
runWithRetry가 예외를 캡처하고 로그만 남긴 뒤 상위로 전파하지 않기 때문에, Redis/매치율 처리 실패가 비즈니스 트랜잭션 결과(응답 성공/실패)에 전혀 영향을 주지 않는 ‘best-effort’ 패턴으로 동작합니다. 또, 최대 3회까지Thread.sleep으로 호출 스레드를 블록하므로, 동기 이벤트 리스너인 현재 구조에서는 최악의 경우(100ms + 200ms) 만큼 요청 레이턴시가 증가할 수 있습니다.
- 캐시/매치율 불일치가 어느 정도까지 허용 가능한지에 따라,
- (1) 지금처럼 완전히 swallow 하는 방식,
- (2) 특정 예외(예: 영구 실패)만 rethrow 해서 알림/롤백 대상으로 만드는 방식,
- (3)
@Async리스너 + 모니터링(메트릭/알람) 조합
중 어떤 모델이 팀 합의에 맞는지 한 번 정리해 두면 이후 운영 시 판단이 더 명확해질 것 같습니다.src/main/java/com/cozymate/cozymate_server/domain/member/service/MemberWithdrawService.java (2)
106-108: 매치율 삭제 로직의 중복 여부
deleteRelatedWithMember에서
lifestyleMatchRateRepository.deleteAllByMemberId(member.getId());를 먼저 호출하고, 이후
MemberStatDeleteEvent를 발행하면 리스너의
lifestyleMatchRateService.deleteAllMatchRateByMemberId(e.memberId());가 다시 한 번 동일한 삭제를 수행하게 됩니다.
동일 쿼리가 두 번 실행되는 정도라 큰 문제는 없겠지만, 책임을 “이벤트 리스너에서 일괄 정리”로 모으고 싶다면:
- 여기의 직접
deleteAllByMemberId호출을 제거하고 이벤트만 발행하거나,- 반대로 이벤트 리스너에서의 삭제를 제거하고 탈퇴 서비스 쪽에만 유지
하는 식으로 한쪽으로 정리해 두는 것도 고려해볼 만합니다.
Also applies to: 138-145
210-218:deleteMemberStat주석과 실제 동작 불일치 (문서화 리팩터링)주석에는 “redis 에서 MemberStat 삭제하고, DB에서 삭제”라고 되어 있지만, 실제 구현은 DB 삭제만 수행하고 Redis 삭제는
MemberStatDeleteEvent→ 리스너를 통해 처리됩니다. 주석을 이벤트 기반 플로우에 맞게 갱신해 두면 후속 유지보수 시 혼동을 줄일 수 있을 것 같습니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/service/MemberStatCommandService.java (2)
48-69: modifyMemberStat의 newSnapshot 생성 단계를 단순화 가능
modifyMemberStat에서Map<String, String> oldAnswers = MemberStatExtractor.extractAnswers(entity); MemberStat newSnapshot = MemberStatConverter.toEntity(member, req); Map<String, String> newAnswers = MemberStatExtractor.extractAnswers(newSnapshot); entity.update(...);처럼 별도의
newSnapshot을 만들어newAnswers를 계산하고 있습니다. 기능적으로는 문제 없지만,
entity.update(...)이후 동일한MemberStatExtractor.extractAnswers(entity)를 호출해도 같은 결과를 얻을 수 있고,- 중간 스냅샷을 만들지 않으면 향후
update로직과toEntity로직이 미묘하게 어긋나도 버그 발생 가능성을 줄일 수 있습니다.예를 들면 다음과 같이 단순화할 수 있습니다:
- Map<String, String> oldAnswers = MemberStatExtractor.extractAnswers(entity); - MemberStat newSnapshot = MemberStatConverter.toEntity(member, req); - Map<String, String> newAnswers = MemberStatExtractor.extractAnswers(newSnapshot); - - entity.update( + Map<String, String> oldAnswers = MemberStatExtractor.extractAnswers(entity); + + entity.update( MemberStatConverter.toMemberUniversityStatFromDto(req), MemberStatConverter.toLifestyleFromDto(req), req.selfIntroduction() ); + + Map<String, String> newAnswers = MemberStatExtractor.extractAnswers(entity);이렇게 하면 코드가 더 단순해지고, 도메인 로직이 한 엔티티 인스턴스에 집중되어 유지보수가 쉬워집니다.
71-84: migrate 작업의 부하 및 실행 방식 고려
migrate()에서 모든MemberStat을 읽어와 Redis에 일괄 반영하고, 마지막에 전체 매치율 재계산을 수행하는 구조는 데이터 양이 많지 않은 환경에서는 충분히 단순하고 이해하기 쉬운 접근입니다.다만 운영 환경에서
MemberStat수가 많아질 수 있다면:
- 배치 크기를 나눠서 처리하거나,
- 오프라인 배치/관리용 엔드포인트에서만 실행되도록 제약을 두고,
- 실행 시간/에러 여부를 모니터링할 수 있는 로그/메트릭을 남기는 것
정도만 추가로 고려해 두면 실제 마이그레이션 시 리스크를 줄이는 데 도움이 될 것 같습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/main/java/com/cozymate/cozymate_server/domain/member/service/MemberWithdrawService.java(6 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/lifestylematchrate/redis/service/LifestyleMatchRateCacheService.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/lifestylematchrate/repository/LifestyleMatchRateRepositoryService.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/lifestylematchrate/service/LifestyleMatchRateService.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatCreatedEvent.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatDeleteEvent.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatEventListener.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatModifiedEvent.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/command/DeleteCommand.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/command/SaveCommand.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/command/UpdateCommand.java(1 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/service/MemberStatCacheService.java(3 hunks)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/service/MemberStatCommandService.java(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/service/MemberStatCommandService.java (3)
src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/service/MemberStatCacheService.java (1)
Slf4j(32-351)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/converter/MemberStatConverter.java (1)
Slf4j(31-388)src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/util/MemberStatExtractor.java (1)
Slf4j(15-116)
🔇 Additional comments (12)
src/main/java/com/cozymate/cozymate_server/domain/memberstat/lifestylematchrate/repository/LifestyleMatchRateRepositoryService.java (1)
47-49: LGTM!메서드 구현이 기존 클래스의 패턴을 잘 따르고 있습니다. 단순 위임 패턴으로 레포지토리에 삭제 작업을 전달하며,
LifestyleMatchRateService의 멤버 삭제 플로우에서 적절히 사용됩니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/command/UpdateCommand.java (1)
6-11: LGTM!
oldAnswers와newAnswers를 모두 포함한 설계가 원자적 업데이트 연산에 적합합니다. PR의 목적인 Lua 스크립트 기반 원자적 연산을 지원하기 위한 올바른 구조입니다.참고:
memberId가String타입인 것은 다른 커맨드들과 일관성이 있지만, 관련 이벤트(MemberStatModifiedEvent)와의 타입 불일치는 위의 다른 커맨드 리뷰에서 언급한 것처럼 확인이 필요합니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/lifestylematchrate/redis/service/LifestyleMatchRateCacheService.java (2)
84-98: LGTM!
deleteAllRelatedTo메서드의 리팩토링이 잘 되었습니다:
- null 안전성: Lines 92-93에서 null 체크를 통해 NPE 방지
- 성능 개선: 모든 키를 수집한 후 단일 bulk delete 수행
- 불필요한 연산 방지: Line 95에서 빈 집합 체크로 불필요한 Redis 호출 방지
- 양방향 패턴 매칭:
memberId:*및*:memberId패턴으로 모든 관련 키 포착이 구현은 기존 방식보다 더 효율적이고 안전합니다.
100-105: LGTM!
generateKey메서드의 위치 이동은 단순 코드 정리입니다. 로직은 변경되지 않았으며,Math.min/Math.max를 사용하여 일관된 키 순서를 보장하는 구현은 올바릅니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/command/DeleteCommand.java (1)
5-9: 타입 변환이 의도적으로 처리되고 있음검증 결과,
MemberStatDeleteEvent의memberId(Long)에서DeleteCommand의memberId(String)로의 타입 변환은MemberStatEventListener.java83번 줄에서 명시적으로.toString()을 사용하여 처리되고 있습니다. 이는 Redis 키 생성을 위한 의도된 설계이며, 타입 불일치나 누락된 변환이 없습니다.Likely an incorrect or invalid review comment.
src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatModifiedEvent.java (1)
5-10: MemberStatModifiedEvent 레코드 정의 적절수정 전/후 답변을 모두 포함하는 불변 이벤트 레코드로 잘 설계되어 있고, 필드 구성이 리스너에서 사용하는 데이터와 일치합니다. 현재 요구사항 기준으로 추가적인 보일러플레이트 없이 이 상태로 충분해 보입니다.
src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/event/MemberStatEventListener.java (1)
51-90: 이벤트 기반 캐시·매치율 동기화 플로우 일관성
onCreated/onModified/onDelete가 모두
- 캐시 연산(
SaveCommand/UpdateCommand/DeleteCommand) →- 매치율 서비스 호출
순으로 처리되고, 세 연산 모두 동일한AFTER_COMMIT+runWithRetry패턴을 사용하고 있어 플로우가 일관적입니다. 멤버 생성/수정/탈퇴 케이스가 한 곳에서 정리되어 추후 유지보수도 용이해 보입니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/service/MemberStatCommandService.java (1)
34-45: 생성 시 이벤트 발행으로 책임 분리된 점 👍
createMemberStat에서 캐시/매치율 갱신을 직접 호출하지 않고,MemberStatCreatedEvent만 발행하도록 분리한 것은 관심사의 분리에 잘 맞는 구조로 보입니다. 엔티티 저장 후MemberStatExtractor를 통해 일관된 answer 맵을 만들고, 나머지 처리는 리스너에 위임하는 방향이 명확합니다.src/main/java/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/service/MemberStatCacheService.java (3)
49-67: Lua 스크립트 기반 upsert/delete 설계 적절
upsertScript와deleteScript가
- 첫 번째 키를 pool,
- 이후 키들을 lifestyle set으로 사용하고,
- ARGV로 uid 및 remove/add 개수를 넘기는 구조가 Java 쪽 키 구성(
poolKey + removes + adds)과 잘 맞습니다.이전의 다중 SADD/SREM 연산을 하나의 Lua 스크립트로 묶어 원자성을 확보한 점이 이번 PR 목적(동시성 이슈 해소)에 정확히 부합해 보입니다.
Also applies to: 70-82, 84-108
135-159: 필터 + hasRoom 조합 로직 일관성 확인
filterUsers와excludeUsersInHasRoom조합이 다음과 같이 잘 맞춰져 있는 것 같습니다.
getBasePool/getUnionedSetPerFilter단계에서normalizeUserId를 통해"123"/123포맷을 모두 정규화하여 digits-only 문자열 집합으로 유지- 교집합은 항상 이 정규화된 ID 기준으로 수행
hasRoom세트는encodeMemberId로"123"형태로 저장하지만,excludeUsersInHasRoom에서 다시normalizeUserId를 거쳐 동일 포맷으로 비교- 최종적으로
List<Long>로 변환해 상위 서비스에 반환따라서 ID 인코딩/디코딩 규칙이 읽기/쓰기 경로 전반에서 일관되게 유지되고 있고,
hasRoom == true/false/null세 가지 케이스도 모두 자연스럽게 처리되는 것으로 보입니다.Also applies to: 161-191, 206-231
267-299: diffKeys의 빈 값 처리 로직이 캐시 갱신 실패를 초래할 수 있음 - 검증 필요코드 분석 결과:
extractAnswers의 동작 확인:
toStringOrEmpty(): null 값을""로 변환- Multi-value 필드 (
sleepingHabits,personalities): 비트마스크 값이 0이면 빈 문자열 생성Lifestyle 엔티티 제약조건:
sleepingHabit:@Range(min = 1, max = 62)→ 0은 범위 위반personality:@Range(min = 1, max = 4095)→ 0은 범위 위반- 주석: "1 : 잠버릇 없음"으로 명시 (1이 "선택 없음" 상태)
문제점:
- 도메인 제약(
@Range(min=1))이 모든 경로에서 강제되면 0/빈 값은 불가능- 하지만 검증이 우회되거나 레거시 데이터가 있다면,
sleepingHabit=0또는personality=0상태의 레코드 발생 가능- 이 경우 extractAnswers → diffKeys → 예외 발생 → MemberStatEventListener 3회 재시도 후 실패로 끝남
- 결과: 해당 사용자의 Redis 인덱스가 영구적으로 오래된 상태 유지
MemberUniversityStat 필드:
dormitoryName,numberOfRoommate,acceptance: @range 제약 없음 → null 가능- 이들은 빈 문자열이 의도된 동작일 수 있음
권장사항:
- Lifestyle의
sleepingHabit/personality값이 실제로 항상 >= 1인지 검증 필요- 혹은 diffKeys에서 이들 필드에 대해 빈 문자열을 "값 없음"으로 논리적으로 처리 (예외 대신)
- 기존 데이터에 제약 위반 레코드가 있는지 확인
src/main/java/com/cozymate/cozymate_server/domain/member/service/MemberWithdrawService.java (1)
104-145: DB 삭제 후 LAZY 로딩 필드 접근으로 인한 LazyInitializationException 위험 확인코드 검증 결과:
MemberStat 엔티티의 LAZY 로딩 (라인 37):
Member필드가@OneToOne(fetch = FetchType.LAZY)로 설정되어 있습니다.삭제 순서 문제:
- 104줄에서
deleteMemberStat(member.getId())로 DB에서 MemberStat 삭제- 반환된
Optional<MemberStat>의 엔티티에서 LAZY 필드(member) 미초기화 상태- 143줄
MemberStatExtractor.extractAnswers(stat)호출 시 embedded 필드들과 함께 member.getUniversity() 같은 연관된 필드 접근 가능lifestyleMatchRate 중복 삭제 (선택적):
- 107줄:
lifestyleMatchRateRepository.deleteAllByMemberId(member.getId())- 이벤트 리스너(MemberStatEventListener 88줄):
lifestyleMatchRateService.deleteAllMatchRateByMemberId(e.memberId())
두 곳에서 중복으로 삭제 수행개선 권장사항:
extractAnswers호출을 DB 삭제 전으로 이동하거나, deleteMemberStat 내부에서 삭제 전 answers를 추출해Optional<Map<String, String>>형태로 반환- lifestyleMatchRate 중복 삭제 제거 (107줄 또는 이벤트 리스너 중 하나)
.../cozymate_server/domain/memberstat/lifestylematchrate/service/LifestyleMatchRateService.java
Show resolved
Hide resolved
...ava/com/cozymate/cozymate_server/domain/memberstat/memberstat/redis/command/SaveCommand.java
Show resolved
Hide resolved
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class MemberStatEventListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tx 커밋을 이벤트에 따라 동작하는 메소드들을 만들어놨습니다.
기본적을 3회까지 재시도합니다.
| "for i=2,#KEYS do redis.call('SREM', KEYS[i], uid) end\n" + | ||
| "return 1\n"; | ||
| deleteScript = new DefaultRedisScript<>(del, Long.class); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
원자적 연산을 위한 lua script
| package com.cozymate.cozymate_server.domain.memberstat.memberstat.redis.command; | ||
|
|
||
| import java.util.Map; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lua script 적용을 위해 command 객체를 만들었습니다.
answers를 문자열로 변환하기 위함입니다.
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class MemberStatCacheService { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전반적으로 같은 기능을 하는데 Lua를 적용한걸로 바뀐 코드들입니다.
| if (!val.isBlank() && !newSet.contains(val)) { | ||
| removeLifestyle(universityId, questionKey, val.trim(), userId); | ||
| } | ||
| private String encodeMemberId(String memberId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 함수는 기존에 key 구조가
university:1:gender:1:lifestyle:wakeUpTime:2가 아니라university:1:gender:1:lifestyle:wakeUpTime:"2"로 되어있어서 그 구조를 유지하기 위함입니다. 기존 구조가 불명확해 수정해야하지만 이 pr이 머지된 다음에 마이그레이션하고 반영하도록 하겠습니다.
| return "\"" + memberId + "\""; | ||
| } | ||
|
|
||
| private String normalizeUserId(String raw) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위와 마찬가지긴 한데 key 구조를 둘다 적용시키기위해 정규화하는것입니다.
⚒️develop의 최신 커밋을 pull 받았나요?
#️⃣ 작업 내용
단위기능에는 변화가 없습니다.
사용자 필터를 위한 redis 집합 연산이 원자적이지 않는 문제가 있었습니다.
트래픽이 낮은 지금은 문제가 없지만 여러 사용자가 동시에 라이프스타일을 수정해서 같은 집합을 수정한 경우엔 문제가 생길수 있습니다.
그래서 루아스크립트를 통해 원자적 연산을 수행합니다.
또, 트랜잭션이 커밋되면 레디스에 저장하게끔 커밋이벤트를 발행하고 레디스에 저장하게끔 설계했습니다.
동작 확인
라스 수정에 성공합니다.

로컬서버에 필터 연산

운영서버에 필터 연산

텍스트비교기: 개행이나 공백말곤 다른게 없어요

텍스트 비교 돌린거
```json { "isSuccess": true, "code": "COMMON200", "message": "성공입니다.", "result": { "page": 0, "hasNext": true, "memberList": [ { { "memberDetail": { "memberId": 67, "nickname": "앙규", "gender": "MALE", "birthday": "2004-10-14", "universityName": "인하대학교", "universityId": 1, "majorName": "간호학과", "persona": 12 }, }, "equality": 82, "preferenceStats": [ { { "stat": "wakeUpTime", "value": "10", "color": "blue" }, { }, { "stat": "turnOffTime", "value": "23", "color": "blue" }, { }, { "stat": "heatingIntensity", "value": "약하게 틀어요", "color": "blue" }, { }, { "stat": "coolingIntensity", "value": "약하게 틀어요", "color": "blue" } ] } ] }, { "memberDetail": { "memberId": 66, "nickname": "이서준", "gender": "MALE", "birthday": "2004-09-09", "universityName": "인하대학교", "universityId": 1, "majorName": "스포츠과학과", "persona": 9 }, }, "equality": 75, "preferenceStats": [ { { "stat": "wakeUpTime", "value": "10", "color": "blue" }, { { "stat": "turnOffTime", "value": "23", "color": "blue" }, { { "stat": "heatingIntensity", "value": "약하게 틀어요", "color": "blue" }, { { "stat": "coolingIntensity", "value": "약하게 틀어요", "color": "blue" } ] }, { } ] }, { "memberDetail": { "memberId": 25, "nickname": "레오야", "gender": "MALE", "birthday": "2001-11-22", "universityName": "인하대학교", "universityId": 1, "majorName": "신소재공학과", "persona": 1 }, "equality": 68, "preferenceStats": [ { "stat": "wakeUpTime", "value": "9", "color": "red" }, }, { { "stat": "turnOffTime", "value": "12", "color": "red" }, { { "stat": "heatingIntensity", "value": "적당하게 틀어요", "color": "red" }, { { "stat": "coolingIntensity", "value": "약하게 틀어요", "color": "blue" } ] ] }, { "memberDetail": { "memberId": 73, "nickname": "강현", "gender": "MALE", "birthday": "2005-04-07", "universityName": "인하대학교", "universityId": 1, "majorName": "기계공학과", "persona": 1 }, }, "equality": 67, "preferenceStats": [ { { "stat": "wakeUpTime", "value": "6", "color": "red" }, }, { "stat": "turnOffTime", "value": "22", "color": "red" }, }, { "stat": "heatingIntensity", "value": "적당하게 틀어요", "color": "red" }, }, { "stat": "coolingIntensity", "value": "적당하게 틀어요", "color": "red" } ] }, { } ] }, { "memberDetail": { "memberId": 71, "nickname": "윌슨", "gender": "MALE", "birthday": "2003-07-08", "universityName": "인하대학교", "universityId": 1, "majorName": "경영학과", "persona": 1 }, }, "equality": 64, "preferenceStats": [ { { "stat": "wakeUpTime", "value": "7", "color": "red" }, { }, { "stat": "turnOffTime", "value": "22", "color": "red" }, { }, { "stat": "heatingIntensity", "value": "약하게 틀어요", "color": "blue" }, { }, { "stat": "coolingIntensity", "value": "적당하게 틀어요", "color": "red" } ] } ] } ] } }