oshi@log
밴드 성지·라이브 이벤트·커뮤니티를 한곳에서 다루는 위치 기반 서비스입니다. Kotlin과 Spring Boot 3로 서버를 만들고 있으며, 서버는 운영 중이고 앱은 내부 테스트 단계입니다.
- 606
- API 매핑
- 9
- 도메인 모듈
- 111
- 컨트롤러
- 80%
- Kover 게이트
- 1,599
- 테스트
- 153
- 마이그레이션
무엇을 했나
- 기능을 identity·place·event·social·music·fanatic·analytics·telemetry·core 9개 모듈로 구분했습니다. 기존 크로스모듈 저장소 참조 230건은 기준선으로 고정하고, 새 위반이 추가되면 CI가 실패하도록 테스트를 만들었습니다.
- PostgreSQL·PostGIS와 Hibernate Spatial을 사용해 주변 장소 검색과 거리 계산을 구현했습니다. 거리·이동 속도·일일 횟수 규칙에 걸리는 위치 요청은 서버에서 거부합니다.
- Google·Apple OAuth2와 이메일 인증을 구현했습니다. 위치 인증 토큰은 기기별 등록 키로 RS256 서명을 검증하고, 프로젝트별 권한과 Redis 기반 요청 횟수 제한을 적용했습니다.
- 읽기 빈도가 높은 장소·이벤트 목록은 Caffeine에, 세션·토큰 차단 목록·요청 횟수는 Redis에 두었습니다. 외부 API 호출에는 Resilience4j 서킷 브레이커를 적용했습니다.
- 게시글·댓글·신고·모더레이션과 미디어 업로드 시 WebP 변환 기능을 만들었습니다.
- Detekt·Ktlint와 Kover 서비스 계층 80% 커버리지 게이트를 CI에 넣었습니다. PostGIS·Redis Testcontainers를 포함한 테스트도 Jenkins에서 실행합니다.
- 서비스명을 girlsbandtabi에서 oshi@log로 바꾸고, 이전 이름이 다시 들어오면 CI에서 찾는 검사 스크립트를 추가했습니다.
01
설계 결정
혼자 개발하는 규모에서 고른 구조와 이유입니다.
Spring Modulith를 고른 이유
도메인별로 코드를 나누고 싶었지만, 혼자 개발하면서 서비스별 배포와 분산 트랜잭션까지 관리하는 것은 부담이 컸습니다. 그래서 하나의 서버로 배포하면서 모듈 경계를 검사할 수 있는 Spring Modulith를 선택했습니다. 아직 크로스모듈 저장소 참조 230건이 남아 있어 완전히 분리된 구조는 아닙니다. 기존 위반은 기준선으로 고정했고 새 위반부터 막고 있습니다.
캐시를 둘로 나눈 기준
장소·이벤트 목록처럼 잠깐 오래된 값이 보여도 되는 데이터는 Caffeine에 저장했습니다. 여러 서버가 같은 값을 봐야 하는 세션, 토큰 차단 목록, 요청 횟수는 Redis만 사용했습니다. 둘을 가른 기준은 데이터를 공유해야 하는 범위였습니다.
위치 검증을 서비스 계층에서 하는 이유
GPS 좌표는 클라이언트가 보내므로 그대로 믿을 수 없습니다. PostGIS는 거리를 계산하고, 서비스 계층은 직전 인증과의 거리·소요 시간·일일 횟수를 확인합니다. 규칙에 걸리는 요청을 거부하는 방식이라 모든 GPS 조작을 판별하는 기능은 아닙니다.
02
시스템 설계
구성 요소와 역할입니다.
아키텍처
identity·place·event·social·music·fanatic·analytics·telemetry·core 9개 모듈로 기능을 구분했습니다. 새 경계 위반은 CI에서 막고 있지만 기존 직접 참조 230건은 남아 있어 단계적으로 줄이고 있습니다. Hibernate Spatial과 JTS로 위치 데이터를 처리합니다.
캐시와 인덱스
외부 API에는 Resilience4j 서킷 브레이커를 적용했습니다. 장소·이벤트 목록은 Caffeine, 여러 인스턴스가 공유하는 상태는 Redis에 저장합니다. 공간 검색과 필터 조건에 맞춰 GIST·GIN 인덱스를 추가했고, 스키마 변경은 Flyway로 관리합니다.
인증과 요청 제한
Google·Apple OAuth2와 이메일 인증, 프로젝트별 권한 검사를 구현했습니다. 위치 요청은 거리·속도·횟수 규칙으로 확인하고, Redis 기반 요청 횟수 제한과 신고·모더레이션 기능을 운영합니다.
03
데이터베이스 아키텍처
지리공간 데이터를 다루는 방법입니다.
지리공간 데이터 설계
Hibernate Spatial과 JTS를 사용해 PostGIS geography 타입과 EPSG:4326 좌표를 저장합니다. 거리 임계값·이동 속도·일일 횟수 규칙에 걸리는 위치 요청은 서버에서 거부합니다.
이벤트 아키텍처
9개 모듈을 Spring Modulith로 구분하고 일부 모듈 간 작업은 이벤트로 연결했습니다. 프로젝트 → 유닛 → 멤버 계층과 지역 트리 구조를 데이터 모델에 반영했습니다.
관측성 및 테스트
Prometheus와 Actuator로 메트릭을 수집하고, Testcontainers로 PostgreSQL·Redis 통합 테스트를 실행합니다. 서비스 계층 Kover 80% 게이트와 Detekt·Ktlint 검사를 CI에서 확인합니다.
풀어낸 케이스
마주친 문제 · 고민하고 적용한 접근 · 실제 결과를 한 세트로 정리했습니다.
- 문제
- 성지 체크인은 사용자가 보낸 GPS 좌표만 믿으면 위치를 조작한 요청을 구분하기 어렵습니다.
- 고민 · 접근
- PostGIS 거리 계산 결과에 이동 속도·일일 방문 횟수 규칙을 적용했습니다. 정한 규칙을 벗어난 요청을 거부합니다. 모든 GPS 조작을 찾지는 못합니다.
- 결과
- 설정한 거리·속도·횟수 규칙에 걸리는 체크인 요청을 서버에서 거부합니다.
- 문제
- 초기에 모듈을 열어 둔 채 개발하면서 다른 모듈의 저장소를 직접 참조한 곳이 230개까지 늘었습니다.
- 고민 · 접근
- 현재 위반 230건을 baseline 파일에 기록하고, 새 위반이 추가되면 CI가 실패하는 아키텍처 테스트를 만들었습니다.
- 결과
- 기존 참조는 목록으로 남기고, 새 직접 참조는 CI에서 막습니다. 기존 참조는 단계적으로 줄이는 중입니다.
주요 성과
- REST 매핑 606개·컨트롤러 111개와 OpenAPI 문서 생성
- Flyway 마이그레이션 153개·테스트 1,599개·Kover 서비스 계층 80% 게이트
- 9개 모듈과 기존 경계 위반 230건 기준선·신규 위반 0 CI 검사
- 위치 이상 패턴 검사와 성지 체크인 기능 구현
- Jenkins 테스트와 Portainer arm64 배포를 Noraneko Platform에서 운영
기술 스택
- Kotlin
- Spring Boot 3
- Spring Modulith
- PostgreSQL
- Hibernate Spatial
- Redis
- Caffeine
- Resilience4j
- JWT · OAuth2
- Flyway
- HikariCP
- Prometheus
- Cloudflare R2
- Testcontainers
- Docker
- OpenAPI 3.0