-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: 부하 테스트 기반 성능 개선 #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4777f9f
17a08c8
e78f6c8
02fe3df
91cf950
200c37b
046d886
c4255b8
2c2187a
2cedecb
f8745f4
9b26106
60a361a
8c646a0
2b56748
80f53ca
07de346
8abbe95
e2eaebc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| -- PostGIS geography 컬럼 추가 + GIST 인덱스 마이그레이션 | ||
| -- 실행: docker exec -i catchtable-db psql -U $DB_USER -d $DB_NAME < data/migration_v2_postgis_geography.sql | ||
| -- 로컬에서 이미 실행했다면 운영 서버에서만 실행할 것 | ||
|
|
||
| -- 1. PostGIS 확장 활성화 | ||
| CREATE EXTENSION IF NOT EXISTS postgis; | ||
|
|
||
| -- 2. stores 테이블에 geography 컬럼 추가 | ||
| -- geometry 대신 geography 사용: ::geography 캐스팅 없이 GIST 인덱스 직접 활용 가능 | ||
| ALTER TABLE stores | ||
| ADD COLUMN IF NOT EXISTS location geography(Point, 4326); | ||
|
|
||
| -- 3. 기존 latitude/longitude 데이터를 geography 컬럼으로 복사 | ||
| UPDATE stores | ||
| SET location = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography | ||
| WHERE location IS NULL | ||
| AND latitude IS NOT NULL | ||
| AND longitude IS NOT NULL; | ||
|
|
||
| -- 4. GIST 공간 인덱스 생성 | ||
| CREATE INDEX IF NOT EXISTS idx_stores_location_gist | ||
| ON stores USING GIST (location); | ||
|
|
||
| -- 5. nearby 바운딩박스 선필터용 복합 B-tree 인덱스 | ||
| CREATE INDEX IF NOT EXISTS idx_stores_lat_lng | ||
| ON stores (latitude, longitude) | ||
| WHERE is_deleted = false; | ||
|
|
||
| -- 검증 | ||
| -- SELECT pg_typeof(location) FROM stores LIMIT 1; -- geography 이어야 정상 | ||
| -- SELECT COUNT(*) FROM stores WHERE location IS NULL; -- 0 이어야 정상 | ||
| -- SELECT indexname FROM pg_indexes WHERE tablename = 'stores' AND indexname LIKE 'idx_stores%'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||||||||||||
| package com.catchtable.global.config; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import org.springframework.cache.annotation.EnableCaching; | ||||||||||||||||||||||
| import org.springframework.context.annotation.Bean; | ||||||||||||||||||||||
| import org.springframework.context.annotation.Configuration; | ||||||||||||||||||||||
| import org.springframework.data.redis.cache.RedisCacheConfiguration; | ||||||||||||||||||||||
| import org.springframework.data.redis.cache.RedisCacheManager; | ||||||||||||||||||||||
| import org.springframework.data.redis.connection.RedisConnectionFactory; | ||||||||||||||||||||||
| import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; | ||||||||||||||||||||||
| import org.springframework.data.redis.serializer.RedisSerializationContext; | ||||||||||||||||||||||
| import org.springframework.data.redis.serializer.StringRedisSerializer; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.time.Duration; | ||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @EnableCaching | ||||||||||||||||||||||
| @Configuration | ||||||||||||||||||||||
| public class CacheConfig { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Bean | ||||||||||||||||||||||
| public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { | ||||||||||||||||||||||
| RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig() | ||||||||||||||||||||||
| .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) | ||||||||||||||||||||||
| .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) | ||||||||||||||||||||||
| .disableCachingNullValues(); | ||||||||||||||||||||||
|
Comment on lines
+21
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Spring Cache 환경에서는 클래스 타입 정보를 함께 저장해 주는
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Map<String, RedisCacheConfiguration> cacheConfigs = Map.of( | ||||||||||||||||||||||
| "popularStores", defaults.entryTtl(Duration.ofMinutes(5)) | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return RedisCacheManager.builder(connectionFactory) | ||||||||||||||||||||||
| .cacheDefaults(defaults.entryTtl(Duration.ofMinutes(10))) | ||||||||||||||||||||||
| .withInitialCacheConfigurations(cacheConfigs) | ||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,13 +4,21 @@ | |||||||||||||||||
| import com.catchtable.global.exception.ErrorCode; | ||||||||||||||||||
| import jakarta.persistence.*; | ||||||||||||||||||
| import lombok.*; | ||||||||||||||||||
| import org.locationtech.jts.geom.Point; | ||||||||||||||||||
| import org.hibernate.annotations.CreationTimestamp; | ||||||||||||||||||
| import org.hibernate.annotations.UpdateTimestamp; | ||||||||||||||||||
|
|
||||||||||||||||||
| import java.time.LocalDateTime; | ||||||||||||||||||
|
|
||||||||||||||||||
| @Entity | ||||||||||||||||||
| @Table(name = "stores") | ||||||||||||||||||
| @Table( | ||||||||||||||||||
| name = "stores", | ||||||||||||||||||
| indexes = { | ||||||||||||||||||
| @Index(name = "idx_store_lat_lng", columnList = "latitude, longitude"), | ||||||||||||||||||
| @Index(name = "idx_store_deleted_category", columnList = "is_deleted, category"), | ||||||||||||||||||
| @Index(name = "idx_store_deleted_district", columnList = "is_deleted, district") | ||||||||||||||||||
| } | ||||||||||||||||||
| ) | ||||||||||||||||||
| @Getter | ||||||||||||||||||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||||||||||||||||||
| @AllArgsConstructor | ||||||||||||||||||
|
|
@@ -37,6 +45,11 @@ public class Store { | |||||||||||||||||
| @Column(nullable = false) | ||||||||||||||||||
| private Double longitude; | ||||||||||||||||||
|
|
||||||||||||||||||
| // PostGIS geometry 컬럼 — ST_DWithin/KNN 공간 인덱스 활용 | ||||||||||||||||||
| // migration_v2_postgis_geometry.sql 실행 후 GIST 인덱스 적용됨 | ||||||||||||||||||
| @Column(columnDefinition = "geography(Point, 4326)", insertable = false, updatable = false) | ||||||||||||||||||
| private Point location; | ||||||||||||||||||
|
Comment on lines
+48
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 데이터베이스 마이그레이션 스크립트에서
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| @Column(nullable = false) | ||||||||||||||||||
| private String address; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
벌크 연산(
@ModifyingUPDATE)은 영속성 컨텍스트(1차 캐시)를 거치지 않고 데이터베이스에 직접 쿼리를 실행합니다. 이로 인해 영속성 컨텍스트에 이미 로드되어 있는 엔티티들의 상태와 실제 DB 상태 간의 불일치(Stale Data)가 발생할 수 있습니다.안전한 처리를 위해
@Modifying(clearAutomatically = true)옵션을 추가하여 벌크 연산 실행 후 영속성 컨텍스트를 자동으로 비워주도록 설정하는 것을 권장합니다.