노래하자(Let's Sing) 서버 EC2 → 통큰아이 마이그레이션 계획
작성일: 2026-07-30 · 대상: letssing-server (Spring Boot 2.7.2, Java 17 + Kotlin)
0. TL;DR
- 핵심 목표: 통큰아이로 서비스 이관 + 전 벤더(lukus.kr / yuventius125 EC2) 인프라 종속 완전 제거.
- 배치: 통큰아이 서버 1대에 전 인프라 올인원 (Spring 앱 + MariaDB + Redis + MinIO 모두 같은 호스트, docker-compose 스택). DB/Redis/MinIO 포트는 로컬 바인딩만.
- 이관 범위: 애플리케이션 JAR + MySQL/MariaDB DB(스토어드 프로시저 포함) + Redis + 로컬 업로드 볼륨 + MinIO 오브젝트 스토리지(자체 호스팅 신설).
- 이관 제외: Firebase, Google Play/Apple 결제 SaaS (외부 서비스). AWS S3 관련 코드는 실사용 흔적 없으면 정리 대상 (확인 필요).
- 소요 예상: 다운타임 창 1–2시간(DB 스냅샷 + restore + 파일 볼륨/오브젝트 증분 sync). 사전 리허설 1회 필수.
- 컷오버 방식: 도메인 A레코드 스위치 (TTL 사전에 60초로 다운). DB write 동결 → mysqldump → 신규 서버 restore → 파일/오브젝트 증분 sync → DNS 전환.
이관 페이즈 흐름
1. 현행 인프라 스냅샷 (as-is)
애플리케이션
- 빌드 산출물:
build/libs/lets-sing-0.0.1-SNAPSHOT.jar - 런타임: JRE 17 (
eclipse-temurin:17-jdk-alpine컨테이너) - 리스닝 포트: 컨테이너 내
8080→ 호스트50004매핑 - 프로파일:
--spring.profiles.active=dev하드코딩(Dockerfile) — prd로 갈지 확인 필요 - 스케줄러: 매일 KST 00:00 커리큘럼 상태 갱신 (
CurriculumStatusScheduler) — 시간대 유지 필수
데이터베이스
- 접속 문자열(현재
application.properties):jdbc:mysql://43.201.138.237:3306/sing - 계정:
develop/(리포에 노출된 값) docker-compose.yml에는 별도의 MariaDB 컨테이너 정의(호스트 포트 50014)가 있음 — 실제로 뭐가 프로덕션인지 확인 필요(같은 IP 43.201.138.237이 EC2 자체이므로 self-hosted MariaDB일 가능성 높음)- 스토어드 프로시저 8종 사용 — mysqldump 시
--routines --triggers --events반드시 포함:RT_INSERT_USER,RT_GET_USER_AUTH_INFO,RT_UPDATE_USER_LOGIN_BY_ID(회원가입/로그인)RT_DELETE_BLOCK_USER(차단 해제)RT_DELETE_BOARD,RT_DELETE_REPLY(게시글/댓글 삭제)RT_UPDATE_IN_APP_GOOGLE(구글 인앱결제 상태 갱신)RT_GET_ADMIN_LIST(관리자 목록)
- 스키마 스냅샷 파일:
letssing.mwb(MySQL Workbench 모델 파일)이 리포 루트에 존재
Redis
- 호스트:
43.201.138.237:6379(prd), docker-compose로는 호스트 포트50024 - 데이터 성격: 얇은 카운터 캐시(출석 체크, 무료댓글 3회 제한). RDB 스냅샷 없어도 서비스 지속 가능하지만, 컷오버 순간 출석 카운트가 리셋되면 유저 CS 발생 가능
파일 스토리지
- MinIO:
https://dev-s3.lukus.kr/ bucketlets-sing-dev— 외부 벤더 인프라. 이관 대상. dev/prd 프로파일 모두 이 dev 버킷을 공유(격리 없음). 접근키는 리포에 커밋됨. - 로컬 업로드 볼륨:
docker-compose.yml에서/home/yuventius125/lets-sing-server/files/upload→/upload마운트. 실제 사용됨 —FileService.uploadFiles()(FileService.java:24)가 여기 저장, 리턴 URL은 옛 dev DNS554a04dfe064.sn.mynetname.net:50004하드코딩 (FileService.java:18). 게시판/QnA/마이페이지/프리미엄 댓글 첨부 경로. - AWS S3: 코드에 설정과 클래스는 있으나 실행 경로에서 호출되는 흔적 미확인. 확인 필요 — 어디서도 호출 안 되면 정리 대상.
벤더 종속 인벤토리 (이관 후 전부 제거 목표)
| 종속 지점 | 소유 | 대체 방향 |
|---|---|---|
dev-s3.lukus.kr (MinIO 호스트) | 외부(추정: Lukus) | 통큰아이 자체 호스팅 MinIO 신설, 새 도메인/서브도메인 |
43.201.138.237 (EC2 IP) | 외부 EC2 (AWS 계정 소유 확인 필요) | 통큰아이 서버 IP |
/home/yuventius125/... (Linux 계정) | 외부 벤더 계정 | 통큰아이 표준 계정 |
554a04dfe064.sn.mynetname.net (하드코딩 DNS) | 외부 dyn-DNS | 코드에서 제거 |
AWS 액세스 키 AKIA...IVBM5 | 외부 벤더 IAM | disable + 신규 발급(우리 계정) |
| MinIO 접근 키 | 외부 | 신규 발급 |
MariaDB root/develop 계정 | 외부 EC2 | 신규 계정 발급 |
| Firebase service account JSON | 코드에 커밋 (프로젝트 소유는 확인 필요) | 프로젝트 소유권 확인, 필요 시 이관 |
외부 SaaS 연동 (이관 대상 아니지만 확인 필요)
| 서비스 | 용도 | 재구성 필요 지점 |
|---|---|---|
| Firebase | 푸시(FCM) | 프로젝트 소유권 확인 필요. 소유 이관 안 되면 신규 프로젝트로 이전 |
| Google Play Billing | 인앱 구독/결제 검증, RTDN | 콜백이 도메인이면 그대로 유지, IP 등록돼 있으면 재등록 필요 |
| Apple StoreKit | 인앱 구독 알림 | 콜백이 도메인이면 그대로 유지, IP 등록돼 있으면 재등록 필요 |
배포 형태
docker-compose.yml로 app + db + redis 3-container 스택- 볼륨:
/ssd/lets-sing-db-data/_data(MariaDB 데이터),/home/yuventius125/lets-sing-server/files/upload - 리버스 프록시(nginx 등) 여부는 리포에 없음 — EC2 위에 별도 설치돼 있을 가능성 있음
2. 이관 대상 / 제외
목표 아키텍처 (to-be)
이관 대상 매트릭스
| 항목 | 이관 | 방법 |
|---|---|---|
| 애플리케이션 JAR | O | 신규 서버에서 재빌드 (하드코딩 URL·프로파일 정리 후) |
| DB 스키마 + 데이터 + 프로시저 | O | mysqldump --routines --triggers --events --single-transaction |
| Redis 데이터 | 선택 | RDB dump.rdb 복사, 또는 fresh start 후 유저 안내 |
로컬 업로드 볼륨 (/upload) | O | rsync — 단, FileService가 하드코딩 URL 반환하므로 코드 수정 병행 |
| MinIO 버킷 → 신규 자체 호스팅 | O | 통큰아이에 MinIO 서버 신설 + mc mirror로 오브젝트 복제 + 코드 endpoint 교체 |
| Firebase/Play/Apple 계정 | X | 외부 SaaS. 콜백 URL이 IP면 도메인으로 재등록 |
application-*.properties 시크릿 | 재발급 | 그대로 복사 금지 — 로테이트 필수 |
| AWS S3 관련 코드 | X | 실사용 여부 확인 필요. 미사용 확인되면 이관 김에 제거 |
3. 사전 결정이 필요한 항목 (마이그 시작 전 회신)
컷오버 전에 아래가 확정되어야 계획이 확정된다:
- 통큰아이 서버 스펙 — 단일 노드에 앱+DB+Redis+MinIO 전부 올라가므로 여유 있게. 참고 최소치(실측 필요):
- CPU 4 vCPU 이상
- RAM 8 GB 이상 (앱 2 + MariaDB 2 + Redis 0.5 + MinIO 0.5 + OS/버퍼 3)
- 디스크: DB 실측 × 3 (백업 헤드룸) + MinIO 오브젝트 실측 × 1.5 + 로컬 업로드 볼륨 + OS 20 GB
- OS: Ubuntu LTS 또는 Rocky/Alma. Docker/docker-compose 설치 가능한 최신 커널
- 네트워크 접근 — 공인 IP 또는 도메인, 방화벽에서 열 포트(HTTP/HTTPS + 관리자용 포트)
- 클라이언트 앱 base URL — 도메인인지,
43.201.138.237직접 IP인지- 도메인이면 → DNS만 잘라 넘기면 앱 업데이트 없이 이관 가능
- IP면 → 앱 재배포 필요. 하위호환 위해 이관 후 옛 EC2를 프록시로 남기는 기간 필요
- DB의 실체 — AWS RDS인가, EC2 self-hosted MariaDB인가 (43.201.138.237가 EC2 자체면 후자)
- 데이터 볼륨 — 현재 DB 사이즈, 업로드 볼륨 사이즈(이관 시간 산정)
- 용인 가능한 다운타임 — 1시간 vs 30분 vs 무중단 목표?
- 신규 MinIO 도메인/서브도메인 — 예:
s3.<신규도메인>/ prd 버킷명 (lets-sing-prd등 dev와 격리) - prd 프로파일 실제 사용 여부 — Dockerfile은 dev 프로파일로 실행 중임
HTTPS/도메인/인증서— 결정됨: 통큰아이 서버에서 nginx + certbot(Let's Encrypt)으로 자체 처리. 앱 도메인 +s3.<신규도메인>두 개 인증서 발급 + 90일 자동 갱신 cron- 벤더 인계 항목 확보 — Firebase 프로젝트 소유권, AWS 계정(EC2 인스턴스 owner), 도메인 소유 이력, MinIO 서버 관리자 접근권 등 (자세한 항목은 준비 4-1)
4. 준비 (컷오버 전 완료해야 할 것)
4-1. 현행 파악 & 벤더 자산 인계
- 현행 EC2 접근 확보 (
yuventius125계정 or root) - 실행 중인 컨테이너/프로세스 확인 (
docker ps,systemctl) - DB 크기·업로드 볼륨 크기 측정 (
du -sh,SELECT SUM(data_length + index_length) FROM information_schema.tables WHERE table_schema='sing') - Redis 사용량 확인 (
INFO keyspace) - 로그 로테이션 상태 (
/var/log, docker log 크기) - MinIO(lukus.kr) 관리자 접근권 확보 + 오브젝트 총 용량·개수 파악 (
mc ls,mc du) - 벤더 인계 항목 확인 (인벤토리 표 기준):
- Firebase 프로젝트 소유권 이관 가능한가
- AWS 계정 owner 확인 — EC2/RDS/키가 벤더 계정인지 우리 계정인지
- 도메인(들) 등록 정보
- Google Play / Apple Store 개발자 계정 접근권
lukus.krMinIO 서버 자체 접근 여부 (mirror만 할지, 아예 서버 접근권 받아 이전할지)
- 옛 dev DNS
554a04dfe064.sn.mynetname.net사용 흔적 grep (FileService, 클라이언트 앱 코드) - AWS S3 관련 코드 실사용 흔적 확인 —
S3Config/S3Upload가 어디서도 주입/호출 안 되면 정리 대상. 확인 방법:
grep -rn "S3Upload\|S3Config\|@Autowired.*[Ss]3\|new S3" src/- 호출 흔적 없으면 → 이관 시 코드/의존성/AWS 키 모두 제거
- 호출 흔적 있으면 → 어떤 경로에서 쓰는지 파악 후 이관 방식 별도 결정
4-2. 통큰아이 서버 준비 (단일 노드 올인원)
설계: 서버 1대에 리버스 프록시(호스트) → docker-compose 스택(app + MariaDB + Redis + MinIO) → 각 서비스별 bind mount 볼륨.
- 통큰아이 서버 구매/발급 — 스펙(3-1 기준) 결정 → 주문 → 접근 정보 수령. 아래 항목들은 이 서버가 손에 들어와야 시작 가능.
- OS 업데이트 + Docker/docker-compose 설치
- 방화벽: 80/443 + SSH만 외부 오픈. DB(3306)/Redis(6379)/MinIO(9000, 9001) 모두
127.0.0.1로컬 바인딩만 — 리버스 프록시가s3.<신규도메인>→127.0.0.1:9000으로 프록시 - 볼륨 디렉터리 생성 (bind mount용 절대경로):
/data/mariadb— DB 데이터/data/redis— Redis 데이터 (RDB 파일)/data/minio— MinIO 오브젝트/data/letssing/upload— 로컬 업로드 볼륨 (FileService저장 경로)
- 시간대
Asia/Seoul세팅 확인 (스케줄러가 KST 고정) - nginx 설치 + certbot으로 Let's Encrypt 인증서 발급 — 앱 도메인 +
s3.<신규도메인>두 개.certbot renew자동갱신 cron 등록(90일 만료) - MinIO 컨테이너 기동 + prd 버킷 생성 (
lets-sing-prd) + 신규 access/secret key 발급 - MinIO endpoint (
s3.<신규도메인>) TLS 인증서 발급 확인 - 백업 정책 세팅 — 단일 노드라 서버 자체가 SPOF. 최소한:
/data디렉터리 일일 스냅샷 + 외부 저장소(S3/외부 디스크)로 원격 백업. mysqldump는 매일 별도 스냅샷.
4-3. 시크릿 로테이트 & 코드 수정
application*.properties의 인라인 시크릿 →.env또는 시스템 환경변수로 이전- 로테이트 대상 (상세 부록 A):
- MySQL
develop계정 비번 - JWT 서명키 (
jwt.password,jwt.in-app.private) - MinIO 신규 access/secret key (통큰아이 자체 호스팅용)
- AWS access key
AKIA...IVBM5— git 이력에도 남아 있어 AWS 콘솔에서 disable + 신규 발급 필수 - Firebase service account 재발급 검토
AppConst.DEV_MODE = true하드코딩 정리 (prd 프로파일 여부에 따라)- Dockerfile의
--spring.profiles.active=dev→ prd로 갈지 결정 반영 - 코드 수정 (부록 C) —
FileService의 하드코딩 URL, MinIO endpoint, AWS S3 잔재 정리 - Play Console / ASC 콜백 URL이 도메인 기반인지 확인 — 도메인이면 스킵, IP면 신규 도메인으로 사전 등록
4-4. 초기 데이터 동기화 (컷오버 시간 단축용)
- MinIO 오브젝트 초기 미러링:
mc alias set old https://dev-s3.lukus.kr <OLD_AK> <OLD_SK>
mc alias set new https://s3.<신규도메인> <NEW_AK> <NEW_SK>
mc mirror --overwrite old/lets-sing-dev new/lets-sing-prd- 로컬 업로드 볼륨 초기 rsync (증분은 컷오버 때 다시)
- mysqldump → 신규 DB restore 총 소요 시간 리허설로 측정
4-5. 리허설
- 통큰아이에 이관 스택을 별 도메인/포트로 띄우고 스냅샷 데이터로 기동
- 스모크 테스트 (부록 B) 전체 통과 확인
- 총 다운타임 소요 실측 → 실제 컷오버 창 산정
4-6. 컷오버 직전 점검
- 클라이언트 배포 필요 시 마켓 심사 상태 확인 (도메인 방식이면 불필요)
- DNS TTL 사전에 60초로 하향 조정
- 모니터링/알림 수신 채널 확인
- 롤백 결정권자·소통 채널 확정
- 유저 안내 공지(앱 내 배너/푸시) — 다운타임 시각 명시
5. 컷오버 실행 (순서대로 진행)
컷오버 시퀀스 개요
5-1. 트래픽 컷 & 데이터 동결
- 온콜 인원 대기 확인
- EC2 리버스 프록시에서 신규 요청 503으로 응답 (writes 동결)
- 진행 중 트랜잭션 완료 대기 (30초~1분)
- Google Play RTDN, Apple ASSN은 재시도 큐가 있으므로 이 시점 이후 도착분은 신규 서버가 받도록 설계됨을 재확인
5-2. DB 최종 스냅샷 & 전송
# 옛 EC2에서 실행
mysqldump -u root -p \
--routines --triggers --events \
--single-transaction --hex-blob \
--databases sing > sing_final.sqlsing_final.sql통큰아이로 전송 (scp 또는 rsync)
5-3. 파일/오브젝트 증분 sync
로컬 업로드 볼륨:
rsync -avz --delete \
/home/yuventius125/lets-sing-server/files/upload/ \
new-server:/data/letssing/upload/MinIO 오브젝트 증분:
mc mirror --overwrite --remove old/lets-sing-dev new/lets-sing-prd- 초기 sync를 준비 단계에서 마쳐뒀으면 증분은 몇 분
5-4. Redis 스냅샷 (선택)
- 필요 시
redis-cli SAVE후dump.rdb를 통큰아이 Redis 데이터 디렉터리로 복사 - 유지 부담이면 skip — 유저에게 "출석/무료댓글 카운트 리셋" 안내
5-5. 통큰아이 스택 기동
- DB restore:
mysql -u root -p < sing_final.sql - 프로시저 존재 확인:
SHOW PROCEDURE STATUS WHERE Db='sing';— 8개 확인 - Redis 컨테이너 up
- MinIO 컨테이너 up (이미 4-2에서 세팅됨, 상태만 확인)
- app 컨테이너 up (
--spring.profiles.active=prd) /users/version호출로 살아있는지 확인 (인증 불요 엔드포인트)
5-6. DNS/트래픽 스위치
- 도메인 방식: DNS A 레코드 → 통큰아이 IP로 변경 (TTL 60초로 낮춰놨어야 함)
- IP 직결 방식: 옛 EC2에 nginx로 통큰아이 프록시하는 임시 설정 → 앱 강제 업데이트 후 옛 EC2 종료
5-7. 컷오버 마무리
- Play Console/ASC 콜백 URL — 도메인 기반이면 처리 완료, IP 기반이었다면 신규 도메인으로 갱신
- 유저 공지 해제
6. 컷오버 이후 검증
6-1. 즉시 확인 (컷오버 완료 직후)
- 로그인/회원가입 (카카오/네이버/애플/구글 4가지 각 1회)
- 게시판 조회, 댓글 작성
- 파일 업로드 → 신규 MinIO(
s3.<신규도메인>)에 오브젝트 생성 확인 (mc ls new/lets-sing-prd) - 옛 파일 접근: 예전에 lukus.kr에 저장된 오브젝트도 신규 endpoint로 접근되는지 (mirror 검증)
- 로컬 업로드 볼륨 첨부 파일 접근 확인 (URL 도메인이 신규로 바뀌었는지)
- 푸시 발송 테스트
6-2. 하루 이내
- 실제 유저 결제 완료 케이스 1건 이상 확인 (
/in-app/auto-notification-google유입 로그) - Redis 카운터 정상 증가
CurriculumStatusScheduler가 KST 00:00에 정상 동작했는지 확인- 옛 EC2로 유입되는 잔여 트래픽 로그 (있으면 클라이언트 캐시/구버전)
6-3. 일주일 이내 (안정화 후 정리)
- 옛 EC2 종료/스냅샷 보관 후 폐기
- lukus.kr MinIO 접근 차단 요청 — 신규 MinIO에 모든 오브젝트 복제 확인 후 벤더에 종료 요청
- 리포에서 시크릿 흔적 제거 및 git history rewrite 검토 (BFG Repo-Cleaner)
- 벤더 종속 인벤토리 재점검 — 남은 것 있나 최종 확인
7. 롤백 계획
언제 롤백?
- 컷오버 직후 스모크 테스트 실패율 > 10%
- 결제/인증 등 결정적 기능이 15분 이상 실패
어떻게?
- DNS 되돌리기(도메인 방식) 또는 옛 EC2 nginx 프록시 role 재활성 (IP 방식)
- 통큰아이 DB는 그대로 두되 opt-write 정지
- 컷오버 창 이후 발생한 통큰아이 DB write는 별도 추출 후 옛 EC2로 병합 스크립트 준비 (사전 작성 필수)
롤백 불가 지점: 유저가 신규 회원가입/결제를 통큰아이에서 수 분간 실행한 시점. 이후엔 forward-only.
부록 A. 시크릿 로테이션 대상
리포에 커밋되어 있어 이관을 계기로 반드시 재발급:
- MySQL
develop/(리포에 노출된 값) - MariaDB root /
(리포에 노출된 값)(docker-compose) - JWT
jwt.password(리포에 리터럴 커밋됨) — 로테이트 시 발급된 모든 유저 토큰 무효화됨 → 클라이언트 자동 재로그인 flow 검증 - JWT
jwt.in-app.private(리포에 리터럴 커밋됨) - AWS access key
AKIA...IVBM5+ secret — 즉시 disable, 이력에서 제거 어렵더라도 disable이 최우선 - MinIO
QG...BXJV+ secret - Firebase service account JSON (재발급 여부 결정)
로테이트 방법: 신규 값을 통큰아이의 시스템 환경변수 or .env 파일로 주입, application.properties에는 ${JWT_PASSWORD} 형태로 참조. 파일에 값 직기록 금지.
부록 C. 코드 수정 필요 지점 (재빌드 필수)
벤더 종속 제거를 위해 이관 전 반드시 손봐야 하는 코드:
| 파일 | 라인 | 현재 상태 | 조치 |
|---|---|---|---|
service/FileService.java | 16 | String UPLOAD_PATH = "upload/"; (상대경로) | properties 값 참조로 전환 |
service/FileService.java | 18 | String UPLOAD_URL = "http://554a04dfe064.sn.mynetname.net:50004/upload/"; | 하드코딩된 옛 dev DNS 제거. ${app.upload.base-url} 참조 |
application.properties (전 프로파일) | - | minio.url=https://dev-s3.lukus.kr | https://s3.<신규도메인> |
application.properties (전 프로파일) | - | minio.bucket=lets-sing-dev | prd 프로파일은 lets-sing-prd 등으로 격리 |
application.properties | - | AWS 액세스 키 인라인 | 제거 (실사용 안 하면 관련 코드도 정리) |
config/AppConst.java | 6 | DEV_MODE = true 하드코딩 | prd에서 false로 판단할 수 있게 프로파일 참조 or 상수 제거 |
Dockerfile | 3 | --spring.profiles.active=dev | prd 배포는 prd 프로파일로 |
resources/service-account-*.json | - | Firebase 서비스 계정 JSON 커밋 | 프로젝트 소유권 확인 후 필요 시 신규 발급, git ignore + 배포 시크릿 주입 방식으로 |
config/S3Config.java, config/S3Upload.java | 전체 | AWS S3 코드 잔재(실사용 X) | 제거 여부 결정 |
부록 B. 스모크 테스트 체크리스트
컷오버 후 15분 안에 통과해야 하는 시나리오:
- 인증
POST /users/sign-up(KAKAO/NAVER/APPLE/GOOGLE 각 신규 UID)POST /users/login— 기존 유저 로그인, JWT 발급 확인- Bearer 토큰으로
GET /main200 응답
- DB write/read
POST /board-free게시글 작성GET /board-free목록에 포함되는지
- 파일 스토리지
POST /feedback-filemultipart 업로드 → 신규 MinIO(s3.<신규도메인>)에 실제 오브젝트 생성 확인POST /board-free첨부 (구 FileService 경로) → 로컬 볼륨에 저장, 반환 URL이 신규 도메인인지
- 결제/인앱
POST /in-app/notification모의 페이로드 처리- 실사용자 결제 1건이
/in-app/auto-notification-google로 유입되는지 로그 확인
- 푸시
- 테스트 계정에 FCM 발송 성공
- 스케줄러
- 로그 시작 문구
커리큘럼 상태 업데이트 시작익일 00:00 KST 확인
- 관리자
POST /admin/login관리자 계정 로그인- 관리자 대시보드 주요 조회 API 200
오픈 이슈 / 결정 대기
- 3-1: 통큰아이 서버 스펙 회신
- 3-2: 방화벽 정책
- 3-3: 클라이언트 앱 base URL 확인 (도메인/IP)
- 3-4: 현 DB의 실체 (RDS/EC2 self-hosted)
- 3-5: 데이터 볼륨 크기 (DB + 로컬 업로드 + MinIO 오브젝트)
- 3-6: 허용 다운타임
- 3-7: 신규 MinIO 도메인/서브도메인 및 prd 버킷명
- 3-8: prd 프로파일 사용 여부
- 3-9: HTTPS 처리 — nginx + certbot(Let's Encrypt) 자체 처리로 결정
- 3-10: 벤더 인계 항목 확보 상태 (Firebase 프로젝트, AWS 계정, 도메인, MinIO 서버 접근 등)