1. 문제 발생
(1) 서비스 이해
- 프로젝트에서 Tag를 2가지 형태로 나누어서 표현하였습니다.
- 게임 RootTag에는 FPS, RPG, TPS 등의 여러 Tag들이 존재합니다.
- 서비스에서 생성되는 폴더에 여러 Tag들을 붙일 수 있습니다.
(2) 기존 구현 방식
- 데이터베이스 Table을 사용해서 Tag와 RootTag 들의 값들을 관리하였습니다.
Tag
RootTag
(3) Tag 조회 성능 문제
- 프론트에게 태그 리스트를 보내 주기 위해 항상 테이블 조회 쿼리가 나가야만 했습니다.
- 서비스 로직 상 해당 Tag가 들어왔을 때 Tag가 어떤 RootTag에 속하는 지 찾기 위해서는 DB에 조회 쿼리를 날려야만 했습니다.
- 태그 이름으로 검색 Tag, RootTag 테이블의 조인이 필요하였습니다.
@Query(value = "SELECT DISTINCT f FROM Folder f " +
"JOIN FETCH f.user JOIN FETCH f.folderTags ft " +
"WHERE f.isPrivate = false AND f.id IN " +
"(SELECT subf.id FROM Folder subf JOIN subf.folderTags subft JOIN subft.tag subt JOIN subt.rootTag subrt " +
"ON subrt.name = :rootTag)",
countQuery = "SELECT count(f) FROM Folder f")
Page<Folder> findByRootTag(RootTagCategory rootTag, Pageable pageable);
@Query(value = "SELECT DISTINCT f FROM Folder f " +
"JOIN FETCH f.user JOIN FETCH f.folderTags ft " +
"WHERE f.isPrivate = false AND f.id IN " +
"(SELECT subf.id FROM Folder subf JOIN subf.folderTags subft JOIN subft.tag subt " +
"ON subt.name = :tag)",
countQuery = "SELECT count(f) FROM Folder f")
Page<Folder> findByTag(TagCategory tag, Pageable pageable);
2. 문제 해결
- Tag 데이터를 Enum으로 전환하였습니다.
- RootTag, Tag의 관계를 명확히 표현하며 각 타입은 본인이 수행해야할 기능과 책임만 가질 수 있게 만들 수 있습니다.
- DB 조회 쿼리를 날려서 해당 값이 존재하는지 검증하는 것이 아니라 Enum 클래스에서 직접 필터링을 거친다면 어플리케이션의 성능이 향상 될 것이라고 생각하였습니다.
RootTagCategory
@Getter
@AllArgsConstructor
public enum RootTagCategory {
ALL("전체 카테고리", new TagCategory[0]),
DAILY("일상", new TagCategory[]{
TagCategory.HOBBY,
TagCategory.LIFEHACK
}),
ANIMAL("동물", new TagCategory[]{
TagCategory.DOG,
TagCategory.CAT,
TagCategory.REPTILE,
TagCategory.INSECT
}),
GAME("게임", new TagCategory[]{
TagCategory.FPS,
TagCategory.RPG,
TagCategory.TPS,
TagCategory.SPORTS_GAME,
TagCategory.RACING_GAME,
TagCategory.SIMULATION_GAME,
TagCategory.RHYTHM_GAME
}),
MOVIE("영화", new TagCategory[]{
TagCategory.ROMANCE_MOVIE,
TagCategory.ACTION_MOVIE,
TagCategory.CRIME_MOVIE,
TagCategory.SF_MOVIE,
TagCategory.COMEDY_MOVIE,
TagCategory.THRILLER_MOVIE,
TagCategory.HORROR_MOVIE,
TagCategory.SPORTS_MOVIE,
TagCategory.FANTASY_MOVIE
}),
MUSIC("음악", new TagCategory[]{
TagCategory.POP,
TagCategory.JPOP,
TagCategory.BALLADE,
TagCategory.RAP,
TagCategory.BAND,
TagCategory.ACOUSTIC,
TagCategory.JAZZ
}),
HUMOR("유머", new TagCategory[]{
TagCategory.BLACK_COMEDY,
TagCategory.DAD_JOKE,
TagCategory.SATIRE
}),
HEALTH("헬스", new TagCategory[]{
TagCategory.WORKOUT,
TagCategory.PILATES,
TagCategory.POWER_LIFTING,
TagCategory.CROSSFIT
}),
BEAUTY("뷰티", new TagCategory[]{
TagCategory.MAKEUP,
TagCategory.CLOTHES
}),
SPORTS("스포츠", new TagCategory[]{
TagCategory.SOCCER,
TagCategory.BASKETBALL,
TagCategory.BASEBALL,
TagCategory.BILLIARDS,
TagCategory.BOWLING,
TagCategory.VOLLEYBALL
}),
DEVELOP("개발", new TagCategory[]{
TagCategory.FRONTEND,
TagCategory.BACKEND,
TagCategory.INFRA,
TagCategory.DEVOPS,
TagCategory.DATA_VISUALIZATION,
TagCategory.AI
}),
TRAVEL("여행", new TagCategory[]{
TagCategory.DOMESTIC_TRAVEL,
TagCategory.OVERSEAS_TRAVEL,
TagCategory.BACKPACKING
}),
FOOD("음식", new TagCategory[]{
TagCategory.KOREAN_FOOD,
TagCategory.JAPANESE_FOOD,
TagCategory.CHINESE_FOOD,
TagCategory.VIETNAMESE_FOOD,
TagCategory.WESTERN_FOOD,
TagCategory.DESSERT
});
private String viewName;
private TagCategory[] tags;
public static RootTagCategory findByTag(TagCategory tagCategory) {
return Arrays.stream(RootTagCategory.values())
.filter(rootTagCategory -> rootTagCategory.hasRootTag(tagCategory))
.findAny()
.orElseThrow(() -> new NoDataException());
}
public boolean hasRootTag(TagCategory target) {
return Arrays.stream(tags)
.anyMatch(tagCategory -> tagCategory.equals(target));
}
@JsonCreator
public static RootTagCategory from(String root) {
for (RootTagCategory tag : RootTagCategory.values()) {
if (tag.getViewName().equals(root)) {
return tag;
}
}
return null;
}
}
- RootTagCategory는 TagCategory 배열을 필드로 가지고 있습니다.
- 스포츠 RootTagCategory는 축구, 농구 TagCategory를 가지고 있습니다.
- findByTag 메서드
- TagCategory를 입력받아 해당 RootTag을 찾을 수 있는 메서드들을 만들어 주었습니다.
TagCategory
@Getter
@AllArgsConstructor
public enum TagCategory {
// 일상
HOBBY("취미"),
LIFEHACK("꿀팁"),
// 동물
DOG("개"),
CAT("고양이"),
REPTILE("파충류"),
INSECT("곤충"),
// 게임
FPS("FPS"),
RPG("RPG"),
TPS("TPS"),
SPORTS_GAME("스포츠 게임"),
RACING_GAME("레이싱 게임"),
SIMULATION_GAME("시뮬레이션 게임"),
RHYTHM_GAME("리듬 게임"),
// 영화
ROMANCE_MOVIE("로맨스"),
ACTION_MOVIE("액션"),
CRIME_MOVIE("범죄"),
SF_MOVIE("SF"),
COMEDY_MOVIE("코미디"),
THRILLER_MOVIE("스릴러"),
HORROR_MOVIE("공포"),
SPORTS_MOVIE("스포츠"),
FANTASY_MOVIE("판타지"),
// 음악
POP("pop"),
JPOP("jpop"),
BALLADE("발라드"),
RAP("랩"),
BAND("밴드"),
ACOUSTIC("어쿠스틱"),
JAZZ("재즈"),
// 유머
BLACK_COMEDY("블랙 코미디"),
DAD_JOKE("아재 개그"),
SATIRE("풍자"),
// 헬스
WORKOUT("헬스"),
PILATES("필라테스"),
POWER_LIFTING("파워 리프팅"),
CROSSFIT("크로스핏"),
// 뷰티
MAKEUP("화장"),
CLOTHES("옷"),
// 스포츠
SOCCER("축구"),
BASKETBALL("농구"),
BASEBALL("야구"),
BILLIARDS("당구"),
BOWLING("볼링"),
VOLLEYBALL("배구"),
// 개발
FRONTEND("프론트엔드"),
BACKEND("백엔드"),
INFRA("인프라"),
DEVOPS("데브옵스"),
DATA_VISUALIZATION("데이터 시각화"),
AI("인공지능"),
// 여행
DOMESTIC_TRAVEL("국내 여행"),
OVERSEAS_TRAVEL("해외 여행"),
BACKPACKING("배낭 여행"),
// 음식
KOREAN_FOOD("한식"),
CHINESE_FOOD("중식"),
JAPANESE_FOOD("일식"),
VIETNAMESE_FOOD("베트남"),
WESTERN_FOOD("양식"),
DESSERT("디저트");
private String viewName;
@JsonCreator
public static TagCategory from(String sub) {
for (TagCategory tag : TagCategory.values()) {
if (tag.getViewName().equals(sub)) {
return tag;
}
}
return null;
}
}
- TagCategory는 존재하는 모든 Tag들을 가지고 있습니다.
- Enum Class를 계층화하여 카테고리를 분리하였습니다.
DB에서 객체로 전환
- Tag와 RootTag의 조회를 이제 DB 조회 쿼리가 아닌 Enum Class에서 조회하게 되었습니다.
- 이는 성능 향상에 도움이 되었습니다.
3. 결론
- RootTag, Tag 테이블을 별도로 두고 이를 조회하여 사용하는 것 보다 가독성 측면에서 향상되었다고 생각합니다.
- DB 조회 쿼리에서 객체 조회로의 전환으로 인해 성능 향상에 도움이 되었습니다.
Reference
반응형
'프로젝트 > LinkBook' 카테고리의 다른 글
RefreshToken 도입 (0) | 2022.08.23 |
---|---|
AccessToken 로그인 로직 (feat. Spring Security) (0) | 2022.08.23 |
토큰(JWT)기반 인증 도입 (0) | 2022.08.23 |
회원가입 이메일 인증 구현 (0) | 2022.08.21 |
Batch Fetch Size를 통해 페이징 문제 해결 (0) | 2022.08.20 |
댓글