Skip to content

Commit e42c263

Browse files
authored
Merge pull request #190 from DevKor-github/develop
main <- develop
2 parents 0915891 + d1cef96 commit e42c263

File tree

13 files changed

+422
-36
lines changed

13 files changed

+422
-36
lines changed

build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ dependencies {
9999
// actuator
100100
implementation 'org.springframework.boot:spring-boot-starter-actuator'
101101

102-
// prometheus
103-
implementation 'io.micrometer:micrometer-registry-prometheus'
102+
// cloudwatch
103+
implementation 'io.micrometer:micrometer-registry-cloudwatch2'
104+
implementation 'software.amazon.awssdk:cloudwatch'
104105
}
105106
dependencyManagement {
106107
imports {

src/main/java/devkor/com/teamcback/domain/bookmark/entity/Bookmark.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public Bookmark(CreateBookmarkReq req) {
3535
this.memo = req.getMemo();
3636
this.locationType = req.getLocationType();
3737
this.locationId = req.getLocationId();
38-
3938
}
4039

4140
public void update(String memo) {

src/main/java/devkor/com/teamcback/domain/bookmark/repository/BookmarkRepository.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44
import devkor.com.teamcback.domain.bookmark.entity.Category;
55
import devkor.com.teamcback.domain.common.LocationType;
66
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
79

810
import java.util.List;
9-
import java.util.Optional;
1011

1112
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
12-
List<Bookmark> findAllByCategoryBookmarkList_CategoryId(Long categoryId);
13+
@Query("select cb.bookmark from CategoryBookmark cb where cb.category.id = :categoryId")
14+
List<Bookmark> findAllByCategoryBookmarkList_CategoryId(@Param("categoryId") Long categoryId);
1315

14-
Bookmark findByLocationIdAndLocationTypeAndCategoryBookmarkList_Category(Long locationId, LocationType locationType, Category category);
16+
@Query("select b from Bookmark b join b.categoryBookmarkList cb where b.locationId = :locationId and b.locationType = :locationType and cb.category = :category")
17+
Bookmark findByLocationIdAndLocationTypeAndCategoryBookmarkList_Category(@Param("locationId")Long locationId, @Param("locationType")LocationType locationType, @Param("category")Category category);
1518

16-
Bookmark findByLocationIdAndLocationTypeAndCategoryBookmarkList_CategoryIn(Long locationId, LocationType locationType,
17-
List<Category> userCategoryList);
19+
@Query("select b from Bookmark b join b.categoryBookmarkList cb where b.locationId = :locationId and b.locationType = :locationType and cb.category IN :categories")
20+
Bookmark findByLocationIdAndLocationTypeAndCategoryBookmarkList_CategoryIn(@Param("locationId")Long locationId, @Param("locationType")LocationType locationType,
21+
@Param("categories")List<Category> userCategoryList);
1822

19-
boolean existsByLocationIdAndLocationTypeAndCategoryBookmarkList_CategoryIn(Long locationId, LocationType locationType,
20-
List<Category> categories);
23+
@Query("select case when count(b) > 0 then true else false end from Bookmark b join b.categoryBookmarkList cb where b.locationId = :locationId and b.locationType = :locationType and cb.category IN :categories")
24+
boolean existsByLocationIdAndLocationTypeAndCategoryBookmarkList_CategoryIn(@Param("locationId")Long locationId, @Param("locationType")LocationType locationType,
25+
@Param("categories") List<Category> categories);
2126
}

src/main/java/devkor/com/teamcback/domain/bookmark/repository/CategoryBookmarkRepository.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
import devkor.com.teamcback.domain.bookmark.entity.Category;
55
import devkor.com.teamcback.domain.bookmark.entity.CategoryBookmark;
66
import devkor.com.teamcback.domain.common.LocationType;
7-
import devkor.com.teamcback.domain.user.entity.User;
8-
import java.util.List;
97
import java.util.Optional;
108
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
1111

1212
public interface CategoryBookmarkRepository extends JpaRepository<CategoryBookmark, Long> {
1313
boolean existsByCategoryAndBookmark(Category category, Bookmark bookmark);
1414

1515
Optional<CategoryBookmark> findByCategoryAndBookmark(Category category, Bookmark bookmark);
1616

17-
CategoryBookmark findByCategoryAndBookmarkLocationIdAndBookmarkLocationType(Category category, Long locationId, LocationType type);
1817
}

src/main/java/devkor/com/teamcback/domain/bookmark/repository/CategoryRepository.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {
1313
List<Category> findAllByUser(User user);
1414
Long countAllByUser(User user);
1515

16-
@Query("SELECT c FROM CategoryBookmark cb " +
17-
"JOIN cb.category c " +
18-
"JOIN cb.bookmark b " +
19-
"WHERE c.user = :user AND b.locationType = :locationType AND b.locationId = :locationId")
16+
@Query("""
17+
SELECT c FROM CategoryBookmark cb
18+
JOIN cb.category c
19+
JOIN cb.bookmark b
20+
WHERE c.user = :user AND b.locationType = :locationType AND b.locationId = :locationId
21+
""")
2022
List<Category> findCategoriesByUserAndLocationTypeAndLocationId(
2123
@Param("user") User user,
2224
@Param("locationType") LocationType locationType,

src/main/java/devkor/com/teamcback/domain/bookmark/service/BookmarkService.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,6 @@ private void checkAuthority(User user, Bookmark bookmark) {
154154
}
155155
}
156156

157-
private void checkPlaceDuplication(Category category, LocationType locationType, Long locationId) {
158-
CategoryBookmark categoryBookmark = categoryBookmarkRepository.findByCategoryAndBookmarkLocationIdAndBookmarkLocationType(category, locationId, locationType);
159-
160-
// 같은 카테고리에 동일 북마크가 존재하는 경우
161-
if (categoryBookmark != null) {
162-
throw new GlobalException(DUPLICATED_BOOKMARK);
163-
}
164-
}
165-
166157
private void checkPlaceExists(LocationType locationType, Long locationId) {
167158
if (LocationType.BUILDING.equals(locationType) && !buildingRepository.existsById(locationId)) {
168159
throw new GlobalException(NOT_FOUND_BUILDING);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package devkor.com.teamcback.global.config;
2+
3+
import devkor.com.teamcback.infra.cloudwatch.MetricsInterceptor;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
7+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
8+
9+
@Configuration
10+
public class WebConfig implements WebMvcConfigurer {
11+
@Autowired
12+
private MetricsInterceptor metricsInterceptor;
13+
14+
@Override
15+
public void addInterceptors(InterceptorRegistry registry) {
16+
registry.addInterceptor(metricsInterceptor);
17+
}
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package devkor.com.teamcback.infra.cloudwatch;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import software.amazon.awssdk.regions.Region;
6+
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
7+
8+
@Configuration
9+
public class CloudWatchConfig {
10+
11+
@Bean
12+
public CloudWatchAsyncClient cloudWatchAsyncClient() {
13+
return CloudWatchAsyncClient
14+
.builder()
15+
.region(Region.AP_NORTHEAST_2)
16+
.build();
17+
}
18+
}
19+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package devkor.com.teamcback.infra.cloudwatch;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.stereotype.Component;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.springframework.web.method.HandlerMethod;
8+
import org.springframework.web.servlet.HandlerInterceptor;
9+
import org.springframework.web.servlet.HandlerMapping;
10+
11+
import java.util.List;
12+
13+
@Component
14+
@RequiredArgsConstructor
15+
public class MetricsInterceptor implements HandlerInterceptor {
16+
17+
private final MetricsService metricsService;
18+
19+
@Override
20+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
21+
if(handler instanceof HandlerMethod) {
22+
String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
23+
if(shouldTrack(pattern)) {
24+
metricsService.recordApiRequest(pattern);
25+
}
26+
return true;
27+
}
28+
return true;
29+
}
30+
31+
private boolean shouldTrack(String uri) {
32+
return List.of(
33+
"/api/bookmarks",
34+
"/api/categories",
35+
"/api/routes",
36+
"/api/users/login",
37+
"/api/users/login/release",
38+
"/api/users/mypage",
39+
"/api/search",
40+
"/api/search/buildings",
41+
"/api/search/buildings/{buildingId}",
42+
"/api/search/buildings/{buildingId}/facilities",
43+
"/api/search/facilities",
44+
"/api/search/place/{placeId}"
45+
).contains(uri);
46+
}
47+
}
48+
49+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package devkor.com.teamcback.infra.cloudwatch;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.scheduling.annotation.Scheduled;
6+
import org.springframework.stereotype.Service;
7+
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
8+
import software.amazon.awssdk.services.cloudwatch.model.*;
9+
import software.amazon.awssdk.services.cloudwatch.model.Dimension;
10+
11+
import java.time.Instant;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Objects;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
18+
@Service
19+
@RequiredArgsConstructor
20+
public class MetricsService {
21+
@Value("${metrics.environment}")
22+
private String environment;
23+
24+
private final String TARGET = "prod";
25+
26+
private final CloudWatchAsyncClient cloudWatchAsyncClient;
27+
private final Map<String, AtomicInteger> uriCountMap = new ConcurrentHashMap<>();
28+
29+
public void recordApiRequest(String uri) {
30+
if(TARGET.equalsIgnoreCase(environment)) {
31+
uriCountMap.computeIfAbsent(uri, k -> new AtomicInteger(0)).incrementAndGet();
32+
}
33+
}
34+
35+
@Scheduled(fixedRate = 60_000)
36+
public void sendMetricsToCloudWatch() {
37+
if (TARGET.equalsIgnoreCase(environment)) {
38+
if(uriCountMap.isEmpty()) return;
39+
List<MetricDatum> metricDataList = uriCountMap.entrySet().stream()
40+
.map(entry -> {
41+
String uri = entry.getKey();
42+
int count = entry.getValue().getAndSet(0);
43+
44+
if (count > 0) {
45+
return MetricDatum.builder()
46+
.metricName("ApiRequestCount")
47+
.dimensions(
48+
Dimension.builder().name("URI").value(uri).build()
49+
)
50+
.unit(StandardUnit.COUNT)
51+
.value((double) count)
52+
.timestamp(Instant.now())
53+
.build();
54+
}
55+
return null;
56+
})
57+
.filter(Objects::nonNull)
58+
.toList();
59+
60+
if (!metricDataList.isEmpty()) {
61+
PutMetricDataRequest request = PutMetricDataRequest.builder()
62+
.namespace("Kodaero/Metrics")
63+
.metricData(metricDataList)
64+
.build();
65+
66+
cloudWatchAsyncClient.putMetricData(request).whenComplete((resp, err) -> {
67+
if (err != null) {
68+
System.err.println("Failed to send metric: " + err.getMessage());
69+
}
70+
});
71+
}
72+
}
73+
}
74+
}

src/main/resources/application.yml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,6 @@ spring:
3030
port: 6379
3131
password: ${REDIS_PASSWORD}
3232

33-
management:
34-
endpoints:
35-
web:
36-
exposure:
37-
include: prometheus, health, info, metrics
38-
prometheus:
39-
metrics:
40-
export:
41-
enabled: true
4233

4334
logging:
4435
level:
@@ -111,4 +102,7 @@ date:
111102
holiday:
112103
end-point: ${HOLIDAY_API_END_POINT}
113104
encoded-key: ${HOLIDAY_API_ENCODED_KEY}
114-
decoded-key: ${HOLIDAY_API_DECODED_KEY}
105+
decoded-key: ${HOLIDAY_API_DECODED_KEY}
106+
107+
metrics:
108+
environment: dev
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package devkor.com.teamcback;
2+
3+
4+
public class TestTimer {
5+
6+
public static void run(String label, Runnable testLogic) {
7+
long start = System.currentTimeMillis();
8+
testLogic.run();
9+
long duration = System.currentTimeMillis() - start;
10+
System.out.printf("⏱️ [%s] 실행 시간: %dms%n", label, duration);
11+
}
12+
13+
}

0 commit comments

Comments
 (0)