Skip to content

Commit 1d2e544

Browse files
committed
feature: leaderboard service에 캐시 레이어 추가
1 parent 396adba commit 1d2e544

File tree

2 files changed

+205
-106
lines changed

2 files changed

+205
-106
lines changed
Lines changed: 180 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,127 @@
11
import { Pool } from 'pg';
22
import { DBError } from '@/exception';
33
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
4-
import { LeaderboardService } from '@/services/leaderboard.service';
4+
import { LEADERBOARD_CACHE_TTL, LeaderboardService } from '@/services/leaderboard.service';
5+
import { ICache } from '@/modules/cache/cache.type';
56

67
jest.mock('@/repositories/leaderboard.repository');
8+
jest.mock('@/configs/cache.config', () => ({
9+
cache: {
10+
get: jest.fn(),
11+
set: jest.fn(),
12+
},
13+
}));
714

815
describe('LeaderboardService', () => {
916
let service: LeaderboardService;
10-
let repo: jest.Mocked<LeaderboardRepository>;
17+
let mockRepo: jest.Mocked<LeaderboardRepository>;
1118
let mockPool: jest.Mocked<Pool>;
19+
let mockCache: jest.Mocked<ICache>;
1220

1321
beforeEach(() => {
1422
const mockPoolObj = {};
1523
mockPool = mockPoolObj as jest.Mocked<Pool>;
1624

1725
const repoInstance = new LeaderboardRepository(mockPool);
18-
repo = repoInstance as jest.Mocked<LeaderboardRepository>;
26+
mockRepo = repoInstance as jest.Mocked<LeaderboardRepository>;
1927

20-
service = new LeaderboardService(repo);
28+
const { cache } = jest.requireMock('@/configs/cache.config');
29+
mockCache = cache as jest.Mocked<ICache>;
30+
31+
service = new LeaderboardService(mockRepo);
2132
});
2233

2334
afterEach(() => {
2435
jest.clearAllMocks();
2536
});
2637

2738
describe('getUserLeaderboard', () => {
28-
it('응답 형식에 맞게 변환된 사용자 리더보드 데이터를 반환해야 한다', async () => {
29-
const mockRawResult = [
39+
const mockRawResult = [
40+
{
41+
id: '1',
42+
43+
username: 'test',
44+
total_views: '100',
45+
total_likes: '50',
46+
total_posts: '1',
47+
view_diff: '20',
48+
like_diff: '10',
49+
post_diff: '1',
50+
},
51+
{
52+
id: '2',
53+
54+
username: 'test2',
55+
total_views: '200',
56+
total_likes: '100',
57+
total_posts: '2',
58+
view_diff: '10',
59+
like_diff: '5',
60+
post_diff: '1',
61+
},
62+
];
63+
64+
const mockResult = {
65+
users: [
3066
{
3167
id: '1',
3268
3369
username: 'test',
34-
total_views: '100',
35-
total_likes: '50',
36-
total_posts: '1',
37-
view_diff: '20',
38-
like_diff: '10',
39-
post_diff: '1',
70+
totalViews: 100,
71+
totalLikes: 50,
72+
totalPosts: 1,
73+
viewDiff: 20,
74+
likeDiff: 10,
75+
postDiff: 1,
4076
},
4177
{
4278
id: '2',
4379
4480
username: 'test2',
45-
total_views: '200',
46-
total_likes: '100',
47-
total_posts: '2',
48-
view_diff: '10',
49-
like_diff: '5',
50-
post_diff: '1',
81+
totalViews: 200,
82+
totalLikes: 100,
83+
totalPosts: 2,
84+
viewDiff: 10,
85+
likeDiff: 5,
86+
postDiff: 1,
5187
},
52-
];
53-
54-
const mockResult = {
55-
users: [
56-
{
57-
id: '1',
58-
59-
username: 'test',
60-
totalViews: 100,
61-
totalLikes: 50,
62-
totalPosts: 1,
63-
viewDiff: 20,
64-
likeDiff: 10,
65-
postDiff: 1,
66-
},
67-
{
68-
id: '2',
69-
70-
username: 'test2',
71-
totalViews: 200,
72-
totalLikes: 100,
73-
totalPosts: 2,
74-
viewDiff: 10,
75-
likeDiff: 5,
76-
postDiff: 1,
77-
},
78-
],
79-
};
80-
81-
repo.getUserLeaderboard.mockResolvedValue(mockRawResult);
88+
],
89+
};
90+
91+
beforeEach(() => {
92+
jest.clearAllMocks();
93+
});
94+
95+
it('응답 형식에 맞게 변환된 사용자 리더보드 데이터를 반환해야 한다', async () => {
96+
mockCache.get.mockResolvedValue(null);
97+
mockRepo.getUserLeaderboard.mockResolvedValue(mockRawResult);
98+
8299
const result = await service.getUserLeaderboard('viewCount', 30, 10);
83100

84101
expect(result.users).toEqual(mockResult.users);
85102
});
86103

87104
it('쿼리 파라미터가 올바르게 적용되어야 한다', async () => {
88-
repo.getUserLeaderboard.mockResolvedValue([]);
105+
mockCache.get.mockResolvedValue(null);
106+
mockRepo.getUserLeaderboard.mockResolvedValue([]);
89107

90108
await service.getUserLeaderboard('postCount', 30, 10);
91109

92-
expect(repo.getUserLeaderboard).toHaveBeenCalledWith('postCount', 30, 10);
110+
expect(mockRepo.getUserLeaderboard).toHaveBeenCalledWith('postCount', 30, 10);
93111
});
94112

95113
it('쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다', async () => {
96-
repo.getUserLeaderboard.mockResolvedValue([]);
114+
mockCache.get.mockResolvedValue(null);
115+
mockRepo.getUserLeaderboard.mockResolvedValue([]);
97116

98117
await service.getUserLeaderboard();
99118

100-
expect(repo.getUserLeaderboard).toHaveBeenCalledWith('viewCount', 30, 10);
119+
expect(mockRepo.getUserLeaderboard).toHaveBeenCalledWith('viewCount', 30, 10);
101120
});
102121

103122
it('데이터가 없는 경우 빈 배열을 반환해야 한다', async () => {
104-
repo.getUserLeaderboard.mockResolvedValue([]);
123+
mockCache.get.mockResolvedValue(null);
124+
mockRepo.getUserLeaderboard.mockResolvedValue([]);
105125

106126
const result = await service.getUserLeaderboard();
107127

@@ -111,91 +131,124 @@ describe('LeaderboardService', () => {
111131
it('쿼리 오류 발생 시 예외를 그대로 전파한다', async () => {
112132
const errorMessage = '사용자 리더보드 조회 중 문제가 발생했습니다.';
113133
const dbError = new DBError(errorMessage);
114-
repo.getUserLeaderboard.mockRejectedValue(dbError);
134+
135+
mockCache.get.mockResolvedValue(null);
136+
mockRepo.getUserLeaderboard.mockRejectedValue(dbError);
115137

116138
await expect(service.getUserLeaderboard()).rejects.toThrow(errorMessage);
117-
expect(repo.getUserLeaderboard).toHaveBeenCalledTimes(1);
139+
expect(mockRepo.getUserLeaderboard).toHaveBeenCalledTimes(1);
140+
});
141+
142+
it('캐시 히트 시 Repository를 호출하지 않고 캐시된 데이터를 반환해야 한다', async () => {
143+
mockCache.get.mockResolvedValue(mockResult);
144+
145+
const result = await service.getUserLeaderboard('viewCount', 30, 10);
146+
147+
expect(mockCache.get).toHaveBeenCalledWith('leaderboard:user:viewCount:30:10');
148+
expect(mockRepo.getUserLeaderboard).not.toHaveBeenCalled();
149+
expect(result).toEqual(mockResult);
150+
});
151+
152+
it('캐시 미스 시 Repository를 호출하고 결과를 캐싱해야 한다', async () => {
153+
mockCache.get.mockResolvedValue(null);
154+
mockRepo.getUserLeaderboard.mockResolvedValue(mockRawResult);
155+
156+
const result = await service.getUserLeaderboard('postCount', 30, 10);
157+
158+
expect(mockRepo.getUserLeaderboard).toHaveBeenCalledWith('postCount', 30, 10);
159+
expect(mockCache.set).toHaveBeenCalledWith('leaderboard:user:postCount:30:10', mockResult, LEADERBOARD_CACHE_TTL);
160+
expect(result).toEqual(mockResult);
118161
});
119162
});
120163

121164
describe('getPostLeaderboard', () => {
122-
it('응답 형식에 맞게 변환된 게시물 리더보드 데이터를 반환해야 한다', async () => {
123-
const mockRawResult = [
165+
const mockRawResult = [
166+
{
167+
id: '1',
168+
title: 'test',
169+
slug: 'test-slug',
170+
username: 'test',
171+
total_views: '100',
172+
total_likes: '50',
173+
view_diff: '20',
174+
like_diff: '10',
175+
released_at: '2025-01-01',
176+
},
177+
{
178+
id: '2',
179+
title: 'test2',
180+
slug: 'test2-slug',
181+
username: 'test2',
182+
total_views: '200',
183+
total_likes: '100',
184+
view_diff: '10',
185+
like_diff: '5',
186+
released_at: '2025-01-02',
187+
},
188+
];
189+
190+
const mockResult = {
191+
posts: [
124192
{
125193
id: '1',
126194
title: 'test',
127195
slug: 'test-slug',
128196
username: 'test',
129-
total_views: '100',
130-
total_likes: '50',
131-
view_diff: '20',
132-
like_diff: '10',
133-
released_at: '2025-01-01',
197+
totalViews: 100,
198+
totalLikes: 50,
199+
viewDiff: 20,
200+
likeDiff: 10,
201+
releasedAt: '2025-01-01',
134202
},
135203
{
136204
id: '2',
137205
title: 'test2',
138206
slug: 'test2-slug',
139207
username: 'test2',
140-
total_views: '200',
141-
total_likes: '100',
142-
view_diff: '10',
143-
like_diff: '5',
144-
released_at: '2025-01-02',
208+
totalViews: 200,
209+
totalLikes: 100,
210+
viewDiff: 10,
211+
likeDiff: 5,
212+
releasedAt: '2025-01-02',
145213
},
146-
];
147-
148-
const mockResult = {
149-
posts: [
150-
{
151-
id: '1',
152-
title: 'test',
153-
slug: 'test-slug',
154-
username: 'test',
155-
totalViews: 100,
156-
totalLikes: 50,
157-
viewDiff: 20,
158-
likeDiff: 10,
159-
releasedAt: '2025-01-01',
160-
},
161-
{
162-
id: '2',
163-
title: 'test2',
164-
slug: 'test2-slug',
165-
username: 'test2',
166-
totalViews: 200,
167-
totalLikes: 100,
168-
viewDiff: 10,
169-
likeDiff: 5,
170-
releasedAt: '2025-01-02',
171-
},
172-
],
173-
};
174-
175-
repo.getPostLeaderboard.mockResolvedValue(mockRawResult);
214+
],
215+
};
216+
217+
beforeEach(() => {
218+
jest.clearAllMocks();
219+
});
220+
221+
it('응답 형식에 맞게 변환된 게시물 리더보드 데이터를 반환해야 한다', async () => {
222+
mockCache.get.mockResolvedValue(null);
223+
mockRepo.getPostLeaderboard.mockResolvedValue(mockRawResult);
224+
176225
const result = await service.getPostLeaderboard('viewCount', 30, 10);
177226

178227
expect(result.posts).toEqual(mockResult.posts);
179228
});
180229

181230
it('쿼리 파라미터가 올바르게 적용되어야 한다', async () => {
182-
repo.getPostLeaderboard.mockResolvedValue([]);
231+
mockCache.get.mockResolvedValue(null);
232+
mockRepo.getPostLeaderboard.mockResolvedValue([]);
183233

184234
await service.getPostLeaderboard('likeCount', 30, 10);
185235

186-
expect(repo.getPostLeaderboard).toHaveBeenCalledWith('likeCount', 30, 10);
236+
expect(mockRepo.getPostLeaderboard).toHaveBeenCalledWith('likeCount', 30, 10);
187237
});
188238

189239
it('쿼리 파라미터가 입력되지 않은 경우 기본값으로 처리되어야 한다', async () => {
190-
repo.getPostLeaderboard.mockResolvedValue([]);
240+
mockCache.get.mockResolvedValue(null);
241+
mockRepo.getPostLeaderboard.mockResolvedValue([]);
191242

192243
await service.getPostLeaderboard();
193244

194-
expect(repo.getPostLeaderboard).toHaveBeenCalledWith('viewCount', 30, 10);
245+
expect(mockRepo.getPostLeaderboard).toHaveBeenCalledWith('viewCount', 30, 10);
195246
});
196247

197248
it('데이터가 없는 경우 빈 배열을 반환해야 한다', async () => {
198-
repo.getPostLeaderboard.mockResolvedValue([]);
249+
mockCache.get.mockResolvedValue(null);
250+
mockRepo.getPostLeaderboard.mockResolvedValue([]);
251+
199252
const result = await service.getPostLeaderboard();
200253

201254
expect(result).toEqual({ posts: [] });
@@ -204,10 +257,33 @@ describe('LeaderboardService', () => {
204257
it('쿼리 오류 발생 시 예외를 그대로 전파한다', async () => {
205258
const errorMessage = '게시물 리더보드 조회 중 문제가 발생했습니다.';
206259
const dbError = new DBError(errorMessage);
207-
repo.getPostLeaderboard.mockRejectedValue(dbError);
260+
261+
mockCache.get.mockResolvedValue(null);
262+
mockRepo.getPostLeaderboard.mockRejectedValue(dbError);
208263

209264
await expect(service.getPostLeaderboard()).rejects.toThrow(errorMessage);
210-
expect(repo.getPostLeaderboard).toHaveBeenCalledTimes(1);
265+
expect(mockRepo.getPostLeaderboard).toHaveBeenCalledTimes(1);
266+
});
267+
268+
it('캐시 히트 시 Repository를 호출하지 않고 캐시된 데이터를 반환해야 한다', async () => {
269+
mockCache.get.mockResolvedValue(mockResult);
270+
271+
const result = await service.getPostLeaderboard('viewCount', 30, 10);
272+
273+
expect(mockCache.get).toHaveBeenCalledWith('leaderboard:post:viewCount:30:10');
274+
expect(mockRepo.getPostLeaderboard).not.toHaveBeenCalled();
275+
expect(result).toEqual(mockResult);
276+
});
277+
278+
it('캐시 미스 시 Repository를 호출하고 결과를 캐싱해야 한다', async () => {
279+
mockCache.get.mockResolvedValue(null);
280+
mockRepo.getPostLeaderboard.mockResolvedValue(mockRawResult);
281+
282+
const result = await service.getPostLeaderboard('likeCount', 30, 10);
283+
284+
expect(mockRepo.getPostLeaderboard).toHaveBeenCalledWith('likeCount', 30, 10);
285+
expect(mockCache.set).toHaveBeenCalledWith('leaderboard:post:likeCount:30:10', mockResult, LEADERBOARD_CACHE_TTL);
286+
expect(result).toEqual(mockResult);
211287
});
212288
});
213289
});

0 commit comments

Comments
 (0)