웨딩 플래닝 서비스, Wedit 아키텍처 의사결정 기록
작성자: 오현우, 김현빈
결정: 모놀리식 아키텍처 채택
상황
- 시즌톤 해커톤 프로젝트로 짧은 개발 기간
- 팀 규모가 작음 (백엔드 개발자 2명)
- 웨딩 도메인의 비즈니스 요구사항이 명확함
- 빠른 프로토타이핑 요구
선택지
- 마이크로서비스 아키텍처
- 모놀리식 아키텍처
결정 이유
- ✅ 개발 속도: 빠른 프로토타이핑 가능
- ✅ 운영 복잡성 최소화: 단일 배포 단위
- ✅ 팀 규모에 적합: 적은 인원으로 관리 가능
- ✅ 비용 효율성: 최소 비용으로 인프라 구성 가능
Trade-Offs
- 단일 장애점(SPOF): 하나의 컴포넌트 장애가 전체 서비스 중단 가능성 존재
- 배포 경직성: 작은 수정사항도 전체 애플리케이션의 재배포 요구
결과
- Spring Boot 단일 애플리케이션으로 구성
- 도메인별 패키지 구조로 분리하여 결합도를 낮추는데 집중
- 도메인별 패키지 분리로 향후 마이크로서비스 전환 준비
결정: Java 21 + Spring Boot 3.5.5 사용
상황
- 최신 기술 활용으로 성능과 생산성 향상 필요
- 장기적인 유지보수성 고려
선택지
- Java 8 + Spring Boot 2.x
- Java 17 + Spring Boot 3.x
- Java 21 + Spring Boot 3.x
결정 이유
- ✅ 최신 LTS 버전: Java 21의 안정성과 성능 개선
- ✅ Virtual Threads: 높은 동시성 처리 가능
- ✅ Record Classes: 간결한 DTO 작성
- ✅ Spring Boot 3.x: Native 이미지 지원, 성능 개선
결정: QueryDSL 채택
상황
- 복잡한 동적 쿼리 작성 필요
- 지역별, 가격별, 스타일별 다중 조건 검색
선택지
- JPA Criteria API
- QueryDSL
- Native Query
결정 이유
- ✅ 타입 안정성: 컴파일 타임 쿼리 오류 검출
- ✅ 가독성: SQL과 유사한 직관적 문법
- ✅ 동적 쿼리: BooleanBuilder로 조건부 쿼리 작성 용이
결정: MySQL 8.0 채택
상황
- 웨딩 업체 및 예약 데이터 저장
- 지역별 계층 구조 데이터 처리
- AWS RDS 사용 예정
선택지
- MySQL
- PostgreSQL
- MongoDB (NoSQL)
결정 이유
- ✅ AWS RDS 최적화: Aurora MySQL 호환성
- ✅ 팀 친숙도: 기존 MySQL 경험 보유
- ✅ JSON 지원: MySQL 8.0의 향상된 JSON 기능
- ✅ 성능: 읽기 중심 워크로드에 적합
결정: Self-Referencing 테이블 구조 + 계층별 쿼리 최적화
상황
- 시/도(level 1) → 시/군/구(level 2) → 읍/면/동(level 3) 계층 구조
- level 2 입력 시 모든 하위 level 3 지역 검색 필요
선택지
- Adjacency List (Self-Referencing)
- Nested Set Model
- Materialized Path
결정 이유
- ✅ 단순성: 이해하기 쉬운 구조
- ✅ 확장성: 새로운 지역 추가 용이
- ✅ 쿼리 최적화: 별도 메서드로 계층별 조회
구현
@Entity
public class Region {
@Id
private Long id;
private String name;
private int level; // 1: 시/도, 2: 시/군/구, 3: 읍/면/동
private String code;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Region parent;
}
// 최적화된 조회 쿼리
@Query("SELECT r.code FROM Region r WHERE r.parent.code = :parentCode AND r.level = 3")
List<String> findLevel3CodesByParentCode(@Param("parentCode") String parentCode);결정: JWT Access Token + OAuth2 소셜 로그인 조합
상황
- 간편한 회원가입 및 로그인 필요
- 보안성과 사용성 두 가지 모두 중요
선택지
- 세션 기반 인증
- JWT 단독
- OAuth2 + JWT 조합
결정 이유
- ✅ Stateless: 서버 확장성 향상, 향후 수평 확장 시 세션 불일치 문제 제거
- ✅ 소셜 로그인: 사용자 진입 장벽 낮춤
- ✅ 토큰 관리: Access/Refresh Token 분리 및 Refresh Token Rotate 전략 수립
- ✅ 다중 플랫폼: 웹/모바일 대응 가능
- ✅ 사용자 편의성: 카카오, 네이버, 구글 소셜 로그인 지원
결정: CoolSMS API 활용
상황
- 휴대폰 번호 기반 본인 인증 필요
선택지
- 자체 SMS 시스템 구축
- CoolSMS API
- AWS SNS
결정 이유
- ✅ 국내 특화: 한국 통신사 최적화
- ✅ 안정성: 높은 전송 성공률
- ✅ 개발 효율성: 빠른 구현 가능
- ✅ 비용 효율성: 소규모 서비스에 적합
결정: REST 아키텍처 + Swagger UI 문서화
상황
- 프론트엔드와의 명확한 API 계약 필요
- API 문서 자동화 요구
선택지
- REST API
- GraphQL
- gRPC
결정 이유
- ✅ 표준성: HTTP 표준 활용
- ✅ 캐싱: HTTP 캐싱 메커니즘 활용
- ✅ 도구 생태계: Swagger/OpenAPI 지원
- ✅ 학습 곡선: 팀 친숙도 높음
결정: 표준화된 ApiResponse 래퍼 사용
상황
- 일관된 API 응답 형식 필요
- 성공/실패 상태 명확한 구분
구현
@Getter
@Builder
public class ApiResponse<T> {
private final boolean success;
private final T data;
private final String message;
private final String code;
public static <T> ResponseEntity<ApiResponse<T>> success(T data, SuccessStatus status) {
return ResponseEntity.ok(ApiResponse.<T>builder()
.success(true)
.data(data)
.message(status.getMessage())
.code(status.getCode())
.build());
}
}결정: S3 저장 + CloudFront CDN 제공 조합
상황
- 사용자 및 업체의 이미지, 비디오, 오디오 저장
- 전국 사용자 대상 빠른 이미지 로딩 필요
선택지
- 로컬 파일 시스템
- AWS S3 단독
- S3 + CloudFront
결정 이유
- ✅ 확장성: 무제한 저장 공간
- ✅ 성능: CDN 캐싱을 통한 빠른 전송
- ✅ 보안: Pre-signed URL 지원
- ✅ 비용 절감: CloudFront를 통한 저렴한 전송
구현
@Service
public class S3Service {
public String generatePresignedUploadUrl(String fileName, MediaDomain domain) {
String key = buildMediaKey(domain, fileName);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType("image/jpeg")
.build();
return presigner.presignPutObject(putObjectRequest).url().toString();
}
public String toCdnUrl(String mediaKey) {
return cloudFrontUrl + "/" + mediaKey;
}
}결정: AWS 클라우드 인프라 활용
상황
- 안정적인 서비스 운영 필요
- 확장 가능한 인프라 구성
선택지
- 온프레미스 서버
- AWS 클라우드
- 다른 클라우드 제공자
결정 이유
- ✅ 관리형 서비스: RDS, S3 등 운영 부담 감소
- ✅ 프리티어 활용: AWS 프리티어 제도 적극 활용
- ✅ 안정성: 99.9% 가용성 보장
- ✅ 생태계: 다양한 서비스 연동 가능
- ✅ 인프라 제어 유연성: 인프라 전반에 대한 제어권 확보 가능
결정: 컨테이너 기반 배포 파이프라인
상황
- 빠르고 안정적인 배포 필요
- 환경 일관성 보장
선택지
- 직접 서버 배포
- Docker 컨테이너
- Kubernetes
결정 이유
- ✅ 환경 일관성: 개발/운영 환경 동일화
- ✅ 배포 속도: 빠른 이미지 빌드 및 배포
- ✅ 롤백 용이성: 이전 이미지로 빠른 복원
- ✅ 낮은 난이도: K8s 대비 운영 및 구축 난이도 낮음
- ✅ 비용 최소화: 제한적 무료로 운영 가능
결정: 매트릭 시각화 APM 구축
상황
- 애플리케이션 및 DB 상태 모니터링 필요
- 성능 병목 구간 식별
- 장애 발생 시 신속한 원인 파악
선택지
- 커스텀 헬스체크
- Spring Actuator
- 외부 모니터링 도구
결정 이유
- ✅ 데이터 기반 의사결정: 정량적 데이터를 통한 성능 개선 및 문제 해결 추구
- ✅ 장애 대응 시간 단축: 장애 발생 시 신속한 원인 파악
- ✅ 비용 효율성 및 유연성: 상용 APM 대비 저렴 및 커스텀 가능
결정: 애플리케이션과 DB 로깅
상황
- 애플리케이션 레벨 로깅 요구
- 실제 JPA 쿼리 파악
선택지
- Spring Boot의 show-sql, format_sql
- P6Spy + Slf4J
결정 이유
- ✅ 파라미터 바인딩 명시: 실제 바인딩 값 확인 가능
- ✅ 포맷팅: 로깅 포맷을 커스텀하여 선택적 확인 가능
결정: 다층 최적화 전략 적용
최적화 방법
- 연관관계 최적화
// N+1 문제 해결
@Query("SELECT v FROM Vendor v JOIN FETCH v.region LEFT JOIN FETCH v.logoMedia")
List<Vendor> findVendorsWithRegionAndLogo();- 인덱스 설계
@Table(indexes = {
@Index(name = "idx_region_level", columnList = "level"),
@Index(name = "idx_vendor_type_region", columnList = "vendorType, region_id")
})- 쿼리 최적화
// QueryDSL 프로젝션 활용
.select(Projections.constructor(VendorWithMinPrice.class,
vendor, weddingHall.basePrice.min()))
.groupBy(vendor.id)
.orderBy(weddingHall.basePrice.min().asc())마이크로서비스 전환 준비
- 도메인별 패키지 분리 완료
- 독립적인 데이터베이스 스키마 설계
- API Gateway 도입 고려
기능 확장 로드맵
- 실시간 알림: WebSocket 또는 SSE 도입
- 결제 시스템: 포트원(아임포트) 연동
- 모바일 앱: React Native 또는 Flutter
성능 확장 계획
- 인스턴스: Auto Scaling 도입
- 데이터베이스: Flyway DB 마이그레이션 툴 도입
- 캐싱: Redis 클러스터 구축
- 검색: Elasticsearch 도입
- CI/CD: 무중단 그린/블루 배포 도입
작성자: 오현우, 김현빈 버전: 1.0.1