Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

// GEO-HASH 의존성 추가
implementation 'ch.hsr:geohash:1.4.0'



//kafka
implementation "org.springframework.kafka:spring-kafka"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package spot.spot.domain.job.query.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -27,4 +28,25 @@ public class NearByJobResponse {
private double dist;
@Schema(description = "카카오페이 결제 번호")
private String tid;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NearByJobResponse that = (NearByJobResponse) o;
return id == that.id &&
Double.compare(that.lat, lat) == 0 &&
Double.compare(that.lng, lng) == 0 &&
money == that.money &&
Double.compare(that.dist, dist) == 0 &&
Objects.equals(title, that.title) &&
Objects.equals(content, that.content) &&
Objects.equals(picture, that.picture);
}

@Override
public int hashCode() {
return Objects.hash(id, title, content, picture, lat, lng, money, dist);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class SearchingListQueryDsl implements SearchingListQueryDocs { // java
private final QWorkerAbility workerAbility = QWorkerAbility.workerAbility;
private final QMatching matching = QMatching.matching;
private final QMember member = QMember.member;
private final JPAQueryFactory jpaQueryFactory;

@Transactional(readOnly = true)
public Slice<NearByJobResponse> findNearByJobsWithQueryDSL(double lat, double lng, double dist, Pageable pageable) {
Expand Down Expand Up @@ -188,4 +189,28 @@ public List<CertificationImgResponse> findWorkersCertificationImgList(long jobId
.where(matching.job.id.eq(jobId).and(matching.status.notIn(MatchingStatus.OWNER, MatchingStatus.ATTENDER, MatchingStatus.REQUEST)))
.fetch();
}


@Transactional(readOnly = true)
public List<NearByJobResponse> findJobsforGeoHashSync() {
// MySQL 인식 용
// QueryDSL 실행 (SPATIAL INDEX 사용)
return jpaQueryFactory
.select(Projections.constructor(
NearByJobResponse.class,
job.id,
job.title,
job.content,
job.img.as("picture"),
job.lat,
job.lng,
job.money,
Expressions.constant(0.0),
job.tid
))
.from(job)
.where(job.startedAt.isNull(), job.location.isNotNull())
.fetch();
// 다음 페이지가 있는지 계산
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package spot.spot.domain.job.query.util.caching;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import spot.spot.global.scheduler.SchedulingTask;
import static spot.spot.global.util.ConstantUtil.SYNC_INTERVAL;

@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncGeoCacheScheduler {

private final ThreadPoolTaskScheduler taskScheduler;
private final JobGeoCacheSyncUtil jobGeoCacheSyncUtil;
private final TransactionTemplate transactionTemplate;

@PostConstruct
public void start() {
SchedulingTask<JobGeoCacheSyncUtil> task = new SchedulingTask<>(
jobGeoCacheSyncUtil,
JobGeoCacheSyncUtil::syncGeoHashCache,
transactionTemplate
);

taskScheduler.scheduleWithFixedDelay(task, SYNC_INTERVAL);
log.info("[GeoHashCache] 비동기 스케줄링 시작 ({} 간격)", SYNC_INTERVAL);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package spot.spot.domain.job.query.util.caching;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class GeoCacheWarmUp {

private final JobGeoCacheSyncUtil jobGeoCacheSyncUtil;

@PostConstruct
public void warmUpGeoCacheOnStart() {
log.info("[GeoHashCache] 서버 부팅에 따른 동기화 시작");
jobGeoCacheSyncUtil.syncGeoHashCache();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package spot.spot.domain.job.query.util.caching;

import ch.hsr.geohash.GeoHash;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Component;

@Component
public class GeoHashUtil {
// 위도 경도를 Geo-hash 문자열로 인코딩 -> Redis caching 시 Key로 쓰인다.
// precision: 정밀도, Geo-hash는 격자 기반, 여기서 precision 값이 커지면 검색 범위 격자가 좁아지고, 작아지면 검색 범위 격자가 넓어진다.
public String encode (double lat, double lng, int precision) {
return GeoHash.geoHashStringWithCharacterPrecision(lat, lng, precision);
}

public List<String> geoNeighborHashes(double lat, double lng, int precision) {
GeoHash center = GeoHash.withCharacterPrecision(lat, lng, precision);
Set<String> geoHashes = new HashSet<>();
geoHashes.add(center.toBase32());

for(GeoHash neighbor : center.getAdjacent()) {
geoHashes.add(neighbor.toBase32());
}

return new ArrayList<>(geoHashes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package spot.spot.domain.job.query.util.caching;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import spot.spot.domain.job.query.dto.response.NearByJobResponse;
import spot.spot.domain.job.query.repository.dsl.SearchingListQueryDsl;
import static spot.spot.global.util.ConstantUtil.GEOHASH_PRECISION;

@Slf4j
@Service
@RequiredArgsConstructor
public class JobGeoCacheSyncUtil {

private final SearchingListQueryDsl searchingListQueryDsl;
private final CacheManager cacheManager;
private final GeoHashUtil geoHashUtil;


@Transactional(readOnly = true)
public void syncGeoHashCache () {
int updateCnt = 0;
Cache cache = cacheManager.getCache("job-geohash");

List<NearByJobResponse> allResponses = searchingListQueryDsl.findJobsforGeoHashSync();
Map<String, List<NearByJobResponse>> geoGrouped = new HashMap<>();

// KEY: GEOHASH 문자열 값, VALUE: response dto 값
for(NearByJobResponse response : allResponses) {
String geoHash = geoHashUtil.encode(response.getLat(), response.getLng(), GEOHASH_PRECISION);
geoGrouped.computeIfAbsent(geoHash, key -> new ArrayList<>()).add(response);
}

for(Map.Entry<String, List<NearByJobResponse>> entry : geoGrouped.entrySet()){
String nowKey = entry.getKey();
List<NearByJobResponse> dbList = entry.getValue();
List<NearByJobResponse> cacheList = cache.get(nowKey, List.class);

if(isChanged(cacheList, dbList)) {
cache.put(nowKey, dbList);
log.debug("[근처 일거리 찾기] {} 캐싱 갱신 {} 건", nowKey, dbList.size());
updateCnt += dbList.size();
}
log.info("[GeoHashCache] 캐시 동기화 완료 - 총 {}개 갱신 / 전체 {}", updateCnt, geoGrouped.size());
}
}

private boolean isChanged(List<NearByJobResponse> cacheList, List<NearByJobResponse> dbList) {
if(cacheList == null || cacheList.size() != dbList.size()) return true;

Map<Long, NearByJobResponse> dbListwithPK = dbList.stream()
.collect(Collectors.toMap(NearByJobResponse::getId, j -> j));

for(NearByJobResponse old : cacheList) {
NearByJobResponse updated = dbListwithPK.get(old.getId());
if(updated == null || !updated.equals(old)) return true;
}
return false;
}
}
2 changes: 1 addition & 1 deletion src/main/java/spot/spot/global/config/CacheConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class CacheConfig {
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("job-geohash");
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.maximumSize(200_000)
.expireAfterWrite(Duration.ofDays(1))
.recordStats());
return manager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public enum ErrorCode {
// ALRAM
INVALID_FCM_TOKEN(HttpStatus.NOT_ACCEPTABLE, "❌ 이 회원은 FCM 토큰이 전무하네요! 오래 접속하지 않았거나, 탈퇴회원 입니다. ❌ "),
INVALID_TITLE(HttpStatus.NOT_ACCEPTABLE, "❌ 조회하신 일이 존재하지않습니다.! 결제준비된 상품인지 확인해주세요 ❌ "),
// POINT
ALREADY_PAY_FAIL(HttpStatus.BAD_REQUEST, "이미 결제 취소된 내역입니다."),
EMPTY_MEMBER(HttpStatus.BAD_REQUEST, "멤버 값이 비어있습니다."),
EMPTY_TITLE(HttpStatus.BAD_REQUEST, "일 타이틀 값이 비어있습니다."),
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/spot/spot/global/util/ConstantUtil.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package spot.spot.global.util;

import java.time.Duration;

public class ConstantUtil {
// FOR SECURITY
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
public static final String TOKEN_PREFIX = "token:";
// 거리 계산
public static final double EARTH_RADIUS_KM = 6371;
// WORKER STAUTS
// WORKER STATUS
public static final Integer STILL_WORKING = 0;
public static final Integer LITTLE_BREAK = 1;
// MESSAGE TYPE
public static final String TYPE = "type";
public static final String PERMIT_ALL = "permitAll";
public static final String AUTH_ERROR = "auth_error";
// GEO-HASH
public static final int GEOHASH_PRECISION = 6;
public static final Duration SYNC_INTERVAL = Duration.ofDays(1);
}
Loading