2021. 6. 24. 17:01ใ๐ฑ Develop/Server
[Springboot] '์ต๊ทผ ์ฝ์ ๊ธ' Dto๋ก Redis์ ์ ์ฅํ๊ณ ์กฐํํ๊ธฐ AtoZ
โ redis ๋ฅผ ์ ํํ ์ด์ ๋ธ๋ก๊ทธ ํํ์ด์ง๋ฅผ ๋ง๋๋ ํ๋ก์ ํธ์์, "์ต๊ทผ ์ฝ์ ๊ธ" ์ ๊ฐ๋ฐํด์ผํ๋ ์ํฉ์ด ์์์ต๋๋ค. velog ๊ฐ์ ์๋น์ค์์๋ ๊ฝค ์ค๋ ๊ธฐ๊ฐ๋์ ๋ด๊ฐ ์ฝ์ ๊ธ์ ์ ์ฅํ๋ ๋ฏ ํ
tape22.tistory.com
์ด์ ๊ธ์์๋ ๊ฐ๋จํ๊ฒ 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
zepinos BLOG
zepinos SW Developer Blog.
blog.zepinos.com
Spring์์ Redis ZSET๊ตฌ์กฐ ์ด์ฉํ๊ธฐ
Redis์ ZSET๊ตฌ์กฐ๋ฅผ ์ด์ฉํ์ฌ LEADER BOARD ๊ตฌํํ๊ธฐ @Repository public class LeaderBoardRedisRepository { public static final String KEY = "leaderBoard"; @Autowired private RedisTemplate redisTemplate..
effectivesquid.tistory.com
ZSetOperations (Spring Data Redis 2.5.2 API)
Set reverseRangeByScore(K key, double min, double max, long offset, long count) Get elements in range from start to end where score is between min and max from sorted set ordered high -> low.
docs.spring.io