DOCS/book/300_architecture/database/250820_postgresql_ssh_tunnel_guide.md
happybell80 0252dd1a7f fix: 51123 서버 IP 주소 업데이트 (성수 이전)
192.168.219.45 → 192.168.0.100 일괄 변경

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:52:26 +09:00

12 KiB

PostgreSQL SSH 터널 및 데이터베이스 접속 가이드

📋 목차

  1. SSH 터널 설정
  2. PostgreSQL 접속
  3. Python으로 DB 조회
  4. 주요 테이블 구조
  5. 자주 사용하는 쿼리
  6. 문제 해결

SSH 터널 설정

1. 현재 SSH 터널 확인

# 실행 중인 SSH 터널 확인
ps aux | grep "ssh.*5433" | grep -v grep

# 특정 포트 사용 확인
netstat -tlnp | grep 5433
lsof -i :5433

2. SSH 터널 생성

# 기본 SSH 터널 생성 (백그라운드 실행)
ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123

# StrictHostKeyChecking 비활성화 (테스트용)
ssh -o StrictHostKeyChecking=no -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123

# 옵션 설명:
# -f : 백그라운드 실행
# -N : 원격 명령 실행하지 않음 (포트 포워딩만)
# -L : 로컬 포트 포워딩 (로컬포트:원격호스트:원격포트)
# -p : SSH 포트 지정

3. SSH 터널 종료

# 프로세스 ID 찾기
ps aux | grep "ssh.*5433"

# 프로세스 종료
sudo kill [PID]

# 또는 한 번에
sudo pkill -f "ssh.*5433"

PostgreSQL 접속

1. 연결 정보

Host: localhost (SSH 터널 사용시) 또는 192.168.0.100 (직접 연결)
Port: 5433 (터널 로컬 포트) 또는 5432 (직접 연결)
Database: main_db (기존 auth_db는 더 이상 사용 안함)
Username: robeings
Password: robeings

2. psql 명령어로 접속

# psql 설치 (Ubuntu/Debian)
sudo apt-get install postgresql-client

# DB 접속 (51123 서버에서 직접)
psql postgresql://robeings:robeings@localhost:5432/main_db

# 또는 (51124 서버에서 SSH 터널 사용)
psql postgresql://robeings:robeings@localhost:5433/main_db

# 또는
psql -h localhost -p 5432 -U robeings -d main_db

3. psql 기본 명령어

-- 테이블 목록 보기
\dt

-- 특정 테이블 구조 보기
\d gmail_token
\d users

-- 데이터베이스 목록
\l

-- 현재 데이터베이스 정보
\conninfo

-- 종료
\q

Python으로 DB 조회

1. 필요한 패키지 설치

pip install psycopg2-binary

2. 기본 연결 및 조회

import psycopg2
import json

# DB 연결 (51123 서버에서)
conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5432/main_db')

# DB 연결 (51124 서버에서 SSH 터널 사용)
conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5433/main_db')
cur = conn.cursor()

# 쿼리 실행
cur.execute("SELECT * FROM user LIMIT 5")
rows = cur.fetchall()

for row in rows:
    print(row)

# 연결 종료
cur.close()
conn.close()

3. 안전한 쿼리 실행 (파라미터 바인딩)

# SQL Injection 방지를 위해 항상 파라미터 바인딩 사용
user_id = 'b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0'
cur.execute(
    "SELECT * FROM user WHERE id = %s",
    (user_id,)  # 튜플로 전달
)

주요 테이블 구조

1. users 테이블

CREATE TABLE user (
    id UUID PRIMARY KEY,
    email VARCHAR UNIQUE,
    name VARCHAR,
    picture VARCHAR,
    oauth_provider VARCHAR,
    oauth_id VARCHAR,
    is_active BOOLEAN,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    last_login_at TIMESTAMP,
    username VARCHAR
);

2. gmail_token 테이블

CREATE TABLE gmail_token (
    id SERIAL PRIMARY KEY,
    user_id UUID REFERENCES user(id),
    slack_id VARCHAR(100),   -- Slack 사용자 ID (새로 추가)
    is_equipped BOOLEAN,
    has_token BOOLEAN,       -- token_data 존재 여부
    access_token TEXT,       -- 개별 컬럼으로 분리
    refresh_token TEXT,      -- 개별 컬럼으로 분리
    token_type VARCHAR,
    expires_at FLOAT,
    token_data JSONB,        -- 레거시 호환용
    oauth_config JSONB,      -- OAuth 설정 정보
    scopes JSONB,           -- Gmail API 권한 목록
    metadata JSONB,
    expiry TIMESTAMP,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    robeing_id VARCHAR,      -- 로빙 ID (rb8001 등)
    equipped_to VARCHAR
);

3. conversation_log 테이블

CREATE TABLE conversation_log (
    id SERIAL PRIMARY KEY,
    robeing_id VARCHAR,
    channel_id VARCHAR,
    message VARCHAR,
    response VARCHAR,
    intent VARCHAR,
    confidence DOUBLE PRECISION,
    timestamp TIMESTAMP,
    user_id UUID REFERENCES user(id),  -- NULL 허용으로 변경됨
    slack_user_id VARCHAR(100)          -- Slack ID 직접 저장 (새로 추가)
);

4. slack_user_mapping 테이블

CREATE TABLE slack_user_mapping (
    id UUID PRIMARY KEY,
    slack_user_id VARCHAR(100) NOT NULL,
    slack_workspace_id UUID,
    user_id UUID REFERENCES user(id),
    workspace_member_id UUID,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

자주 사용하는 쿼리

1. Gmail 토큰 확인

import psycopg2
import json

conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5432/main_db')
cur = conn.cursor()

# Gmail 토큰 상태 확인
query = """
    SELECT 
        u.username,
        u.email,
        g.user_id,
        g.is_equipped,
        g.robeing_id,
        g.token_data,
        g.scopes
    FROM gmail_token g
    JOIN user u ON g.user_id = u.id
    ORDER BY g.created_at DESC
"""

cur.execute(query)
rows = cur.fetchall()

for row in rows:
    print(f"\nUser: {row[0]} ({row[1]})")
    print(f"  UUID: {row[2]}")
    print(f"  Equipped: {row[3]}")
    print(f"  Robeing: {row[4]}")
    
    token_data = row[5] if row[5] else {}
    if token_data and 'access_token' in token_data:
        access_token = token_data.get('access_token', '')
        if access_token.startswith('ya29'):
            print(f"  Token: 실제 OAuth 토큰 있음")
        else:
            print(f"  Token: 테스트 토큰")
    
    scopes = row[6] if row[6] else []
    if scopes:
        print(f"  Scopes: {len(scopes)}개")

cur.close()
conn.close()

2. Slack User ID를 UUID로 변환

import uuid

def slack_id_to_uuid(slack_user_id):
    """Slack User ID를 일관된 UUID로 변환"""
    namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
    # 51123 매핑 API 호출로 UUID 조회
    return call_mapping_api(slack_user_id)

# 예시
slack_id = 'U091UNVE41M'
user_uuid = slack_id_to_uuid(slack_id)
print(f"Slack ID: {slack_id}")
print(f"UUID: {user_uuid}")
# 출력: b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0

3. 사용자별 Gmail 토큰 추가/업데이트

import psycopg2
import json
import uuid

def add_gmail_token(slack_user_id, email, access_token, refresh_token):
    """Gmail 토큰 추가 또는 업데이트"""
    
    # Slack ID를 UUID로 변환
    namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
    # 51123 매핑 API를 통해 UUID 조회
    user_uuid = await get_uuid_from_mapping_api(slack_user_id)
    
    conn = psycopg2.connect('postgresql://robeings:robeings@localhost:5432/main_db')
    cur = conn.cursor()
    
    try:
        # 1. users 테이블에 사용자 추가 (없으면)
        cur.execute("""
            INSERT INTO user (id, email, username, created_at)
            VALUES (%s, %s, %s, NOW())
            ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email
        """, (user_uuid, email, slack_user_id))
        
        # 2. gmail_token 추가 또는 업데이트
        token_data = {
            "access_token": access_token,
            "refresh_token": refresh_token,
            "token_type": "Bearer"
        }
        
        cur.execute("""
            INSERT INTO gmail_token (user_id, token_data, is_equipped, robeing_id, created_at)
            VALUES (%s, %s::jsonb, true, 'rb8001', NOW())
            ON CONFLICT (user_id) DO UPDATE 
            SET token_data = EXCLUDED.token_data,
                updated_at = NOW()
        """, (user_uuid, json.dumps(token_data)))
        
        conn.commit()
        print(f"✅ Gmail 토큰 저장 완료: {email}")
        
    except Exception as e:
        conn.rollback()
        print(f"❌ 오류 발생: {e}")
    finally:
        cur.close()
        conn.close()

문제 해결

1. SSH 터널이 연결되지 않을 때

# 기존 터널 프로세스 종료
sudo pkill -f "ssh.*5433"

# 포트 사용 확인
lsof -i :5433

# 다시 연결
ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123

2. psycopg2 설치 오류

# Ubuntu/Debian
sudo apt-get install libpq-dev python3-dev
pip install psycopg2

# 또는 바이너리 버전 사용
pip install psycopg2-binary

3. UUID 타입 오류

# UUID 문자열을 PostgreSQL UUID 타입으로 변환
import uuid

# 방법 1: 직접 캐스팅
cur.execute(
    "SELECT * FROM user WHERE id = %s::uuid",
    ('b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0',)
)

# 방법 2: uuid 객체 사용
user_uuid = uuid.UUID('b6ea2ee0-a15a-5cf4-93a9-a9ca20d4c4a0')
cur.execute(
    "SELECT * FROM user WHERE id = %s",
    (str(user_uuid),)
)

4. JSONB 필드 처리

import json

# JSONB 데이터 삽입
data = {"key": "value", "nested": {"field": "data"}}
cur.execute(
    "INSERT INTO table_name (jsonb_column) VALUES (%s::jsonb)",
    (json.dumps(data),)
)

# JSONB 데이터 조회
cur.execute("SELECT jsonb_column FROM table_name")
row = cur.fetchone()
json_data = row[0]  # 자동으로 Python dict로 변환됨

환경 변수 설정

.env 파일에 추가:

# PostgreSQL Connection (51123 서버 직접 연결)
POSTGRES_CONNECTION_STRING=postgresql://robeings:robeings@localhost:5432/main_db

# PostgreSQL Connection (51124 서버 SSH 터널)
POSTGRES_CONNECTION_STRING=postgresql://robeings:robeings@localhost:5433/main_db

# SSH Tunnel Config
SSH_HOST=124.55.18.179
SSH_PORT=51123
SSH_USER=admin
LOCAL_PORT=5433
REMOTE_PORT=5432

Python에서 사용:

import os
from dotenv import load_dotenv

load_dotenv()

conn_string = os.getenv('POSTGRES_CONNECTION_STRING')
conn = psycopg2.connect(conn_string)

자동화 스크립트

SSH 터널 자동 재연결 스크립트

#!/bin/bash
# /home/heejae/scripts/ssh_tunnel_monitor.sh

LOCAL_PORT=5433
SSH_CMD="ssh -f -N -L 5433:localhost:5432 admin@124.55.18.179 -p 51123"

# 터널이 살아있는지 확인
if ! nc -z localhost $LOCAL_PORT 2>/dev/null; then
    echo "SSH tunnel is down. Restarting..."
    
    # 기존 프로세스 종료
    pkill -f "ssh.*$LOCAL_PORT"
    
    # 새 터널 생성
    $SSH_CMD
    
    echo "SSH tunnel restarted"
else
    echo "SSH tunnel is running"
fi

crontab에 추가 (5분마다 체크):

*/5 * * * * /home/heejae/scripts/ssh_tunnel_monitor.sh

참고 링크



서버별 DB 연결 상태 (2025-08-26 현재)

51123 서버 (192.168.0.100)

  • PostgreSQL: 로컬 실행 중 (포트 5432)
  • 데이터베이스: main_db
  • 직접 연결: postgresql://robeings:robeings@localhost:5432/main_db

51124 서버 (192.168.219.52)

  • PostgreSQL: 없음 (51123 서버 DB 사용)
  • 연결 방법: SSH 터널
  • 터널 설정: ssh -N -L 5433:localhost:5432 admin@192.168.0.100
  • 연결 문자열: postgresql://robeings:robeings@localhost:5433/main_db

주요 서비스별 DB 연결

서비스 서버 DB 연결 방법
auth-server 51123 직접 연결 (localhost:5432)
robeing-gateway 51123 직접 연결 (localhost:5432)
rb8001 51124 SSH 터널 (localhost:5433)
State Service 51124 SSH 터널 (localhost:5433)
skill-email 51124 SSH 터널 (localhost:5433)

주의사항

  • auth_db는 더 이상 사용하지 않음 (main_db로 통합)
  • State Service의 DATABASE_URL이 auth_db를 참조하면 main_db로 수정 필요
  • SSH 터널은 시스템 재시작 시 재생성 필요

마지막 업데이트: 2025-08-26