서론
- 우리 팀은 여러 서비스를 하나의 백엔드 서버와 하나의 DB에서 모두 관리하고 있습니다.
- 그러다보니 Category 모델의 개념의 분류화, 유형화 등을 쓸일이 많습니다.
- 이제 그부분을 두가지 방법으로 관리할수 있을것입니다.
- 단순 String 컬럼을 추가한다.
- 각각의 모델에 따라 Cateogry 모델을 만들어서 관리한다.
- 이러다보니, 현재 팀의 모토인 빠르게 서비스를 개발하고 사용자에게 가치를 주는 일을 주기 위해서 개발의 시간이 많이 드는것을 느꼈습니다.
- 따라서 이번에 시간이 조금 들여도 공통 모델을 만들어서 관리할수 있도록 하는것을 목표로 하였습니다.
해결하고자 하는 방안
- Category의 개념이 필요할때마다 Cateogry를 의미하는 모델을 새롭게 추가할 필요가 없어야 한다.
- 서비스와 타입에 따라 카테고리가 관리되어야 한다.
- 최대한 Category의 공통화를 적용해야 한다.
Cateogry 모델 설계
-
그래서 일단 Category라는 모델을 만드는것을 목표로 했습니다.
-
이제부터는 AI와 작업하기 위한 문서를 작성해봅니다.
-
모델링
class Cateogry { id: Long // PK values: Map<String, String> // 다국어화를 위해 처리합니다. ex) {ko: 운동종목, en: excerciseList} service: String // weggle-plus 서비스를 의미합니다. key: string // 해당 Category의 분류값 createdAt: DateTime updatedAt: DateTiem } -
API
- 프로덕션 레벨
- 목록 조회
- key, service를 이용하여 필터링이 가능해야 합니다.
- 목록 조회
- 어드민 레벨
- 목록조회
- 단건조회
- 생성
- 수정
- 삭제
- 프로덕션 레벨
-
아주 간단하게 위의 설계를 통한 프롬프트를 전달합니다.
-
그 이후에 제가 원하는 방식의 테이블, 모델, API들이 모두 생성되었습니다.
-
이 설계의 단점이 있습니다. key에 대해 타입 안정성이 떨어진다는 점입니다.
- 이것또한 enum으로 하면 최대한 지킬수 있겠지만, 그렇게 되면 확장성이 떨어지기 떄문에 문자열로 일단 처리합니다.
여러 모델과의 관계를 위한 설계
- 이제 이걸 모델과 연결하기 위해서는 중간 테이블이 필요합니다.
- 이부분에 대해서는 생산성과 관련되어 있습니다. 따라서 고려한 대안은 아래와 같습니다.
- 모든 중간 테이블을 다 만듭니다.
- 예를들어 A모델, B모델이라면 ACategory, BCategory를 모두 만듭니다.
- 이거는 생산성을 많이 해결하지 못합니다. 이유는 일단 저는 추가되는 모델에서 Category의 기능을 만들때, 최소한의 생산비용을 원합니다. 모델을 새로 만드는것 자체는 그 비용을 줄이지 못한다고 생각합니다.
- 예를들어 A모델, B모델이라면 ACategory, BCategory를 모두 만듭니다.
- Super Entity 상속구조
- 위에 방식과 큰 차이는 BaseEntity를 만들어서 조금이나마 코드를 적게 적는것입니다.
- 그러나 이것또한 많은 생산성을 해결해주지 못합니다.
- 하나의 테이블에서 Polymorphic을 통한 해결
- 예를들어 tagertType, tagetId를 두고 하나의 테이블에서 여러 모델을 관리하는것입니다.
- Ruby On Rails에서 많이 쓰는 방식입니다.
- 이렇게 한다면 단점은 하나입니다. JPA의 ManyToMany 형식을 못쓴다는것
- 즉 ServiceLayer에서 데이터의 조합이 필요합니다.
- 그러나 새로운 테이블, 모델을 만드는 비용보단 해당 비용이 가장 적을것이라 기대합니다.
- 따라서 3번을 적용합니다.
- 모든 중간 테이블을 다 만듭니다.
하나의 테이블에서 Polymorphic 설계
Category와 여러 모델 간의 관계를 관리하기 위해 Polymorphic 관계를 활용한 설계를 적용하겠습니다.
CategoryRelation 모델 설계
class CategoryRelation {
id: Long // PK
categoryId: Long // [categories.id](http://categories.id) FK
targetType: String // 어떤 도메인과 연결됐는지 (ex. BuddyFitFacility)
targetId: Long // 도메인 PK
createdAt: DateTime
updatedAt: DateTime
}
이 설계는 다음과 같은 이점을 제공합니다:
- 새로운 모델에 카테고리 관계가 필요할 때 새 테이블을 만들 필요가 없음
- 하나의 테이블로 모든 카테고리 관계 관리 가능
- 유연한 확장성 제공
사용 방법 예시
BuddyFitFacility에 카테고리를 연결하는 경우:
// 새로운 카테고리 관계 생성
val categoryRelation = CategoryRelation(
categoryId = [category.id](http://category.id),
targetType = "BuddyFitFacility",
targetId = [facility.id](http://facility.id)
)
// 저장
[categoryRelationRepository.save](http://categoryRelationRepository.save)(categoryRelation)
관계 조회 예시
특정 모델의 카테고리 목록 조회:
// BuddyFitFacility 모델의 ID가 1인 항목에 연결된 모든 카테고리 조회
val categoryRelations = categoryRelationRepository.findAllByTargetTypeAndTargetId("BuddyFitFacility", 1L)
val categoryIds = [categoryRelations.map](http://categoryRelations.map) { it.categoryId }
val categories = categoryRepository.findAllById(categoryIds)
특정 카테고리와 연결된 모든 모델 조회:
// 카테고리 ID가 5인 항목과 연결된 모든 관계 조회
val relations = categoryRelationRepository.findAllByCategoryId(5L)
단점
- JPA의 ManyToMany 관계를 직접 사용할 수 없으므로 서비스 레이어에서 관계 매핑을 처리해야 함
- 타입 안전성이 부족하므로 targetType 값의 일관성을 유지하기 위한 상수나 enum 사용 권장
- 조회 시 JOIN이 복잡해질 수 있으므로 성능에 주의
결론
이번 공통 Category 시스템 설계의 핵심 목표는 다양한 서비스와 도메인 모델에서 반복적으로 발생하는 ‘분류/유형 관리 기능’을 하나의 통합된 구조로 효율적으로 처리하는 것입니다. 빠른 서비스 개발이라는 팀의 모토를 유지하면서도, 장기적으로 확장 가능한 구조를 만드는 것이 중요한 기준이었습니다.
그 결과, 우리는 하나의 Category 모델 + Polymorphic 기반 CategoryRelation 테이블이라는 구조를 선택했습니다. 이 방식은 다음과 같은 특징과 장점을 가집니다:
1. 개발 생산성 극대화
- 모델마다 중간 테이블을 추가하거나 새로운 Category 모델을 만들 필요가 없습니다.
- 새로운 서비스나 기능에서도 동일한 구조를 그대로 활용할 수 있어 개발 속도가 빨라집니다.
2. 높은 확장성과 일관성
- 어떤 모델이든 Category를 연결할 수 있는 공통 인터페이스가 생기며, API, Repository, 검증 로직을 재사용할 수 있습니다.
- Category 자체도 문자열 기반 key + service 구조로 유연하게 확장 가능합니다.
3. Polymorphic 구조의 장점 활용
- 하나의 관계 테이블로 여러 모델의 카테고리 매핑을 처리하여 구조를 단순화합니다.
- Ruby on Rails에서 널리 쓰이는 검증된 패턴으로, JPA 환경에서도 서비스 레벨 처리만 보완하면 문제없이 운영할 수 있습니다.
4. 다소의 단점은 있지만 충분히 상쇄 가능
- JPA ManyToMany를 그대로 사용할 수 없는 점은 서비스 계층에서 매핑 처리로 보완합니다.
- targetType의 타입 안정성 문제는 enum/상수로 일관성을 유지해 해결할 수 있습니다.
- 조회 성능은 인덱스 설계 및 적절한 Repository 메서드 구성으로 관리 가능합니다.