EC2에 올릴 애플리케이션이 쓸 데이터베이스를 준비한다. 관리형 PostgreSQL 서비스인 Supabase를 사용한다.
학습 목표
Supabase 프로젝트를 만들고 연결 정보를 얻을 수 있다.
직접 연결과 풀러(pooler) 연결의 차이를 이해하고 상황에 맞게 고를 수 있다.
Spring Boot 애플리케이션의 데이터베이스를 MySQL에서 PostgreSQL로 전환할 수 있다.
접속 정보를 코드가 아닌 환경 변수로 주입할 수 있다.
1. 왜 Supabase인가
DB 서버를 직접 운영하면 설치·백업·장애 복구·버전 업그레이드를 전부 떠안아야 한다. 관리형 데이터베이스는 그 운영 부담을 서비스 제공자가 대신 진다.
AWS에도 RDS라는 관리형 데이터베이스가 있다. 이 교안이 Supabase를 쓰는 이유는 이렇다.
RDS | Supabase | |
|---|---|---|
준비 과정 | VPC·서브넷·보안 그룹·파라미터 그룹 설정 | 프로젝트 생성 한 번 |
비용 | 프리 티어 종료 후 과금. 인스턴스를 켜둔 만큼 계속 | 무료 플랜 존재 |
실습 중 사고 | 인스턴스를 안 지우면 요금이 계속 나간다 | 무료 플랜은 과금되지 않는다 |
DB 엔진 | MySQL·PostgreSQL·MariaDB 등 선택 | PostgreSQL |
실습에서 배우고 싶은 것은 "애플리케이션이 외부 데이터베이스에 붙어서 동작한다" 는 구조이지, VPC 서브넷 설계가 아니다. 네트워크 설정에 시간을 쓰지 않고 본질에 집중하려고 Supabase를 골랐다.
실무에서는?
AWS 위에 서비스를 올린다면 RDS나 Aurora를 쓰는 경우가 더 많다. 같은 VPC 안에 있어 지연이 짧고, IAM·백업·모니터링이 AWS 도구와 그대로 엮이기 때문이다.
Supabase는 AWS 계정 바깥의 서비스다. EC2에서 Supabase로 가는 트래픽은 인터넷을 거친다. 학습용으로는 충분하지만, 이 차이는 알고 있어야 한다.
2. 프로젝트 생성
supabase.com 에 GitHub 계정으로 로그인한다. 조직 화면 오른쪽 위의 New project 를 누른다.
조직 화면 오른쪽 위의 [New project]
프로젝트 생성 폼 — 리전을 EC2와 맞춘다
입력할 것은 세 가지다.
항목 | 값 |
|---|---|
Project name | 예) |
Database password | 직접 정하거나 자동 생성. 반드시 따로 저장한다 |
Region | EC2와 같은 리전 — 서울이면 Northeast Asia (Seoul) |
데이터베이스 비밀번호는 이 화면에서만 보여준다.
나중에 대시보드에서 다시 확인할 수 없다. 분실하면 재설정해야 하고, 재설정하면 그 비밀번호를 쓰던 모든 곳을 함께 고쳐야 한다. 지금 비밀번호 관리자에 저장해 둔다.
리전을 EC2와 맞춘다. 애플리케이션은 EC2에서 돌고 데이터베이스는 Supabase에 있다. 둘이 물리적으로 멀면 쿼리 한 번마다 왕복 지연이 붙는다. 서울 리전 EC2에 미국 리전 DB를 붙이면 체감될 만큼 느려진다.
리전 목록 — Northeast Asia (Seoul) / ap-northeast-2
2-1. Security — 기본값을 그대로 두지 않는다
폼 아래쪽에 체크박스 세 개가 있다.
Security — [Automatically expose new tables]를 끈다
항목 | 기본값 | 이 실습에서 |
|---|---|---|
Enable Data API | 켜짐 | 그대로 둔다 |
Automatically expose new tables | 켜짐 | 끈다 |
Enable automatic RLS | 꺼짐 | 그대로 둔다 |
Automatically expose new tables 는 새로 만든 테이블을 REST API 역할에 자동으로 열어 주는 설정이다. Supabase 자신이 화면에서 "We recommend disabling this to control access manually" 라고 끄기를 권한다.
우리 애플리케이션은 JDBC로 직접 붙으므로 REST API가 필요 없다. 꺼두면 EC2에 Docker로 배포하기에서 Hibernate가 만들 visit_count 테이블이 인터넷에 노출되지 않는다.
Data API 자체를 끄지는 않는다. 대시보드의 일부 기능이 이 API를 쓴다. 자동 노출만 꺼도 목적은 달성된다.
Advanced Configuration 을 펼치면 Postgres 종류를 고를 수 있다. 기본값 Postgres 를 그대로 둔다. 옆의 OrioleDB는 아직 알파다. 이 선택은 프로젝트를 만든 뒤에는 바꿀 수 없다.
Advanced Configuration — Postgres (DEFAULT) 를 그대로 둔다
화면에도 These settings cannot be changed after the project is created 라고 적혀 있다. OrioleDB 는 ALPHA 딱지가 붙어 있고 Supabase 스스로 Not recommended for production workloads 라고 표시한다.
설정을 마쳤으면 폼 맨 아래의 Create new project 를 누른다.
폼 맨 아래의 [Create new project]
프로젝트가 준비되는 데 1~2분 걸린다. STATUS 가 Healthy 가 되면 준비된 것이다.
프로젝트 대시보드 — STATUS가 Healthy가 되면 준비 완료
3. 연결 문자열 — 이 장에서 가장 중요한 부분
화면 맨 위 브레드크럼(조직 / 프로젝트 / 브랜치) 오른쪽의 초록색 Connect 버튼을 누른다.
브레드크럼 오른쪽의 [Connect] 버튼
모달이 열리면 위쪽 탭에서 Direct — Connection string 을 고른다. 나머지 탭(Framework·Server·ORM·MCP)은 클라이언트 라이브러리를 쓸 때 보는 것이다.
Connect 모달 — Direct 탭의 세 가지 연결 방식
Connection Method 에 세 가지 방식이 제시되는데, 아무거나 고르면 안 된다.
방식 | 호스트·포트 | 사용자명 | IPv4로 접속 |
|---|---|---|---|
Direct connection |
|
| ❌ 불가 (IPv6 전용) |
Session pooler |
|
| ✅ 가능 |
Transaction pooler |
|
| ✅ 가능 |
표에 나오는 <프로젝트REF> 는 프로젝트마다 다른 20자짜리 식별자다. Connect 모달의 연결 문자열에서 db. 와 .supabase.co 사이, 또는 사용자명 postgres. 뒤에 붙어 있는 부분이 그것이다. 대시보드 첫 화면의 프로젝트 URL https://<프로젝트REF>.supabase.co 에서도 보인다.
풀러 호스트는 aws-<번호>-<리전>.pooler.supabase.com 꼴이다.
앞의 번호를 0 이라고 외우지 않는다. 이것은 리전 번호가 아니라 풀러 클러스터 번호다. 같은 리전에 클러스터가 여러 개 있어서 리전만 알아서는 알아낼 수 없고, 프로젝트가 어느 클러스터에 배치됐는지에 따라 aws-0- 일 수도 aws-1- 일 수도 있다.
반드시 Connect 모달에서 복사한다. 다른 클러스터 주소로 붙으면 접속은 되는데 인증에서 FATAL: Tenant or user not found 가 뜬다. 비밀번호가 틀린 것처럼 보이지만 원인은 주소다.
Direct connection을 EC2에서 쓰면 반드시 실패한다.
무료 플랜의 직접 연결 주소는 IPv6 주소만 가지고 있다. 그런데 EC2 인스턴스는 기본적으로 IPv4 주소만 받는다. IPv4만 있는 서버에서 IPv6 전용 주소로 접속하려 하면 연결 자체가 성립하지 않는다.
증상은 Network is unreachable(ENETUNREACH) 계열의 오류다. 비밀번호나 방화벽 문제로 착각하기 쉬운데, 네트워크 프로토콜이 안 맞아서 패킷이 나가지도 못하는 상태다. 비밀번호를 아무리 확인해도 해결되지 않는다.
이 실습에서는 Session pooler를 쓴다.
직접 연결은 IPv6가 기본이라고 화면이 직접 알려준다
직접 눈으로 확인하기
교안을 믿지 말고 DNS를 조회해 본다. 명령 한 줄이면 끝난다.
nslookup -type=A db.<프로젝트REF>.supabase.co
nslookup -type=AAAA db.<프로젝트REF>.supabase.co직접 연결 주소는 A 레코드(IPv4)가 없고 AAAA 레코드(IPv6)만 나온다.
# -type=A → 주소 줄이 아예 없다
# -type=AAAA
Address: 2406:da12:5ca:b702:...풀러는 반대다.
# 아래 주소는 예시다. 자기 프로젝트의 풀러 호스트로 바꿔서 조회한다
nslookup -type=A aws-<번호>-ap-northeast-2.pooler.supabase.com이름: pool-tcp-ap-northeast-2-....elb.ap-northeast-2.amazonaws.com
Addresses: 15.164.120.176
15.165.245.138
13.124.111.232
Aliases: aws-<번호>-ap-northeast-2.pooler.supabase.comIPv4 주소가 여러 개 나온다. 뒤에 AWS 로드 밸런서가 서 있다는 것까지 보인다. IPv4만 가진 EC2가 붙을 수 있는 쪽은 이쪽이다.
nslookup 은 Windows·macOS·Linux에 모두 들어 있다. 리눅스에서 dig 를 쓴다면 dig A db.<프로젝트REF>.supabase.co +short 처럼 쓴다.
왜 Session pooler인가
풀러(pooler)는 애플리케이션과 데이터베이스 사이에 서서 연결을 중개한다. Supabase의 풀러는 IPv4로도 접속할 수 있어서 위 문제가 해결된다.
풀러에는 두 가지 모드가 있다.
Session mode (5432) — 클라이언트가 연결을 끊을 때까지 DB 연결 하나를 붙잡고 있다. 일반적인 DB 접속과 동작이 같다.
Transaction mode (6543) — 트랜잭션 단위로 연결을 빌려주고 반납한다. 연결을 아낄 수 있지만 제약이 따른다.
우리 애플리케이션은 컨테이너로 계속 떠 있고 HikariCP가 커넥션 풀을 유지한다. 이런 구조에는 Session mode가 맞다.
세션 모드에는 동시 접속 클라이언트 상한이 있다.
무료 플랜(NANO)에서는 15개다. 넘으면 애플리케이션이 이렇게 죽는다.
FATAL: (EMAXCONNSESSION) max clients reached in session mode
- max clients are limited to pool_size: 15그 뒤로 Hibernate 는 엉뚱한 메시지를 덧붙인다.
Unable to determine Dialect without JDBC metadata
(please set 'jakarta.persistence.jdbc.url' ...)이 두 번째 메시지에 낚이지 않는다. 설정이 빠진 것처럼 보이지만, 사실은 「연결을 못 얻어서 DB 종류를 알아낼 수 없었다」 는 뜻이다. 진짜 원인은 그 위의 EMAXCONNSESSION 이다.
그래서 maximum-pool-size 를 작게 잡는다. 무중단 배포 장에서는 컨테이너가 잠시 두 개 동시에 뜨므로 그 순간 커넥션도 두 배가 된다.
컨테이너가 비정상 종료하면 그 세션이 바로 반납되지 않는다. 풀러가 정리할 때까지 10분 가까이 걸리기도 하고, 그동안 새 컨테이너는 계속 같은 오류로 실패한다. 배포가 갑자기 실패하기 시작하면 이것을 의심한다.
Transaction mode를 쓴다면 prepareThreshold=0 이 필요하다.
Transaction mode는 prepared statement를 지원하지 않는다. PostgreSQL JDBC 드라이버는 같은 쿼리가 여러 번 실행되면 자동으로 prepared statement로 전환하는데, 그 시점에 연결이 다른 트랜잭션으로 넘어가 있으면 prepared statement "S_1" does not exist 로 실패한다.
고약한 점은 처음 몇 번은 멀쩡히 동작한다는 것이다. 드라이버가 기본값으로 같은 쿼리가 5번 실행된 뒤부터 전환하기 때문에, 개발 중에는 안 터지다가 트래픽이 붙으면 터진다.
굳이 써야 한다면 JDBC URL에 ?prepareThreshold=0 을 붙여 이 기능을 끈다.
대시보드 안내문과 실제가 어긋나는 곳이 있다.
Transaction pooler를 고르면 "Transaction pooler uses IPv6 by default" 라는 안내가 뜬다. 그런데 공용 풀러는 세션 모드와 같은 호스트를 쓰고, 그 호스트에는 AAAA 레코드가 없다. 위 nslookup 으로 직접 확인할 수 있다.
이 안내문은 유료 플랜의 전용(dedicated) 풀러를 기준으로 쓰인 것으로 보인다. 안내문보다 조회 결과를 믿는다.
호스트 주소는 반드시 대시보드에서 복사한다.
풀러 호스트 이름은 프로젝트가 만들어진 시점과 리전에 따라 다르다. 다른 사람의 예제나 블로그에 적힌 주소를 그대로 쓰면 접속되지 않는다.
사용자명도 눈여겨본다. 직접 연결은 postgres 지만 풀러는 postgres.<프로젝트REF> 처럼 점 뒤에 프로젝트 식별자가 붙는다. 이걸 빠뜨리는 실수가 잦다.
Type 드롭다운에서 JDBC 를 고르면 Java용 연결 문자열을 바로 얻을 수 있다.
Type 드롭다운에서 JDBC를 고를 수 있다
Session pooler — 사용자명이 postgres. 다
대시보드가 주는 JDBC 문자열은 이렇게 생겼다.
jdbc:postgresql://<풀러호스트>:5432/postgres?user=postgres.<프로젝트REF>&password=<비밀번호>이 형태를 그대로 쓰지는 않는다. 비밀번호가 URL 안에 들어가면 로그·프로세스 목록·오류 메시지에 그대로 찍힌다. 다음 절에서는 주소와 계정 정보를 분리해 환경 변수로 나눠 넣는다.
4. 애플리케이션을 PostgreSQL로 전환
Docker Compose 장에서 만든 방문자 카운터 애플리케이션을 그대로 쓴다. 데이터베이스만 MySQL에서 PostgreSQL로 바꾼다.
4-1. 드라이버 교체
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// MySQL 드라이버를 PostgreSQL 드라이버로 교체한다
runtimeOnly 'org.postgresql:postgresql'
}엔티티 코드는 고치지 않는다. JPA가 DB 종류에 맞는 SQL을 대신 만들어 주기 때문이다. @Entity·@Table·@Column 은 그대로 두면 된다.
Hibernate가 접속한 DB를 보고 방언(dialect)을 자동으로 판단하므로 spring.jpa.database-platform 도 지정할 필요가 없다.
4-2. 접속 설정
src/main/resources/application.yml
spring:
datasource:
url: ${SPRING_DATASOURCE_URL}
username: ${SPRING_DATASOURCE_USERNAME}
password: ${SPRING_DATASOURCE_PASSWORD}
hikari:
# 블루·그린 배포 중에는 컨테이너 두 개가 잠시 동시에 뜬다.
# 그만큼 커넥션도 두 배가 되므로 넉넉하게 잡지 않는다.
maximum-pool-size: 5
jpa:
hibernate:
ddl-auto: update접속 정보는 코드에 적지 않고 환경 변수로 주입한다. 소스에 비밀번호를 적으면 Git에 올라가고, 이미지에 구우면 이미지를 받을 수 있는 사람이 모두 보게 된다.
주입할 값은 이렇게 생겼다.
SPRING_DATASOURCE_URL=jdbc:postgresql://<풀러호스트>:5432/postgres
SPRING_DATASOURCE_USERNAME=postgres.<프로젝트REF>
SPRING_DATASOURCE_PASSWORD=<2단계에서 저장한 비밀번호>데이터베이스 이름은 postgres 다. 연결 문자열 맨 뒤의 /postgres 가 그것이다. 프로젝트 이름을 DB 이름으로 착각해 /visit-counter 라고 적으면 접속되지 않는다.
5. 로컬에서 접속 확인
EC2에 올리기 전에 로컬에서 먼저 확인한다. 문제가 생겼을 때 원인이 애플리케이션인지 서버인지 나눠서 볼 수 있다.
이미지를 만들어 컨테이너로 띄운다. 이렇게 하면 로컬에 JDK나 Gradle을 깔지 않아도 된다 — Docker 기본 명령어 장에서 확인한 성질 그대로다.
docker build -t visit-counter:pg .
docker run --rm --name visit-counter-local -p 8080:8080 \
-e SPRING_DATASOURCE_URL='jdbc:postgresql://<풀러호스트>:5432/postgres' \
-e SPRING_DATASOURCE_USERNAME='postgres.<프로젝트REF>' \
-e SPRING_DATASOURCE_PASSWORD='<비밀번호>' \
visit-counter:pg# Windows PowerShell — 줄 잇기는 \ 가 아니라 백틱(`)이다
docker run --rm --name visit-counter-local -p 8080:8080 `
-e SPRING_DATASOURCE_URL='jdbc:postgresql://<풀러호스트>:5432/postgres' `
-e SPRING_DATASOURCE_USERNAME='postgres.<프로젝트REF>' `
-e SPRING_DATASOURCE_PASSWORD='<비밀번호>' `
visit-counter:pg비밀번호에 셸 특수문자가 있으면 작은따옴표로 감싼다. $, `, ! 가 들어간 비밀번호를 큰따옴표로 감싸면 셸이 먼저 해석해 버려 엉뚱한 값이 들어간다. 위 예시처럼 작은따옴표를 쓴다.
-e 와 달리 EC2에 Docker로 배포하기에서 쓰는 --env-file 은 따옴표를 값의 일부로 본다. 규칙이 반대이므로 헷갈리지 않게 주의한다.
기동되면 다른 터미널에서 curl 로 확인한다.
curl http://localhost:8080
# 방문 횟수: 1
curl http://localhost:8080/actuator/health
# {"groups":["liveness","readiness"],"status":"UP"}기동에는 10초 안팎이 걸린다. 그 사이에 curl 을 하면 Connection refused 가 난다. 로그에 Started VisitCounterApplication 이 찍힌 뒤에 확인한다.
ddl-auto: update 설정 덕분에 애플리케이션이 뜰 때 visit_count 테이블이 자동으로 만들어진다. Supabase 대시보드의 Table Editor 에서 확인할 수 있다.
ddl-auto: update 는 실습용이다. 운영에서는 쓰지 않는다.
엔티티를 고치면 Hibernate 가 알아서 테이블을 바꿔 주므로 실습에서는 편하다. 하지만 무엇을 바꿀지 결정하는 주체가 애플리케이션 코드라는 것이 문제다.
컬럼을 지우는 변경은 반영하지 않는다. 쓰이지 않는 컬럼이 계속 남는다.
어떤 DDL 이 실행될지 배포 전에 알 수 없고, 검토할 수도 없다.
무중단 배포 중에는 구버전과 신버전이 같은 테이블을 동시에 본다. 한쪽이 스키마를 바꾸면 다른 쪽이 깨진다 — Nginx로 무중단 배포하기 11절에서 다루는 문제다.
실무에서는 Flyway 나 Liquibase 로 마이그레이션 스크립트를 버전 관리하고, 애플리케이션은 ddl-auto: validate(스키마가 맞는지 확인만) 로 둔다.
Table Editor — visit_count 테이블에 행이 생겼다
SQL Editor 에서 직접 조회해도 된다.
select * from visit_count;SQL Editor — 조회할 때마다 count 가 올라간다
요청할 때마다 count 가 올라가고 updated_at 이 갱신되면 애플리케이션과 데이터베이스가 제대로 연결된 것이다.
이 테이블은 REST API에 노출되지 않는다. 2-1에서 Automatically expose new tables 를 껐기 때문이다.
체크를 켠 채로 프로젝트를 만들었다면 visit_count 가 Data API로 열린다. 그때는 Project Settings에서 Data API를 끄거나, 테이블에 RLS를 켜서 막는다.
우리 애플리케이션은 JDBC로 직접 붙으므로 REST API는 쓰지 않는다.
6. 무료 플랜에서 주의할 것
활동이 1주일간 없으면 프로젝트가 일시 정지된다.
수업이 주 1회라면, 지난주에 만든 데이터베이스가 이번 주에 멈춰 있을 수 있다. 정지된 프로젝트는 대시보드에서 직접 재개해야 하고 복구에 시간이 걸린다.
수업 전날 대시보드에 들어가 상태를 확인한다. 정지돼 있으면 미리 재개해 둔다.
정지된 프로젝트를 열면 이 화면이 나온다. Resume project 를 누르고 확인 창에서 한 번 더 Resume 을 누른다.
일시 정지된 프로젝트 — 데이터는 그대로 남아 있고 [Resume project] 로 되살린다
이 교안을 쓰며 한 달 쉰 프로젝트를 재개해 보니 이랬다.
항목 | 실측 |
|---|---|
| 약 8분 |
그 직후 STATUS | 잠시 Unhealthy — 부가 서비스가 아직 뜨는 중이다. DB 접속은 이미 된다 |
데이터 | 그대로다. 방문 횟수가 정지 전 값에서 이어졌다 |
재개에 몇 분이 걸리므로 수업 시작 직전에 누르면 늦는다. 화면에는 재개 가능 기한(약 1년)도 함께 나온다. 그 뒤로는 재개할 수 없고 데이터 백업 파일만 내려받을 수 있다.
그 밖의 제약은 다음과 같다.
항목 | 무료 플랜 |
|---|---|
활성 프로젝트 | 2개 |
데이터베이스 용량 | 500MB |
일시 정지 | 1주일간 활동 없으면 |
실습용 방문자 카운터는 행이 하나뿐이라 용량은 문제되지 않는다. 정지만 신경 쓰면 된다.
7. 실습 정리
Supabase 무료 플랜은 켜둬도 요금이 나가지 않으므로 프로젝트를 지우지 않아도 된다. Part 4 의 남은 장에서 계속 쓴다.
다만 비밀번호와 연결 문자열은 EC2에 Docker로 배포하기부터 계속 필요하다. 아래 세 가지를 손이 닿는 곳에 정리해 둔다.
풀러 호스트 주소 (Session pooler, 5432)
사용자명
postgres.<프로젝트REF>데이터베이스 비밀번호
이 세 가지는 GitHub 저장소에 올리지 않는다. application.yml 에 직접 적어 커밋하면 저장소를 볼 수 있는 사람이 모두 데이터베이스에 접속할 수 있게 된다. 이 교안은 EC2에 Docker로 배포하기에서 서버의 ~/app.env 파일에 두고 컨테이너에 주입하는 방식을 쓴다.
다음 장 — EC2에 Docker로 배포하기