Skip to content

♻️ refactor: 미션 목록 조회 N+1 제거 및 성능 측정 정리 - #120

Merged
JoonKyoLee merged 7 commits into
developfrom
feature/mission-list-n-plus-one-optimization
Aug 7, 2026
Merged

JoonKyoLee merged 7 commits into
developfrom
feature/mission-list-n-plus-one-optimization

Conversation

@JoonKyoLee

@JoonKyoLee JoonKyoLee commented Aug 6, 2026

Copy link
Copy Markdown
Member

#️⃣ Issue Number

📝 요약(Summary)

  • 미션 목록 조회에서 오늘 편성된 미션별로 반복 조회되던 MissionCompletion 조회를 배치 조회 방식으로 정리해 N+1 패턴을 제거했습니다.
  • 오늘 편성된 미션 ID를 먼저 모은 뒤, 해당 미션들의 오늘 제출 이력을 한 번에 조회하고 missionId 기준으로 매핑해 기존 응답 구조와 상태 계산 의미를 유지하도록 변경했습니다.
  • 개선 전후를 비교할 수 있도록 로컬 SQL 측정, k6 부하테스트 시나리오, 포트폴리오 문서를 함께 정리했습니다.

📝 리뷰 요청사항

  • 미션 목록 조회 응답 값은 유지한 상태에서 조회 방식만 바꿔 N+1을 제거했습니다.
  • MissionCompletion은 하루 안에도 여러 제출이 가능해 단순 fetch join 대신 "오늘 범위 조회 → missionId별 최신 1건 선택" 방식으로 정리했습니다.
  • 성능 문서는 SQL 수 변화와 부하테스트 결과를 함께 남겼고, 작은 데이터셋/dev 환경이라 응답 시간 차이보다 SQL 구조 변화에 더 초점을 맞췄습니다.

💻 테스트 결과

  • ./gradlew test --tests 'com.zerost.api.mission.application.MissionQueryServiceTest' 통과
  • ./gradlew test --tests 'com.zerost.api.mission.application.MissionQueryServiceMeasurementTest' 통과
  • k61 / 10 / 30 / 50 / 100 / 200 VU 시나리오 측정 및 비교 완료

📌 포인트

  • 단순히 쿼리 수를 줄이는 데서 끝내지 않고, 개선 전후를 같은 조건으로 비교할 수 있는 측정 흐름까지 같이 정리했습니다.
  • 작은 데이터셋에서는 응답 시간이 크게 달라지지 않더라도, SQL 수를 7 -> 4로 줄여 조회 구조를 단순화한 점을 수치로 확인했습니다.
  • 100 VU 구간에서는 평균 응답 시간과 p95가 소폭 개선됐고, 200 VU 구간은 dev 환경 편차도 함께 관찰했습니다.

✍️ 회고

  • 이번 작업은 "눈에 띄게 빨라진 API"를 만드는 것보다, 자주 호출되는 조회 API의 구조를 먼저 단순하게 정리하는 데 의미가 있었습니다.
  • 로컬 단건 시간만 보면 변화가 거의 없어 보여도, SQL 수 감소와 부하 시나리오 비교를 함께 남겨두니 개선 내용을 더 정확하게 설명할 수 있었습니다.
  • 이후 비슷한 조회 최적화 작업도 같은 방식으로 baseline을 먼저 확보한 뒤 비교하는 흐름으로 가져갈 수 있을 것 같습니다.

Summary by CodeRabbit

  • Performance Improvements

    • Improved mission list loading by retrieving today’s completion data in a single batch operation.
    • Reduced unnecessary repeated data requests while preserving mission status and reward information.
  • Testing

    • Added automated measurements for database query counts and response times.
    • Added configurable load testing across multiple concurrent-user scenarios.
  • Documentation

    • Added Korean guides documenting performance measurement, load testing, optimization results, and before-and-after comparisons.

@JoonKyoLee JoonKyoLee self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c56e303-9c44-4427-a688-66e5579da0f9

📥 Commits

Reviewing files that changed from the base of the PR and between f39f004 and b5b3752.

📒 Files selected for processing (3)
  • docs/performance/mission-list-n-plus-one-measurement.md
  • scripts/loadtest/mission-list.k6.js
  • src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceMeasurementTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • scripts/loadtest/mission-list.k6.js
  • src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceMeasurementTest.kt
  • docs/performance/mission-list-n-plus-one-measurement.md

📝 Walkthrough

Walkthrough

The mission-list API now retrieves today’s completions in one batch query, maps the latest completion per mission, and preserves response status fields. Integration measurements, k6 scenarios, execution guidance, and before-and-after results were added.

Changes

Mission-list completion batching

Layer / File(s) Summary
Batch completion query and summary mapping
src/main/kotlin/com/zerost/api/mission/domain/MissionCompletionRepository.kt, src/main/kotlin/com/zerost/api/mission/application/MissionQueryService.kt
The repository adds an ordered batch query for selected missions. MissionQueryService groups today’s completions and uses the latest record in each mission summary.
Integration measurement and regression validation
src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceMeasurementTest.kt, src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceTest.kt
The tests measure SQL counts and execution times, validate response statuses, and stub the batch repository call.
Configurable k6 load-test workflow
scripts/loadtest/mission-list.k6.js, docs/performance/mission-list-n-plus-one-measurement.md
The load test supports authenticated requests, configurable VU scenarios, pacing, and thresholds. The guide documents execution, repetition, and result collection.
Before-and-after optimization record
docs/portfolio/mission-list-n-plus-one-optimization.md
The portfolio record describes the batching design and compares baseline and post-optimization SQL and load-test measurements.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MissionQueryService
  participant MissionCompletionRepository
  participant MissionListResponse

  Client->>MissionQueryService: Request mission list
  MissionQueryService->>MissionCompletionRepository: Query today's completions for selected mission IDs
  MissionCompletionRepository-->>MissionQueryService: Return ordered completion records
  MissionQueryService->>MissionListResponse: Map latest completion into mission summaries
  MissionListResponse-->>Client: Return mission list response
Loading

Possibly related PRs

  • team-0st/BE#16: Directly relates to the MissionQueryService and MissionCompletionRepository optimization.
  • team-0st/BE#45: Also changes completion repository queries for today’s mission completions.
  • team-0st/BE#97: Also changes mission-list completion handling in MissionQueryService.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the N+1 removal and performance measurement work.
Description check ✅ Passed The description includes the issue, summary, review focus, test results, measurements, and retrospective.
Linked Issues check ✅ Passed The changes satisfy issue #17 by batching completion queries, mapping by missionId, and documenting tests and performance comparisons.
Out of Scope Changes check ✅ Passed The implementation, tests, load test, and documentation directly support the linked issue objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/mission-list-n-plus-one-optimization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
scripts/loadtest/mission-list.k6.js (1)

72-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Scope thresholds by scenario when multiple scenarios run together.

The default configuration enables multiple scenarios, but these thresholds aggregate all requests. A fast scenario can dilute a slow spike_load scenario and allow the combined p95 to pass. Add scenario-scoped thresholds or require one enabled scenario for each gated measurement. k6 supports tag-filtered thresholds. (grafana.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/loadtest/mission-list.k6.js` around lines 72 - 77, Scope the
http_req_failed and http_req_duration thresholds in options to the relevant
scenario using k6 tag-filtered metric thresholds, especially ensuring spike_load
is evaluated independently. Preserve the existing threshold limits while
preventing results from other concurrently enabled scenarios from diluting gated
measurements.

Source: MCP tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/performance/mission-list-n-plus-one-measurement.md`:
- Line 250: Update the performance-results statement around the mission
completion batch lookup to remove the claim that response-time distributions
stabilized. State only that SQL query counts decreased, while latency results
were mixed across load levels and improved only in the 100 VU comparison.
- Around line 122-128: Update
docs/performance/mission-list-n-plus-one-measurement.md at lines 122-128 to
document the reproducible spike runs: two SPIKE_VUS=50 runs, three SPIKE_VUS=100
runs, and two SPIKE_VUS=200 runs; alternatively, remove unsupported portfolio
results from docs/portfolio/mission-list-n-plus-one-optimization.md at lines
43-51. Keep the measurement guide and recorded portfolio results aligned.

In `@scripts/loadtest/mission-list.k6.js`:
- Around line 11-15: Update constantVuScenario to validate the resolved vus
value as a finite, non-negative integer before creating the scenario; reject
malformed and fractional values with an error that identifies vusEnvName, while
preserving 0 as the disable value that returns null.
- Around line 91-100: Update the load-test options thresholds to include a
checks threshold requiring a 100% success rate, alongside the existing
http_req_failed and http_req_duration thresholds. This must make failures
recorded by the response check in the check call cause the test to fail.

In `@src/main/kotlin/com/zerost/api/mission/application/MissionQueryService.kt`:
- Around line 125-133: Update getMission to use the half-open range query
findTopByUserIdAndMissionIdAndSubmittedAtGreaterThanEqualAndSubmittedAtLessThanOrderBySubmittedAtDesc,
matching the mission-list endpoint’s inclusive-start/exclusive-end behavior. Add
a regression test covering a completion timestamped at
today.plusDays(1).atStartOfDay() and verify it is excluded consistently.

In
`@src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceMeasurementTest.kt`:
- Around line 121-122: Update the assertions in
MissionQueryServiceMeasurementTest to enforce the documented optimized SQL
limit: require statementCount to be at most four instead of merely greater than
zero, while preserving the avgElapsedMs validation.

---

Nitpick comments:
In `@scripts/loadtest/mission-list.k6.js`:
- Around line 72-77: Scope the http_req_failed and http_req_duration thresholds
in options to the relevant scenario using k6 tag-filtered metric thresholds,
especially ensuring spike_load is evaluated independently. Preserve the existing
threshold limits while preventing results from other concurrently enabled
scenarios from diluting gated measurements.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 811daafc-0fb3-4877-9abd-2b5f656c82dd

📥 Commits

Reviewing files that changed from the base of the PR and between f76d532 and f39f004.

⛔ Files ignored due to path filters (14)
  • docs/assets/mission-list-after-k6-100vu.png is excluded by !**/*.png
  • docs/assets/mission-list-after-k6-10vu.png is excluded by !**/*.png
  • docs/assets/mission-list-after-k6-1vu.png is excluded by !**/*.png
  • docs/assets/mission-list-after-k6-200vu.png is excluded by !**/*.png
  • docs/assets/mission-list-after-k6-30vu.png is excluded by !**/*.png
  • docs/assets/mission-list-after-k6-50vu.png is excluded by !**/*.png
  • docs/assets/mission-list-after-test-and-sql.png is excluded by !**/*.png
  • docs/assets/mission-list-before-k6-100vu.png is excluded by !**/*.png
  • docs/assets/mission-list-before-k6-10vu.png is excluded by !**/*.png
  • docs/assets/mission-list-before-k6-1vu.png is excluded by !**/*.png
  • docs/assets/mission-list-before-k6-200vu.png is excluded by !**/*.png
  • docs/assets/mission-list-before-k6-30vu.png is excluded by !**/*.png
  • docs/assets/mission-list-before-k6-50vu.png is excluded by !**/*.png
  • docs/assets/mission-list-before-test-and-sql.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • docs/performance/mission-list-n-plus-one-measurement.md
  • docs/portfolio/mission-list-n-plus-one-optimization.md
  • scripts/loadtest/mission-list.k6.js
  • src/main/kotlin/com/zerost/api/mission/application/MissionQueryService.kt
  • src/main/kotlin/com/zerost/api/mission/domain/MissionCompletionRepository.kt
  • src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceMeasurementTest.kt
  • src/test/kotlin/com/zerost/api/mission/application/MissionQueryServiceTest.kt

Comment on lines +122 to +128
포트폴리오 용도로는 아래 3개 시나리오만 우선 비교해도 충분합니다.

- `1 VU`
- `10 VU`
- `30 VU`

`50 VU`는 스파이크 참고 자료로 선택적으로 사용하면 됩니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant docs lines =="
for f in docs/performance/mission-list-n-plus-one-measurement.md docs/portfolio/mission-list-n-plus-one-optimization.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,180p'
  else
    echo "missing $ f"
  fi
done

echo "== spike VUS references =="
rg -n "SPIKE_VUS|100 VU|200 VU|spike|스파이크|VU" docs/performance docs/portfolio -S || true

echo "== related load script candidates =="
fd -a -i 'load|test|k6|artillery|ab|wrk|siege|benchmark' . | sed 's#^\./##' | head -100
rg -n "n_plus_one|mission.*list|스파이크|SPIKE|v-us|vu" . -g '!node_modules' -g '!dist' -g '!build' --glob '!*.md' || true

Repository: team-0st/BE

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant docs lines =="
for f in docs/performance/mission-list-n-plus-one-measurement.md docs/portfolio/mission-list-n-plus-one-optimization.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,180p'
  else
    echo "missing $f"
  fi
done

echo "== spike VUS references =="
rg -n "SPIKE_VUS|100 VU|200 VU|spike|스파이크|VU" docs/performance docs/portfolio -S || true

echo "== related load script candidates =="
fd -a -i 'load|test|k6|artillery|ab|wrk|siege|benchmark' . | sed 's#^\./##' | head -100
rg -n "n_plus_one|mission.*list|스파이크|SPIKE|v-us|vu" . -g '!node_modules' -g '!dist' -g '!build' --glob '!*.md' || true

Repository: team-0st/BE

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for f in [
    Path("docs/performance/mission-list-n-plus-one-measurement.md"),
    Path("docs/portfolio/mission-list-n-plus-one-optimization.md"),
]:
    print(f"--- {f} ---")
    if f.exists():
        text = f.read_text(encoding="utf-8")
        for i, line in enumerate(text.splitlines(), 1):
            if 1 <= i <= 180:
                print(f"{i:4}: {line}")
    else:
        print("missing")
PY

echo "== spike VUS references =="
python3 - <<'PY'
from pathlib import Path
terms = ["SPIKE_VUS","100 VU","200 VU","spike","스파이크","VU"]
roots = [Path("docs/performance"), Path("docs/portfolio")]
for root in roots:
    for p in root.rglob("*"):
        if p.is_file() and p.suffix in [".md",".txt",".yml",".yaml",".json",".sh",".js",".ts",".py"]:
            try:
                text = p.read_text(encoding="utf-8", errors="ignore")
            except Exception:
                continue
            for term in terms:
                if term in text:
                    lines = [i for i, line in enumerate(text.splitlines(), 1) if term in line]
                    print(f"{p}:{','.join(str(i) for i in lines[:10])}: {term}")
                    break
PY

echo "== related load script candidates =="
python3 - <<'PY'
from pathlib import Path
import re
terms = ["load","test","k6","artillery","ab","wrk","siege","benchmark","n_plus_one","mission.*list","스파이크","SPIKE","v-us","vu"]
for root in Path(".").iterdir():
    if root.name in {".git","node_modules","dist","build"} or root.is_file():
        continue
    for p in root.rglob("*"):
        if p.is_file():
            filename = p.name.lower()
            if any(x in filename for x in ["load","test","k6","artillery","ab","wrk","siege","benchmark"]) or p.suffix in {".sh",".js",".ts",".py",".mjs",".cjs"}:
                try:
                    text = p.read_text(encoding="utf-8", errors="ignore")
                except Exception:
                    continue
                for term in terms:
                    if re.search(term, text, re.I):
                        print(f"{p}")
                        break
PY

Repository: team-0st/BE

Length of output: 17754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("scripts/loadtest/mission-list.k6.js")
if p.exists():
    text = p.read_text(encoding="utf-8")
    lines = text.splitlines()
    for term in ["SMALL_VUS", "MEDIUM_VUS", "SPIKE_VUS", "threshold"]:
        for i, line in enumerate(lines, 1):
            if term in line:
                start = max(1, i-6)
                end = min(len(lines), i+6)
                print(f"\n--- {term} at line {i} ---")
                for n in range(start, end+1):
                    print(f"{n:4}: {lines[n-1]}")
else:
    print("missing scripts/loadtest/mission-list.k6.js")
PY

python3 - <<'PY'
from pathlib import Path
p = Path("scripts/loadtest/mission-list.k6.js")
if p.exists():
    text = p.read_text(encoding="utf-8")
    import re
    for m in re.finditer(r'export function runScenario\([^)]*\)\s*\{', text):
        idx = text.count("\n", 0, m.start()) + 1
        body = text[m.start():]
        end_br = body.find("}")
        while body.count("{") - body.count("}") > 1 and end_br != -1:
            end_br = body.find("}", end_br + 1)
        print(f"--- runScenario starts around line {idx} ---")
        print("\n".join(body[:2000].splitlines()))
PY

python3 - <<'PY'
from pathlib import Path
files = [
    Path("docs/performance/mission-list-n-plus-one-measurement.md"),
    Path("docs/portfolio/mission-list-n-plus-one-optimization.md"),
    Path("scripts/loadtest/mission-list.k6.js"),
]
for f in files:
    if f.exists():
        text = f.read_text(encoding="utf-8")
    else:
        continue
    print(f"\n== {f} ==")
    for pattern in [
        r'for i in 1 2 3; do',
        r'-e SMALL_VUS=.+',
        r'-e MEDIUM_VUS=.+',
        r'-e SPIKE_VUS=.+',
        r'100 VU spike|200 VU spike',
    ]:
        print(f"\n--- pattern {pattern} ---")
        for i, line in enumerate(text.splitlines(), 1):
            if re.search(pattern, line):
                print(f"{i}: {line}")
PY

Repository: team-0st/BE

Length of output: 1861


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
from pathlib import Path

docs_performance = Path("docs/performance/mission-list-n-plus-one-measurement.md")
docs_portfolio = Path("docs/portfolio/mission-list-n-plus-one-optimization.md")
script = Path("scripts/loadtest/mission-list.k6.js")

for f in [docs_performance, docs_portfolio, script]:
    text = f.read_text(encoding="utf-8")
    print(f"\n== {f} ==")
    for pattern in [
        r'for i in 1 2 3; do',
        r'spike_load\s*,\s*SPIKE_VUS\s*,\s*50',
        r'-e SMALL_VUS=(0|1)',
        r'-e MEDIUM_VUS=(0|30)',
        r'-e SPIKE_VUS=(50|100|200)',
        r'100 VU spike',
        r'200 VU spike',
        r'100 VU',
        r'200 VU',
    ]:
        print(f"\n--- pattern {pattern} ---")
        for i, line in enumerate(text.splitlines(), 1):
            if re.search(pattern, line):
                print(f"{i}: {line}")
PY

Repository: team-0st/BE

Length of output: 2589


Align the measurement guide with the recorded spike runs.

docs/portfolio/mission-list-n-plus-one-optimization.md documents 100 VU spike and 200 VU spike results, but the measurement guide only covers up to 50 VU. Add the exact spikes: two SPIKE_VUS=50 runs, three SPIKE_VUS=100 runs, and two SPIKE_VUS=200 runs, or remove the portfolio results that cannot be reproduced from these docs.

📍 Affects 2 files
  • docs/performance/mission-list-n-plus-one-measurement.md#L122-L128 (this comment)
  • docs/portfolio/mission-list-n-plus-one-optimization.md#L43-L51
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/performance/mission-list-n-plus-one-measurement.md` around lines 122 -
128, Update docs/performance/mission-list-n-plus-one-measurement.md at lines
122-128 to document the reproducible spike runs: two SPIKE_VUS=50 runs, three
SPIKE_VUS=100 runs, and two SPIKE_VUS=200 runs; alternatively, remove
unsupported portfolio results from
docs/portfolio/mission-list-n-plus-one-optimization.md at lines 43-51. Keep the
measurement guide and recorded portfolio results aligned.

Comment thread docs/performance/mission-list-n-plus-one-measurement.md Outdated
Comment thread scripts/loadtest/mission-list.k6.js
Comment thread scripts/loadtest/mission-list.k6.js
Comment on lines +125 to +133
return missionCompletionRepository
.findAllByUserIdAndMissionIdInAndSubmittedAtGreaterThanEqualAndSubmittedAtLessThanOrderByMissionIdAscSubmittedAtDesc(
userId = userId,
missionId = requireNotNull(id),
missionIds = missionIds,
start = todayRange.first,
end = todayRange.second,
)
.groupBy { requireNotNull(it.mission.id) }
.mapValues { (_, completions) -> completions.first() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg 'MissionQueryService\.kt|Mission.*Repository|Mission.*Controller|Mission.*Service' || true

echo
echo "== target file outline/lines =="
wc -l src/main/kotlin/com/zerost/api/mission/application/MissionQueryService.kt 2>/dev/null || true
sed -n '1,180p' src/main/kotlin/com/zerost/api/mission/application/MissionQueryService.kt 2>/dev/null || true

echo
echo "== repository query methods =="
rg -n "findAllByUserIdAndMissionIdIn|missionCompletionRepository|Between|GreaterThanEqual|LessThan|todayRange|atStartOfDay|now" src/main/kotlin -S

Repository: team-0st/BE

Length of output: 21419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository query methods =="
rg -n "findAllByUserIdAndMissionIdIn|missionCompletionRepository|Between|GreaterThanEqual|LessThan|todayRange|atStartOfDay|now" src/main/kotlin -S

Repository: team-0st/BE

Length of output: 10214


🌐 Web query:

Spring Data JPA Between query method includes bounds SQL between documentation

💡 Result:

In Spring Data JPA, the Between query method keyword translates directly to the standard SQL BETWEEN operator [1][2][3]. According to the official Spring Data JPA documentation, a query method defined with the Between keyword (e.g., findByStartDateBetween) is translated into the SQL clause:... where x.startDate between?1 and?2 [1][2][3]. Because it maps to the standard SQL BETWEEN operator, the bounds are inclusive [1][2][3]. This means the query will retrieve records where the field value is greater than or equal to the first parameter and less than or equal to the second parameter.

Citations:


Use the same half-open day range in both endpoints.

getMission uses SubmittedAtBetween, which includes todayRange.second. A completion at midnight can appear in the detail response but not in the mission-list response. Use findTopByUserIdAndMissionIdAndSubmittedAtGreaterThanEqualAndSubmittedAtLessThanOrderBySubmittedAtDesc and add a regression test for a completion at today.plusDays(1).atStartOfDay().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/com/zerost/api/mission/application/MissionQueryService.kt`
around lines 125 - 133, Update getMission to use the half-open range query
findTopByUserIdAndMissionIdAndSubmittedAtGreaterThanEqualAndSubmittedAtLessThanOrderBySubmittedAtDesc,
matching the mission-list endpoint’s inclusive-start/exclusive-end behavior. Add
a regression test covering a completion timestamped at
today.plusDays(1).atStartOfDay() and verify it is excluded consistently.

@JoonKyoLee
JoonKyoLee merged commit 796375b into develop Aug 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔨 [fix] 미션 목록 조회 N+1 쿼리 문제 수정

1 participant