Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions data/migration_v2_postgis_geography.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- PostGIS geography ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + GIST ์ธ๋ฑ์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
-- ์‹คํ–‰: docker exec -i catchtable-db psql -U $DB_USER -d $DB_NAME < data/migration_v2_postgis_geography.sql
-- ๋กœ์ปฌ์—์„œ ์ด๋ฏธ ์‹คํ–‰ํ–ˆ๋‹ค๋ฉด ์šด์˜ ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰ํ•  ๊ฒƒ

-- 1. PostGIS ํ™•์žฅ ํ™œ์„ฑํ™”
CREATE EXTENSION IF NOT EXISTS postgis;

-- 2. stores ํ…Œ์ด๋ธ”์— geography ์ปฌ๋Ÿผ ์ถ”๊ฐ€
-- geometry ๋Œ€์‹  geography ์‚ฌ์šฉ: ::geography ์บ์ŠคํŒ… ์—†์ด GIST ์ธ๋ฑ์Šค ์ง์ ‘ ํ™œ์šฉ ๊ฐ€๋Šฅ
ALTER TABLE stores
ADD COLUMN IF NOT EXISTS location geography(Point, 4326);

-- 3. ๊ธฐ์กด latitude/longitude ๋ฐ์ดํ„ฐ๋ฅผ geography ์ปฌ๋Ÿผ์œผ๋กœ ๋ณต์‚ฌ
UPDATE stores
SET location = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
WHERE location IS NULL
AND latitude IS NOT NULL
AND longitude IS NOT NULL;

-- 4. GIST ๊ณต๊ฐ„ ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX IF NOT EXISTS idx_stores_location_gist
ON stores USING GIST (location);

-- 5. nearby ๋ฐ”์šด๋”ฉ๋ฐ•์Šค ์„ ํ•„ํ„ฐ์šฉ ๋ณตํ•ฉ B-tree ์ธ๋ฑ์Šค
CREATE INDEX IF NOT EXISTS idx_stores_lat_lng
ON stores (latitude, longitude)
WHERE is_deleted = false;

-- ๊ฒ€์ฆ
-- SELECT pg_typeof(location) FROM stores LIMIT 1; -- geography ์ด์–ด์•ผ ์ •์ƒ
-- SELECT COUNT(*) FROM stores WHERE location IS NULL; -- 0 ์ด์–ด์•ผ ์ •์ƒ
-- SELECT indexname FROM pg_indexes WHERE tablename = 'stores' AND indexname LIKE 'idx_stores%';
10 changes: 5 additions & 5 deletions grafana/provisioning/dashboards/catcheat-loadtest-dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -898,18 +898,18 @@
"type": "prometheus",
"uid": "DS_PROMETHEUS"
},
"expr": "{__name__=~\"k6_.*_spike_duration_p95\"} * 1000",
"expr": "label_replace({__name__=~\"k6_.*_spike_duration_p95\"} * 1000, \"scenario\", \"$1\", \"__name__\", \"k6_(.*)_spike_duration_p95\")",
"refId": "A",
"legendFormat": "spike p95 ({{__name__}})"
"legendFormat": "{{scenario}} spike p95"
},
{
"datasource": {
"type": "prometheus",
"uid": "DS_PROMETHEUS"
},
"expr": "{__name__=~\"k6_.*_ramp_duration_p95\"} * 1000",
"expr": "label_replace({__name__=~\"k6_.*_ramp_duration_p95\"} * 1000, \"scenario\", \"$1\", \"__name__\", \"k6_(.*)_ramp_duration_p95\")",
"refId": "B",
"legendFormat": "ramp p95 ({{__name__}})"
"legendFormat": "{{scenario}} ramp p95"
}
]
},
Expand Down Expand Up @@ -2070,7 +2070,7 @@
"type": "timeseries",
"targets": [
{
"expr": "sum by (kind) (rate(resilience4j_circuitbreaker_calls_total{name=~\"ai-api|chatbot\"}[1m]))",
"expr": "sum by (kind) (rate(resilience4j_circuitbreaker_calls_seconds_count{name=~\"ai-api|chatbot\"}[1m])) or label_replace(sum(rate(resilience4j_circuitbreaker_not_permitted_calls_total{name=~\"ai-api|chatbot\"}[1m])), \"kind\", \"not_permitted\", \"\", \"\")",
"legendFormat": "{{kind}}",
"refId": "A",
"datasource": {
Expand Down
1 change: 1 addition & 0 deletions k6/01-store-browse.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const rampDuration = new Trend('browse_ramp_duration', true);
const errorRate = new Rate('browse_error_rate');

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// โ”€โ”€ SCENARIO A: ์ด๋ฒคํŠธ ์ŠคํŒŒ์ดํฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Expand Down
3 changes: 1 addition & 2 deletions k6/02-reservation-concurrency.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ const errorRate = new Rate('reservation_error_rate');
const reserveDuration = new Trend('reservation_duration', true);

export const options = {
// stdout ๋ฉ”ํŠธ๋ฆญ์— p99 ๋…ธ์ถœ (๊ธฐ๋ณธ์€ p95๊นŒ์ง€๋งŒ)
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)'],
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {
// 100๋ช…์ด ๋™์‹œ์— ํ•œ๊บผ๋ฒˆ์— ์˜ˆ์•ฝ ์š”์ฒญ โ€” ์ŠคํŒŒ์ดํฌ ์‹œ๋‚˜๋ฆฌ์˜ค
spike: {
Expand Down
1 change: 1 addition & 0 deletions k6/03-full-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const stepDurations = {
const errorRate = new Rate('flow_error_rate');

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: ์ด๋ฒคํŠธ ์ŠคํŒŒ์ดํฌ โ€” 50๋ช… ์ฆ‰์‹œ ์ ํ”„๋กœ ์ด๋ฒคํŠธ ์˜คํ”ˆ ์ˆœ๊ฐ„ ์žฌํ˜„ (ํŒจํ„ด ๊ทผ๊ฑฐ๋Š” 01-store-browse.js ํ—ค๋”)
Expand Down
1 change: 1 addition & 0 deletions k6/05-store-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const rampDuration = new Trend('list_ramp_duration', true);
const errorRate = new Rate('store_list_error_rate');

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: ์ด๋ฒคํŠธ ์ŠคํŒŒ์ดํฌ โ€” 50๋ช… ์ฆ‰์‹œ ์ ํ”„๋กœ ์ด๋ฒคํŠธ ์˜คํ”ˆ ์ˆœ๊ฐ„ ์žฌํ˜„ (ํŒจํ„ด ๊ทผ๊ฑฐ๋Š” 01-store-browse.js ํ—ค๋”)
Expand Down
1 change: 1 addition & 0 deletions k6/06-store-nearby.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const LOCATIONS = [
];

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: ์ด๋ฒคํŠธ ์ŠคํŒŒ์ดํฌ โ€” 50๋ช… ์ฆ‰์‹œ ์ ํ”„๋กœ ์ด๋ฒคํŠธ ์˜คํ”ˆ ์ˆœ๊ฐ„ ์žฌํ˜„ (ํŒจํ„ด ๊ทผ๊ฑฐ๋Š” 01-store-browse.js ํ—ค๋”)
Expand Down
9 changes: 5 additions & 4 deletions k6/07-remains.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const STORE_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const REMAINS_SLA_MS = Number(__ENV.REMAINS_SLA_MS || 400);

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {

// SCENARIO A: ์ด๋ฒคํŠธ ์ŠคํŒŒ์ดํฌ โ€” 50๋ช… ์ฆ‰์‹œ ์ ํ”„๋กœ ์ด๋ฒคํŠธ ์˜คํ”ˆ ์ˆœ๊ฐ„ ์žฌํ˜„ (ํŒจํ„ด ๊ทผ๊ฑฐ๋Š” 01-store-browse.js ํ—ค๋”)
Expand Down Expand Up @@ -77,11 +78,11 @@ export default function () {
remainsDuration.add(res.timings.duration);
isSpike ? spikeDuration.add(res.timings.duration) : rampDuration.add(res.timings.duration);

const ok = check(res, {
'์ž”์—ฌ์„ 200': (r) => r.status === 200,
[`์‘๋‹ต ${REMAINS_SLA_MS}ms ์ด๋‚ด`]: (r) => r.timings.duration < REMAINS_SLA_MS,
});
// HTTP ์„ฑ๊ณต ์—ฌ๋ถ€๋งŒ errorRate์— ๋ฐ˜์˜ โ€” SLA ์ดˆ๊ณผ๋Š” ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ๋ณ„๋„ ์ง€ํ‘œ๋กœ ๊ด€์ฐฐ
const ok = check(res, { '์ž”์—ฌ์„ 200': (r) => r.status === 200 });
errorRate.add(!ok);
// SLA ์ฒดํฌ: threshold ์œ„๋ฐ˜ ์—ฌ๋ถ€๋Š” remains_duration p95/p99๋กœ ํŒ๋‹จ
check(res, { [`์‘๋‹ต ${REMAINS_SLA_MS}ms ์ด๋‚ด`]: (r) => r.timings.duration < REMAINS_SLA_MS });
});

sleep(0.5);
Expand Down
13 changes: 7 additions & 6 deletions k6/08-coupon-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* ๋ชฉ์ :
* - POST /coupons/{templateId}/issue ์— 100๋ช… ๋™์‹œ ์š”์ฒญ
* - ๋น„๊ด€์  ๋ฝ(SELECT FOR UPDATE)์œผ๋กœ ์ดˆ๊ณผ ๋ฐœ๊ธ‰ ๋ฐฉ์ง€ ๊ฒ€์ฆ
* - Redis Lua ์›์ž ๋ฐœ๊ธ‰(Redisson)์œผ๋กœ ์ดˆ๊ณผ ๋ฐœ๊ธ‰ ๋ฐฉ์ง€ ๊ฒ€์ฆ
* - ์žฌ๊ณ  ์†Œ์ง„ ํ›„ ์˜ฌ๋ฐ”๋ฅธ ์—๋Ÿฌ ์‘๋‹ต ํ™•์ธ
*
* ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ (๋‹ค์ค‘ ํ† ํฐ ๊ธฐ์ค€):
Expand Down Expand Up @@ -60,8 +60,7 @@ const issueDuration = new Trend('coupon_issue_duration', true);
const errorRate = new Rate('coupon_error_rate');

export const options = {
// stdout ๋ฉ”ํŠธ๋ฆญ์— p99 ๋…ธ์ถœ (๊ธฐ๋ณธ์€ p95๊นŒ์ง€๋งŒ โ†’ ๊ฒฐ๊ณผ ์ถœ๋ ฅ์—์„œ p99=0 ํ‘œ์‹œ ๋ฒ„๊ทธ ํ•ด์†Œ)
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)'],
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {
// ์„ ์ฐฉ์ˆœ ์ŠคํŒŒ์ดํฌ: 5์ดˆ ๋งŒ์— 50๋ช… ๋™์‹œ ์š”์ฒญ
spike: {
Expand All @@ -78,9 +77,11 @@ export const options = {
},
},
thresholds: {
coupon_issue_duration: ['p(95)<2000'], // ๋น„๊ด€์  ๋ฝ ๋Œ€๊ธฐ ํฌํ•จ 2์ดˆ ๊ธฐ์ค€
coupon_error_rate: ['rate<0.01'], // 5xx ์—๋Ÿฌ๋งŒ ์‹คํŒจ (์žฌ๊ณ  ์†Œ์ง„์€ ์ •์ƒ)
http_req_failed: ['rate<0.01'],
coupon_issue_duration: ['p(95)<2000'], // Redis Lua ๋ฐœ๊ธ‰ ๋Œ€๊ธฐ ํฌํ•จ 2์ดˆ ๊ธฐ์ค€
coupon_error_rate: ['rate<0.01'], // 5xx ์—๋Ÿฌ๋งŒ ์‹คํŒจ (์žฌ๊ณ  ์†Œ์ง„/์ค‘๋ณต์€ ์ •์ƒ 4xx)
// http_req_failed ์ œ๊ฑฐ: ์žฌ๊ณ  ์†Œ์ง„(400)/์ค‘๋ณต ๋ฐฉ์ง€(409)๊ฐ€ ์ •์ƒ ์‘๋‹ต์ด๋ฏ€๋กœ
// k6 ๋นŒํŠธ์ธ http_req_failed๊ฐ€ ์ด๋ฅผ ์ „๋ถ€ ์‹คํŒจ๋กœ ์ง‘๊ณ„ํ•ด ์˜คํƒ ๋ฐœ์ƒ
// ์‹ค์ œ ์žฅ์•  ํŒ๋‹จ์€ coupon_error_rate(5xx ์ „์šฉ)๋กœ๋งŒ ํ‰๊ฐ€
},
};

Expand Down
1 change: 1 addition & 0 deletions k6/10-soak.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const START_TIME = Date.now();
const LATE_PHASE_MS = 20 * 60 * 1000;

export const options = {
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
scenarios: {
// 3๋ถ„ ์›Œ๋ฐ์—… ํ›„ 10VU ์œ ์ง€ โ†’ 30๋ถ„ ์žฅ์‹œ๊ฐ„ ์šด์˜
soak: {
Expand Down
3 changes: 2 additions & 1 deletion k6/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Write-Host ""

# k6 v2.0+ ํ˜ธํ™˜ โ€” ๊ธฐ๋ณธ์€ p99๋งŒ ๋…ธ์ถœํ•˜๋ฏ€๋กœ p50/p90/p95๋„ ๋ช…์‹œ push
# ๋Œ€์‹œ๋ณด๋“œ Row A/B์˜ ๋ฐฑ๋ถ„์œ„ ํŒจ๋„์ด ์ž‘๋™ํ•˜๋ ค๋ฉด ํ•„์ˆ˜
$env:K6_PROMETHEUS_RW_TREND_STATS = 'p(50),p(90),p(95),p(99),min,max,avg'
$env:K6_PROMETHEUS_RW_TREND_STATS = 'p(50),p(90),p(95),p(99),min,max,avg'
$env:K6_PROMETHEUS_RW_PUSH_INTERVAL = '2s'

# PrometheusUrl ์ง€์ • ์‹œ์—๋งŒ remote_write ์‚ฌ์šฉ. ๋ฏธ์ง€์ • ์‹œ ์ฝ˜์†” ์ถœ๋ ฅ๋งŒ.
if ($PrometheusUrl) {
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/catchtable/bookmark/entity/Bookmark.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
import java.time.LocalDateTime;

@Entity
@Table(name = "bookmarks")
@Table(
name = "bookmarks",
indexes = {
@Index(name = "idx_bookmark_folder_deleted", columnList = "folder_id, is_deleted"),
@Index(name = "idx_bookmark_store_folder", columnList = "store_id, folder_id")
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.catchtable.bookmark.entity.Bookmark;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -13,6 +14,11 @@ public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
@Query("SELECT b FROM Bookmark b JOIN FETCH b.store WHERE b.folder.id = :folderId AND b.isDeleted = false")
List<Bookmark> findByFolderIdAndIsDeletedFalse(@Param("folderId") Long folderId);

// ํด๋” ์‚ญ์ œ ์‹œ ๋ฒŒํฌ ์†Œํ”„ํŠธ ์‚ญ์ œ โ€” forEach UPDATE N๋ฒˆ โ†’ UPDATE 1๋ฒˆ
@Modifying(clearAutomatically = true)
@Query("UPDATE Bookmark b SET b.isDeleted = true WHERE b.folder.id = :folderId AND b.isDeleted = false")
void softDeleteAllByFolderId(@Param("folderId") Long folderId);

Optional<Bookmark> findByIdAndIsDeletedFalse(Long id);

boolean existsByFolder_IdAndStore_IdAndIsDeletedFalse(Long folderId, Long storeId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ public BookmarkFolderUpdateResponse updateFolder(Long userId, Long folderId, Boo
public BookmarkFolderDeleteResponse deleteFolder(Long userId, Long folderId) {
BookmarkFolder folder = getEditableFolder(userId, folderId);

bookmarkRepository.findByFolderIdAndIsDeletedFalse(folderId)
.forEach(Bookmark::softDelete);
bookmarkRepository.softDeleteAllByFolderId(folderId);

folder.softDelete();
Comment on lines +98 to 100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

BookmarkRepository์˜ softDeleteAllByFolderId ๋ฉ”์„œ๋“œ์—๋Š” @Modifying(clearAutomatically = true) ์„ค์ •์ด ์ ์šฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ฒŒํฌ ์—…๋ฐ์ดํŠธ ์ฟผ๋ฆฌ ์‹คํ–‰ ์งํ›„ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ(Persistence Context)๊ฐ€ ์™„์ „ํžˆ ๋น„์›Œ์ง€๊ฒŒ(clear) ๋˜๋ฉฐ, ์ด์ „์— ์กฐํšŒํ–ˆ๋˜ folder ์—”ํ‹ฐํ‹ฐ๋Š” ์ค€์˜์†(Detached) ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๊ทธ ์ดํ›„์— ํ˜ธ์ถœ๋˜๋Š” folder.softDelete()๋Š” ์ค€์˜์† ์ƒํƒœ์˜ ๊ฐ์ฒด ํ•„๋“œ๋งŒ ๋ณ€๊ฒฝํ•  ๋ฟ, ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ์ ์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๋ฐ˜์˜(Flush)๋˜์ง€ ์•Š์•„ ํด๋” ์ž์ฒด๊ฐ€ ์‚ญ์ œ๋˜์ง€ ์•Š๋Š” ์‹ฌ๊ฐํ•œ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด folder.softDelete()๋ฅผ ๋ฒŒํฌ ์‚ญ์ œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ์ „์— ์‹คํ–‰ํ•˜๋„๋ก ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฒŒํฌ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ์˜ ์ž๋™ ํ”Œ๋Ÿฌ์‹œ(Auto-Flush)์— ์˜ํ•ด ํด๋”์˜ ์‚ญ์ œ ์ƒํƒœ๊ฐ€ DB์— ๋จผ์ € ๋ฐ˜์˜๋œ ํ›„ ๋ฒŒํฌ ์‚ญ์ œ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

Suggested change
bookmarkRepository.softDeleteAllByFolderId(folderId);
folder.softDelete();
folder.softDelete();
bookmarkRepository.softDeleteAllByFolderId(folderId);


Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/catchtable/global/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.catchtable.global.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Map;

@EnableCaching
@Configuration
public class CacheConfig {

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();

Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"popularStores", defaults.entryTtl(Duration.ofMinutes(5))
);

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaults.entryTtl(Duration.ofMinutes(10)))
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
7 changes: 6 additions & 1 deletion src/main/java/com/catchtable/menu/entity/Menu.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
import java.time.LocalDateTime;

@Entity
@Table(name = "menu")
@Table(
name = "menu",
indexes = {
@Index(name = "idx_menu_store_deleted", columnList = "store_id, is_deleted")
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Menu {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/catchtable/reservation/entity/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,28 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(
name = "reservations",
indexes = {
@Index(name = "idx_reservation_user_id", columnList = "user_id"),
@Index(name = "idx_reservation_user_status", columnList = "user_id, status"),
Comment on lines +33 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

๋ณตํ•ฉ ์ธ๋ฑ์Šค idx_reservation_user_status (user_id, status)๊ฐ€ ์กด์žฌํ•˜๋ฏ€๋กœ, ์„ ๋‘ ์ปฌ๋Ÿผ์ธ user_id ๋‹จ๋… ๊ฒ€์ƒ‰ ์‹œ์—๋„ ์ด ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋‹จ์ผ ์ปฌ๋Ÿผ ์ธ๋ฑ์Šค์ธ idx_reservation_user_id (user_id)๋Š” ์ค‘๋ณต์ด๋ฉฐ, ๋ถˆํ•„์š”ํ•œ ๋””์Šคํฌ ๊ณต๊ฐ„ ์ฐจ์ง€ ๋ฐ ์“ฐ๊ธฐ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์œ ๋ฐœํ•˜๋ฏ€๋กœ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

Suggested change
@Index(name = "idx_reservation_user_id", columnList = "user_id"),
@Index(name = "idx_reservation_user_status", columnList = "user_id, status"),
@Index(name = "idx_reservation_user_status", columnList = "user_id, status"),

@Index(name = "idx_reservation_remain_id", columnList = "remain_id"),
@Index(name = "idx_reservation_status", columnList = "status")
}
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Reservation {

Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/catchtable/review/entity/Review.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "reviews")
@Table(
name = "reviews",
indexes = {
@Index(name = "idx_review_store_deleted", columnList = "store_id, is_deleted"),
@Index(name = "idx_review_user_deleted", columnList = "user_id, is_deleted")
}
)
public class Review {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.catchtable.review.repository;

import com.catchtable.review.entity.Review;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand All @@ -9,13 +10,13 @@

public interface ReviewRepository extends JpaRepository<Review, Long> {

// ํŠน์ • ๋งค์žฅ์˜ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ชจ๋“  ๋ฆฌ๋ทฐ ์กฐํšŒ (๊ฐ€์žฅ ์ตœ๊ทผ ์ž‘์„ฑ๋œ ์ˆœ)
// ํŠน์ • ๋งค์žฅ์˜ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ฆฌ๋ทฐ ์กฐํšŒ โ€” ํŽ˜์ด์ง• ์ ์šฉ (์ตœ๊ทผ N๊ฑด)
@Query("SELECT r FROM Review r JOIN FETCH r.user WHERE r.store.id = :storeId AND r.isDeleted = false ORDER BY r.createdAt DESC")
List<Review> findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("storeId") Long storeId);
List<Review> findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("storeId") Long storeId, Pageable pageable);

// ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ชจ๋“  ๋ฆฌ๋ทฐ ์กฐํšŒ
// ํŠน์ • ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์‚ญ์ œ๋˜์ง€ ์•Š์€ ๋ฆฌ๋ทฐ ์กฐํšŒ โ€” ํŽ˜์ด์ง• ์ ์šฉ
@Query("SELECT r FROM Review r JOIN FETCH r.store WHERE r.user.id = :userId AND r.isDeleted = false ORDER BY r.createdAt DESC")
List<Review> findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("userId") Long userId);
List<Review> findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(@Param("userId") Long userId, Pageable pageable);

// ํ•ด๋‹น ์˜ˆ์•ฝ์„ ํ†ตํ•ด ์ด๋ฏธ ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ–ˆ๋Š”์ง€ ํ™•์ธ (์ค‘๋ณต ๋ฆฌ๋ทฐ ๋ฐฉ์ง€)
boolean existsByReservationIdAndIsDeletedFalse(Long reservationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.catchtable.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -81,7 +82,8 @@ public List<ReviewResponseDto> getStoreReviews(Long storeId) {
throw new CustomException(ErrorCode.STORE_NOT_FOUND);
}

List<Review> reviews = reviewRepository.findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(storeId);
List<Review> reviews = reviewRepository.findAllByStoreIdAndIsDeletedFalseOrderByCreatedAtDesc(
storeId, PageRequest.of(0, 20));
return reviews.stream()
.map(ReviewResponseDto::from)
.toList();
Expand All @@ -93,7 +95,8 @@ public List<ReviewResponseDto> getMyReviews(Long userId) {
throw new CustomException(ErrorCode.USER_NOT_FOUND);
}

List<Review> reviews = reviewRepository.findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(userId);
List<Review> reviews = reviewRepository.findAllByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(
userId, PageRequest.of(0, 20));
return reviews.stream()
.map(ReviewResponseDto::from)
.toList();
Expand Down
Loading
Loading