ํ”„๋กœ์ ํŠธ/MLOps ํ†ตํ•ฉ

PgBouncer ๋’ค์—์„œ @Transactional ์“ฐ๊ธฐ๋งŒ ์ฃฝ์—ˆ๋‹ค — log4jdbc๊ฐ€ ๋ถ€๋ฅธ ํŠธ๋žœ์žญ์…˜ ์žฅ์•  ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

THE NICOLE 2026. 6. 14. 16:08

์ฝ๊ธฐ๋Š” ๋ฉ€์ฉกํ•œ๋ฐ ์“ฐ๊ธฐ๋งŒ ์ „๋ถ€ 500์œผ๋กœ ์ฃฝ์—ˆ๋‹ค. ๊ทธ๊ฒƒ๋„ ์–ด์ œ๊นŒ์ง€ ์ž˜ ๋Œ๋˜ ์„œ๋น„์Šค๊ฐ€. ๋ฒ”์ธ์€ ์˜์™ธ๋กœ "SQL ๋กœ๊ทธ ์ข€ ์˜ˆ์˜๊ฒŒ ๋ณด๋ ค๊ณ " ๋ผ์›Œ ๋„ฃ์€ ๋“œ๋ผ์ด๋ฒ„์˜€๋‹ค. PgBouncer ๋’ค์—์„œ @Transactional ์“ฐ๊ธฐ ์š”์ฒญ๋งŒ ๊ณจ๋ผ ์ฃฝ์ด๋˜ ์ด ์žฅ์• ๋ฅผ ์–ด๋–ป๊ฒŒ ์žก์•˜๋Š”์ง€ ์ •๋ฆฌํ•œ๋‹ค.

ํ•ต์‹ฌ ์š”์•ฝ

  • ์ฆ์ƒ: ์กฐํšŒ(SELECT)๋Š” ์ •์ƒ, @Transactional ์“ฐ๊ธฐ ์š”์ฒญ๋งŒ ์ „๋ถ€ 500 (CannotCreateTransactionException). ํ—ฌ์Šค์ฒดํฌ๋„ DOWN.
  • ํ™˜๊ฒฝ: ์•ฑ์ด PostgreSQL์— ์ง์ ‘ ๋ถ™์ง€ ์•Š๊ณ  PgBouncer(์ปค๋„ฅ์…˜ ํ’€๋Ÿฌ)๋ฅผ ๊ฒฝ์œ . pool_mode = transaction.
  • ๋ฐฉ์•„์‡ : ๋ˆ„๊ตฐ๊ฐ€ JDBC ๋“œ๋ผ์ด๋ฒ„๋ฅผ org.postgresql.Driver → log4jdbc(DriverSpy, SQL ๋กœ๊น… ํ”„๋ก์‹œ)๋กœ ๋ฐ”๊ฟˆ.
  • ๊ทผ๋ณธ ์›์ธ: PgBouncer ํŠธ๋žœ์žญ์…˜ ํ’€๋ง + server-side prepared statement ์ถฉ๋Œ. ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๋ฐ”๊พธ๋ฉฐ prepareThreshold=0 ์˜ต์…˜์ด ๋น ์ง.
  • ํ•ด๊ฒฐ: ๋“œ๋ผ์ด๋ฒ„ ์›๋ณต + prepareThreshold=0 (๋˜๋Š” ํ•ด๋‹น DB๋งŒ pool_mode=session).

1. ๋ฐฐ๊ฒฝ — ์šฐ๋ฆฌ DB ์•ž์—” PgBouncer๊ฐ€ ์žˆ๋‹ค

๋จผ์ € ๊ตฌ์กฐ๋ถ€ํ„ฐ. ๋ณดํ†ต ์Šคํ”„๋ง ์•ฑ์€ ์ด๋ ‡๊ฒŒ PostgreSQL์— ๋ฐ”๋กœ ๋ถ™๋Š”๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

App (Spring/JPA)  โ”€โ”€โ–บ  PostgreSQL

๊ทธ๋Ÿฐ๋ฐ ์šฐ๋ฆฌ ํ™˜๊ฒฝ์€ ์ค‘๊ฐ„์— ํ•œ ๋‹จ์ด ๋” ์žˆ๋‹ค. ์ปค๋„ฅ์…˜ ์ˆ˜๊ฐ€ ํญ์ฆํ•ด DB ์ปค๋„ฅ์…˜ ์Šฌ๋กฏ์ด ๊ณ ๊ฐˆ๋˜๋Š” ๊ฑธ ๋ง‰์œผ๋ ค๊ณ , ์„œ๋ฒ„ ๋‹ด๋‹น์ด PgBouncer๋ผ๋Š” ์ปค๋„ฅ์…˜ ํ’€๋Ÿฌ๋ฅผ ์•ž๋‹จ์— ๋‘” ๊ฒƒ์ด๋‹ค.

App (Spring/JPA)  โ”€โ”€โ–บ  PgBouncer  โ”€โ”€โ–บ  PostgreSQL
                       (pool_mode=transaction)

PgBouncer๋Š” "์ˆ˜์‹ญ ๊ฐœ์˜ ์•ฑ ์ปค๋„ฅ์…˜์„ ์†Œ์ˆ˜์˜ ์‹ค์ œ DB ์ปค๋„ฅ์…˜์œผ๋กœ ๋‹ค์ค‘ํ™”"ํ•ด์ฃผ๋Š” ์ค‘๊ฐ„ ๊ด€๋ฆฌ์ž๋‹ค. ๋ฌธ์ œ๋Š” ์ด ๋‹ค์ค‘ํ™” ๋ฐฉ์‹(pool_mode)์ด ๋ช‡ ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค.

pool_mode ์„œ๋ฒ„ ์ปค๋„ฅ์…˜์„ ์–ธ์ œ๊นŒ์ง€ ๋นŒ๋ ค์ฃผ๋‚˜ ํŠน์ง•
session ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ์„ ๋Š์„ ๋•Œ๊นŒ์ง€ ์„ธ์…˜ ๊ธฐ๋Šฅ ์•ˆ์ „, ์ปค๋„ฅ์…˜ ์ ˆ์•ฝ ํšจ๊ณผ ์ ์Œ
transaction ํŠธ๋žœ์žญ์…˜ ํ•˜๋‚˜ ๋๋‚  ๋•Œ๊นŒ์ง€๋งŒ ์ปค๋„ฅ์…˜ ์ ˆ์•ฝ ํผ, ์„ธ์…˜ ์ƒํƒœ ์•ˆ ์‚ด์•„๋‚จ์Œ
statement ์ฟผ๋ฆฌ ํ•œ ๋ฌธ์žฅ ๋‹จ์œ„ ๊ฐ€์žฅ ๊ณต๊ฒฉ์ , ์ œ์•ฝ ๋งŽ์Œ

์šฐ๋ฆฌ๋Š” ์ปค๋„ฅ์…˜ ์ ˆ์•ฝ์ด ์ ˆ์‹คํ•ด์„œ transaction ๋ชจ๋“œ์˜€๋‹ค. ์ด ํ•œ ์ค„์ด ์ด๋ฒˆ ์žฅ์• ์˜ ์ ˆ๋ฐ˜์„ ์ฐจ์ง€ํ•œ๋‹ค. "ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๋ฉด ์„œ๋ฒ„ ์ปค๋„ฅ์…˜์„ ํšŒ์ˆ˜ํ•ด์„œ ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋„˜๊ธด๋‹ค"๋Š” ๋™์ž‘์„ ๊ผญ ๊ธฐ์–ตํ•˜์ž.

2. ๋ฌธ์ œ ๋ฐœ์ƒ — ์ฝ๊ธฐ๋Š” ๋˜๋Š”๋ฐ ์“ฐ๊ธฐ๋งŒ ์ฃฝ๋Š”๋‹ค

์–ด๋А ๋‚  ์•„์นจ, ๋ฐฑ์—”๋“œ๊ฐ€ ์ด์ƒํ•˜๊ฒŒ ๊ตด๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค.

  • ํ™”๋ฉด ์กฐํšŒ(๋‹จ์ˆœ SELECT) → ์ •์ƒ
  • ๋“ฑ๋ก/์ˆ˜์ •์ฒ˜๋Ÿผ @Transactional์ด ๊ฑธ๋ฆฐ ์“ฐ๊ธฐ API → ์ „๋ถ€ 500
  • ์Šคํ”„๋ง ์•ก์ถ”์—์ดํ„ฐ /health → DOWN

๋กœ๊ทธ๋ฅผ ๊นŒ๋ณด๋‹ˆ ์“ฐ๊ธฐ ์š”์ฒญ๋งˆ๋‹ค ์ด ์˜ˆ์™ธ๊ฐ€ ์ฐํ˜”๋‹ค.

org.springframework.transaction.CannotCreateTransactionException:
    Could not open JDBC Connection for transaction;
    nested exception is org.postgresql.util.PSQLException: ...
  at o.s.jdbc.datasource.DataSourceTransactionManager.doBegin(...)
  at o.s.jdbc.datasource.DataSourceTransactionManager.doBegin(...)
  ...

CannotCreateTransactionException. ์Šคํ”„๋ง์ด "ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘์กฐ์ฐจ ๋ชป ํ•˜๊ฒ ๋‹ค"๊ณ  ์†์„ ๋“  ๊ฒƒ์ด๋‹ค. ์ฝ๊ธฐ๋Š” ๋˜๋Š”๋ฐ ํŠธ๋žœ์žญ์…˜ ์“ฐ๊ธฐ๋งŒ ๊นจ์ง„๋‹ค๋‹ˆ, ์ง๊ด€์ ์œผ๋กœ ์ž˜ ์•ˆ ์™€๋‹ฟ๋Š” ๋น„๋Œ€์นญ์ด์—ˆ๋‹ค.

3. ์ง„๋‹จ ๊ณผ์ • — ๋ฒ”์œ„๋ฅผ ์ขํ˜€๋‚˜๊ฐ€๋‹ค

3-1. DB๊ฐ€ ์ฃฝ์—ˆ๋‚˜? → ์•„๋‹ˆ๋‹ค

๊ฐ€์žฅ ๋จผ์ € ์˜์‹ฌํ•œ ๊ฑด DB ์ž์ฒด์˜€๋‹ค. ๊ทธ๋ž˜์„œ ์•ฑ์„ ๊ฑฐ์น˜์ง€ ์•Š๊ณ , ํŒŒ์ด์ฌ psycopg2๋กœ DB์— ์ง์ ‘ INSERT๋ฅผ ๋‚ ๋ ค๋ดค๋‹ค.

import psycopg2
conn = psycopg2.connect(host="db-host", port=5432, dbname="appdb",
                        user="app", password="****")
cur = conn.cursor()
cur.execute("INSERT INTO t_test(name) VALUES ('hello')")
conn.commit()   # ← ์ •์ƒ ์ปค๋ฐ‹๋จ!
print("write ok")

์ •์ƒ์ ์œผ๋กœ ๋“ค์–ด๊ฐ”๋‹ค. DB๋„, ์Šคํ‚ค๋งˆ๋„, ๊ถŒํ•œ๋„ ๋ฉ€์ฉกํ•˜๋‹ค. ์ฆ‰ ๋ฌธ์ œ๋Š” PostgreSQL์ด ์•„๋‹ˆ๋ผ ์•ฑ ↔ DB ์‚ฌ์ด ์–ด๋”˜๊ฐ€์— ์žˆ๋‹ค. ์ด ํ•œ ๋ฒˆ์˜ ์‹คํ—˜์œผ๋กœ ์šฉ์˜์„ ์ƒ์—์„œ "DB ์„œ๋ฒ„"๋ฅผ ์ง€์› ๋‹ค. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์—์„œ ์ œ์ผ ์ค‘์š”ํ•œ ๊ฑด ์ด๋ ‡๊ฒŒ ๋ฒ”์œ„๋ฅผ ๋ฐ˜์”ฉ ์ž˜๋ผ๋‚ด๋Š” ๊ฒƒ์ด๋‹ค.

3-2. ์ตœ๊ทผ ๋ฐ”๋€ ๊ฒŒ ๋ญ์ง€? → ๋“œ๋ผ์ด๋ฒ„ ๊ต์ฒด

"์–ด์ œ๊นŒ์ง€ ๋๋Š”๋ฐ ์˜ค๋Š˜ ์•ˆ ๋œ๋‹ค"๋ฉด ๊ทธ ์‚ฌ์ด ๋ณ€๊ฒฝ์ ์„ ์˜์‹ฌํ•ด์•ผ ํ•œ๋‹ค. ์ปค๋ฐ‹ ๋กœ๊ทธ๋ฅผ ๋’ค์ ธ๋ณด๋‹ˆ, ์ „๋‚  ๋ˆ„๊ตฐ๊ฐ€ SQL ๋กœ๊ทธ๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ์ฐ์œผ๋ ค๊ณ  JDBC ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๋ฐ”๊ฟ”๋†จ๋‹ค.

# before (์ •์ƒ)
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://pgbouncer-host:6432/appdb?prepareThreshold=0

# after (์žฅ์• )
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:postgresql://pgbouncer-host:6432/appdb

๋‘ ๊ฐ€์ง€๊ฐ€ ๋™์‹œ์— ๋ฐ”๋€Œ์—ˆ๋‹ค. โ‘  ๋“œ๋ผ์ด๋ฒ„๊ฐ€ org.postgresql.Driver → log4jdbc DriverSpy(์‹ค์ œ ๋“œ๋ผ์ด๋ฒ„ ์•ž์— ๋ผ๋Š” ๋กœ๊น… ํ”„๋ก์‹œ). โ‘ก ๊ทธ๋Ÿฌ๋ฉด์„œ URL์˜ prepareThreshold=0 ์˜ต์…˜์ด ์Šฌ๊ทธ๋จธ๋‹ˆ ์‚ฌ๋ผ์กŒ๋‹ค.

3-3. ๊ต์ฐจ๊ฒ€์ฆ — ์„ธ ๋ชจ๋ธ์—๊ฒŒ ๊ฐ™์€ ์งˆ๋ฌธ์„ ๋˜์กŒ๋‹ค

์›์ธ์ด PgBouncer์ธ์ง€ ๋“œ๋ผ์ด๋ฒ„์ธ์ง€ ํ™•์‹ ์ด ์•ˆ ์„œ์„œ, ๊ฐ™์€ ์ฆ์ƒ·ํ™˜๊ฒฝ์„ ์ •๋ฆฌํ•ด Claude·GPT·Gemini ์„ธ ๋ชจ๋ธ์— ๋˜‘๊ฐ™์ด ๋ฌผ์–ด๋ดค๋‹ค. ์…‹ ๋‹ค ์ž…์„ ๋ชจ์•„ "PgBouncer ํŠธ๋žœ์žญ์…˜ ๋ชจ๋“œ × server-side prepared statement ์ถฉ๋Œ"์„ ์ง€๋ชฉํ–ˆ๋‹ค. ํŠนํžˆ GPT๊ฐ€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ฐ€์žฅ ์ •ํ™•ํžˆ ์งš์–ด์คฌ๋‹ค. ํ•œ ๋ชจ๋ธ๋งŒ ๋ดค์œผ๋ฉด ๋†“์ณค์„ ๋””ํ…Œ์ผ์„ ๊ต์ฐจ๊ฒ€์ฆ์ด ์žก์•„์คฌ๋‹ค.

4. ๊ทผ๋ณธ ์›์ธ — prepared statement๋Š” "๊ทธ ์ปค๋„ฅ์…˜์˜ ์‚ฌ๋ฌผํ•จ"์ด๋‹ค

ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ์ด๋ ‡๋‹ค. PostgreSQL JDBC ๋“œ๋ผ์ด๋ฒ„(pgJDBC)๋Š” ๊ฐ™์€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต๋˜๋ฉด, ๋งค๋ฒˆ ํŒŒ์‹ฑํ•˜์ง€ ์•Š์œผ๋ ค๊ณ  ์ผ์ • ํšŸ์ˆ˜(prepareThreshold, ๊ธฐ๋ณธ 5) ์ดํ›„ server-side prepared statement๋กœ ์Šน๊ฒฉ์‹œํ‚จ๋‹ค. ์„œ๋ฒ„์— S_1 ๊ฐ™์€ ์ด๋ฆ„ํ‘œ๋ฅผ ๋ถ™์—ฌ ๋ฏธ๋ฆฌ ์ค€๋น„ํ•ด๋‘๊ณ , ๋‹ค์Œ๋ถ€ํ„ด ๊ทธ ์ด๋ฆ„ํ‘œ๋งŒ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

๋น„์œ : prepared statement๋Š” ํ—ฌ์Šค์žฅ ์‚ฌ๋ฌผํ•จ์— ๋ฏธ๋ฆฌ ๋„ฃ์–ด๋‘” ์šด๋™ํ™”๋‹ค. "3๋ฒˆ ์‚ฌ๋ฌผํ•จ์— ๋‚ด ์‹ ๋ฐœ ์žˆ์–ด์š”"๋ผ๊ณ  ์™ธ์น˜๋ฉด ์ง์›์ด ๊บผ๋‚ด์ค€๋‹ค. ๊ทธ๋Ÿฐ๋ฐ PgBouncer transaction ๋ชจ๋“œ๋Š”, ์šด๋™ ํ•œ ์„ธํŠธ(=ํŠธ๋žœ์žญ์…˜)๊ฐ€ ๋๋‚  ๋•Œ๋งˆ๋‹ค ์‚ฌ๋ฌผํ•จ์„ ํšŒ์ˆ˜ํ•ด ๋‹ค๋ฅธ ํšŒ์›์—๊ฒŒ ๋ฐฐ์ •ํ•œ๋‹ค. ๋‹ค์Œ์— ๊ฐ”๋”๋‹ˆ ๋‚˜ํ•œํ…Œ๋Š” 5๋ฒˆ ์‚ฌ๋ฌผํ•จ์ด ๋ฐฐ์ •๋๋Š”๋ฐ, ๊ฑฐ๊ธฐ์—” ๋‚ด ์‹ ๋ฐœ์ด ์—†๋‹ค. "3๋ฒˆ์— ์žˆ๋‹ค๋ฉฐ์š”?" → ์ง์›: "๊ทธ๋Ÿฐ ์‹ ๋ฐœ ์—†๋Š”๋ฐ์š”(prepared statement does not exist)."

์ฆ‰ ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๋ฉด PgBouncer๊ฐ€ ์„œ๋ฒ„ ์ปค๋„ฅ์…˜(backend)์„ ํšŒ์ˆ˜ํ•˜๋ฏ€๋กœ, ๋‹ค์Œ ํŠธ๋žœ์žญ์…˜์€ ๋‹ค๋ฅธ backend์— ๋ถ™์„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋“œ๋ผ์ด๋ฒ„๋Š” "์•„๊นŒ ๋งŒ๋“  S_1 ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด ๋˜์ง€" ํ•˜๊ณ  ํ˜ธ์ถœํ•œ๋‹ค. ์ƒˆ backend์—” S_1์ด ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋Ÿฐ ๋ฅ˜์˜ ์—๋Ÿฌ๊ฐ€ ํ„ฐ์ง„๋‹ค.

ERROR: prepared statement "S_1" does not exist        (SQLSTATE 26000)
-- ๋˜๋Š”
ERROR: prepared statement "S_3" already exists         (SQLSTATE 42P05)

๊ทธ๋Ÿผ ์™œ ์ฝ๊ธฐ๋Š” ๋˜๊ณ  ์“ฐ๊ธฐ๋งŒ ์ฃฝ์—ˆ๋‚˜?

  • ์ฝ๊ธฐ: ๋‹จ๋ฐœ์„ฑ autocommit SELECT๋Š” ํŠธ๋žœ์žญ์…˜์— ๊ธธ๊ฒŒ ๋ฌถ์ด์ง€ ์•Š๊ณ , prepared statement ์Šน๊ฒฉ ์ž„๊ณ„์น˜์—๋„ ์ž˜ ์•ˆ ๋‹ฟ๋Š”๋‹ค. ๊ทธ๋ž˜์„œ ํ‘œ๋ฉด์ ์œผ๋กœ ๋ฉ€์ฉกํ•ด ๋ณด์ธ๋‹ค.
  • ์“ฐ๊ธฐ: @Transactional์€ BEGIN์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ๋ช…์‹œ์ ์œผ๋กœ ์—ด๊ณ , Hibernate๊ฐ€ INSERT/UPDATE/์‹œํ€€์Šค ์กฐํšŒ ๋“ฑ ์—ฌ๋Ÿฌ PreparedStatement๋ฅผ ๋ฐ˜๋ณต ์‚ฌ์šฉํ•œ๋‹ค. prepared statement mismatch๊ฐ€ ์ •ํ†ต์œผ๋กœ ํ„ฐ์ง€๋Š” ๊ตฌ๊ฐ„์ด๋‹ค.

์—ฌ๊ธฐ์— log4jdbc ํ”„๋ก์‹œ๊ฐ€ ๊ธฐ๋ฆ„์„ ๋ถ€์—ˆ๋‹ค. ์ •์ƒ์ด๋˜ prepareThreshold=0(=server-side prepared statement ๋„๊ธฐ) ์˜ต์…˜์ด ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๋ฐ”๊พธ๋ฉฐ ๋น ์ง€์ž, pgJDBC ๊ธฐ๋ณธ๊ฐ’(5)์œผ๋กœ ๋Œ์•„๊ฐ€ prepared statement๊ฐ€ ๋‹ค์‹œ ์ผœ์กŒ๊ณ , PgBouncer ํŠธ๋žœ์žญ์…˜ ๋ชจ๋“œ์™€ ์ •๋ฉด์ถฉ๋Œํ–ˆ๋‹ค. ๊ฒฐ๊ตญ ์Šคํ”„๋ง ์ž…์žฅ์—์„  ํŠธ๋žœ์žญ์…˜์„ ์ •์ƒ์ ์œผ๋กœ ์‹œ์ž‘·์œ ์ง€ํ•  ์ˆ˜ ์—†์œผ๋‹ˆ CannotCreateTransactionException์œผ๋กœ ๊ฐ์‹ธ ๋˜์ง„ ๊ฒƒ์ด๋‹ค.

5. ํ•ด๊ฒฐ — ๋‘ ๋‹จ๊ณ„๋กœ

5-1. ์ฆ‰์‹œ ๋ณต๊ตฌ: ๋“œ๋ผ์ด๋ฒ„ ์›๋ณต

์šด์˜ ์žฅ์•  ์ƒํ™ฉ์ด๋ผ ๋จผ์ € ์ถœํ˜ˆ์„ ๋ง‰์•„์•ผ ํ–ˆ๋‹ค. ๋กœ๊น… ํ”„๋ก์‹œ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๊ฑท์–ด๋‚ด๊ณ  ํ‘œ์ค€ ๋“œ๋ผ์ด๋ฒ„๋กœ ๋˜๋Œ๋ ธ๋‹ค.

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://pgbouncer-host:6432/appdb?prepareThreshold=0

๋˜๋Œ๋ฆฌ์ž๋งˆ์ž ์“ฐ๊ธฐ ํŠธ๋žœ์žญ์…˜์ด ์ฆ‰์‹œ ๋ณต๊ตฌ๋๊ณ , ํ—ฌ์Šค์ฒดํฌ๋„ UP์œผ๋กœ ๋Œ์•„์™”๋‹ค.

5-2. ์˜๊ตฌ ๋Œ€์‘: ์˜ต์…˜์„ ๋ช…์‹œ์ ์œผ๋กœ ๋ฐ•๋Š”๋‹ค

ํ•ต์‹ฌ์€ PgBouncer ํŠธ๋žœ์žญ์…˜ ๋ชจ๋“œ ๋’ค์—์„œ๋Š” server-side prepared statement๋ฅผ ๋„๋Š” ๊ฒƒ์ด๋‹ค. JDBC URL์— ๋‹ค์Œ์„ ๋ช…์‹œํ•œ๋‹ค.

jdbc:postgresql://pgbouncer-host:6432/appdb?prepareThreshold=0&preparedStatementCacheQueries=0
  • prepareThreshold=0 : server-side prepared statement๋กœ ์Šน๊ฒฉํ•˜์ง€ ์•Š์Œ (ํ•ต์‹ฌ).
  • preparedStatementCacheQueries=0 : ํด๋ผ์ด์–ธํŠธ ์ธก statement ์บ์‹œ๊นŒ์ง€ ์ค„์ด๋Š” ๋ณด์กฐ ์˜ต์…˜.

์„ ํƒ์ง€๋ฅผ ํ‘œ๋กœ ์ •๋ฆฌํ•˜๋ฉด ์ด๋ ‡๋‹ค.

๋Œ€์‘ ๋ฐฉ๋ฒ• ํŠธ๋ ˆ์ด๋“œ์˜คํ”„
prepareThreshold=0 ์•ฑ JDBC URL์— ์˜ต์…˜ ์ถ”๊ฐ€ (๊ถŒ์žฅ) ๊ฐ€์žฅ ํ”ํ•œ ํ•ด๊ฒฐ์ฑ…. ์•ฑ๋งŒ ๊ณ ์น˜๋ฉด ๋จ
pool_mode=session ํ•ด๋‹น DB/์œ ์ €๋งŒ ์„ธ์…˜ ํ’€๋ง ํ˜ธํ™˜์„ฑ ์ตœ๊ณ , ์ปค๋„ฅ์…˜ ์ ˆ์•ฝ ํšจ๊ณผ ๊ฐ์†Œ
max_prepared_statements PgBouncer 1.21+์—์„œ 0์ด ์•„๋‹Œ ๊ฐ’ ๋ฒ„์ „·๋ฉ”๋ชจ๋ฆฌ ๊ณ ๋ ค ํ•„์š”
๋กœ๊น… ํ”„๋ก์‹œ ์ œ๊ฑฐ ์šด์˜์—์„  log4jdbc ์ƒ์‹œ ์‚ฌ์šฉ ์ง€์–‘ SQL ๋กœ๊น…์€ ๊ด€์ธก ๋„๊ตฌ/์Šฌ๋กœ์šฐ์ฟผ๋ฆฌ๋กœ ๋Œ€์ฒด

์ง„๋‹จ์— ์“ด ๋ช…๋ น

PgBouncer ํ’€ ์ƒํƒœ๋Š” ๊ด€๋ฆฌ ์ฝ˜์†”์—์„œ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ํ’€์ด ์†Œ์ง„๋œ ๊ฑด์ง€, ์„ค์ •(mode) ๋ฌธ์ œ์ธ์ง€ ๊ฐ€๋ฅด๋Š” ๋ฐ ์œ ์šฉํ•˜๋‹ค.

$ psql -p 6432 -U pgbouncer_admin -d pgbouncer -c "SHOW POOLS"
# database | pool_mode | cl_active | cl_waiting | maxwait | ...
#  appdb   | transaction |    3     |     0      |   0     | ...

cl_waiting·maxwait๊ฐ€ ์Œ“์ด๋ฉด ํ’€ ์†Œ์ง„, ๊ทธ๊ฒŒ ์•„๋‹ˆ๋ผ๋ฉด pool_mode์™€ ํด๋ผ์ด์–ธํŠธ JDBC ์„ค์ •์„ ์˜์‹ฌํ•˜๋ฉด ๋œ๋‹ค.

๋ณด๋„ˆ์Šค โ‘  — log4jdbc(DriverSpy)๋Š” ๋Œ€์ฒด ๋ญ˜ ํ•˜๊ธธ๋ž˜

log4jdbc๋Š” ๊ทธ ์ž์ฒด๋กœ DB์— ๋ถ™๋Š” ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ์•„๋‹ˆ๋‹ค. ์‹ค์ œ ๋“œ๋ผ์ด๋ฒ„ ์•ž์— ๋ผ์–ด๋“œ๋Š” ํ”„๋ก์‹œ(wrapper)๋‹ค. ๊ตฌ์กฐ๋ฅผ ๊ทธ๋ฆผ์œผ๋กœ ๋ณด๋ฉด ์ด๋ ‡๋‹ค.

// ํ‘œ์ค€ ๊ฒฝ๋กœ
App  โ”€โ”€โ–บ  org.postgresql.Driver  โ”€โ”€โ–บ  DB

// log4jdbc๋ฅผ ๋ผ์šฐ๋ฉด
App  โ”€โ”€โ–บ  DriverSpy (ํ”„๋ก์‹œ)  โ”€โ”€โ–บ  org.postgresql.Driver  โ”€โ”€โ–บ  DB
              โ”‚
              โ””โ”€โ–บ ์˜ค๊ฐ€๋Š” JDBC ํ˜ธ์ถœ(SQL/ํŒŒ๋ผ๋ฏธํ„ฐ/ํƒ€์ด๋ฐ)์„ ๊ฐ€๋กœ์ฑ„ SLF4J๋กœ ๋กœ๊น…

๊ทธ๋ž˜์„œ ์“ฐ๋ ค๋ฉด ๋‘ ๊ฐ€์ง€๋ฅผ ๋ฐ”๊ฟ”์•ผ ํ•œ๋‹ค. โ‘  ๋“œ๋ผ์ด๋ฒ„ ํด๋ž˜์Šค๋ฅผ net.sf.log4jdbc.sql.jdbcapi.DriverSpy๋กœ โ‘ก JDBC URL ์•ž์— jdbc:log4jdbc: ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์ด๊ธฐ. ๋ฌธ์ œ๋Š” ์ด ๊ณผ์ •์—์„œ ๊ธฐ์กด URL์— ๋ถ™์–ด ์žˆ๋˜ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ(?prepareThreshold=0 ๋“ฑ)๋ฅผ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ฒจ ์ ์–ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. ์šฐ๋ฆฌ๋Š” ๋ฐ”๋กœ ์ด๊ฑธ ๋†“์ณค๋‹ค. ํ”„๋ก์‹œ๊ฐ€ ๋ชจ๋“  JDBC ํ˜ธ์ถœ์„ ํ•œ ๋ฒˆ ๋” ๊ฐ์‹ธ ๋“ค์—ฌ๋‹ค๋ณด๋Š” ๊ตฌ์กฐ๋ผ, ๋ฏธ๋ฌ˜ํ•œ ๋“œ๋ผ์ด๋ฒ„ ๋™์ž‘ ์ฐจ์ด๋‚˜ ์˜ต์…˜ ๋ˆ„๋ฝ์ด ๊ทธ๋Œ€๋กœ ์‚ฌ๊ณ ๋กœ ์ด์–ด์ง„๋‹ค. "๋กœ๊น…ํ•˜๋ ค๊ณ  ๋ผ์šด ๊ฒŒ ๋™์ž‘๊นŒ์ง€ ๋ฐ”๊พผ๋‹ค"๋Š” ๊ฒŒ ํ”„๋ก์‹œ ๋“œ๋ผ์ด๋ฒ„์˜ ์–‘๋‚ ์˜ ๊ฒ€์ด๋‹ค.

๋ณด๋„ˆ์Šค โ‘ก — ๋น„์Šทํ•œ ์ฆ์ƒ์ด๋ฉด ์ด ์ˆœ์„œ๋กœ ์ ๊ฒ€

ํ˜น์‹œ ์ง€๊ธˆ "์กฐํšŒ๋Š” ๋˜๋Š”๋ฐ ์“ฐ๊ธฐ๋งŒ ๊นจ์ง€๋Š”" ์ƒํ™ฉ์ด๋ผ๋ฉด, ์•„๋ž˜ ์ˆœ์„œ๋กœ ์ขํ˜€๊ฐ€๋ฉด ๋น ๋ฅด๋‹ค.

  1. DB ์ง์ ‘ ์“ฐ๊ธฐ๋กœ DB ๊ฒฐ๋ฐฑ๋ถ€ํ„ฐ ํ™•์ธ (psycopg2 / psql). ๋˜๋ฉด DB๋Š” ์šฉ์˜์„ ์ƒ์—์„œ ์ œ์™ธ.
  2. ์˜ˆ์™ธ์˜ nested cause๋ฅผ ๋๊นŒ์ง€ ๊นŒ๋ณธ๋‹ค. prepared statement "..." does not exist / already exists, SQLSTATE 26000 / 42P05๊ฐ€ ๋ณด์ด๋ฉด ๊ฑฐ์˜ ํ™•์ •.
  3. ์•ž๋‹จ์— PgBouncer/ํ’€๋Ÿฌ๊ฐ€ ์žˆ๋Š”์ง€, ์žˆ๋‹ค๋ฉด SHOW POOLS๋กœ pool_mode๋ฅผ ํ™•์ธ. transaction์ด๋ฉด ์˜์‹ฌ ๊ฐ•ํ™”.
  4. JDBC URL์— prepareThreshold=0์ด ์žˆ๋Š”์ง€ ํ™•์ธ. ์—†์œผ๋ฉด ์ถ”๊ฐ€ํ•˜๊ณ  ์žฌ๊ธฐ๋™ํ•ด ๋ฐ”๋กœ ์‚ด์•„๋‚˜๋Š”์ง€ ๋ณธ๋‹ค.
  5. ์ตœ๊ทผ ๋“œ๋ผ์ด๋ฒ„/๋ฐ์ดํ„ฐ์†Œ์Šค ์„ค์ • ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ํ™•์ธ. ๋กœ๊น… ํ”„๋ก์‹œ ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ๋ผ์—ˆ๋Š”์ง€ ๋ณธ๋‹ค.

์ด ๋‹ค์„ฏ ์ค„์ด๋ฉด ๋Œ€๋ถ€๋ถ„ 30๋ถ„ ์•ˆ์— ์›์ธ์ด ์žกํžŒ๋‹ค.

6. ๊ตํ›ˆ

  • "๋กœ๊ทธ ์ข€ ์˜ˆ์˜๊ฒŒ"๊ฐ€ ์šด์˜์„ ๋ฉˆ์ถœ ์ˆ˜ ์žˆ๋‹ค. log4jdbc ๊ฐ™์€ ํ”„๋ก์‹œ ๋“œ๋ผ์ด๋ฒ„๋Š” ์‹ค์ œ ๋“œ๋ผ์ด๋ฒ„ ์•ž์— ๋ผ์–ด๋“ค์–ด ๋™์ž‘์„ ๋ฐ”๊พผ๋‹ค. ํŠนํžˆ PgBouncer ํŠธ๋žœ์žญ์…˜ ๋ชจ๋“œ์ฒ˜๋Ÿผ ๊นŒ๋‹ค๋กœ์šด ํ™˜๊ฒฝ์—์„  ์ƒ์‹œ ์‚ฌ์šฉ์„ ํ”ผํ•˜์ž. SQL ๋กœ๊น…์€ p6spy ์ƒ์‹œ ํˆฌ์ž…๋ณด๋‹ค ๊ด€์ธก ๋„๊ตฌ·์Šฌ๋กœ์šฐ์ฟผ๋ฆฌ ๋กœ๊ทธ๋กœ ๋นผ๋Š” ๊ฒŒ ์•ˆ์ „ํ•˜๋‹ค.
  • ์„ค์ •์„ ๋ฐ”๊ฟ€ ๋• "์˜ต์…˜์ด ๋”ฐ๋ผ์™”๋Š”์ง€"๊นŒ์ง€ ๋ณธ๋‹ค. ์ด๋ฒˆ ์žฅ์• ์˜ ์ง„์งœ ํŠธ๋ฆฌ๊ฑฐ๋Š” ๋“œ๋ผ์ด๋ฒ„ ์ž์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ, ๊ต์ฒด ๊ณผ์ •์—์„œ ์กฐ์šฉํžˆ ๋น ์ง„ prepareThreshold=0 ํ•œ ์ค„์ด์—ˆ๋‹ค.
  • PgBouncer transaction ๋ชจ๋“œ = ์„ธ์…˜ ์ƒํƒœ๊ฐ€ ์•ˆ ์‚ด์•„๋‚จ๋Š”๋‹ค. prepared statement, SET, ์ž„์‹œ ํ…Œ์ด๋ธ”, advisory lock, LISTEN/NOTIFY ๊ฐ™์€ ์„ธ์…˜ ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ์€ ์ด ๋ชจ๋“œ์™€ ์ƒ๊ทน์ด๋‹ค. ํ’€๋Ÿฌ ๋’ค์— ์žˆ๋‹ค๋Š” ๊ฑธ ํ•ญ์ƒ ์˜์‹ํ•ด์•ผ ํ•œ๋‹ค.
  • "์ฝ๊ธฐ๋Š” ๋˜๋Š”๋ฐ ์“ฐ๊ธฐ๋งŒ ์ฃฝ๋Š”๋‹ค"๋Š” ๊ฐ•๋ ฅํ•œ ๋‹จ์„œ๋‹ค. DB๊ฐ€ ํ†ต์งธ๋กœ ์ฃฝ์€ ๊ฒŒ ์•„๋‹ˆ๋ผ, ํŠธ๋žœ์žญ์…˜·์„ธ์…˜ ์ƒํƒœ๊ฐ€ ์–ฝํžŒ ๊ฒฝ๋กœ๋งŒ ๊นจ์ง„๋‹ค๋Š” ๋œป. ์ฆ์ƒ์˜ ๋น„๋Œ€์นญ์„ ๊ทธ๋ƒฅ ๋„˜๊ธฐ์ง€ ๋ง์ž.
  • ๋ฒ”์œ„๋ฅผ ๋ฐ˜์”ฉ ์ž˜๋ผ๋ผ. psycopg2 ์ง์ ‘ ์“ฐ๊ธฐ ํ•œ ๋ฒˆ์œผ๋กœ "DB๋Š” ๊ฒฐ๋ฐฑ"์„ ์ฆ๋ช…ํ•˜๊ณ  ๋‚˜๋‹ˆ, ๋‚จ์€ ๊ฑด ์•ฑ↔ํ’€๋Ÿฌ↔๋“œ๋ผ์ด๋ฒ„ ๊ตฌ๊ฐ„๋ฟ์ด์—ˆ๋‹ค. ์˜์‹ฌ ๋ฒ”์œ„๋ฅผ ๋น ๋ฅด๊ฒŒ ์ขํžˆ๋Š” ๊ฒŒ ๊ณง ํ•ด๊ฒฐ ์†๋„๋‹ค.

๊ฐ™์€ ๊ตฌ์กฐ(์•ฑ → PgBouncer → PostgreSQL)๋ฅผ ์“ฐ๋Š” ๋ถ„์ด๋ผ๋ฉด, pool_mode๊ฐ€ transaction์ธ์ง€ ๋จผ์ € ํ™•์ธํ•˜๊ณ  JDBC URL์— prepareThreshold=0์ด ๋ฐ•ํ˜€ ์žˆ๋Š”์ง€ ์ ๊ฒ€ํ•ด๋ณด๊ธธ. ์–ด๋А ๋‚  ๊ฐ‘์ž๊ธฐ ์“ฐ๊ธฐ๋งŒ ์ฃฝ๋Š” ๊ทธ ํ™ฉ๋‹นํ•œ ์žฅ์• ๋ฅผ, ํ•œ ์ค„๋กœ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ๋‹ค.