2021. 6. 24. 17:01ใ๐ฑ Develop/Server
์ด์ ๊ธ์์๋ ๊ฐ๋จํ๊ฒ redis์ list๋ก ์กฐํํ ๊ธ Dto๋ฅผ ์ ์ฅํ์์ต๋๋ค. ํ์ง๋ง ์ค์ ๋ก ํ๋ก ํธ์ ๋ฐฐํฌ๋ฅผ ํ๊ณ ๋๋, ํฌ๊ฒ ๋ ๊ฐ์ง ์ด์๋ฅผ ๊ฐ๊ณผํ๊ณ ์์ด์ ๋ ๋ฒ์งธ ๊ธ์ ์ฐ๊ฒ ๋์์ต๋๋ค.
ํ์ฌ ๋ก์ง์ผ๋ก๋ postIdx ๋ฅผ ํตํด ํด๋น post ์กฐํ ๊ฐ์ Dto๋ก ๋ณํํ๊ณ , ์ด Dto๋ฅผ list์ ์ ์ฅํ๋ ๋ฐฉ์์ธ๋ฐ, "์กฐํ ๋ด์ฉ ๊ฒฐ๊ณผ"๋ฅผ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์๊น๋๋ค.
- post ์ ๊ธ์ด ์์ , ์ญ์ ๋์์ ๋ ๋ด์ฉ์ด ๋ฐ์๋์ง ์๋๋ค.
- list๋ ์ค๋ณต์ ํ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ฐ์ ๊ธ์ ์ฌ๋ฌ ๋ฒ ์ฝ์ผ๋ฉด ๋๊ฐ์ ๊ธ์ด redis์ ์์ธ๋ค.
๊ทธ๋์ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํ์์ต๋๋ค.
๐ List ํ์ ์์ '์ ๋ ฌ์ด ๊ฐ๋ฅํ set ํ์ '์ธ zSet ๋ก ์ค๋ณต์ ์ ๊ฑฐํ๊ณ , ์ต๊ทผ ์ฝ์ ์์๋๋ก ์ ๋ ฌํ๋๋ก ๋ณ๊ฒฝ
๐ ์กฐํ ๋ด์ฉ ๊ฒฐ๊ณผ ์ ์ฅ์ด ์๋๋ผ postIdx๋ฅผ redis์ ์ ์ฅ
1. RecentLog ์์
๋จผ์ recentLog ๋๋ฉ์ธ๋ถํฐ ์์ ํด์ผํฉ๋๋ค. ์ด์ ์๋ userIdx์ postDto.ListDto ๋ฅผ ์ ์ฅํ๋๋ก ํ์ง๋ง, ์ง๊ธ์ postIdx๋ฅผ ์ ์ฅํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ Long ํ์ ์ผ๋ก ์์ ํด์ค๋๋ค.
๐ domain/RecentLog
@RedisHash("userIdx")
@Getter @ToString @NoArgsConstructor @AllArgsConstructor @Builder
public class RecentLog {
@Id
private Long userIdx;
private Long postIdx;
}
2. Service
1๏ธโฃ Redis์ ์ ์ฅํ๊ธฐ
RedisTemplate ๋ <String, PostDto.List> ์์ <String, RecentLog> ๋ก ๋ณ๊ฒฝํ๊ณ , opsForList() ๋ฅผ opsForZSet() ์ผ๋ก ๋ณ๊ฒฝํด์ค๋๋ค. RedisTemplate ์๋ ๋ค์ํ ๋ฉ์๋๋ค ์ค๋น๋์ด ์๋๋ฐ, RedisTemplate๋ก ๋ค์ด๊ฐ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ฉ์๋๋ค์ ํ์ธํ ์ ์์ต๋๋ค.
์์ ๋งํ๋ฏ์ด, Set์ด ์๋๋ผ ZSet์ ๊ณ ๋ฅธ ์ด์ ๋ '์์'๊ฐ ์๊ณ ์๊ณ ์ ์ฐจ์ด๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์๋ฅผ ๋ค์ด์ Set์ผ๋ก postIdx๋ฅผ ์ ์ฅํ๋ฉด ์ฝ์ ์์์ ์๊ด์์ด ์ ์ฅ์ด ๋ฉ๋๋ค. ํ์ง๋ง '์ต๊ทผ ์ฝ์ ๊ธ'์ ๋ด๊ฐ ๊ฐ์ฅ ์ต๊ทผ์ ์ฝ์ ์์๋๋ก ์ ๋ ฌ์ด ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์, ZSet์ผ๋ก ์ค์ ํด์ฃผ์์ต๋๋ค.
๐ listService
private final PostMapper postMapper;
private final PostRepository postRepository;
private final RedisTemplate<String, RecentLog> redisTemplate;
// ์ต๊ทผ ์ฝ์ ๊ธ ์ ์ฅ -> set์ผ๋ก ๋ณ๊ฒฝ, postIdx ์ ์ฅ
@Transactional
public void saveRecentReadPosts(Long userIdx, Long postIdx){
ZSetOperations<String, RecentLog> zSetOps = redisTemplate.opsForZSet();
RecentLog recentLog = RecentLog.builder().userIdx(userIdx).postIdx(postIdx).build();
zSetOps.add(setKey(userIdx), recentLog, new java.util.Date().getTime()); // score์ ํ์์คํฌํ(์ต์ ์ฝ์ ์๋๋ก ์ ๋ ฌ์ํด)
redisTemplate.expireAt(setKey(userIdx), Date.from(ZonedDateTime.now().plusDays(7).toInstant())); // ์ ํจ๊ธฐ๊ฐ TTL ์ผ์ฃผ์ผ ์ค์
}
- zSetOps์๋ add๋ฅผ ํ ๋ (key ๊ฐ, ์ ์ฅํ ๊ฐ, score) ๊ฐ ํ์ํฉ๋๋ค. ์ด์ ๊ณผ ๋ฌ๋ฆฌ score์ด๋ ๊ฐ์ด ์ถ๊ฐ๋์์ต๋๋ค. ์ฌ๊ธฐ์ score์ ๊ฐ์ ์ ๋ ฌ์ํ๋ก ์ ์งํ๊ธฐ ์ํด ํ์ํ ๊ฐ์ผ๋ก, ์ฝ๊ฒ ์ค๋ช ํ์๋ฉด userIdx::2๋ผ๋ "key"๋ก ์กฐํํ๋ฉด key(recentLog) : value (score)์ set ๊ฐ์ ์กฐํํฉ๋๋ค.
- list ๋์ ๋์ผํ๊ฒ redisTemplate๋ก ์ ํจ๊ธฐ๊ฐ์ 7์ผ๋ก ์ค์ ํฉ๋๋ค.
2๏ธโฃ Redis์์ ์กฐํํ๊ธฐ
๋ค์์ ์กฐํํ๋ ๊ธฐ๋ฅ์ ์์ ํด๋ณด๊ฒ ์ต๋๋ค.
๐ listService
// ์ต๊ทผ ์ฝ์ ๊ธ ์กฐํ
@Transactional
public List<PostDto.ListResponse> findRecentPosts(Long userIdx) {
ZSetOperations<String, RecentLog> zSetOps = redisTemplate.opsForZSet();
ObjectMapper objectMapper = new ObjectMapper(); // linkedHashMap์ผ๋ก ์ ์ฅ๋ redis ๊ฐ๋ค์ List๋ก ๋ณํํด์ค
List<RecentLog> result = objectMapper.convertValue(Objects.requireNonNull(zSetOps.reverseRange(setKey(userIdx), 0, -1)),
new TypeReference<List<RecentLog>>() {
});
return result.stream().map(x -> postMapper.postToListResponse(findPostById(x.getPostIdx()), userIdx)).collect(Collectors.toList());
}
private String setKey(Long userIdx){
return "userIdx::"+userIdx;
}
- zSetOps์์ reverseRange๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ฅ ๋์ค์ ์ถ๊ฐ๋ ๊ธ( == ๊ฐ์ฅ ์ต๊ทผ์ ์ฝ์ ๊ธ)์ ๋จผ์ ์ค๋๋ก ๊ฐ์ ๊ฐ์ ธ์ต๋๋ค. reverseRange(key๊ฐ, ์์ ์ธ๋ฑ์ค, ๋ ์ธ๋ฑ์ค)๋ฅผ ์ค์ ํ์ฌ ํ๊บผ๋ฒ์ ๊ฐ์ ๊ฐ์ ธ์์ต๋๋ค.
- ํ์ง๋ง ์ด๋๋ก Redis์์ ์กฐํํ ๊ฐ์ map์ผ๋ก dto ๋ณํํ๋ คํ๋ฉด ํด๋น ์๋ฌ๊ฐ ๋ฉ๋๋ค.
java.util.LinkedHashMap cannot be cast to ......List๋ dto.....ํด๋น ์๋ฌ๊ฐ ๋๋ ์ด์ ๋ redis์์๋ LinkedHashMap์ผ๋ก ๊ฐ์ด ์ ์ฅ๋๋๋ฐ, zSetOperations ๋ก ์ ์ฅํ๋ฉด [ {ํ๋}, {๋}, {์ } ]... ์ด๋ฐ ์์ผ๋ก ์ ์ฅ๋์ด ์กฐํ๋ฉ๋๋ค. ๊ทธ๋์ ์ด ๋ฐฐ์ด์ ๊ฐ๊ณ ์์ dto๋ก ๋ณํํ๋ ค๊ณ ํ๋ฉด,์๋ฌ๋ฅผ ๋ง์ดํ๋ ๊ฒ์ ๋๋ค. - ๊ทธ๋์ objectMapper์ ์ฌ์ฉํด์ LinkedHashMap์ list๋ก ๋ณํํ ๋ค, dto๋ก ๋ค์ ๋ฐ๊ฟ์ฃผ์ด์ผํฉ๋๋ค.
ObjectMapper objectMapper = new ObjectMapper(); // linkedHashMap์ผ๋ก ์ ์ฅ๋ redis ๊ฐ๋ค์ List๋ก ๋ณํํด์ค List<RecentLog> result = objectMapper.convertValue(Objects.requireNonNull(zSetOps.reverseRange(setKey(userIdx), 0, -1)), new TypeReference<List<RecentLog>>() { });
- ์ด ์ดํ๋ก๋ ํ์ ํ๋๋๋ก list๋ฅผ dto๋ก ๋ฐ๊พธ์ด ๋ฆฌํดํด์ฃผ๋ฉด ๋ฉ๋๋ค.
3๏ธโฃ ์๊ธ ์ญ์ ์ redis์์๋ ๊ฐ ์ญ์
์๊ธ์ ์์ ์ฌํญ์ด ๋ฐ์๋์ง ์๋ ๋ฌธ์ ์ธ์๋, ๊ธ์ด ์ญ์ ๋๋ฉด "์ต๊ทผ ์กฐํํ ๊ธ" ๋ชฉ๋ก์์๋ ์์ฐ์ค๋ฝ๊ฒ ์ญ์ ๋์ด์ผ ํฉ๋๋ค. ๊ทธ๋์ ์ด๋ฒ์๋ postService๋ฅผ ๊ฐ์ด ์์ ํด์ฃผ๊ฒ ์ต๋๋ค.
๐ postService
// ํ๊ณ ๊ธ ์ญ์
@Transactional
public boolean deletePosts(Long userIdx,Long postIdx) {
Post post = postRepository.findById(postIdx)
.orElseThrow(() -> new EntityNullException(ErrorInfo.POST_NULL));
if (isWriter(post.getUser().getUserIdx(), userIdx)){
postRepository.deleteById(postIdx);
if (listService.isPostsExist(userIdx, postIdx)) listService.deleteRedisPost(userIdx, postIdx); // redis ์์๋ ์ญ์
return true;
}
return false;
}
- isPostExist ๋ฉ์๋๋ก "Redis์ ํด๋น postIdx" ๊ฐ ์๋์ง ๊ฒ์ฌํฉ๋๋ค.
- ๋ง์ฝ ๊ธ์ด ์์ผ๋ฉด ์๊ธ์ด ์ญ์ ๋ ๋ Redis์์๋ ๊ฐ์ด ์ญ์ ํด์ฃผ๋ deleteRedisPost ๋ฅผ ์คํํฉ๋๋ค.
๐ listService
Redis์ ๊ฐ์ด ์กด์ฌํ๋์ง ์ฒดํฌํ๋ ๋ฉ์๋์ ๋๋ค. "์ต๊ทผ ์ฝ์ ๊ธ ์กฐํ" ์ ๋์ผํ๊ฒ Redis์ ์๋ ๋ชฉ๋ก๋ค์ ๊ฐ์ ธ์์ ํด๋น postIdx๋ฅผ ํฌํจํ๊ณ ์๋์ง ํ๋ณํฉ๋๋ค.
public boolean isPostsExist(Long userIdx, Long postIdx){
ZSetOperations<String, RecentLog> zSetOps = redisTemplate.opsForZSet();
RecentLog recentLog = RecentLog.builder().userIdx(userIdx).postIdx(postIdx).build();
ObjectMapper objectMapper = new ObjectMapper();
List<RecentLog> result = objectMapper.convertValue(Objects.requireNonNull(zSetOps.reverseRange(setKey(userIdx), 0, -1)),
new TypeReference<List<RecentLog>>() {
});
return result.contains(recentLog);
}
์ญ์ ํ ๋๋ RecentLog ๊ฐ์ ์ค์ key๋ก ์ ์ฅํ์ผ๋ฏ๋ก, key๋ฅผ ์ฐพ์ ์ญ์ ํ๋ remove ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ค๋๋ค.
// redis์์ value ์ญ์
public void deleteRedisPost(Long userIdx,Long postIdx){
ZSetOperations<String, RecentLog> zSetOps = redisTemplate.opsForZSet();
RecentLog recentLog = RecentLog.builder().userIdx(userIdx).postIdx(postIdx).build();
zSetOps.remove(setKey(userIdx), recentLog);
}
3. ๊ฒฐ๊ณผ
swagger์์ ํ ์คํธํด๋ณด๊ฒ ์ต๋๋ค. ํ์ฌ userIdx๋ accessToken์์ ๋ฐ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ๋๊ฒจ์ฃผ๋ ๊ฐ ์์ด ์คํ๋ง ์์ผ์ฃผ์์ต๋๋ค.
{
"statusCode": 200,
"responseMessage": "์ต๊ทผ ์ฝ์ ๊ธ ์กฐํ ์ฑ๊ณต",
"data": [
{
"postIdx": 67,
"title": "KB ์นด๋ ๊ณต๋ชจ์ ์ ํ๋ฉด์ ์งํํ๋ ํ์๋ค์ ํ๊ณ ํ๊ณ ์ํ๋ค.",
"category": "plan",
"contents": "<div contenteditable=\"true\" class=\"tui-editor-contents\" style=\"min-height: 168px;\"><h1><div>1. Drop<br></div></h1><ul><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">๋์ด์ง๋ ํ์์๊ฐ</span><br></li><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">๋ฏธ์ํ ์ค๋น์ํ</span><br></li><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">๊ฐ์ ์ ์ธ ๋ฐ์๋ค</span><br></li><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">๊ฐ๊ฐ์ธ์ ํผ๊ณคํ ์ํ</span><br><br></li></ul><h1><div>2. Add<br></div></h1><ul><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">ํ์ ์๊ฑด์ ์ ์ํ์ฌ ๊ฐ๊ฒฐํ๊ฒ ํ์ ์งํ</span><br></li></ul><div><br></div><h1><div>3. Keep<br></div></h1><ul><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">ํธํ๊ฒ ์๊ฒฌ์ ๊ณต์ ํ๋ ๋ถ์๊ธฐ</span><br></li><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">ํ์ ํ ๋น ๋ฅธ ์ ๋ฆฌ ๋ฐ ํผ๋๋ฐฑ</span><br></li></ul><div><br></div><h1><div>4. Improve<br></div></h1><ul><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">ํธํ๊ฒ ์๊ฒฌ์ ๊ณต์ ํ์ง๋ง, ์ค๋น๋ ์ฒ ์ ํ๊ฒํ ๊ฒ</span><br></li><li><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">โ</span><span class=\"colour\" style=\"color: rgb(0, 0, 0);\">ํ์ ํ ์ ๋ฆฌ๊ฐ ๋น ๋ฅธ ๋งํผ ์ค๊ฐ ์๋ฃ ๊ณต์ ๋๋ ์ฒด๊ณ์ ์ผ๋ก ์ ๋ฆฌํ๊ธฐ</span><br><br></li></ul><div><br></div><div><br></div></div><div><br></div>",
"nickname": "์ผ๋ฏผ",
"profile": "https://s3doraboda.s3.ap-northeast-2.amazonaws.com/images/7/profiles/%EA%B3%A0%EC%96%91%EC%9D%B4.jpg",
"tagList": [
{
"tag": "ํ๋ฅด์๋์์์์ํ๊ธฐ"
}
],
"view": 242,
"createdAt": "May 30, 2021",
"commentCnt": 3,
"scrapCnt": 4,
"scrap": true
},
{
"postIdx": 153,
"title": "์ด๋ฏธ์ง ์์์ ์ฅ ํ
์คํธ์
๋๋ค.",
"category": "develop",
"contents": "์๋ฒ!์ด์ !์ด์ !์ด์ !ํต์ !ํต์ !ํต์ !์ผ์์!",
"nickname": "์ฉก",
"profile": "https://s3doraboda.s3.ap-northeast-2.amazonaws.com/images/2/profiles/%E1%84%8B%E1%85%AE%E1%86%BA%E1%84%8B%E1%85%A5%3F.jpeg",
"tagList": [
{
"tag": "์ด๋ ค์ค"
},
{
"tag": "ํ ์ ์์ด"
}
],
"view": 18,
"createdAt": "Jun 11, 2021",
"commentCnt": 0,
"scrapCnt": 0,
"scrap": false
},
{
"postIdx": 120,
"title": "sdf",
"category": "marketing",
"contents": "<div class=\"tui-editor-contents\" style=\"min-height: 168px;\"><h1><div>1. Plus<br></div></h1><div><br></div><div><br></div><h1><div>2. Minus<br></div></h1><div><br></div><div><br></div><h1><div>3. Interesting<br></div></h1><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div></div>",
"nickname": "์ ๋์ด",
"profile": "https://s3doraboda.s3.ap-northeast-2.amazonaws.com/images/3/profiles/img_20190529_001242.jpg",
"tagList": [
{
"tag": "lala"
}
],
"view": 85,
"createdAt": "Jun 11, 2021",
"commentCnt": 3,
"scrapCnt": 1,
"scrap": false
}
]
}
๐ค ์ต๊ทผ ์ฝ์ ๊ธ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๋ฉด์ ํ์ฌ ๋ฆฌํฉํ ๋ง์ด ํ์ํ๋ค๊ณ ์๊ฐํ ๋ถ๋ถ์, redis์ ๊ฐ์ด ์กด์ฌํ๋์ง ์ฒดํฌํ๋ ๋ถ๋ถ์์ "์ต๊ทผ ์ฝ์ ๊ธ ์กฐํ" ๊ธฐ๋ฅ๊ณผ ๋์ผํ๊ฒ ๋ค์ด๊ฐ๋ค๋ ์ ์ ๋๋ค. ์ด์ธ์๋ ์ค๋ณต๋๋ ์์๋ค์ด ์๊ฐ๋ณด๋ค ๋ง์ด ์์ด์ ๋ฉ์๋๋ก ๋ฐ๋ก ๋นผ๋ ๊ฒ์ด ๋ ๋์์ง ๋ณด๊ณ ,๋์๋ฆฌ ๋ฐํ ์ดํ๋ก ์์ ํด๋ณผ ์์ ์ ๋๋ค.
๐ ์ฐธ๊ณ ๋ฌธํ
http://blog.zepinos.com/java-redis/2017/09/09/Redis-ZSET-01.html