예배 콘티 관리 프로젝트를 실무 설계 기준으로 구현해 나가며 깨달은 점들을 기록해보자
Team: 콘티를 카테고리별로 분류하기 위한 개념
Conti: 하나의 콘티, 예배 시 사용할 곡 모음
Song: 콘티 안에 포함된 개별 찬양 곡
각 도메인의 관계:
Team 1:N Conti
Conti 1:N Song
@Column(length = ?) 설정 기준팀명 컬럼 길이 선정
한글 10자 내외 → UTF-8 기준 약 30 bytes (2~3 bytes/자)
@Column(length = 50)이면 충분하며, 여유 있게 100으로 설정해도 무방
URL 저장 시 컬럼 길이
이미지 URL은 S3, Firebase, Supabase 등 클라우드 저장소 기반이 많음
대부분 200자 내외이나, 넉넉하게 length = 255 설정
@Column(length = 100)
val teamName: String
@Column(length = 255)
val logoUrl: String
이미지 자체를 DB에 넣는 것은 다음 이유로 지양했다:
이미지 용량이 크면 DB 성능 저하
텍스트 기반 쿼리에선 다루기 힘듦
클라우드 저장소를 사용하고 링크만 저장하는 것이 실무 표준
콘티에는 찬양 가사 같은 대용량 텍스트가 들어간다. 이를 위해 @Lob과 TEXT 타입을 사용하고, 꼭 필요한 경우에만 로딩되게 설정했다.
@Lob
@Basic(fetch = FetchType.LAZY)
@Column(columnDefinition = "TEXT")
val lyrics: String
@Lob은 기본적으로 EAGER 로딩이므로 Lazy로 명시하지 않으면 콘티 목록 조회 시 전체 가사까지 로딩되는 성능 문제 발생한다.
상세 페이지에서만 가사를 보므로 Lazy 설정
콘티와 곡도 1:N 관계, 단 곡은 콘티에 종속된 구조 (독립적으로 CRUD 하지 않았다.)
@OneToMany(mappedBy = "conti", cascade = [CascadeType.ALL], orphanRemoval = true)
val songs: MutableList<Song> = mutableListOf()
cascade = ALL → 콘티 저장 시 곡도 함께 저장됨
orphanRemoval = true → 곡 제거 시 DB에서도 삭제됨
title, date → 의미가 모호해
→ contiTitle, songTitle로 변경하여 가독성 향상
응답 DTO는 용도별로 나눴다.
| 용도 | DTO 이름 | 설명 |
|---|---|---|
| 홈 전용 | ContiPreviewResponse | 최신 콘티 요약 (곡 제목들만) |
| 목록 | ContiSummaryResponse | 콘티 리스트 보기용 정보 요약 |
| 상세 | ContiDetailResponse | 콘티 + 곡 정보 + 링크 등 전체 포함 |
| 방식 | 설명 | 구현 방식 |
|---|---|---|
| 페이지네이션 | 1,2,3 버튼 클릭 | PageRequest.of(page, size) |
| 무한 스크롤 | 스크롤 시 10개씩 로딩 | offset + limit (Pageable) |
현재는:
홈 최신 1건 → findFirstBy...OrderByIdDesc() (단건 조회)
목록 조회 → Page<Conti>로 페이징 적용
리더 인증이 된 사용자만 콘티를 작성할 수 있도록 할 예정
지금은 isLeader, userId 하드코딩 상태지만 추후 JWT로 전환 예정
@PreAuthorize("hasRole('LEADER')")
fun createConti(...) { ... }
@Configuration
@EnableMethodSecurity
class SecurityConfig