Skip to content

Commit f1bf3a3

Browse files
committed
refactor: QueryDSL 적용 + 추가 개선
1 parent b559217 commit f1bf3a3

File tree

13 files changed

+226
-47
lines changed

13 files changed

+226
-47
lines changed

build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ dependencies {
5151

5252
// Swagger
5353
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1")
54+
55+
// QueryDSL
56+
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
57+
annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
58+
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
59+
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
5460
}
5561

5662
tasks.withType<Test> {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.example.log4u.common.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
import com.querydsl.jpa.impl.JPAQueryFactory;
7+
8+
import jakarta.persistence.EntityManager;
9+
10+
@Configuration
11+
public class QueryDslConfig {
12+
13+
private final EntityManager entityManager;
14+
15+
public QueryDslConfig(EntityManager entityManager) {
16+
this.entityManager = entityManager;
17+
}
18+
19+
@Bean
20+
public JPAQueryFactory jpaQueryFactory() {
21+
return new JPAQueryFactory(entityManager);
22+
}
23+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example.log4u.domain.diary;
2+
3+
public enum SortType {
4+
LATEST,
5+
POPULAR
6+
}

src/main/java/com/example/log4u/domain/diary/controller/DiaryController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.springframework.web.bind.annotation.RestController;
1414

1515
import com.example.log4u.common.dto.PageResponse;
16+
import com.example.log4u.domain.diary.SortType;
1617
import com.example.log4u.domain.diary.dto.DiaryRequestDto;
1718
import com.example.log4u.domain.diary.dto.DiaryResponseDto;
1819
import com.example.log4u.domain.diary.service.DiaryService;
@@ -42,7 +43,7 @@ public ResponseEntity<Void> createDiary(
4243
@GetMapping
4344
public ResponseEntity<PageResponse<DiaryResponseDto>> searchDiaries(
4445
@RequestParam(required = false) String keyword,
45-
@RequestParam(defaultValue = "LATEST") String sort,
46+
@RequestParam(defaultValue = "LATEST") SortType sort,
4647
@RequestParam(defaultValue = "0") int page
4748
) {
4849
return ResponseEntity.ok(

src/main/java/com/example/log4u/domain/diary/dto/DiaryRequestDto.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.List;
44

55
import com.example.log4u.domain.diary.VisibilityType;
6+
import com.example.log4u.domain.diary.WeatherInfo;
67
import com.example.log4u.domain.diary.entity.Diary;
78
import com.example.log4u.domain.media.dto.MediaRequestDto;
89

@@ -15,9 +16,9 @@ public record DiaryRequestDto(
1516
String content,
1617
Double latitude,
1718
Double longitude,
18-
String weatherInfo,
19+
WeatherInfo weatherInfo,
1920
@NotBlank(message = "공개 범위는 필수입니다.")
20-
String visibility,
21+
VisibilityType visibility,
2122
List<MediaRequestDto> mediaList
2223
) {
2324
public static Diary toEntity(Long userId, DiaryRequestDto diaryRequestDto, String thumbnailUrl) {
@@ -28,7 +29,7 @@ public static Diary toEntity(Long userId, DiaryRequestDto diaryRequestDto, Strin
2829
.latitude(diaryRequestDto.latitude)
2930
.longitude(diaryRequestDto.longitude)
3031
.weatherInfo(diaryRequestDto.weatherInfo)
31-
.visibility(VisibilityType.valueOf(diaryRequestDto.visibility))
32+
.visibility(diaryRequestDto.visibility)
3233
.thumbnailUrl(thumbnailUrl)
3334
.build();
3435
}

src/main/java/com/example/log4u/domain/diary/dto/DiaryResponseDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static DiaryResponseDto of(Diary diary, List<Media> media) {
3434
.longitude(diary.getLongitude())
3535
.title(diary.getTitle())
3636
.content(diary.getContent())
37-
.weatherInfo(diary.getWeatherInfo())
37+
.weatherInfo(diary.getWeatherInfo().name())
3838
.visibility(diary.getVisibility().name())
3939
.createdAt(diary.getCreatedAt())
4040
.updatedAt(diary.getUpdatedAt())

src/main/java/com/example/log4u/domain/diary/entity/Diary.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.example.log4u.common.entity.BaseEntity;
44
import com.example.log4u.domain.diary.VisibilityType;
5+
import com.example.log4u.domain.diary.WeatherInfo;
56
import com.example.log4u.domain.diary.dto.DiaryRequestDto;
67

78
import jakarta.persistence.Column;
@@ -43,7 +44,8 @@ public class Diary extends BaseEntity {
4344

4445
private Double longitude;
4546

46-
private String weatherInfo;
47+
@Enumerated(EnumType.STRING)
48+
private WeatherInfo weatherInfo;
4749

4850
@Enumerated(EnumType.STRING)
4951
@Column(nullable = false)
@@ -59,7 +61,7 @@ public void update(DiaryRequestDto request, String newThumbnailUrl) {
5961
this.latitude = request.latitude();
6062
this.longitude = request.longitude();
6163
this.weatherInfo = request.weatherInfo();
62-
this.visibility = VisibilityType.valueOf(request.visibility());
64+
this.visibility = request.visibility();
6365
this.thumbnailUrl = newThumbnailUrl;
6466
}
6567

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.example.log4u.domain.diary.repository;
2+
3+
import java.util.List;
4+
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.domain.Slice;
8+
9+
import com.example.log4u.domain.diary.SortType;
10+
import com.example.log4u.domain.diary.VisibilityType;
11+
import com.example.log4u.domain.diary.entity.Diary;
12+
13+
public interface CustomDiaryRepository {
14+
Page<Diary> searchDiaries(
15+
String keyword,
16+
List<VisibilityType> visibilities,
17+
SortType sort,
18+
Pageable pageable
19+
);
20+
21+
Slice<Diary> findByUserIdAndVisibilityInAndCursorId(
22+
Long userId,
23+
List<VisibilityType> visibilities,
24+
Long cursorId,
25+
Pageable pageable
26+
);
27+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package com.example.log4u.domain.diary.repository;
2+
3+
import java.util.List;
4+
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.PageImpl;
7+
import org.springframework.data.domain.Pageable;
8+
import org.springframework.data.domain.Slice;
9+
import org.springframework.data.domain.SliceImpl;
10+
import org.springframework.util.StringUtils;
11+
12+
import com.example.log4u.domain.diary.SortType;
13+
import com.example.log4u.domain.diary.VisibilityType;
14+
import com.example.log4u.domain.diary.entity.Diary;
15+
import com.example.log4u.domain.diary.entity.QDiary;
16+
import com.querydsl.core.types.OrderSpecifier;
17+
import com.querydsl.core.types.dsl.BooleanExpression;
18+
import com.querydsl.jpa.impl.JPAQuery;
19+
import com.querydsl.jpa.impl.JPAQueryFactory;
20+
21+
import lombok.RequiredArgsConstructor;
22+
23+
@RequiredArgsConstructor
24+
public class CustomDiaryRepositoryImpl implements CustomDiaryRepository {
25+
private final JPAQueryFactory queryFactory;
26+
27+
@Override
28+
public Page<Diary> searchDiaries(
29+
String keyword,
30+
List<VisibilityType> visibilities,
31+
SortType sort,
32+
Pageable pageable
33+
) {
34+
QDiary diary = QDiary.diary;
35+
36+
// 조건 생성
37+
BooleanExpression condition = createCondition(diary, keyword, visibilities, null);
38+
39+
// 쿼리 실행
40+
JPAQuery<Diary> query = queryFactory
41+
.selectFrom(diary)
42+
.where(condition);
43+
44+
// 전체 카운트 조회
45+
long total = query.fetchCount();
46+
47+
// 데이터 조회
48+
List<Diary> content = query
49+
.orderBy(createOrderSpecifier(diary, sort))
50+
.offset(pageable.getOffset())
51+
.limit(pageable.getPageSize())
52+
.fetch();
53+
54+
return new PageImpl<>(content, pageable, total);
55+
}
56+
57+
@Override
58+
public Slice<Diary> findByUserIdAndVisibilityInAndCursorId(
59+
Long userId,
60+
List<VisibilityType> visibilities,
61+
Long cursorId,
62+
Pageable pageable
63+
) {
64+
QDiary diary = QDiary.diary;
65+
66+
// 조건 생성
67+
BooleanExpression condition = createCondition(diary, null, visibilities, userId);
68+
69+
// limit + 1로 다음 페이지 존재 여부 확인
70+
List<Diary> content = queryFactory
71+
.selectFrom(diary)
72+
.where(condition)
73+
.orderBy(diary.diaryId.desc())
74+
.limit(pageable.getPageSize() + 1)
75+
.fetch();
76+
77+
return checkAndCreateSlice(content, pageable);
78+
}
79+
80+
// 하나의 메소드로 조건 생성
81+
private BooleanExpression createCondition(
82+
QDiary diary,
83+
String keyword,
84+
List<VisibilityType> visibilities,
85+
Long userId
86+
) {
87+
BooleanExpression condition = diary.visibility.in(visibilities);
88+
89+
if (keyword != null && StringUtils.hasText(keyword)) {
90+
condition = condition.and(diary.title.containsIgnoreCase(keyword)
91+
.or(diary.content.containsIgnoreCase(keyword)));
92+
}
93+
94+
if (userId != null) {
95+
condition = condition.and(diary.userId.eq(userId));
96+
}
97+
98+
return condition;
99+
}
100+
101+
// 정렬 조건 생성
102+
private OrderSpecifier<?> createOrderSpecifier(QDiary diary, SortType sort) {
103+
if (sort == null) {
104+
return diary.createdAt.desc();
105+
}
106+
107+
return switch (sort) {
108+
case POPULAR -> diary.likeCount.desc();
109+
case LATEST -> diary.createdAt.desc();
110+
};
111+
}
112+
113+
// Slice 생성 및 hasNext 처리
114+
private Slice<Diary> checkAndCreateSlice(List<Diary> content, Pageable pageable) {
115+
boolean hasNext = content.size() > pageable.getPageSize();
116+
117+
// 다음 페이지가 있으면 마지막 항목 제거
118+
if (hasNext) {
119+
content.removeLast();
120+
}
121+
122+
return new SliceImpl<>(content, pageable, hasNext);
123+
}
124+
}
Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,10 @@
11
package com.example.log4u.domain.diary.repository;
22

3-
import java.util.List;
4-
5-
import org.springframework.data.domain.Page;
6-
import org.springframework.data.domain.Pageable;
7-
import org.springframework.data.domain.Slice;
83
import org.springframework.data.jpa.repository.JpaRepository;
9-
import org.springframework.data.jpa.repository.Query;
10-
import org.springframework.data.repository.query.Param;
114
import org.springframework.stereotype.Repository;
125

13-
import com.example.log4u.domain.diary.VisibilityType;
146
import com.example.log4u.domain.diary.entity.Diary;
157

168
@Repository
17-
public interface DiaryRepository extends JpaRepository<Diary, Long> {
18-
@Query("""
19-
SELECT d FROM Diary d
20-
WHERE d.visibility IN :visibilities
21-
AND (:keyword IS NULL OR
22-
LOWER(d.content) LIKE LOWER(CONCAT('%', :keyword, '%')) OR
23-
LOWER(d.title) LIKE LOWER(CONCAT('%', :keyword, '%')))
24-
ORDER BY
25-
CASE WHEN :sort = 'POPULAR' THEN d.likeCount
26-
ELSE d.createdAt END DESC
27-
""")
28-
Page<Diary> searchDiaries(
29-
@Param("keyword") String keyword,
30-
@Param("visibilities") List<VisibilityType> visibilities,
31-
@Param("sort") String sort,
32-
Pageable pageable
33-
);
34-
35-
@Query("""
36-
SELECT d FROM Diary d
37-
WHERE d.userId = :userId
38-
AND d.visibility IN :visibilities
39-
AND (:cursorId IS NULL OR d.diaryId < :cursorId)
40-
ORDER BY d.diaryId DESC
41-
""")
42-
Slice<Diary> findByUserIdAndVisibilityInAndCursorId(
43-
@Param("userId") Long userId,
44-
@Param("visibilities") List<VisibilityType> visibilities,
45-
@Param("cursorId") Long cursorId,
46-
Pageable pageable
47-
);
9+
public interface DiaryRepository extends JpaRepository<Diary, Long>, CustomDiaryRepository {
4810
}

0 commit comments

Comments
 (0)