레이블이 데이터베이스인 게시물을 표시합니다. 모든 게시물 표시
레이블이 데이터베이스인 게시물을 표시합니다. 모든 게시물 표시

목요일

PostgreSQL Connection Pool 고갈 해결하기

🔍 검색 키워드: PostgreSQL connection pool 에러, 커넥션 풀 소진, 데이터베이스 연결 오류, 최대 연결 초과

PostgreSQL Connection Pool 고갈 해결하기

증상

애플리케이션이 실행되다가 갑자기 다음과 같은 에러가 발생한다:

FATAL: sorry, too many clients already

또는:

Error: connect ECONNREFUSED - PostgreSQL connection failed

또는 connection pool 라이브러리에서:

Error: timeout acquiring a connection from the pool
  Error: Client has already been released to the pool

특히 트래픽이 많아지거나 장시간 실행되는 배치 작업 후에 자주 발생한다. 새로운 요청이 들어와도 DB 연결을 못 하고 응답 불가 상태가 된다.

원인

PostgreSQL은 최대 동시 연결 수 제한이 있고 (기본값 100), connection pool은 재사용 가능한 연결 수를 미리 정해둔다. 고갈 현상은 다음 경우에 발생:

  1. 연결을 반환 안 함: 쿼리 후 연결을 pool에 반환하지 않아 계속 증가
  2. 타임아웃 후 좀비 연결: 응답 없는 연결이 pool에 남아있음
  3. Pool 사이즈 설정 과소: 동시 사용자/요청량에 비해 pool이 너무 작음
  4. 쿼리 오래 걸림: 느린 쿼리가 연결을 오래 점유
  5. Connection leak: 예외 발생 시 연결을 close하지 않는 코드

해결방법

1. Connection Pool 설정 최적화

Node.js + pg (node-postgres):

const { Pool } = require('pg');
  
  const pool = new Pool({
    host: 'localhost',
      port: 5432,
        database: 'mydb',
          user: 'postgres',
            password: 'password',
              max: 20,                    // 최대 연결 수 (기본 10)
                idleTimeoutMillis: 30000,   // 30초 idle 후 닫기
                  connectionTimeoutMillis: 2000, // 연결 생성 타임아웃
                  });
                  
                  module.exports = pool;

Python + psycopg2:

import psycopg2.pool
  
  connection_pool = psycopg2.pool.SimpleConnectionPool(
      1,      // minimum connections
          20,     // maximum connections
              database="mydb",
                  user="postgres",
                      password="password",
                          host="localhost"
                          )
                          
                          // 사용
                          conn = connection_pool.getconn()
                          try:
                              // 쿼리 실행
                                  pass
                                  finally:
                                      connection_pool.putconn(conn)

Java + HikariCP:

HikariConfig config = new HikariConfig();
  config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
  config.setUsername("postgres");
  config.setPassword("password");
  config.setMaximumPoolSize(20);          // 최대 연결
  config.setMinimumIdle(5);               // 최소 유휴 연결
  config.setConnectionTimeout(2000);       // 2초 타임아웃
  config.setIdleTimeout(600000);          // 10분 idle 타임아웃
  config.setMaxLifetime(1800000);         // 30분 최대 수명
  
  HikariDataSource dataSource = new HikariDataSource(config);

2. 연결이 제대로 반환되는지 확인

Node.js에서 연결 누수 확인:

const pool = new Pool(config);
  
  pool.on('error', (err, client) => {
    console.error('Unexpected error on idle client', err);
      process.exit(-1);
      });
      
      pool.on('connect', () => {
        console.log('New connection created');
        });
        
        // 항상 try-finally로 연결 반환 보장
        const client = await pool.connect();
        try {
          const result = await client.query('SELECT * FROM users WHERE id = $1', [1]);
            return result.rows;
            } finally {
              client.release();  // 반드시 실행
              }

또는 with 문 활용 (Python):

from contextlib import contextmanager
  
  @contextmanager
  def get_db_connection():
      conn = connection_pool.getconn()
          try:
                  yield conn
                      finally:
                              connection_pool.putconn(conn)
                              
                              // 사용
                              with get_db_connection() as conn:
                                  cursor = conn.cursor()
                                      cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
                                          // 예외 발생해도 자동으로 반환됨

3. 느린 쿼리 최적화

-- 인덱스 추가
  CREATE INDEX idx_users_id ON users(id);
  
  -- 실행계획 확인
  EXPLAIN ANALYZE SELECT * FROM users WHERE status = 'active';
  
  -- 필요 없는 조인 제거, WHERE 조건 최적화
  SELECT u.id, u.name
  FROM users u
  WHERE u.created_at > NOW() - INTERVAL '7 days'
    AND u.status = 'active'
    LIMIT 100;

4. PostgreSQL 서버 설정 확인

# PostgreSQL 최대 연결 수 조회
  psql -U postgres -c "SHOW max_connections;"
  
  # 현재 연결 수 조회
  psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
  
  # 연결 상세 정보
  psql -U postgres -c "SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;"

필요하면 postgresql.conf에서:

max_connections = 200   # 기본값 100에서 증가

5. 좀비 연결 정리

-- 유휴 연결 종료
  SELECT pg_terminate_backend(pid)
  FROM pg_stat_activity
  WHERE datname = 'mydb'
    AND state = 'idle'
      AND query_start < now() - interval '30 minutes';
      
      -- 슬로우 쿼리 강제 종료 (주의)
      SELECT pg_terminate_backend(pid)
      FROM pg_stat_activity
      WHERE query_start < now() - interval '5 minutes'
        AND state != 'idle';

정리표

상황 원인 해결법
연결 고갈 후 다시 증가 연결을 close하지 않음 try-finally 또는 context manager로 보장
Pool 타임아웃 pool이 너무 작음 max 크기 증가, idleTimeoutMillis 조정
"too many clients" 에러 PostgreSQL 최대 연결 도달 max_connections 증가 또는 app pool 감소
간헐적 연결 오류 connectionTimeoutMillis 너무 짧음 타임아웃 값 증가 또는 네트워크 확인
메모리 누수 좀비 연결이 메모리 점유 주기적으로 idle 연결 정리 (pg_terminate_backend)

TIP: 프로덕션에서는 반드시 connection pool 크기, 타임아웃, idle 설정을 검토하고, 정기적으로 pg_stat_activity로 모니터링하자.

수요일

Prisma N+1 쿼리 문제 해결하기

Redis WRONGTYPE 에러: 데이터 타입 불일치 해결

🔍 검색 키워드: Redis WRONGTYPE 에러, Redis 데이터 타입, Redis 명령어 호환성, INCR 문자열, Redis 키 재사용

Redis WRONGTYPE 에러: 데이터 타입 불일치 해결

Redis는 강타입 데이터베이스입니다. 같은 키에 다른 타입의 데이터를 저장하려고 하면 WRONGTYPE 에러가 발생합니다. 흔히 개발 중에 같은 키명을 여러 목적으로 재사용할 때 마주치는 실수입니다. 이 글에서는 에러 원인과 해결 방법을 실제 상황으로 설명하겠습니다.

1. 증상: WRONGTYPE 에러 메시지

ERR WRONGTYPE Operation against a key holding the wrong kind of value
  at Object.writeError (/app/node_modules/redis/lib/reply.js:71:15)
  at Socket.onreply (/app/node_modules/redis/lib/index.js:304:17)

또는 Redis CLI에서:

> set user:1 "{name: john}"
  OK
  > lpush user:1 "new_value"
  (error) WRONGTYPE Operation against a key holding the wrong kind of value

2. 원인 분석

Redis는 5가지 기본 데이터 타입을 가집니다:

  • String: set, get, incr
  • List: lpush, rpush, lrange
  • Set: sadd, scard, smembers
  • Hash: hset, hget, hgetall
  • Sorted Set: zadd, zscore, zrange

원인 1: 같은 키를 다른 타입으로 사용

// 기존 코드: user:1을 String으로 사용
  await redis.set('user:1', JSON.stringify({ name: 'john' }));
  
  // 신규 코드: 같은 키를 List로 변경
  await redis.lpush('user:1', 'new_value');
  // ❌ WRONGTYPE 에러!

원인 2: 개발 중 타입 변경

// 처음에는 값 저장 (String)
  redis.set('counter', '10');
  
  // 나중에 증가시키려고 시도 (INCR은 String 전용)
  redis.incr('counter');  // 기존 값이 JSON이면 에러
  
  // 또는 반대로
  redis.lpush('counter', 1);  // 이제 List
  redis.incr('counter');       // WRONGTYPE!

원인 3: TTL 만료 후 타입 재사용

redis.set('session:abc', 'token123', 'EX', 3600);  // String으로 저장
  
  // TTL이 지나 자동 삭제됨
  // 몇 시간 후...
  
  redis.hset('session:abc', 'user_id', '123');  // Hash로 저장 시도
  // 만약 키가 남아있으면 WRONGTYPE!

3. 해결 방법

방법 1: 기존 키 삭제 후 재생성

// 안전한 재설정
  await redis.del('user:1');
  await redis.lpush('user:1', 'value1', 'value2');

방법 2: 다른 키명 사용

// AS-IS: 충돌하는 키
  const key = 'user:1';
  
  // TO-BE: 타입 명시하는 키명
  const userKey = 'user:1:string';      // String용
  const userListKey = 'user:1:list';    // List용
  const userHashKey = 'user:1:hash';    // Hash용
  
  // 또는 버전 붙이기
  const userKeyV2 = 'user:1:v2';

방법 3: 타입 확인 후 처리

// Node.js Redis 클라이언트
  async function safeSet(key, value, type = 'string') {
    const existingType = await redis.type(key);
    
      if (existingType !== 'none' && existingType !== type) {
          console.warn(`키 '${key}'의 기존 타입: ${existingType}, 요청 타입: ${type}`);
              await redis.del(key);  // 기존 데이터 삭제
                }
                
                  if (type === 'string') {
                      await redis.set(key, value);
                        } else if (type === 'list') {
                            await redis.lpush(key, value);
                              } else if (type === 'hash') {
                                  await redis.hset(key, ...Object.entries(value).flat());
                                    }
                                    }
                                    
                                    // 사용
                                    await safeSet('user:1', { name: 'john' }, 'hash');

방법 4: Redis에서 타입 조회

# 기존 모든 키와 타입 확인
  $ redis-cli KEYS '*' | while read key; do echo -n "$key: "; redis-cli TYPE "$key"; done
  
  # 특정 패턴 확인
  $ redis-cli SCAN 0 MATCH "user:*" | xargs -I {} redis-cli TYPE {}

방법 5: 마이그레이션 스크립트

// 전체 키를 새 구조로 마이그레이션
  const redis = require('redis');
  const client = redis.createClient();
  
  async function migrateKeys() {
    const keys = await new Promise((resolve, reject) => {
        let allKeys = [];
            const stream = client.scanStream();
                stream.on('data', (keys) => allKeys.push(...keys));
                    stream.on('end', () => resolve(allKeys));
                        stream.on('error', reject);
                          });
                          
                            for (const key of keys) {
                                if (key.startsWith('user:')) {
                                      const type = await client.type(key);
                                      
                                            if (type === 'string') {
                                                    const value = await client.get(key);
                                                            await client.del(key);
                                                                    await client.hset(`${key}:v2`, 'data', value);
                                                                          }
                                                                                // 다른 타입도 처리...
                                                                                    }
                                                                                      }
                                                                                      
                                                                                        console.log('마이그레이션 완료');
                                                                                        }
                                                                                        
                                                                                        await migrateKeys();

4. 타입별 명령어 정리

명령어 대상 타입 예제
set / get String set key value
lpush / lrange List lpush list_key value
sadd / smembers Set sadd set_key member
hset / hget Hash hset hash_key field value
zadd / zrange Sorted Set zadd zset_key 1 member
incr String만 incr counter
lpop List만 lpop list_key

5. 예방 가이드

개발 단계:

  • 키 설계 시 타입 명시: {domain}:{id}:{type}
  • 예: user:123:string, session:456:hash, queue:789:list

테스트 단계:

  • Redis 모의 객체로 타입 검증
  • 개발/스테이징 Redis 정기 초기화

배포 단계:

  • 마이그레이션 스크립트로 기존 키 처리
  • TTL 만료 후 재사용 금지 정책 수립

WRONGTYPE 에러는 Redis를 처음 다룰 때 흔한 실수입니다. 핵심은 같은 키에는 하나의 타입만 유지하는 것. 타입을 바꿔야 한다면 새 키를 사용하거나, 기존 데이터를 명시적으로 삭제하세요. 이를 통해 안정적인 Redis 운영이 가능합니다.

금요일

Prisma N+1 쿼리 문제 완벽 해결 가이드

🔍 검색 키워드: Prisma N+1 쿼리 문제, relation 로드 최적화, Prisma include, eager loading, 데이터베이스 쿼리 최적화

Prisma N+1 쿼리 문제 완벽 해결 가이드

1. 증상: N+1 쿼리 문제 발생

API 요청 하나에 데이터베이스 쿼리가 1 + N개 날아간다:

// 문제 있는 코드
const users = await prisma.user.findMany(); // Query 1: 사용자 100명 조회

for (const user of users) {
  user.posts = await prisma.post.findMany({
    where: { authorId: user.id }
  }); // Query 2-101: 각 사용자마다 posts 조회 (100번 반복)
}

// 총 101개의 쿼리 발생! 🚨

// 로그:
// Query 1: SELECT * FROM "User" WHERE 1=1
// Query 2: SELECT * FROM "Post" WHERE "authorId" = 1
// Query 3: SELECT * FROM "Post" WHERE "authorId" = 2
// ...
// Query 101: SELECT * FROM "Post" WHERE "authorId" = 100

API 응답 속도가 매우 느리고, 데이터베이스 CPU 사용률이 급증한다. 동시 사용자가 많아지면 connection pool 고갈로 이어진다.

2. 원인 분석

Lazy Loading의 위험성

Prisma의 기본 동작은 "필요할 때만 로드하는" lazy loading이다. relation을 접근할 때마다 새로운 쿼리를 발생시킨다.

Include 미사용

relation 데이터가 필요한데도 includeselect를 사용하지 않아 N번의 추가 쿼리가 필요하다.

깊은 relation 체인

user.posts.comments.author.profile 같은 깊은 relation 체인은 기하급수적으로 쿼리가 증가한다.

루프 내에서의 데이터베이스 접근

forEach, for-of 루프 내에서 직접 데이터베이스 쿼리를 실행하는 구조.

3. 해결방법

방법 1: Include를 사용한 Eager Loading

// 1단계: 단순 include
const users = await prisma.user.findMany({
  include: {
    posts: true  // 모든 posts를 함께 로드
  }
});
// 총 2개 쿼리:
// Query 1: SELECT * FROM "User"
// Query 2: SELECT * FROM "Post" WHERE "authorId" IN (1, 2, ..., 100)

// 2단계: 조건부 include
const users = await prisma.user.findMany({
  include: {
    posts: {
      where: { published: true },  // published=true인 posts만
      orderBy: { createdAt: 'desc' },
      take: 5  // 최근 5개만
    }
  }
});

// 3단계: 깊은 relation include (2단계까지만 권장)
const users = await prisma.user.findMany({
  include: {
    posts: {
      include: {
        comments: {  // posts의 comments도 로드
          include: {
            author: true  // comments의 author도 로드
          }
        }
      }
    }
  }
});

방법 2: Select를 사용한 필드 최적화

// 필요한 필드만 선택
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    email: true,
    posts: {
      select: {
        id: true,
        title: true,
        slug: true
      }
    }
  }
});

// 장점:
// 1. 필요 없는 컬럼(password, bio 등)을 제외 → 네트워크 대역폭 절약
// 2. 쿼리 성능 향상
// 3. 클라이언트에 민감한 정보 노출 방지

방법 3: BatchLoad 패턴 (DataLoader)

// dataloader 라이브러리 설치: npm install dataloader

import DataLoader from 'dataloader';

// User의 posts를 배치로 로드하는 DataLoader
const postsByUserIdLoader = new DataLoader(async (userIds) => {
  const postsByUserId = await prisma.post.findMany({
    where: { authorId: { in: userIds } }
  });

  // userIds 순서대로 결과 반환
  return userIds.map(userId =>
    postsByUserId.filter(post => post.authorId === userId)
  );
});

// GraphQL resolver에서 사용
const userResolver = {
  posts: (user) => postsByUserIdLoader.load(user.id)
};

// 효과:
// 100개의 개별 쿼리 대신 1개의 배치 쿼리로 통합

방법 4: 정규화된 API 응답 구조

// 비정규화 응답 (nested - 중복 데이터)
{
  users: [
    {
      id: 1,
      name: 'Alice',
      posts: [
        { id: 101, title: 'Post 1', author: { id: 1, name: 'Alice' } }
      ]
    }
  ]
}

// 정규화 응답 (flat - 효율적)
{
  users: [{ id: 1, name: 'Alice' }],
  posts: [{ id: 101, title: 'Post 1', authorId: 1 }]
}

// Prisma에서 정규화 응답 생성
const [users, posts, comments] = await Promise.all([
  prisma.user.findMany(),
  prisma.post.findMany(),
  prisma.comment.findMany()
]);

const response = {
  users,
  posts,
  comments
};
// 이렇게 하면 클라이언트가 필요한 대로 조합 가능

방법 5: 쿼리 성능 측정 및 디버깅

// .prisma/client 에서 로깅 활성화
const prisma = new PrismaClient({
  log: [
    { emit: 'stdout', level: 'query' },
    { emit: 'stdout', level: 'info' },
    { emit: 'stdout', level: 'warn' },
    { emit: 'stderr', level: 'error' }
  ]
});

// 또는 런타임 환경에서
prisma.$on('query', (e) => {
  console.log(`${e.query} - ${e.duration}ms`);
});

// 성능 프로파일링 (Node.js console.time)
console.time('fetch users with posts');
const users = await prisma.user.findMany({
  include: { posts: true }
});
console.timeEnd('fetch users with posts');
// fetch users with posts: 45.231ms

4. 정리표

상황 해결 방법 쿼리 수 성능 개선도
N+1 발생 include 미사용 1 + N -
단순 관계 include true 2 ⬆⬆⬆
조건 필터 include with where 2 ⬆⬆⬆
깊은 관계 (2단계) 중첩 include 3-4 ⬆⬆⬆
GraphQL 환경 DataLoader 1-2 (배치) ⬆⬆⬆⬆
민감한 필드 select 사용 2 + 대역폭↓ ⬆⬆⬆
매우 깊은 관계 정규화 응답 N (분산) ⬆⬆⬆

핵심: N+1은 Prisma만의 문제가 아니라 ORM의 근본적인 특성. include로 eager loading을 하거나, API 설계 단계부터 정규화된 응답 구조를 고려해야 한다. GraphQL을 사용한다면 DataLoader는 필수다.

MongoDB ECONNREFUSED ::1:27017 에러 해결 — 연결 거부의 원인과 진단법

🔍 검색 키워드: MongoDB connection refused 해결, MongoDB ECONNREFUSED 27017, MongoDB connect ECONNREFUSED ::1:27017, MongoDB 연결 거부 에러, MongoDB IPv6 IPv4 연결 에러, MongoServerError connection refused, Node.js MongoDB 연결 안 됨

이 에러가 뜨는 상황

Node.js 앱을 로컬에서 실행하는데 이런 에러가 나온다.

MongoServerSelectionError: connect ECONNREFUSED ::1:27017
    at Topology.selectServer (/node_modules/mongoose/...)
    at ...

또는 Python 쪽이라면:

pymongo.errors.ServerSelectionTimeoutError: localhost:27017: [Errno 111] Connection refused

분명히 mongod를 실행했다고 생각하는데 연결이 안 된다. mongodb://localhost:27017로 접속 시도하는데 왜 ::1(IPv6)로 가는지도 모르겠다. 이게 왜 생기는지, 어떻게 잡는지 순서대로 정리한다.

핵심 원인 세 가지

1. MongoDB 서비스가 실제로 안 돌고 있음

가장 기본적인 원인인데 의외로 많다. mongod 명령어를 쳤는데 포그라운드로 떠서 터미널 닫을 때 같이 죽는 경우, 또는 서비스 등록 없이 수동 실행했다가 시스템 재시작 후 안 뜨는 경우다.

# Linux/macOS — MongoDB 상태 확인
sudo systemctl status mongod    # Linux (systemd)
brew services list | grep mongodb  # macOS (Homebrew)

# 안 떠있으면 시작
sudo systemctl start mongod     # Linux
brew services start mongodb-community  # macOS
# 실제로 27017 포트 열려있는지 확인
netstat -tlnp | grep 27017  # Linux
lsof -i :27017              # macOS

포트 리스닝이 없으면 MongoDB가 안 뜬 것이다.

2. localhost가 ::1(IPv6)로 해석되는 문제

Node.js v17부터 DNS 리졸버가 IPv6를 우선한다. localhost를 DNS로 조회하면 ::1(IPv6)를 먼저 반환하는데, MongoDB가 IPv4(127.0.0.1)에서만 리스닝 중이면 연결이 거부된다.

에러 메시지의 ::1:27017이 바로 이 경우다.

빠른 확인: MongoDB가 IPv4로 리스닝하는지 확인

netstat -tlnp | grep 27017
# 결과 예시:
# tcp  0  0  127.0.0.1:27017  0.0.0.0:*  LISTEN  (IPv4만 리스닝)
# tcp6 0  0  :::27017         :::*       LISTEN  (IPv6도 리스닝)

3. bindIp 설정이 제한되어 있음

/etc/mongod.confbindIp127.0.0.1로만 설정돼 있으면 IPv6(::1)로 오는 연결은 거부된다.

상황별 해결 방법

방법 1: 연결 문자열에서 localhost 대신 IP 직접 지정

가장 빠른 임시 해결책이다. localhost 대신 127.0.0.1을 명시한다.

// 변경 전
mongoose.connect('mongodb://localhost:27017/mydb');

// 변경 후
mongoose.connect('mongodb://127.0.0.1:27017/mydb');
# Python pymongo
from pymongo import MongoClient
# 변경 전
client = MongoClient('localhost', 27017)
# 변경 후
client = MongoClient('127.0.0.1', 27017)

localhost 대신 127.0.0.1을 쓰면 DNS 조회 없이 바로 IPv4로 연결한다. 많은 경우데 이것만으로 해결된다.

방법 2: MongoDB bindIp에 IPv6 추가

근본 해결을 원하면 MongoDB 설정에서 IPv6도 리스닝하도록 한다.

# /etc/mongod.conf
net:
  port: 27017
  bindIp: 127.0.0.1,::1  # IPv4, IPv6 모두 리스닝

설정 변경 후 재시작:

sudo systemctl restart mongod

방법 3: Docker로 MongoDB 실행 중인 경우

Docker로 MongoDB를 띄웠는데 포트 매핑을 빠뜨린 경우다.

# 잘못된 예 — 포트 매핑 없음
docker run -d --name mongo mongo:7

# 올바른 예 — 27017 포트 매핑
docker run -d --name mongo -p 27017:27017 mongo:7

Docker Compose라면:

services:
  mongo:
    image: mongo:7
    ports:
      - "27017:27017"  # 이 줄이 없으면 호스트에서 접속 불가
    volumes:
      - mongo_data:/data/db

상황별 체크리스트

체크 항목명령어기대 결과
MongoDB 프로세스 실행 중ps aux | grep mongodmongod 프로세스 보임
27017 포트 리스닝lsof -i :27017mongod 프로세스가 27017 점유
IPv4 리스닝 확인netstat -tlnp | grep 27017127.0.0.1:27017 또는 0.0.0.0:27017
Docker 포트 매핑docker ps0.0.0.0:27017->27017/tcp 확인
연결 테스트 (IPv4)nc -zv 127.0.0.1 27017Connection succeeded
연결 테스트 (IPv6)nc -zv ::1 27017succeeded/failed 여부 확인

MongoDB 로그로 원인 확인

# 로그 실시간 확인
sudo tail -f /var/log/mongodb/mongod.log

# 최근 에러만
sudo grep -i error /var/log/mongodb/mongod.log | tail -20

로그에서 bind failed 또는 exception in initAndListen 같은 메시지가 보이면 포트 충돌이나 권한 문제다.

# 27017 포트 점유 프로세스 확인 (MongoDB 아닌 다른 프로세스일 수도 있음)
sudo lsof -i :27017

MongoServerError: Authentication Failed도 같이 나온다면

연결은 됐는데 인증에서 막히는 경우는 별개 이슈다. ECONNREFUSED(연결 거부)와 헷갈리기 쉬운데, 연결 거부는 TCP 자체가 안 되는 것이고 인증 실패는 연결 후 자격증명 검증 단계다.

인증 실패 에러:

MongoServerError: Authentication failed.

이 경우는 username/password 오류이거나 authSource 설정 문제다.

// authSource 명시
mongoose.connect('mongodb://user:pass@127.0.0.1:27017/mydb?authSource=admin');

마무리

ECONNREFUSED ::1:27017 에러는 90%가 두 가지 원인 중 하나다:

  1. MongoDB가 실제로 안 떠있음
  2. localhost가 IPv6로 해석되는데 MongoDB는 IPv4만 리스닝

연결 문자열의 localhost127.0.0.1로 바꾸는 것만으로도 대부분 해결된다. Docker를 쓴다면 포트 매핑(-p 27017:27017)을 빠뜨리지 않았는지 확인하자.

DB 연결 에러 관련해서 MySQL 연결 에러는 MySQL connection error 해결, Redis 연결 에러는 Redis connection refused 해결 글을 참고하면 된다.

작성일: 2026-06-25

🔍 검색 키워드: PostgreSQL connection pool, 데이터베이스 연결 풀, pg-pool, pgBouncer, 데이터베이스 성능 최적화, 커넥션 풀 설정

PostgreSQL Connection Pool 설정과 최적화 전략

PostgreSQL 데이터베이스에서 성능 병목의 70%는 연결 관리 문제에서 비롯됩니다. 특히 고트래픽 환경에서는 연결 수가 제한되어 있어서, 적절한 Connection Pool 설정이 없으면 요청이 대기 상태에 빠지고 타임아웃이 발생합니다.

문제 증상

다음과 같은 에러가 주기적으로 발생합니다:

Error: connect ECONNREFUSED 127.0.0.1:5432
Error: Client already has a client in it
FATAL: remaining connection slots are reserved
Error: query timeout - Client request timeout

원인 분석

원인 1: Connection Pool 미설정

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});
// 각 쿼리마다 새 연결 생성 → 연결 고갈

원인 2: Connection Leak - 연결 반환 미흡

const client = await pool.connect();
const result = await client.query('SELECT * FROM users');
// ✗ client.release()를 호출하지 않음

해결 방법

해결책 1: Connection Pool 설정

const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
  statement_timeout: '30s'
});

app.get('/user/:id', async (req, res) => {
  let client;
  try {
    client = await pool.connect();
    const result = await client.query(
      'SELECT * FROM users WHERE id = $1',
      [req.params.id]
    );
    res.json(result.rows[0]);
  } finally {
    if (client) client.release();
  }
});

해결책 2: pgBouncer 설정 (고트래픽 환경)

sudo apt-get install pgbouncer

# /etc/pgbouncer/pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp_prod

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
idle_in_transaction_timeout = 60

성능 최적화

항목최적값
max 연결 수CPU 코어 × 2~4
idleTimeoutMillis30~60초
statement_timeout30~60초

Connection Pool을 올바르게 설정하면 데이터베이스 성능이 2~5배 향상될 수 있습니다.

월요일

PostgreSQL deadlock detected 해결 — 두 트랜잭션이 서로 락을 기다리는 교착상태 원인 해결

PostgreSQL deadlock detected 에러 해결 — 교착상태 원인과 방지 전략

핵심 답변: PostgreSQL deadlock detected는 두 트랜잭션이 서로 상대방이 보유한 락을 기다리는 교착상태에서 발생한다. PostgreSQL은 이를 자동으로 감지해 하나를 강제 롤백한다. 방지법은 ① 일관된 락 순서 유지, ② SELECT FOR UPDATE 사용, ③ 배치 단위 COMMIT, ④ 외래키 인덱스 추가, ⑤ 재시도 로직 구현이다.

운영 중인 서비스 로그에 갑자기 이런 에러가 쏟아진다.

ERROR: deadlock detected

DETAIL: Process 12345 waits for ShareLock on transaction 678;

blocked by process 67890.

Process 67890 waits for ShareLock on transaction 456;

blocked by process 12345.

HINT: See server log for query details.

교착상태(Deadlock)는 두 트랜잭션이 서로 상대방이 보유한 락을 기다리는 상황이다. 양쪽 모두 진행할 수 없는 상태가 되면 PostgreSQL이 감지해 하나를 강제 롤백하고 ERROR 40P01을 반환한다.
트랜잭션 A: 행 1 잠금 → 행 2 잠금 시도 (대기)

트랜잭션 B: 행 2 잠금 → 행 1 잠금 시도 (대기)

→ 영원히 풀리지 않음 → PostgreSQL이 하나를 강제 종료


원인 1: UPDATE 순서가 트랜잭션마다 다른 경우

가장 흔한 원인이다. 같은 테이블의 여러 행을 업데이트할 때 트랜잭션마다 접근 순서가 다르면 교착상태가 생긴다.

-- 트랜잭션 A (id=1 → id=2 순서)

BEGIN;

UPDATE orders SET status = 'processing' WHERE id = 1;

UPDATE orders SET status = 'processing' WHERE id = 2;

COMMIT;

-- 트랜잭션 B (id=2 → id=1 역순 — 충돌!)

BEGIN;

UPDATE orders SET status = 'cancelled' WHERE id = 2;

UPDATE orders SET status = 'cancelled' WHERE id = 1; -- deadlock

COMMIT;

해결: 모든 트랜잭션에서 동일한 순서(id 오름차순)로 접근
-- SQL에서 ORDER BY로 순서 보장

UPDATE orders SET status = 'processing'

WHERE id IN (1, 2)

ORDER BY id;

# 애플리케이션에서 정렬 후 처리

def update_orders(session, order_ids, status):

for order_id in sorted(order_ids): # 항상 오름차순

session.query(Order).filter(Order.id == order_id).update({"status": status})

session.commit()


원인 2: SELECT FOR UPDATE 없이 나중에 UPDATE

조회 후 수정하는 패턴에서 자주 발생한다. 조회 시 락을 잡지 않으면 다른 트랜잭션이 끼어든다.

-- ❌ 락 없이 조회 후 수정 — 위험

BEGIN;

SELECT balance FROM accounts WHERE id = 100;

-- 이 사이에 다른 트랜잭션이 같은 행을 수정 가능

UPDATE accounts SET balance = balance - 1000 WHERE id = 100;

COMMIT;

-- ✅ SELECT FOR UPDATE로 조회 시 즉시 락 획득

BEGIN;

SELECT balance FROM accounts WHERE id = 100 FOR UPDATE;

UPDATE accounts SET balance = balance - 1000 WHERE id = 100;

COMMIT;

// Spring Data JPA — 비관적 락 사용

@Repository

public interface AccountRepository extends JpaRepository<Account, Long> {

@Lock(LockModeType.PESSIMISTIC_WRITE)

@Query("SELECT a FROM Account a WHERE a.id = :id")

Optional<Account> findByIdForUpdate(@Param("id") Long id);

}

@Transactional

public void transfer(Long fromId, Long toId, BigDecimal amount) {

// 작은 id부터 락 획득 — 순서 일관성 보장

Long firstId = Math.min(fromId, toId);

Long secondId = Math.max(fromId, toId);

Account first = accountRepository.findByIdForUpdate(firstId).orElseThrow();

Account second = accountRepository.findByIdForUpdate(secondId).orElseThrow();

}


원인 3: 인덱스 없이 대량 UPDATE/DELETE

인덱스 없는 컬럼으로 넓은 범위를 업데이트하면 테이블 전체에 락이 걸린다.

-- ❌ 인덱스 없는 컬럼으로 대량 업데이트 — 테이블 전체 잠금

UPDATE orders SET processed = true WHERE created_at < '2026-01-01';

-- ✅ 배치로 나눠서 처리

DO $$

DECLARE

batch_size INT := 1000;

last_id BIGINT := 0;

BEGIN

LOOP

UPDATE orders SET processed = true

WHERE id IN (

SELECT id FROM orders

WHERE created_at < '2026-01-01'

AND processed = false

AND id > last_id

ORDER BY id

LIMIT batch_size

)

RETURNING max(id) INTO last_id;

EXIT WHEN NOT FOUND OR last_id IS NULL;

COMMIT;

PERFORM pg_sleep(0.01); -- 다른 트랜잭션에 기회 부여

END LOOP;

END $$;


원인 4: 외래키 인덱스 누락

PostgreSQL은 외래키 참조 시 부모 테이블에 ShareLock을 건다. 외래키 컬럼에 인덱스가 없으면 잠금 범위가 커진다.

-- 외래키 컬럼 인덱스 누락 여부 확인

SELECT

tc.table_name,

kcu.column_name,

(SELECT 1 FROM pg_indexes

WHERE tablename = tc.table_name

AND indexdef LIKE '%' || kcu.column_name || '%') AS has_index

FROM information_schema.table_constraints tc

JOIN information_schema.key_column_usage kcu

��N��B��6�FR7G��S�&&6�w&�V�C�6ccc�FF��s�'�g��&�&FW"�&F�W3�7��f��B�f֖Ǔ�����76S�f��B�6��S��V�#�4UB��6��F��V�WB�sW2s��6�FS������ȹΫN��B� ��Y��Zȉ��莸�B���ࠣ�7G&��s��FVF��6���B� ��9��Y���B����N�K�i�8��B�9ޫ��)�ɩC���7G&��s����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#���XN�����B��7Fw&U5���FVF��6�� �xȹ��Y��)��قث��������Y���BɘN�N���N� �Yθ�B�����N�K��B��ΫH�K�莸�B�8�9κ[����x�Yθ�B��N� � ����x^�x����ȹθ�N�Y���B� θ�B���ࠣƇ"7G��S�&&�&FW#����S�&�&FW"�F���6�ƖB6SSS��&v��3'�#ࠣƃ"7G��S�&f��B�6��S��VVӶ�&v��3'�'��FF��r�&�GF�ӣg��&�&FW"�&�GF�ӣ'�6�ƖB6SSS�6���#�3##"#�� ^�j����#ࠣ�7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#��7Fw&U5�FVF��6�� ��xnɹ˙����ࠣ�7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#���zι���h�ȉ�� Rȹ��7G&��s��Z��8�BɊN�hN� �ȉ���7G&��s�� �{�����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�"��٨�ٸBȉ�� ^���7G&��s�4T�T5Bd�"UDDS��7G&��s��ȹ�������7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�2�������x^����Nث���B�7G&��s� ˙������C��7G&��s���)���4��ԕC����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�B�ɛ����*B˺ι���y� ��9�ȹ��7G&��s��ێ��ȪC��7G&��s��9��K����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�R�ث��������Y������^�Y��7G&��s��z~�(���7G&��s����x����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�b����ȹθ�B���x���7G&��s�W���V�F��&6��fc��7G&��s���Z�وC��ࠣ�7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#���Bn��x�[��x�*N��B�Y� ��8�9�� ��9��ق�^�[��x���Bȉ��莸�B���ࠣƇ"7G��S�&&�&FW#����S�&�&FW"�F���6�ƖB6SSS��&v��3'�#ࠣ�F�b7G��S�&&6�w&�V�C�6S�cFfC�&�&FW"��VgC�G�6�ƖB3s6S��&�&FW"�&F�W3�G��FF��s�G�����&v��#��f��B�6��S��VVӶ6���#�3&S�Ɩ�RֆV�v�C��r#�H� ������&Vc�&�GG3���GV�GV�6�R�&��w7�B�6��"7G��S�&6���#�3s6S�#䆖�&�56���V7F������W��W7FVB�y����[N�+�����F�c���F�c

화요일

HikariCP Connection is not available 해결 — Spring Boot 커넥션 풀 고갈 완벽 분석

🔍 검색 키워드: HikariCP connection is not available, HikariPool request timed out, Spring Boot 커넥션 풀 부족, HikariCP 해결, Spring Boot 데이터베이스 연결 실패, HikariCP maximum-pool-size 설정

증상: 이런 에러 보셨으면 이 글이 맞다

운영 서버에서 갑자기 API가 죽기 시작한다. 로그를 열면 이게 보인다.

Unable to acquire JDBC Connection
HikariPool-1 - Connection is not available, request timed out after 30000ms
(total=10, active=10, idle=0, waiting=23)

total=10, active=10, idle=0, waiting=23 — 커넥션 10개 전부 사용 중이고 23개 요청이 줄 서서 기다리고 있다는 뜻이다. 30초 기다리다가 포기한 것.

트래픽이 갑자기 몰릴 때, 아니면 슬로우 쿼리 하나가 터졌을 때 이 에러가 나온다. Spring Boot 기본 설정을 그대로 쓰다가 프로덕션에서 처음 맞닥뜨리는 경우가 많다.

원인 분류: 고갈의 원인은 크게 세 가지다

1. 풀 사이즈가 애초에 작다

Spring Boot HikariCP 기본값이 maximum-pool-size=10이다. 트래픽이 조금만 몰려도 금방 바닥난다.

2. 슬로우 쿼리로 커넥션이 묶인다

한 번에 처리돼야 할 쿼리가 인덱스 빠진 채로 5초씩 걸리면? 10개 커넥션이 전부 그 느린 쿼리 잡고 늘어진다. 새 요청은 줄 서서 기다리다 timeout.

3. 커넥션 누수 (Connection Leak)

코드에서 커넥션을 받아 쓰고 닫지 않은 경우. 특히 try-catch 안에서 예외 발생 시 close()를 빠뜨렸거나, @Transactional 없이 직접 커넥션 관리할 때 흔히 나온다. 커넥션이 풀로 돌아오지 않으니까 시간이 지날수록 고갈된다.

해결 방법

Step 1. 현재 풀 상태 먼저 파악

로그에서 풀 상태를 보려면 설정 추가:

# application.yml
logging:
  level:
    com.zaxxer.hikari: DEBUG
    com.zaxxer.hikari.HikariConfig: DEBUG

출력되는 로그:

HikariPool-1 - Pool stats (total=10, active=10, idle=0, waiting=23)

MBean으로 실시간 모니터링도 가능:

spring:
  datasource:
    hikari:
      register-mbeans: true

Step 2. 커넥션 풀 사이즈 조정

무조건 늘리는 게 답이 아니다. HikariCP 공식 권장 공식:

connections = (core_count × 2) + effective_spindle_count

SSD 서버 4코어라면: (4 × 2) + 1 = 9. 생각보다 작다. DB 서버가 받을 수 있는 최대 연결 수도 같이 고려해야 한다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 20       # 트래픽에 맞게
      minimum-idle: 5             # 유휴 최소 유지 수
      connection-timeout: 10000   # 30초 → 10초로 줄여서 빠르게 실패
      idle-timeout: 300000        # 유휴 커넥션 유지 시간 5분
      max-lifetime: 1800000       # 커넥션 최대 수명 30분
      validation-timeout: 5000    # 커넥션 유효성 검사 타임아웃
      leak-detection-threshold: 60000  # 60초 이상 반납 안 되면 누수 경고

connection-timeout을 30초에서 10초로 줄이는 게 핵심이다. 30초씩 기다리다 실패하면 그동안 요청이 쌓이고 상황이 더 나빠진다. 빨리 실패해서 로드밸런서가 다른 인스턴스로 트래픽 보내게 해야 한다.

Step 3. 슬로우 쿼리 잡기

커넥션 풀 늘려도 근본 원인이 슬로우 쿼리면 도로 막힌다.

spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true
logging:
  level:
    org.hibernate.stat: DEBUG
    org.hibernate.SQL: DEBUG
-- MySQL 슬로우 쿼리 로그 활성화
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;  -- 2초 이상 쿼리 기록

-- 실행 중인 쿼리 확인
SHOW PROCESSLIST;

Step 4. 커넥션 누수 탐지

leak-detection-threshold 설정하면 누수가 있을 때 이런 로그가 찍힌다:

HikariPool-1 - Connection leak detection triggered for connection com.zaxxer.hikari.pool.ProxyConnection

스택 트레이스도 같이 출력되니까 어느 코드에서 커넥션을 안 닫고 있는지 바로 파악 가능.

// 잘못된 코드 — 예외 발생 시 close() 호출 안 됨
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...");
conn.close(); // 예외 터지면 여기 안 옴

// 올바른 코드 — try-with-resources 사용
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT ...")) {
    // 처리
} // 자동으로 close() 호출

상황별 체크리스트

증상원인조치
트래픽 급증 시 발생풀 사이즈 부족maximum-pool-size 증가
특정 기능 호출 후 발생슬로우 쿼리쿼리 최적화 / 인덱스 추가
서버 오랜 시간 운영 후 발생커넥션 누수leak-detection-threshold 설정, 코드 점검
에러 응답이 30초 후에 옴connection-timeout 기본값10초 이하로 줄이기
DB 서버 부하 높음풀 사이즈 과다공식 기반으로 적정값 계산

실무에서 자주 놓치는 것

connection-timeout 줄이기를 무서워한다. "30초면 여유 있지 않냐"고 생각하는데 틀렸다. 요청이 30초씩 대기하면 그 사이에 더 많은 요청이 쌓인다. 10초 안에 실패 처리하고 클라이언트에 503 돌려주는 게 훨씬 낫다.

풀 사이즈를 무조건 크게 잡는다. DB 서버의 max_connections도 같이 봐야 한다. 애플리케이션 인스턴스가 3개인데 인스턴스당 커넥션 50개 잡으면 DB로는 150개 연결이 맺어진다. DB가 먼저 터진다.

개발환경과 운영환경 설정을 같이 쓴다. 개발 때는 minimum-idle=1, maximum-pool-size=5 정도로 작게 쓰고, 운영은 별도 프로파일로 관리하는 게 기본이다.

정리

HikariCP timeout은 대부분 풀 사이즈 부족, 슬로우 쿼리, 커넥션 누수 셋 중 하나다. 로그에 찍히는 (total=N, active=N, idle=0, waiting=M) 패턴 보고 원인부터 분류하고, connection-timeout은 줄여서 빠른 실패 전략 쓰고, 근본 원인을 잡는 순서로 접근하면 된다.

관련 글: Spring Boot UnexpectedRollbackException 해결 — 트랜잭션 롤백 에러 원인 분석

금요일

PostgreSQL "FATAL: password authentication failed" 에러 완전 해결 가이드

🔍 검색 키워드: postgresql fatal password authentication failed, psql role does not exist, postgresql connection refused, pg_hba.conf, postgresql 비밀번호 에러

상황

PostgreSQL에 접속하려는데 이런 에러가 뜬다.

FATAL: password authentication failed for user "myapp"

또는

FATAL: role "myapp" does not exist

또는

psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL: Peer authentication failed for user "postgres"

전부 다르게 생겼지만 원인은 비슷한 경우가 많다. 하나씩 짚어보자.


원인 1: 비밀번호가 틀렸다 (가장 흔한 경우)

앱 설정 파일에 넣은 비밀번호와 실제 DB 비밀번호가 다른 거다. .env, application.yml, database.yml 등에서 비밀번호 오타나 환경별 혼용이 잦다.

확인 방법:

# postgres 유저로 직접 접속해서 비밀번호 재설정
sudo -u postgres psql

-- 현재 유저 목록 확인
\du

-- 비밀번호 재설정
ALTER USER myapp WITH PASSWORD 'newpassword';

원인 2: 유저 자체가 없다 (role does not exist)

DB는 있는데 유저를 만든 적이 없거나, 다른 환경에서 만든 유저가 이 환경에는 없는 경우다. 로컬에서 개발하다가 스테이징 DB로 붙으려 할 때 자주 발생한다.

해결:

-- postgres 슈퍼유저로 접속 후
CREATE USER myapp WITH PASSWORD 'yourpassword';

-- 데이터베이스 권한 부여
GRANT ALL PRIVILEGES ON DATABASE mydb TO myapp;

-- 스키마 권한도 줘야 하는 경우
GRANT ALL ON SCHEMA public TO myapp;

원인 3: pg_hba.conf 인증 방식 문제 (Peer auth failed)

Peer authentication failed 에러는 pg_hba.conf의 인증 방식 설정 문제다. 로컬 소켓 접속 시 OS 유저명과 DB 유저명이 같아야 하는 peer 방식으로 설정돼 있을 때 발생한다.

pg_hba.conf 위치 확인:

sudo -u postgres psql -c "SHOW hba_file;"
# 보통 /etc/postgresql/14/main/pg_hba.conf 또는 /var/lib/pgsql/data/pg_hba.conf

pg_hba.conf 수정:

sudo nano /etc/postgresql/14/main/pg_hba.conf

수정 전:

local   all   all   peer

수정 후 (비밀번호 인증으로 변경):

local   all   all   md5

또는 특정 유저만:

local   mydb   myapp   md5
host    mydb   myapp   127.0.0.1/32   md5

변경 후 재시작:

sudo systemctl restart postgresql

원인 4: Docker 환경에서 환경변수 미전달

Docker로 PostgreSQL 띄울 때 POSTGRES_PASSWORD 없이 컨테이너를 올리거나, 앱 컨테이너에 DB 접속 정보를 제대로 안 넘긴 경우다.

# docker-compose.yml 올바른 예시
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: mypassword
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5

  app:
    build: .
    environment:
      DATABASE_URL: postgresql://myapp:mypassword@db:5432/mydb
    depends_on:
      db:
        condition: service_healthy
💡 depends_on은 컨테이너 시작 순서만 보장한다. PostgreSQL이 실제로 준비됐는지는 healthcheckcondition: service_healthy로 처리해야 한다.

원인 5: 접속 호스트/포트 오류

Connection refused는 PostgreSQL이 해당 주소에서 리슨하고 있지 않다는 뜻이다.

# PostgreSQL이 실제로 떠 있는지
sudo systemctl status postgresql

# 어느 포트에서 리슨 중인지
sudo ss -tlnp | grep 5432

# 외부 접속 허용 설정 확인
sudo grep listen_addresses /etc/postgresql/14/main/postgresql.conf

외부에서 접속하려면 postgresql.conf에서:

listen_addresses = '*'

그리고 pg_hba.conf에도 원격 접속 허용 라인 추가:

host    all   all   0.0.0.0/0   md5

상황별 체크리스트

증상확인할 것해결책
password authentication failed비밀번호 오타, 환경 혼용ALTER USER ... WITH PASSWORD
role does not exist유저 미생성CREATE USER + 권한 부여
Peer authentication failedpg_hba.conf 설정peer → md5 변경 후 재시작
Connection refusedPostgreSQL 미실행 또는 포트 불일치서비스 상태 확인, listen_addresses 설정
Docker에서만 발생컨테이너 간 네트워크, 환경변수db 호스트명 사용, healthcheck 추가

Node.js 접속 예시 (pg 라이브러리)

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
  connectionTimeoutMillis: 5000,
  idleTimeoutMillis: 30000,
  max: 10,
});

pool.query('SELECT NOW()', (err, res) => {
  if (err) {
    console.error('DB 연결 실패:', err.message);
  } else {
    console.log('DB 연결 성공:', res.rows[0].now);
  }
});

Python (psycopg2, SQLAlchemy)

import psycopg2
from psycopg2 import OperationalError
import os

def create_connection():
    try:
        conn = psycopg2.connect(
            host=os.getenv("DB_HOST", "localhost"),
            port=int(os.getenv("DB_PORT", 5432)),
            database=os.getenv("DB_NAME"),
            user=os.getenv("DB_USER"),
            password=os.getenv("DB_PASSWORD"),
        )
        print("PostgreSQL 연결 성공")
        return conn
    except OperationalError as e:
        print(f"연결 실패: {e}")
        raise

from sqlalchemy import create_engine
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_recycle=3600)

마무리

PostgreSQL 접속 에러는 대부분 세 가지다: 비밀번호 틀림, 유저 없음, pg_hba.conf 설정 문제. 에러 메시지를 정확히 읽으면 원인이 나온다. FATAL: 뒤에 오는 텍스트가 전부다. Connection refused는 PostgreSQL 자체가 안 떠있거나 포트가 다른 거고, authentication failed는 자격증명 문제, role does not exist는 유저 생성을 안 한 거다.

Docker 환경이면 컨테이너 간 네트워크와 healthcheck까지 챙겨야 한다.

월요일

MySQL 연결 에러 완전 정복: Too many connections, Connection refused, Access denied 해결법

Redis 연결 에러 완전 정복: ECONNREFUSED 127.0.0.1:6379 트러블슈팅

🔍 검색 키워드: redis connection refused, redis ECONNREFUSED 6379, redis 연결 안됨, ioredis 연결 에러, spring boot redis 연결 실패, node redis ECONNREFUSED, docker redis 연결 에러

Redis 붙이다가 처음 보는 에러 아니다. 누구나 한 번쯤은 밟는다.

Error: connect ECONNREFUSED 127.0.0.1:6379

이거 뜨면 일단 당황하지 말고 순서대로 확인하면 금방 해결된다. 원인은 대부분 세 가지 중 하나다.


에러가 뜨는 주요 상황

상황에러 메시지
Node.js (ioredis)[ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379
Node.js (node-redis)Error: Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED
Spring BootUnable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException
Python (redis-py)redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379.
Docker 컨테이너 간connect ECONNREFUSED 127.0.0.1:6379 (컨테이너 내부에서 호스트 Redis 접근 시도)

레벨 1: 입문자 — Redis가 켜져 있나요?

가장 흔한 원인. Redis 서버가 꺼져 있으면 당연히 붙을 수 없다.

Redis 실행 상태 확인

# 프로세스 확인
ps aux | grep redis-server

# 포트 리스닝 확인
ss -tlnp | grep 6379

# systemd 기반 (Ubuntu/CentOS)
sudo systemctl status redis

Redis 직접 연결 테스트

redis-cli ping
# 정상이면: PONG
# 실패하면: Could not connect to Redis at 127.0.0.1:6379: Connection refused

Redis 실행 방법

# systemd로 시작
sudo systemctl start redis
sudo systemctl enable redis   # 부팅 시 자동 시작

# 백그라운드 실행
redis-server --daemonize yes

레벨 2: 실무자 — 포트/바인딩/방화벽 확인

Redis는 켜져 있는데 연결이 안 되면 이쪽을 본다.

bind 설정 문제

/etc/redis/redis.conf 기본 설정:

# 기본값: 127.0.0.1만 허용 (로컬호스트 전용)
bind 127.0.0.1

# 외부 접속 허용하려면 (주의: 보안 설정 필수)
bind 0.0.0.0

# 변경 후 재시작
sudo systemctl restart redis

방화벽 확인

# UFW (Ubuntu)
sudo ufw status
sudo ufw allow 6379

# 외부에서 포트 테스트
nc -zv <서버IP> 6379

requirepass 설정 시 인증 필요

redis-cli -a yourpassword ping

# 또는 연결 후 AUTH
redis-cli
> AUTH yourpassword
> PING

레벨 3: 고급 — Docker, Kubernetes 환경

이게 제일 헷갈린다. 컨테이너 안에서 127.0.0.1:6379컨테이너 자신을 가리킨다.

Docker Compose로 Redis 연결

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    environment:
      - REDIS_HOST=redis      # 127.0.0.1이 아니라 서비스명!
      - REDIS_PORT=6379
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
// Node.js - 잘못된 예
const redis = new Redis({ host: '127.0.0.1', port: 6379 });

// 올바른 예
const redis = new Redis({
  host: process.env.REDIS_HOST || 'redis',
  port: parseInt(process.env.REDIS_PORT || '6379'),
});

호스트 머신의 Redis에 접근하는 경우

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - REDIS_HOST=host.docker.internal

언어별 연결 코드 및 에러 핸들링

Node.js — ioredis

const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD || undefined,
  retryStrategy(times) {
    return Math.min(times * 50, 2000);
  },
  maxRetriesPerRequest: 3,
});

redis.on('error', (err) => console.error('Redis 연결 에러:', err.message));
redis.on('connect', () => console.log('Redis 연결 성공'));

Python — redis-py

import redis, os

r = redis.Redis(
    host=os.getenv('REDIS_HOST', '127.0.0.1'),
    port=int(os.getenv('REDIS_PORT', 6379)),
    password=os.getenv('REDIS_PASSWORD'),
    decode_responses=True,
    socket_connect_timeout=5,
    retry_on_timeout=True,
)

try:
    r.ping()
    print("Redis 연결 성공")
except redis.exceptions.ConnectionError as e:
    print(f"Redis 연결 실패: {e}")

Spring Boot — application.yml

spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      timeout: 5000ms
      lettuce:
        pool:
          max-active: 10
          max-idle: 10
          min-idle: 2

원인별 체크리스트

체크 항목확인 방법조치
Redis 서버 실행 중?ps aux | grep redissystemctl start redis
포트 리스닝 중?ss -tlnp | grep 6379포트 충돌 확인
bind 설정 맞음?redis.conf 확인bind 0.0.0.0
방화벽 열려 있음?ufw status포트 허용
Docker 환경?컨테이너 여부 확인서비스명으로 host 변경
인증 필요?requirepass 설정 확인password 파라미터 추가
TLS 사용 중?Redis 6.0+ 설정 확인tls:// 스킴 및 인증서 설정
원격 서버?네트워크 경로 확인VPN, 보안그룹 확인

자주 하는 실수 TOP 3

1. Docker에서 localhost 씀
컨테이너 안에서 localhost는 컨테이너 자신이다. 다른 컨테이너의 Redis에 붙으려면 서비스명을 써야 한다.

2. 환경변수 안 쓰고 하드코딩
로컬에서 127.0.0.1로 하드코딩해두고 스테이징/프로덕션에 그대로 올리면 터진다. 처음부터 환경변수로 빼두자.

3. Redis 안 뜨고 앱부터 뜸
depends_on은 컨테이너 시작 순서만 보장하지 Redis 준비를 보장하지 않는다. healthcheck를 써야 한다.

redis:
  image: redis:7-alpine
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 5s
    retries: 5

app:
  depends_on:
    redis:
      condition: service_healthy

마무리

Redis 연결 에러의 90%는 위 체크리스트로 해결된다. 나머지 10%는 TLS, 클러스터 모드, Sentinel 설정 같은 고급 주제인데 그건 따로 다루겠다.

에러 메시지를 봤을 때 "Redis가 켜져 있냐 → 주소/포트가 맞냐 → 네트워크가 열려 있냐" 이 순서로만 확인해도 대부분 잡힌다.

금요일

mssql sp

set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
/----------------------------------------------------------------------------------------------
1.Stored Procedure : [up_tblboard]
2.관련 Table : tblboard
3.내용 : 글 쓰기 처리 저장 프로시져
4.작성자 : 투이아빠
5.작성일 : 2009.03.19
-----------------------------------------------------------------------------------------------
/
ALTER PROCEDURE [dbo].[up_tblboard]
@idx INT --글번호
,@name VARCHAR(50)
,@password VARCHAR(50)
,@email VARCHAR(50)
,@title VARCHAR(50)
,@contents TEXT
,@readcount INT --카운트
,@WebPath VARCHAR(200) --파일
AS
SET NOCOUNT ON
DECLARE @error INT --에러선언
– 격리 수준을 최소로 설정
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
BEGIN TRAN
INSERT tblboard(
idx, name, pass, email, title, contents, readcount, filename
)VALUES(
@idx, @name, @password, @email, @title, @contents, @readcount, @WebPath
)
SELECT @error = @error + @@ERROR
–시스템 함수 @@error를 써서 에러 처리를 해주는 부분
if @error = 0 or @error IS NULL
begin
SELECT ‘성공’ AS result, null AS message, null as error_code
COMMIT TRAN
end
else if @error <> 0
begin
SELECT ‘실패’ AS result,‘글쓰리 처리 작업중에 에러 발생’ AS message, ‘01’ AS error_code
–트랜잭션을 rollback
ROLLBACK TRAN
end
SET NOCOUNT OFF
– 격리 수준을 원상 복구
SET TRANSACTION ISOLATION LEVEL READ COMMITTED

mssql @@ERROR=0 이란?

“시스템함수”

@@ERROR=0 오류없음

@@ERROR=0 오류없음

@@ERROR=0 오류없음

예문)

if @error = 0 or @error IS NULL
begin
SELECT ‘성공’ AS result, null AS message, null as error_code
COMMIT TRAN
end
else if @error <> 0
begin
SELECT ‘실패’ AS result,‘성적처리 에러’ AS message, ‘01’ AS error_code
–트랜잭션을 rollback
ROLLBACK TRAN
end

mssql 테이블 스캔과 넌클러스터드인덱스의 차이

mssql 동일한 db 서버 상 서로 다른 테이블간 update

update 테이블명1
set 테이블명1.조건코드 = 복사해올테이블.신규조건코드
from 복사해올테이블
where 테이블명1.조건코드 = 복사해올테이블.복사될 테이블과의 매칭코드
and 복사당할테이블.조건 = ‘’

동일한 컬럼과 데이터값을 가진 테이블 insert

nsert into 대상A (
select절과 같은 필드1, select절과 같은 필드2, select절과 같은 필드3

)SELECT insert절과 같은 필드1, insert절과 같은 필드2, insert절과 같은 필드3 from 대상 B
where – 조건절로 원하는 값만 가져옴

대상B에 있는 데이터를 대상A로 복사한다.

대용량 데이터 쪼개기

DECLARE @I INT, @No INT
SET @I = 1
SET @No = 20

BEGIN TRANSACTION

WHILE @I <= @No
BEGIN
UPDATE [Table1] SET A = str(@I) WHERE A1 = '몰랑’
SET @I = @I + 1
WAITFOR DELAY ‘00:00:02’ --2초 있다 다시 루프
END
COMMIT

SET ROWCOUNT

SET ROWCOUNT 10
SELECT * FROM table01

= select top 10 * from table01 질의를 던지거와 같다.

같이 던질경우 top보다 먼저 읽어 처리가 된다.