프로젝트 · 2026.07진행 중

MOIT (모잇)

공유 URL로 회원과 비회원이 함께 참여하는 모바일 우선 모임 관리 서비스

Java 21Spring Boot 3.5Spring SecuritySpring Data JPAPostgreSQLFlywayKakao OAuthNaver OAuthReactTypeScriptViteTanStack QueryJUnitVitestPlaywrightDocker ComposeNginxLet's Encrypt
담당 역할서비스 기획·설계·개발·테스트·배포 및 운영 전 과정
처리 규모
성능 개선

01 / 프로젝트 개요

MOIT는 일회성 모임을 생성하고 공유 URL을 통해 회원과 비회원이 함께 참여할 수 있는 모바일 우선 모임 관리 서비스입니다.

모임 생성부터 참여자 관리, 출석, 투표, 지출 기록, 게임, 모임 종료 및 기록 조회까지 하나의 서비스에서 처리할 수 있도록 구현했습니다.

기능 구현에 그치지 않고 직접 운영 환경을 구성하여 도메인·HTTPS 적용, 애플리케이션 자동 시작, 인증서 갱신, PostgreSQL 백업 및 복구 검증까지 진행했습니다.

02 / 해결하려던 문제

일회성 모임에서는 참석자 전원이 특정 서비스에 가입하도록 요구하기 어렵고, 출석·투표·지출·게임 등의 기능을 각각 다른 도구로 사용하면 모임 진행 과정이 분산됩니다.

이를 해결하기 위해 공유 URL 하나로 회원과 비회원이 동일한 모임에 참여할 수 있도록 설계하고, 모임 운영에 필요한 기능을 하나의 흐름으로 통합했습니다.

특히 비회원도 주요 기능을 사용할 수 있도록 Guest 인증 체계를 별도로 설계하면서도, Participant의 권한과 상태를 서버에서 일관되게 관리하는 것을 주요 기술 과제로 삼았습니다.

03 / 주요 기능

FEATURE 01

소셜 로그인 및 자체 인증

Kakao와 Naver OAuth를 이용한 회원 로그인을 지원합니다.

OAuth Provider는 사용자 신원 확인에만 사용하고, 인증 성공 후 MOIT 자체 Access Token과 Refresh Token을 발급하도록 인증 영역을 분리했습니다.

Callback 과정에서는 최종 Token을 URL에 노출하지 않고 일회용 Login Grant를 발급한 뒤 Frontend가 이를 Token으로 교환합니다.

FEATURE 02

공유 URL 기반 회원·비회원 참여

모임 생성 시 발급되는 공유 URL 하나로 회원과 비회원이 함께 참여할 수 있습니다.

계정이 없는 사용자는 Guest Participant로 참여하며 별도의 Guest Token을 발급받습니다. 다른 브라우저에서 다시 접근하거나 Token을 분실한 경우에는 Recovery Code로 인증을 복구하고 기존 Token을 교체합니다.

FEATURE 03

Participant Lifecycle 및 권한 관리

참가자를 ACTIVE / LEFT / REMOVED 상태를 가진 Participant로 관리합니다.

탈퇴하거나 강제 퇴장된 참가자의 과거 기록은 유지하면서 현재 모임 목록과 기능 접근에서는 제외되도록 조회 정책과 권한 정책을 분리했습니다. 모임 종료 후에는 변경을 제한하고 기존 기록만 조회할 수 있습니다.

FEATURE 04

모임 운영 기능

참가자 관리, 출석·귀가 상태 관리, 투표, 지출 기록, Random Pick, Pointing Game, Balance Game, 모임 종료 및 기록 조회 기능을 제공합니다.

Balance Game은 하나의 Session에서 여러 질문을 진행하는 Round 구조로 설계했고, 질문 중복 방지와 Round별 선택·집계 결과 보존을 구현했습니다.

FEATURE 05

테스트 및 회귀 검증

Backend 단위·통합 테스트, Frontend 테스트와 함께 Playwright 기반 Browser E2E 테스트로 주요 사용자 흐름을 검증했습니다.

수동 QA에서 발견된 Participant 상태, Guest Recovery, Navigation 문제를 수정하고 회귀 테스트를 수행했으며, Balance Game 다중 Round 구조 변경 후에도 전체 E2E 61건을 통과했습니다.

FEATURE 06

직접 배포 및 운영

Mac mini를 개인 서버로 구성하고 Cloudflare DNS, Port Forwarding, Nginx Reverse Proxy와 Let's Encrypt 인증서를 적용해 외부 서비스를 운영하고 있습니다.

애플리케이션 자동 시작과 인증서 갱신을 자동화했으며, PostgreSQL 정기 백업은 별도 데이터베이스에 실제 Restore한 뒤 테이블과 데이터를 비교해 복구 가능성까지 검증했습니다.

04 / 시스템 구조

등록된 아키텍처 이미지가 없습니다.
구조 설명

사용자는 공인 도메인을 통해 서비스에 접근하며 DNS와 공유기의 Port Forwarding을 거쳐 Mac mini의 Nginx에 연결됩니다.

Nginx는 서비스의 단일 외부 진입점으로 동작합니다. React 정적 파일을 직접 제공하고 /api/* 요청은 내부 Spring Boot 애플리케이션으로 Reverse Proxy합니다.

Spring Boot는 인증·인가와 Meeting, Participant, Attendance, Vote, Expense, Game 등의 비즈니스 로직을 담당하며 PostgreSQL에 데이터를 저장합니다. 데이터베이스 Schema 변경은 Flyway Migration으로 관리합니다.

회원은 OAuth 로그인 후 MOIT 자체 인증 Token을 사용하고, 비회원은 Meeting별 Guest Token으로 인증됩니다. 두 인증 방식은 최종적으로 Participant와 Meeting 상태를 기준으로 권한을 검증합니다.

운영 환경에서는 Nginx만 외부에 공개하고 Application과 Database는 내부에서 통신합니다. HTTPS 인증서 갱신과 PostgreSQL 백업을 정기 작업으로 자동화했습니다.

05 / 주요 기술적 결정

기술 결정 01

로그인하지 않은 사용자를 어떻게 안전하게 식별할 것인가

[고민]
공유 URL의 접근성을 유지하려면 비회원 참여가 필요했지만, 닉네임이나 클라이언트 상태만으로는 사용자를 신뢰하기 어렵고 모든 사용자에게 로그인을 요구하면 진입 장벽이 높아집니다.

[결정]
회원 인증과 별도로 Guest Participant + Guest Token 기반 인증 구조를 적용했습니다. 서버에는 Token과 Recovery Code 원문 대신 Hash를 저장하고, Recovery 성공 시 기존 Token을 교체합니다.

[결정 이유]
회원 가입 없는 편의성을 유지하면서도 서버가 인증된 Participant를 기준으로 일관된 권한 판단을 할 수 있기 때문입니다.

기술 결정 02

OAuth 인증 결과를 Frontend에 어떻게 전달할 것인가

[고민]
Access Token과 Refresh Token을 Redirect URL에 직접 전달하면 Browser History, 로그, Referer를 통해 노출될 수 있고, Provider Token을 서비스 인증에 사용하면 외부 Provider와 내부 인증 정책이 강하게 결합됩니다.

[결정]
OAuth Provider는 신원 확인에만 사용하고 Backend가 짧은 만료 시간과 1회 사용 정책을 가진 Login Grant를 발급하도록 구성했습니다. Frontend는 별도 Exchange API에서 Grant를 MOIT Token으로 교환합니다.

[결정 이유]
외부 OAuth 인증과 서비스 자체 인증의 책임을 분리하고 최종 인증 Token이 URL에 노출되는 것을 방지할 수 있기 때문입니다.

기술 결정 03

개인 프로젝트를 어디까지 운영해 볼 것인가

[고민]
단순 배포 서비스를 사용하는 것보다 외부 요청, 프로세스 재시작, 인증서 갱신과 데이터 복구 문제를 직접 경험하는 것을 우선했습니다.

[결정]
Mac mini를 실제 서버로 사용하고 Nginx만 외부에 공개했습니다. Spring Boot와 PostgreSQL은 내부에서 통신하며 HTTPS 인증서 갱신, 애플리케이션 재시작과 PostgreSQL 정기 백업을 자동화했습니다. 백업은 별도 DB에 Restore하여 복구 가능성을 검증했습니다.

[결정 이유]
DNS → Network → Reverse Proxy → Application → Database로 이어지는 요청 경로와 프로세스·인증서·데이터의 운영 Lifecycle을 직접 이해하기 위해서입니다.

06 / 트러블슈팅

트러블슈팅 사례 01

자동 DB 백업에서 Docker 명령을 찾지 못하는 문제

개발자가 직접 실행한 작업과 Scheduler가 실행한 작업은 동일한 실행 환경을 보장하지 않는다는 점을 경험했습니다. 자동화 작업에서는 PATH, Working Directory, 환경변수, 실행 사용자처럼 평소 Shell이 암묵적으로 제공하는 환경까지 명시적으로 관리해야 합니다. 또한 백업은 단순히 파일이 생성됐는지가 아니라 정상적인 결과물이 생성됐고 실제 복구 가능한지를 기준으로 성공 여부를 판단해야 한다는 원칙을 적용했습니다.

트러블슈팅 사례 02

백업 파일 생성만으로 데이터 복구를 보장할 수 없는 문제

실제 Restore를 통해 백업 파일의 사용 가능성을 검증했고, 이후 'Backup 완료'의 기준을 파일 생성이 아닌 복구 가능성까지 확인하는 것으로 변경했습니다. 운영 안정성은 장애를 막는 것뿐 아니라 장애가 발생했을 때 데이터를 복구할 수 있는 절차까지 포함한다는 점을 경험했습니다.

트러블슈팅 사례 03

Participant 상태와 Guest 인증 정보 불일치로 발생한 접근 오류

인증에 성공했다는 사실과 특정 도메인 리소스를 사용할 권한이 있다는 사실은 별개의 문제임을 확인했습니다. 상태가 존재하는 서비스에서는 Authentication → Participant 식별 → Lifecycle 확인 → Authorization 순서로 접근 권한을 판단해야 예외 상황에서도 일관된 동작을 유지할 수 있었습니다. 정상 흐름 중심의 자동화 테스트만으로 발견하기 어려운 문제를 실제 브라우저 수동 QA로 확인했고, Lifecycle 기반 예외 시나리오를 회귀 테스트 범위에 포함했습니다.