diff --git a/.claude/agents/perf-analyzer.md b/.claude/agents/perf-analyzer.md
new file mode 100644
index 00000000..3dcbcf52
--- /dev/null
+++ b/.claude/agents/perf-analyzer.md
@@ -0,0 +1,210 @@
+---
+name: perf-analyzer
+description: Artillery 부하테스트 JSON 리포트, p6spy 쿼리 로그(.txt), MySQL EXPLAIN 실행계획을 분석한다. "부하테스트 결과 분석", "쿼리 로그 확인", "N+1 있는지 봐줘", "EXPLAIN 해석해줘", "성능 개선 전후 비교" 요청 시 호출한다.
+tools: Read, Glob, Grep, Bash
+model: sonnet
+color: yellow
+---
+
+# 성능 분석 에이전트 (perf-analyzer)
+
+당신은 백엔드 성능 전문가다. Artillery 부하테스트 결과, p6spy 쿼리 로그, MySQL EXPLAIN 실행계획을 분석하여 병목 원인과 개선 효과를 정량적으로 보고한다. 모든 출력은 **한국어**로 작성하고 마크다운 표와 리스트를 적극 활용한다.
+
+---
+
+## 입력 해석 규칙
+
+사용자 입력이 없거나 "전체 분석"이면 자동 탐색 모드로 동작한다.
+
+| 입력 예시 | 동작 |
+|-----------|------|
+| (없음) / "전체 분석" | `load-test/` 전체 스캔 |
+| `scenario=place` | `reports/place/`, `query-logs/place-*/` 한정 |
+| `scenario=course` | `reports/course/`, `query-logs/course-*/` 한정 |
+| 파일 경로 직접 지정 | 해당 파일만 분석 |
+| EXPLAIN 텍스트 붙여넣기 | 3단계(EXPLAIN 분석)만 수행 |
+
+---
+
+## 분석 절차
+
+### 0단계: 파일 목록 수집
+
+Glob으로 아래 경로를 스캔한다.
+
+- `load-test/reports/**/*.json`
+- `load-test/query-logs/**/*.txt`
+
+수집한 파일 목록을 시나리오별로 그룹핑하여 분석 범위를 사용자에게 먼저 보여준다.
+
+```
+분석 대상:
+- 리포트: place/01~04 (4개), course/01,02,04 (3개)
+- 쿼리 로그: place-list-bookmark/01,03,04 (3개), folder-preview/01,03,04 (3개)
+```
+
+---
+
+### 1단계: Artillery 리포트 분석
+
+각 JSON 파일을 Read로 읽어 `aggregate` 섹션에서 다음 지표를 추출한다.
+
+**추출 지표:**
+- `http.codes.200` — 성공 요청 수
+- `vusers.failed` — 실패 VU 수
+- `errors.ETIMEDOUT` — 타임아웃 수
+- `http.response_time.mean` — 평균 응답시간 (ms)
+- `http.response_time.p50` / `p95` / `p99` — 백분위수
+- `http.request_rate` — 초당 요청수
+
+**출력 형식 — 단계별 비교표:**
+
+| 단계 | 성공률 | mean (ms) | p50 (ms) | p95 (ms) | p99 (ms) | 실패 수 |
+|------|--------|-----------|----------|----------|----------|---------|
+| 01-baseline | X% | ... | ... | ... | ... | ... |
+| 02-after-redis-cache | X% | ... | ... | ... | ... | ... |
+| 03-after-query-opt | X% | ... | ... | ... | ... | ... |
+| 04-after-index-opt | X% | ... | ... | ... | ... | ... |
+
+성공률 = `http.codes.200 / (http.codes.200 + vusers.failed) * 100`
+
+단계 간 개선율을 계산하여 각 행 아래에 기술한다.
+예: `p99: 8,692ms → 67ms (개선율 99.2%)`
+
+엔드포인트별 메트릭(`plugins.metrics-by-endpoint.*`)이 있으면 별도 표로 분리한다.
+
+KPI 임계값:
+- 성공률 < 95% → [심각]
+- p95 > 3,000ms → [경고]
+- 1,000ms < p95 ≤ 3,000ms → [주의]
+
+---
+
+### 2단계: p6spy 쿼리 로그 분석
+
+각 `.txt` 파일을 Read로 읽는다. 로그 포맷: `{실행시간}ms | {SQL}`
+
+#### 2-1. 쿼리 통계 집계
+
+Bash를 사용해 각 파일의 전체 쿼리 수를 집계한다.
+
+출력:
+
+| 단계 | 총 쿼리 수 | 슬로우 쿼리(>10ms) |
+|------|-----------|-------------------|
+| 01-baseline | N | N건 |
+| 03-after-query-opt | N | N건 |
+| 04-after-index-opt | N | N건 |
+
+#### 2-2. N+1 패턴 탐지
+
+같은 테이블에 대한 동일 쿼리가 반복되는 패턴을 탐지한다.
+
+탐지 기준:
+- 동일한 `FROM
` + `WHERE =?` 패턴이 3회 이상 연속 등장
+- `place_tag`, `places`, `tags`, `place_images` 테이블을 우선 검사
+
+탐지 시 출력 예시:
+```text
+[심각] N+1 탐지됨 (01-baseline.txt)
+ - 테이블: place_tag
+ - 패턴: WHERE pt1_0.place_id=? (단건 조회 반복)
+ - 반복 횟수: 8회
+ - 개선 후(03): 해당 패턴 제거됨 (JOIN 배치 조회로 변경)
+```
+
+#### 2-3. 슬로우 쿼리 목록
+
+실행시간 10ms 초과 쿼리를 파일별로 나열한다.
+
+```text
+[01-baseline.txt] 슬로우 쿼리
+ 44ms | select u1_0.id,... from users where id=?
+ 77ms | select p1_0.id,... from places where ...
+```
+
+#### 2-4. 단계 간 쿼리 구조 변화 요약
+
+baseline → 최종 단계 사이에 쿼리 수와 구조가 어떻게 달라졌는지 서술한다.
+
+---
+
+### 3단계: EXPLAIN 실행계획 분석
+
+사용자가 EXPLAIN 결과를 텍스트로 제공한 경우 아래 항목을 분석한다.
+
+**핵심 컬럼 해석:**
+
+| 컬럼 | 주목 값 | 심각도 | 의미 |
+|------|---------|--------|------|
+| `type` | `ALL` | [심각] | Full Table Scan — 인덱스 없음 |
+| `type` | `index` | [경고] | 인덱스 풀 스캔 — 비효율 |
+| `type` | `range` | [주의] | 범위 인덱스 스캔 |
+| `type` | `ref` / `eq_ref` | [정상] | 인덱스 포인트 조회 |
+| `type` | `const` | [정상] | PK/Unique 조회 — 최상 |
+| `key` | `NULL` | [심각] | 인덱스 미사용 |
+| `Extra` | `Using filesort` | [경고] | 정렬 추가 비용 |
+| `Extra` | `Using temporary` | [경고] | 임시 테이블 생성 |
+
+**출력 형식:**
+
+```
+EXPLAIN 분석 결과
+ 테이블: bookmarks
+ type: ALL → [심각] Full Table Scan 감지
+ key: NULL → 인덱스 미사용
+ rows: 50,000
+
+ 문제: user_id + target_type 복합 인덱스 없음
+ 권장: INDEX(user_id, target_type, target_id) 추가
+ 예상 효과: rows 50,000 → ~10 (사용자당 평균 북마크 수 기준)
+```
+
+---
+
+### 4단계: 종합 분석 요약
+
+위 3단계 결과를 통합하여 최종 보고서를 작성한다.
+
+```markdown
+## 종합 성능 분석 보고서
+
+### 핵심 지표 요약
+(Artillery 비교표 재요약 — 가장 중요한 수치 3~5개)
+
+### 발견된 문제 목록
+1. [심각] ...
+2. [경고] ...
+3. [주의] ...
+
+### 단계별 개선 효과
+| 최적화 항목 | 적용 전 p95 | 적용 후 p95 | 개선율 |
+|------------|------------|------------|--------|
+| Redis 캐시 추가 | ... | ... | ...% |
+| N+1 쿼리 제거 | ... | ... | ...% |
+| 인덱스 최적화 | ... | ... | ...% |
+
+### 추가 권장 사항
+- (미해결 병목이 있다면 구체적인 인덱스/캐시/쿼리 개선안 제시)
+```
+
+---
+
+## 출력 원칙
+
+1. **수치 근거 명시**: "빨라졌다"가 아니라 "p99 기준 8,692ms → 67ms (개선율 99.2%)"처럼 구체적 수치를 포함한다.
+2. **한국어**: 모든 설명, 판단, 권고는 한국어로 작성한다. SQL과 파일명은 원문 유지.
+3. **마크다운**: 표, 코드블록, 리스트를 활용하여 가독성을 높인다.
+4. **심각도 표시**: [심각] / [경고] / [주의] / [정상] 레이블을 붙여 우선순위를 명확히 한다.
+5. **파일 미존재 처리**: 특정 단계 파일이 없으면 "해당 단계 파일 없음"으로 표기하고 분석을 건너뛴다.
+
+---
+
+## Bash 사용 제한
+
+Bash는 아래 목적으로만 허용한다:
+- 쿼리 로그 행 수 집계: `wc -l`
+- 특정 패턴 카운팅: 파이프 + `grep -c`
+- 실행시간 수치 추출 후 정렬: `sort`
+
+파일 생성(`>`, `>>`), 수정(`sed -i`), 삭제(`rm`) 명령은 절대 실행하지 않는다.
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 00000000..19d6d26f
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(grep \"INSERT INTO \\\\`courses\\\\`\" /Users/mkyu/Desktop/SOPT/solply-server/src/main/resources/db/migration/V2__create_initial_data.sql -A 200)"
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 502eda0a..d310221b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,7 +78,9 @@ Thumbs.db
test-output/
reports/
load-test/reports/
+load-test/query-logs/
load-test/node_modules/
+load-test/data/
jacoco.exec
# ====================
diff --git a/build.gradle b/build.gradle
index 8a20b58d..0d6a8079 100644
--- a/build.gradle
+++ b/build.gradle
@@ -130,6 +130,9 @@ dependencies {
// ✅ Spring AI
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
+ // ✅ p6spy (쿼리 로깅)
+ runtimeOnly 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.1'
+
// ✅ 테스트
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
diff --git a/load-test/scenarios/bookmark-course.yml b/load-test/scenarios/bookmark-course.yml
index fe0cfb9d..ec574cf1 100644
--- a/load-test/scenarios/bookmark-course.yml
+++ b/load-test/scenarios/bookmark-course.yml
@@ -1,5 +1,10 @@
config:
target: "http://localhost:8082"
+ payload:
+ path: "../data/users.csv"
+ fields:
+ - token
+ order: random
phases:
- name: "Warm up"
duration: 30
@@ -14,7 +19,7 @@ config:
http:
defaults:
headers:
- Authorization: "Bearer {{ $env.TEST_TOKEN }}"
+ Authorization: "Bearer {{ token }}"
plugins:
metrics-by-endpoint:
useOnlyRequestNames: true
diff --git a/load-test/scenarios/bookmark-place.yml b/load-test/scenarios/bookmark-place.yml
index ecbac946..285b38a6 100644
--- a/load-test/scenarios/bookmark-place.yml
+++ b/load-test/scenarios/bookmark-place.yml
@@ -1,5 +1,10 @@
config:
target: "http://localhost:8082"
+ payload:
+ path: "../data/users.csv"
+ fields:
+ - token
+ order: random
phases:
- name: "Warm up"
duration: 30
@@ -14,7 +19,7 @@ config:
http:
defaults:
headers:
- Authorization: "Bearer {{ $env.TEST_TOKEN }}"
+ Authorization: "Bearer {{ token }}"
plugins:
metrics-by-endpoint:
useOnlyRequestNames: true
diff --git a/src/main/java/org/sopt/solply_server/domain/bookmark/entity/Bookmark.java b/src/main/java/org/sopt/solply_server/domain/bookmark/entity/Bookmark.java
index 0d9131b7..fd0dd370 100644
--- a/src/main/java/org/sopt/solply_server/domain/bookmark/entity/Bookmark.java
+++ b/src/main/java/org/sopt/solply_server/domain/bookmark/entity/Bookmark.java
@@ -24,8 +24,8 @@
columnList = "target_type, target_id"
),
@Index(
- name = "idx_bookmark_user_created",
- columnList = "user_id, created_at"
+ name = "idx_bookmark_user_type",
+ columnList = "user_id, target_type"
)
}
)
diff --git a/src/main/java/org/sopt/solply_server/domain/place/entity/Place.java b/src/main/java/org/sopt/solply_server/domain/place/entity/Place.java
index 56dbcbab..184ab866 100644
--- a/src/main/java/org/sopt/solply_server/domain/place/entity/Place.java
+++ b/src/main/java/org/sopt/solply_server/domain/place/entity/Place.java
@@ -44,7 +44,7 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Table(name = "places",
indexes = {
- @Index(name = "idx_places_town_id", columnList = "town_id"),
+ @Index(name = "idx_places_town_active", columnList = "town_id, active"),
@Index(name = "idx_places_created_by_created_at", columnList = "created_by, created_at")
}
)
diff --git a/src/main/java/org/sopt/solply_server/domain/place/repository/querydsl/PlaceRepositoryImpl.java b/src/main/java/org/sopt/solply_server/domain/place/repository/querydsl/PlaceRepositoryImpl.java
index 616fdd58..75fd8064 100644
--- a/src/main/java/org/sopt/solply_server/domain/place/repository/querydsl/PlaceRepositoryImpl.java
+++ b/src/main/java/org/sopt/solply_server/domain/place/repository/querydsl/PlaceRepositoryImpl.java
@@ -196,15 +196,17 @@ private String sanitizeForBooleanMode(final String token) {
// 전체 조회
private List findPlacesWithoutTags(QPlace place, BooleanBuilder whereCondition) {
- List places = queryFactory
- .selectFrom(place)
+ QPlaceTag placeTag = QPlaceTag.placeTag;
+ QTag tag = QTag.tag;
+
+ return queryFactory
+ .selectDistinct(place)
+ .from(place)
+ .leftJoin(place.placeTags, placeTag).fetchJoin()
+ .leftJoin(placeTag.tag, tag).fetchJoin()
.where(whereCondition)
.orderBy(place.createdAt.desc())
.fetch();
-
- loadPlaceTagsAndTags(places);
-
- return places;
}
// 메인 태그만 있는 경우
@@ -230,6 +232,9 @@ private List findPlacesWithMainTag(QPlace place, BooleanBuilder whereCond
// 메인 태그와 서브 태그가 모두 있는 경우
private List findPlacesWithTags(QPlace place, BooleanBuilder whereCondition,
PlaceSearchConditionDto condition) {
+ QPlaceTag placeTag = QPlaceTag.placeTag;
+ QTag tag = QTag.tag;
+
// 메인 태그 EXISTS 조건
whereCondition.and(createMainTagExistsCondition(place, condition.mainTagId()));
@@ -246,30 +251,13 @@ private List findPlacesWithTags(QPlace place, BooleanBuilder whereConditi
return queryFactory
.selectDistinct(place)
.from(place)
+ .leftJoin(place.placeTags, placeTag).fetchJoin()
+ .leftJoin(placeTag.tag, tag).fetchJoin()
.where(whereCondition)
.orderBy(place.createdAt.desc())
.fetch();
}
- // 영속성 컨텍스트에 미리 로딩
- private void loadPlaceTagsAndTags(List places) {
- if (places.isEmpty()) return;
-
- List placeIds = places.stream().map(Place::getId).toList();
-
- QPlaceTag pt = QPlaceTag.placeTag;
- QTag t = QTag.tag;
-
- queryFactory
- .selectFrom(pt)
- .join(pt.tag, t).fetchJoin()
- .where(
- pt.place.id.in(placeIds),
- t.active.isTrue()
- )
- .fetch();
- }
-
// 기본 조건(동네, 북마크) 추가 메서드
private BooleanBuilder createBasicConditions(QPlace place, PlaceSearchConditionDto condition) {
BooleanBuilder basicCondition = new BooleanBuilder();
diff --git a/src/main/java/org/sopt/solply_server/domain/place/service/PlaceService.java b/src/main/java/org/sopt/solply_server/domain/place/service/PlaceService.java
index 320ce678..37ba3758 100644
--- a/src/main/java/org/sopt/solply_server/domain/place/service/PlaceService.java
+++ b/src/main/java/org/sopt/solply_server/domain/place/service/PlaceService.java
@@ -110,12 +110,28 @@ public PlaceFilterGetResponse getPlacesByTownAndTag(
townValidator.validateTownId(townId);
- List places = getPlacesByCondition(userId, townId, isBookmarkSearch, mainTagId, subTagAIdList, subTagBIdList);
-
- // 북마크 여부: Set.contains()
- Set bookmarkedIds = userId != null
- ? new HashSet<>(placeBookmarkFacade.getBookmarkedPlaceIdsForTown(userId, townId))
- : Collections.emptySet();
+ boolean isOnlyBookmarkSearch = Boolean.TRUE.equals(isBookmarkSearch);
+
+ // isBookmarkSearch=true 시 orderedIds를 한 번만 조회해서 장소 목록 필터링과 북마크 Set에 재사용
+ // (기존에는 getBookmarkedPlacesByLatest()와 북마크 Set 생성 시 각각 1회씩, 총 2회 호출했음)
+ List bookmarkedOrderedIds = (isOnlyBookmarkSearch && userId != null)
+ ? placeBookmarkFacade.getBookmarkedPlaceIdsForTown(userId, townId)
+ : null;
+
+ // 북마크 검색 시 orderedIds를 함께 전달 → 내부에서 북마크 최신순으로 재정렬
+ List places = getPlacesByCondition(townId, isOnlyBookmarkSearch, mainTagId, subTagAIdList, subTagBIdList, bookmarkedOrderedIds);
+
+ Set bookmarkedIds;
+ if (bookmarkedOrderedIds != null) {
+ // 북마크 검색: 이미 조회한 orderedIds를 Set으로 변환해서 재사용
+ bookmarkedIds = new HashSet<>(bookmarkedOrderedIds);
+ } else if (userId != null) {
+ // 일반 장소 목록 + 로그인 상태: 각 장소 카드의 북마크 여부(하트) 표시를 위해 조회
+ bookmarkedIds = new HashSet<>(placeBookmarkFacade.getBookmarkedPlaceIdsForTown(userId, townId));
+ } else {
+ // 비로그인: 북마크 여부 불필요
+ bookmarkedIds = Collections.emptySet();
+ }
List placePreviewDtoList = places.stream()
.map(place -> PlacePreviewDto.of(
@@ -191,14 +207,15 @@ public PlaceSearchResponse searchPlaces(final String keyword) {
//=== Private Methods ===//
- private List getPlacesByCondition(final Long userId, final Long selectedTownId, final boolean isOnlyBookmarkSearch,
- final Long mainTagId, final List subTagAIdList, final List subTagBIdList) {
+ private List getPlacesByCondition(final Long selectedTownId, final boolean isOnlyBookmarkSearch,
+ final Long mainTagId, final List subTagAIdList, final List subTagBIdList,
+ final List bookmarkedOrderedIds) {
if (mainTagId != null) {
tagValidator.validatePlaceTagConditions(mainTagId, subTagAIdList, subTagBIdList);
}
if (isOnlyBookmarkSearch) {
- return getBookmarkedPlacesByLatest(userId, selectedTownId, mainTagId, subTagAIdList, subTagBIdList);
+ return getBookmarkedPlacesByLatest(selectedTownId, mainTagId, subTagAIdList, subTagBIdList, bookmarkedOrderedIds);
}
return placeRepository.findPlacesByConditions(
@@ -208,17 +225,16 @@ private List getPlacesByCondition(final Long userId, final Long selectedT
/**
* 북마크 장소 최신순 조회.
- * ZSET에서 최신순 정렬된 placeIds 추출 → DB에서 태그 조건 필터링 → ZSET 순서 복원.
+ * 상위에서 조회한 orderedIds(ZSET 최신순) → DB에서 태그 조건 필터링 → ZSET 순서 복원.
*/
private List getBookmarkedPlacesByLatest(
- final Long userId,
final Long selectedTownId,
final Long mainTagId,
final List subTagAIdList,
- final List subTagBIdList
+ final List subTagBIdList,
+ final List orderedIds
) {
- List orderedIds = placeBookmarkFacade.getBookmarkedPlaceIdsForTown(userId, selectedTownId);
- if (orderedIds.isEmpty()) return List.of();
+ if (orderedIds == null || orderedIds.isEmpty()) return List.of();
List filtered = placeRepository.findPlacesByConditions(
PlaceSearchConditionDto.of(
@@ -232,7 +248,7 @@ private List getBookmarkedPlacesByLatest(
);
if (filtered.isEmpty()) return List.of();
- // DB 결과를 ZSET 순서(최신순)로 재정렬
+ // DB 결과를 ZSET 순서(북마크 최신순)로 재정렬
Map placeMap = filtered.stream()
.collect(Collectors.toMap(Place::getId, Function.identity()));
return orderedIds.stream()
diff --git a/src/main/resources/db/migration/V10__optimize_indexes_for_bookmark_query.sql b/src/main/resources/db/migration/V10__optimize_indexes_for_bookmark_query.sql
new file mode 100644
index 00000000..5dd87085
--- /dev/null
+++ b/src/main/resources/db/migration/V10__optimize_indexes_for_bookmark_query.sql
@@ -0,0 +1,12 @@
+-- bookmarks: user_id + target_type + created_at 복합 인덱스로 교체
+-- 적용 쿼리 패턴: WHERE user_id = ? AND target_type = 'PLACE' [AND target_id = ?] [ORDER BY created_at DESC]
+DROP INDEX idx_bookmark_user_created ON bookmarks;
+CREATE INDEX idx_bookmark_user_type_created
+ ON bookmarks (user_id, target_type, created_at DESC);
+
+-- places: town_id + active + created_at 복합 인덱스로 교체
+-- 적용 쿼리 패턴: WHERE town_id = ? AND active = true ORDER BY created_at DESC
+-- 주의: idx_places_town_id는 FK 제약 조건에서 사용 중이므로 새 인덱스를 먼저 생성 후 삭제
+CREATE INDEX idx_places_town_active_created
+ ON places (town_id, active, created_at DESC);
+DROP INDEX idx_places_town_id ON places;
diff --git a/src/main/resources/db/migration/V11__simplify_indexes.sql b/src/main/resources/db/migration/V11__simplify_indexes.sql
new file mode 100644
index 00000000..517eae1f
--- /dev/null
+++ b/src/main/resources/db/migration/V11__simplify_indexes.sql
@@ -0,0 +1,13 @@
+-- bookmarks: created_at은 backfill 쿼리에 ORDER BY가 없으므로 불필요
+-- target_id가 인덱스에 없어 커버링 인덱스도 아님
+-- (user_id, target_type) 복합 인덱스로 축소
+DROP INDEX idx_bookmark_user_type_created ON bookmarks;
+CREATE INDEX idx_bookmark_user_type
+ ON bookmarks (user_id, target_type);
+
+-- places: created_at은 LIMIT 없는 전체 조회에서 filesort 비용 절감 효과가 없으므로 제거
+-- 주의: idx_places_town_active_created는 town_id FK 제약에서 사용 중이므로
+-- 새 인덱스를 먼저 생성 후 삭제
+CREATE INDEX idx_places_town_active
+ ON places (town_id, active);
+DROP INDEX idx_places_town_active_created ON places;
diff --git a/src/main/resources/spy.properties b/src/main/resources/spy.properties
new file mode 100644
index 00000000..924a696d
--- /dev/null
+++ b/src/main/resources/spy.properties
@@ -0,0 +1,9 @@
+appender=com.p6spy.engine.spy.appender.Slf4JLogger
+logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
+customLogMessageFormat=%(executionTime)ms | %(sqlSingleLine)
+
+filter.multiline=false
+excludecategories=info,debug,result,resultset,batch,commit,rollback
+
+# HikariCP keepalive
+excludestatements=SELECT 1
diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml
index f2652765..b1c3d881 100644
--- a/src/test/resources/application-test.yml
+++ b/src/test/resources/application-test.yml
@@ -16,6 +16,10 @@ spring:
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
+decorator:
+ datasource:
+ enabled: false
+
app:
env-prefix: test