Skip to content

[Fix] /mcp DB 커넥션 누수 수정 - #125

Merged
kangcheolung merged 4 commits into
developfrom
fix/120
Aug 8, 2026
Merged

[Fix] /mcp DB 커넥션 누수 수정#125
kangcheolung merged 4 commits into
developfrom
fix/120

Conversation

@kangcheolung

@kangcheolung kangcheolung commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

  • /mcp 요청마다 JDBC 커넥션이 반납되지 않고 누적되다가 5회째에 풀이 고갈되는 문제를 수정
  • 원인: spring.jpa.open-in-view 기본값(true)과 MCP Streamable HTTP 응답 처리 방식이 충돌해, OSIV가 커넥션 반납 시점을 인지하지 못함
  • 전역 OSIV는 끄고, /mcp를 제외한 나머지 경로에만 다시 등록해 다른 API의 기존 동작은 그대로 유지

Test plan

  • ./gradlew test — 706개 전체 통과
  • 수정 전: search_documents/get_document_detail 각각 5연속 호출 시 100% 재현 (HikariCP active=5, idle=0 타임아웃, Postgres에서 5개 idle+COMMIT 확인)
  • 수정 후: search_documents 25연속 + 3개 도구 혼합 20회 호출에서 재현 안 됨 (커넥션 개수 고정)
  • /mcp가 아닌 경로(/auth/login)도 수정 후 정상 동작 확인
  • 상세 원인 조사 과정·검증 기록: docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md

closes #120

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 버그 수정
    • MCP 도구를 반복 호출할 때 데이터베이스 커넥션이 반환되지 않아 커넥션 풀이 고갈되던 문제를 해결했습니다.
    • MCP 경로에서는 불필요한 영속성 컨텍스트 처리를 제외해 안정성을 높였습니다.
    • 일반 웹 요청의 데이터베이스 처리 방식은 기존과 동일하게 유지됩니다.
    • MCP 반복 호출 시 커넥션이 정상적으로 관리되도록 개선했습니다.

spring.jpa.open-in-view 기본값(true)과 MCP Streamable HTTP 응답 처리 방식이
충돌해 /mcp 요청마다 JDBC 커넥션이 반납되지 않던 문제를 해결한다.
전역 OSIV를 끄고 WebMvcConfig에서 /mcp를 제외한 나머지 경로에만 다시
등록해, 다른 API의 기존 동작은 그대로 유지한다.
재현 실패/성공 과정, 원인 좁히기(필터 가설 기각 → OSIV로 확정),
해결 방안과 검증 결과(테스트 706개 통과, 수동 e2e 재현 불가 확인)를 기록한다.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kangcheolung, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 945199a5-e7f8-40a0-a5e4-a18e9e6e4b38

📥 Commits

Reviewing files that changed from the base of the PR and between 45b112d and f0586c9.

📒 Files selected for processing (5)
  • docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md
  • src/main/java/com/opensource/docgrid/domain/mcp/security/McpApiKeyAuthFilter.java
  • src/main/java/com/opensource/docgrid/domain/mcp/security/McpRateLimiter.java
  • src/main/java/com/opensource/docgrid/domain/mcp/tool/DocGridMcpTools.java
  • src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java
📝 Walkthrough

Walkthrough

전역 OSIV를 비활성화하고 /mcp 경로를 제외한 요청에만 OSIV 인터셉터를 적용합니다. EntityManagerFactory가 없는 테스트 환경에서는 인터셉터 등록을 생략합니다. 커넥션 누수 재현과 수정 결과를 설계 문서에 기록했습니다.

Changes

MCP 커넥션 관리

Layer / File(s) Summary
커넥션 누수 재현과 원인 확인
docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md
MCP tools/call 반복 호출에서 5회 후 커넥션 풀이 고갈되는 현상과 spring.jpa.open-in-view 원인 확인 결과를 기록했습니다.
경로별 OSIV 인터셉터 구성
src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java, src/main/resources/application.yml, docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md
전역 OSIV를 비활성화했습니다. EntityManagerFactory가 존재하면 /mcp를 제외한 경로에 OpenEntityManagerInViewInterceptor를 등록합니다.
수정 결과 검증 기록
docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md
ObjectProvider 적용 결과, 전체 674개 테스트 통과 결과, MCP 반복 호출 및 다른 API 경로 검증 결과를 기록했습니다.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • DocGrid/backend#95: /mcp 요청 처리와 OSIV 제외 설정을 직접 변경한 PR입니다.
  • DocGrid/backend#104: /mcp 요청 처리 및 Spring MVC와 보안 인프라를 함께 변경한 PR입니다.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 /mcp DB 커넥션 누수 수정이라는 핵심 변경을 간결하고 명확하게 설명합니다.
Description check ✅ Passed 템플릿과 섹션명은 다르지만 문제, 원인, 해결 방법, 검증 결과, 관련 문서와 이슈를 충분히 설명합니다.
Linked Issues check ✅ Passed [#120]의 커넥션 누수 원인 규명, /mcp 반복 호출 검증, 회귀 방지 문서화 요구를 변경과 검증 내용이 충족합니다.
Out of Scope Changes check ✅ Passed OSIV 설정, WebMvcConfig 변경, 설계 문서 추가는 모두 /mcp 커넥션 누수 수정과 관련된 범위입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/120

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: 4

🤖 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/design/kangcheolung-`#120-mcp-db-connection-leak-fix.md:
- Line 9: 수정 대상 Markdown 제목에서 `#117`과 괄호 사이에 공백을 추가해 `#117 (Rate Limiting + 출력
정제)` 형식으로 변경하고, 나머지 내용은 유지하세요.

In `@src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java`:
- Around line 29-43: addInterceptors의 순차 실행 흐름을 주석으로 표시하세요. EntityManagerFactory
조회, 빈이 없을 때 즉시 반환, OpenEntityManagerInViewInterceptor 구성, /mcp를 제외한 인터셉터 등록 단계에
각각 1., 2., 3., 4. 주석을 추가하고 기존 동작과 주석 내용은 유지하세요.
- Line 42: Align the /mcp exclusion logic in WebMvcConfig and
McpApiKeyAuthFilter.shouldNotFilter() so both use the same normalized request
path, including context paths and trailing slashes. Reuse a shared matcher or
normalization rule rather than comparing request.getRequestURI() directly, and
ensure the interceptor and authentication filter exclude exactly the same
requests.

In `@src/main/resources/application.yml`:
- Around line 12-15: Ensure the deployment configuration explicitly preserves
spring.jpa.open-in-view=false by removing any SPRING_JPA_OPEN_IN_VIEW=true
environment overrides and --spring.jpa.open-in-view=true runtime arguments, or
adding an equivalent deployment-level override-prevention step. Verify profile
and Docker Compose configuration do not re-enable OSIV.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8de99731-ffc2-4d9c-a8e4-d0c2c7bbd1c3

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5fdd0 and 45b112d.

📒 Files selected for processing (3)
  • docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md
  • src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java
  • src/main/resources/application.yml

Comment thread docs/design/kangcheolung-#120-mcp-db-connection-leak-fix.md Outdated
Comment thread src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java Outdated
Comment on lines +12 to +15
jpa:
# 전역 OSIV를 끄고, WebMvcConfig에서 /mcp를 제외한 나머지 경로에만 다시 등록한다.
# /mcp는 MCP Streamable HTTP 응답 처리 방식과 OSIV가 충돌해 DB 커넥션이 누수됐다(#120).
open-in-view: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

if ! rg -n -C 3 \
  'spring\.jpa\.open-in-view|open-in-view|SPRING_JPA_OPEN_IN_VIEW' \
  --glob '*.yml' \
  --glob '*.yaml' \
  --glob '*.properties' \
  --glob 'Dockerfile*' \
  --glob '*.sh' \
  .
then
  echo "No repository-level override found."
fi

Repository: DocGrid/backend

Length of output: 627


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked YAML/YML/properties/XML/Docker/deploy files with possible configuration/env references:\n'
git ls-files | rg '(^|/)(application[^/]*\.(yml|yaml|properties|xml)|Dockerfile|docker-compose.*|.*\.env|.*\.env\..*|.*\.sh)$' || true

printf '\nAll tracked files with JPA open-in-view, WebMvcConfig, or OSIV-related terms:\n'
rg -n -i -C 2 'open-in-view|openInView|OpenInView|WebMvcConfig|webMvc|RequestMappingHandlerMapping|WebMvcConfigurer|DispatcherServlet|JpaBaseConfiguration|JpaWebConfiguration|enableAsyncTransactions|transaction|OSIV|open view' . || true

printf '\nRelevant build/config versions:\n'
for f in pom.xml build.gradle settings.gradle build.gradle.kts settings.gradle.kts gradle.properties; do
  [ -f "$f" ] && echo "--- $f" && sed -n '1,220p' "$f"
done

Repository: DocGrid/backend

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files containing exact Spring jpa open-in-view or equivalent property/env keys:\n'
rg -n -i --hidden --glob '!target/**' \
  'spring\.jpa\.open[-_]?in[-_]?view|spring.jpa.open-in-view|SPRING_JPA[_.]OPEN[-_]?IN[-_]?VIEW|OPEN[-_]?IN[-_]?VIEW' . || true

printf '\nApplication-related configuration files (tracked):\n'
git ls-files | rg '(^|/)(application[^/]*|Dockerfile|docker-compose[^/]*|helmchart|\.github|env|local|dev|prod|prod-)' | sed -n '1,200p'

printf '\nBuild dependency/version files containing spring-boot or jpa references:\n'
git ls-files | rg '(^|/)(pom\.xml|build\.gradle|build\.gradle\.kts|settings\.gradle|gradle\.properties)$' | while read -r f; do
  echo "--- $f"
  rg -n -i 'spring-boot|spring-boot-starter|spring-boot-starter-jpa|jpa|web|boot-actuator|spring-cloud|java|bootVersion|springBoot' "$f" || true
done

printf '\nLocate WebMvcConfig and dispatcher/web config classes:\n'
git ls-files | rg '(^|/)(WebMvcConfig|Dispatcher|WebMvc.*Config|Async.*Config).*\.(java|kt)$' | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  rg -n -C 4 'class WebMvcConfig|`@Configuration`|WebMvcConfigurer|addInterceptors|RequestMappingHandlerMapping|DispatcherServlet|/mcp' "$f" || true
done

printf '\nRelevant application.yml top section:\n'
sed -n '1,80p' src/main/resources/application.yml

Repository: DocGrid/backend

Length of output: 7034


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- build.gradle ---\n'
sed -n '1,140p' build.gradle

printf '\n--- application-local.yml ---\n'
sed -n '1,160p' src/main/resources/application-local.yml

printf '\n--- application-prod.yml ---\n'
sed -n '1,160p' src/main/resources/application-prod.yml

printf '\n--- application-test.yml ---\n'
sed -n '1,160p' src/main/resources/application-test.yml

printf '\n--- docker-compose.yml ---\n'
sed -n '1,180p' docker-compose.yml

printf '\n--- workflow files mentioning env or SPRING_PROFILES_ACTIVE ---\n'
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
  [ -f "$f" ] || continue
  echo "--- $f"
  rg -n -i -C 3 'SPRING_PROFILES_ACTIVE|SPRING_JPA_OPEN_IN_VIEW|env:|secrets|GITHUB|CI|docker|compose|podman' "$f" || true
done

printf '\n--- WebMvcConfig full file ---\n'
sed -n '1,90p' src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java

Repository: DocGrid/backend

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- build.gradle ---'
sed -n '1,140p' build.gradle

printf '%s\n' ''
printf '%s\n' '--- application-local.yml ---'
sed -n '1,160p' src/main/resources/application-local.yml

printf '%s\n' ''
printf '%s\n' '--- application-prod.yml ---'
sed -n '1,160p' src/main/resources/application-prod.yml

printf '%s\n' ''
printf '%s\n' '--- application-test.yml ---'
sed -n '1,160p' src/main/resources/application-test.yml

printf '%s\n' ''
printf '%s\n' '--- docker-compose.yml ---'
sed -n '1,180p' docker-compose.yml

printf '%s\n' ''
printf '%s\n' '--- workflow files mentioning env or SPRING_PROFILES_ACTIVE ---'
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
  [ -f "$f" ] || continue
  echo "--- $f"
  rg -n -i -C 3 'SPRING_PROFILES_ACTIVE|SPRING_JPA_OPEN_IN_VIEW|env:|secrets|GITHUB|CI|docker|compose|podman' "$f" || true
done

printf '%s\n' ''
printf '%s\n' '--- WebMvcConfig full file ---'
sed -n '1,90p' src/main/java/com/opensource/docgrid/global/config/WebMvcConfig.java

Repository: DocGrid/backend

Length of output: 10294


🌐 Web query:

Spring Boot 3.5 JpaBaseConfiguration JpaWebConfiguration open-in-view auto configure web interceptor when true

💡 Result:

In Spring Boot 3.5, the Open EntityManager in View (OSIV) pattern remains enabled by default, which auto-configures the OpenEntityManagerInViewInterceptor when using a Servlet-based web application [1][2][3]. This behavior is managed by the JpaWebConfiguration static inner class within JpaBaseConfiguration [4][1]. The auto-configuration process functions as follows: 1. Activation: The JpaWebConfiguration class is activated when the application is a Servlet-based web application, depends on WebMvcConfigurer, and does not already have an OpenEntityManagerInViewInterceptor or OpenEntityManagerInViewFilter bean defined [1][2]. 2. Property Check: It uses the @ConditionalOnBooleanProperty annotation to check the spring.jpa.open-in-view property, which defaults to true if not explicitly set [1][2]. 3. Interceptor Registration: When enabled, the configuration registers an OpenEntityManagerInViewInterceptor bean [1][2]. It also registers a WebMvcConfigurer bean that adds this interceptor to the application's registry, ensuring it is active for web requests [1][2]. If the property is enabled by default (i.e., not set in the configuration), Spring Boot will issue a warning at startup advising that database queries may be performed during view rendering [1][2][5]. To disable this behavior and the associated warning, you should explicitly set the following property in your application.properties or application.yaml file [3][5][6]: spring.jpa.open-in-view=false

Citations:


🌐 Web query:

Spring Boot 3.5 JpaWebConfiguration bean OpenEntityManagerInViewInterceptor bean prevents OSIV auto config addInterceptors

💡 Result:

In Spring Boot, the OpenEntityManagerInViewInterceptor bean is part of the JpaWebConfiguration auto-configuration class [1][2]. This auto-configuration is designed to automatically register the interceptor to support the Open Session in View (OSIV) pattern [1][3]. The auto-configuration is protected by the @ConditionalOnMissingBean annotation, which specifies that it will only run if there are no existing OpenEntityManagerInViewInterceptor or OpenEntityManagerInViewFilter beans present in the application context [1][4]. If you define your own OpenEntityManagerInViewInterceptor bean in your configuration, the Spring Boot auto-configured version will automatically back off and will not register its own interceptor or the associated WebMvcConfigurer [1][5]. This mechanism is intended to allow developers to replace the default interceptor with a custom implementation or to completely disable the default auto-configuration by providing a bean of the specified type [5][6]. If your goal is to disable the default OpenEntityManagerInViewInterceptor behavior, the standard and recommended approach is to set the following property in your application.properties or application.yml file [7][3]: spring.jpa.open-in-view=false This property causes the auto-configuration's conditional check (@ConditionalOnBooleanProperty) to fail, preventing the auto-registration of both the interceptor and the WebMvcConfigurer without requiring you to define custom beans [1][2].

Citations:


🌐 Web query:

Spring Boot 3.5 JpaBaseConfiguration.JpaWebConfiguration registry.addWebRequestInterceptor bean OpenEntityManagerInViewInterceptor

💡 Result:

In Spring Boot 3.5, the Open EntityManager in View (OSIV) pattern is handled by the internal static class JpaWebConfiguration, which is nested within JpaBaseConfiguration [1][2]. This configuration ensures that an OpenEntityManagerInViewInterceptor is registered when the spring.jpa.open-in-view property is enabled (which is true by default) and no conflicting beans or filters are present [1][2]. The registration mechanism works as follows: 1. The JpaWebConfiguration class defines an openEntityManagerInViewInterceptor bean [1][2]. 2. It then provides a WebMvcConfigurer bean named openEntityManagerInViewInterceptorConfigurer, which takes the interceptor as a dependency [1][2]. 3. Within this WebMvcConfigurer, the addInterceptors method is overridden to register the interceptor using registry.addWebRequestInterceptor(interceptor) [1][2]. This pattern ensures that the EntityManager remains bound to the current thread for the duration of a web request, allowing for lazy loading of entity associations during view rendering [3]. However, because this is widely considered an anti-pattern due to potential performance and scalability issues, Spring Boot logs a warning at startup if the property is left at its default value of true, advising developers to explicitly configure spring.jpa.open-in-view if they wish to disable it or silence the warning [1][2][4].

Citations:


spring.jpa.open-in-view 설정을 배포 환경에서 확정하세요.

프로필 파일과 Docker compose에는 덮어쓰기가 없습니다. 배포 툴체인에 SPRING_JPA_OPEN_IN_VIEW=true--spring.jpa.open-in-view=true 같은 실행 인자가 포함되면 기본 OSIV가 적용되어 /mcp 미배제 인터셉터가 함께 등록됩니다. 이 인자는 제거하거나 명시적 오버라이드 방지 단계를 추가하면 됩니다.

🤖 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/resources/application.yml` around lines 12 - 15, Ensure the
deployment configuration explicitly preserves spring.jpa.open-in-view=false by
removing any SPRING_JPA_OPEN_IN_VIEW=true environment overrides and
--spring.jpa.open-in-view=true runtime arguments, or adding an equivalent
deployment-level override-prevention step. Verify profile and Docker Compose
configuration do not re-enable OSIV.

Source: Path instructions

WebMvcConfig의 OSIV 제외 경로와 McpApiKeyAuthFilter의 인증 제외 경로가
각자 "/mcp" 리터럴을 중복 정의하고 있어, 경로가 바뀌면 한쪽만 갱신될
위험이 있었다. MCP_ENDPOINT 상수를 공개해 두 곳이 같은 값을 참조하게
한다. addInterceptors()에 단계별 번호 주석을 추가하고, 설계 문서의
띄어쓰기를 정리한다.

배포 설정에서 open-in-view를 재정의할 수 있다는 지적은 실제로
application-prod.yml/docker-compose.yml 어디에도 그런 override가
없어 반영하지 않는다.
@kangcheolung
kangcheolung merged commit 40d6a94 into develop Aug 8, 2026
1 check 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] /mcp 반복 호출 시 DB 커넥션 누수 (McpApiKeyAuthFilter)

1 participant