Skip to content

Conversation

@Dockerel
Copy link
Contributor

@Dockerel Dockerel commented Aug 12, 2025

#️⃣ 연관된 이슈

📚 배경

  • 유저의 위치를 효율적이고 빠르게 파악하기 위해 S2 라이브러리 도입

📝 작업 내용

초기 경북대학교 영역 데이터 저장

  • geojson/cells.geojson 파일에서 cellId 읽어서 저장
  • ApplicationRunner로 애플리케이션이 시작될 때 실행
  • 배치 INSERT, DELETE로 총 실행 쿼리 수를 660건에서 2건으로 줄이고 처리 시간을 7614ms에서 326ms로 단축

유저 위치 저장

  • redis를 사용하여 3 종류 데이터 저장
    1. [set 저장] key : "cell:{cellId}:user", value : userId, TTL : x, 삭제 배치 작업
      • 현재 셀에 위치해 있는 유저들 저장
      • redis의 set은 데이터의 삭제가 O(1)에 가능하기 때문에 유저의 위치를 빠르게 삭제 및 삽입하기 위해 set 선택
    2. [value 저장] key : "user:{userId}", value : cellId, TTL : 3600s(1h)
      • 현재 유저의 셀 위치를 저장
      • 유저의 위치가 바뀌면 해당 셀을 키로 하는 set에서 삭제해야 하는데, 어느 셀에 있는지 알기 위해 저장
    3. [zset 저장] : key : "cell:{userId}:expiry", value : userId, TTL : x, 삭제 배치 작업
      • 현재 셀에 위치해 있는 유저들을 만료시간(score)과 함께 저장
      • set의 값들은 각각 따로따로 TTL을 설정하지 못하기 때문에 set을 정리하기 위한 zset
      • 1시간마다 zset에서 만료된 데이터들을 삭제하고 동시에 set에서도 삭제
  • set/zset 만료 userId 삭제 배치 작업
    • try-with-resources를 적용하여 리로스 누수 위험 차단
    • 순서
    1. cell:*:expiry인 모든 키 집합 조회
    2. 키 마다 돌아가면서 만료된 멤버 수집. (-inf ~ now) 사이에 존재하면 만료.
    3. 파이프라인으로 set과 zset에서 한번의 통신으로 제거

띱 요청 생성 시 이웃 userIds 조회

  • 추후 띱 요청 생성 시 이웃한 유저들에게 알림을 보내기 위해 이웃 userId를 조회하는 기능 구현

초기 화면에서 인접한 요청 가져오기

  • 띱 초기 화면 조회 시 인접한 요청 가져오기 위한 기능 구현
  • 현재는 띱 요청 엔티티가 존재하지 않으므로 인접한 cellId 가져오는 것만 구현

유저 ID 용량을 줄이기 위한 인코딩 도입

  • 현재 유저 ID는 UUID인데, 레디스에서 최대한 용량을 줄여 사용하고자 Base64로 인코딩하여 사용하였습니다
  • 저장, 전송 크기가 36바이트에서 22바이트로 감소 및 사용자 ID 관련 메모리와 네트워크 사용량 39% 절감됩니다
  • UUID 인코딩 및 디코딩 테스트 완료하였습니다
  • JDK 표준 Base64 구현이라 인코딩 / 디코딩 오버헤드는 거의 존재하지 않습니다

📸 스크린샷

x

💬 리뷰 요구사항

  • 아직 아키텍처가 익숙하지 않아 아키텍처 쪽으로 많은 조언 부탁드립니다

  • 제가 생각한 유저 위치 삭제 / 삽입 프로세스에 엣지 케이스나 궁금하신 부분이 있다면 편하게 말씀해주세요

  • 그리고 나중에 로그인 한 유저를 알 수 있는 @Login 과 같은 커스텀 어노테이션 만들어주시면 감사하겠습니다

리뷰 감사합니다.

✏ Git Close

close #21

@Dockerel Dockerel requested a review from GitJIHO August 12, 2025 01:58
@Dockerel Dockerel self-assigned this Aug 12, 2025
@Dockerel Dockerel added ✨ Feature 새로운 기능 추가 및 구현하는 경우 ✅ Test Code 테스트 관련 작업을 진행하는 경우 labels Aug 12, 2025
@github-actions
Copy link

github-actions bot commented Aug 12, 2025

📊 Code Coverage Report

Overall Project 91.5% -3.45% 🍏
Files changed 88.57% 🍏

File Coverage
ScheduleConfig.java 100% 🍏
GeoJsonInitializer.java 100% 🍏
LocationEntity.java 100% 🍏
LocationReaderImpl.java 98.06% -1.94% 🍏
LocationWriterImpl.java 97.38% -2.62% 🍏
LocationService.java 93.6% -6.4% 🍏
UuidBase64Utils.java 92.11% -7.89% 🍏
RedisOneTimeRunner.java 80.56% -19.44% 🍏
LocationKeyFactory.java 75% -25%
S2Converter.java 75% -25%
RedissonConfig.java 0%
LocationScheduler.java 0%
LocationController.java 0%

@github-actions
Copy link

github-actions bot commented Aug 12, 2025

Test Results

182 tests   182 ✅  6s ⏱️
 29 suites    0 💤
 29 files      0 ❌

Results for commit 7ee71d3.

♻️ This comment has been updated with latest results.

@parknew0
Copy link
Member

멋진 PR 입니다! 제가 이해한 바가 맞는지 확인차 댓글 남깁니다.

요약하자면, 새로운 '띱' 요청이 들어왔을 때 그 주변에 있는 사용자들을 S2 기반으로 빠르게 찾아내는 핵심 로직을 구현해 주셨군요! 정말 고생 많으셨습니다.

이제 여기에

  • '띱' 요청을 생성하는 e.g. API(POST /ddips)
  • 찾아낸 사용자 목록에게 실제 FCM 푸시 알림을 발송하는 로직

이 두개까지 구현되면 저희가 기획했던 실시간 위치 기반 기능의 큰 그림이 완성될 것 같습니다. 👍👍👍

@Dockerel
Copy link
Contributor Author

@parknew0새로운 '띱' 요청이 들어왔을 때 그 주변에 있는 사용자들을 S2 기반으로 빠르게 찾아내는 핵심 로직 이 부분을 구현한 것이 맞습니다! 그리고 사용자의 위치를 실시간으로 저장하는 로직까지 구현되어 있습니다.

그리고

  • '띱' 요청을 생성하는 e.g. API(POST /ddips)
  • 찾아낸 사용자 목록에게 실제 FCM 푸시 알림을 발송하는 로직

두 구현 기능에 대해서는 오늘 @GitJIHO 님과 회의를 통해 결정하도록 하겠습니다!


// targetCellId의 userIds만 가져오기
List<UUID> userIds = targetCellIds.stream()
.map(locationRepository::findUserIdsByCellId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetCellIds 리스트에는 현재 위치 Cell과 주변 이웃 Cell을 합쳐 최대 9개의 Cell ID가 들어있는데요,
해당 지점에서 findUserIdsByCellId 메소드를 각 각 한번씩, 총 9번 호출하게 되어서 Redis 서버와 최대 9번의 개별적인 네트워크 통신을 순차적으로 수행하게 될지 모른다는 우려가 있을 수 있는 것 같습니다.

한 번 확인해 봐주실 수 있을까용?? 가능하다면, Redis 파이프라이닝으로 개선이 가능할 지 여부도 확인해주세용

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 말씀하신대로 레디스 파이프라인으로 구현하였더니

cellCount=9, usersPerCell=10000

데이터에 대해

Single Calls: 564 ms
Pipeline: 197 ms

위와 같은 성능개선이 이루어졌습니다!

}
}

public void saveUserLocation(UUID userId, UpdateMyLocationRequest request) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 가지 엣지 케이스가 궁금해 질문드립니다. 동시성 문제, 경쟁 상태에 관한 질문인데요,

만약 네트워크가 불안정한 사용자가 아주 짧은 간격으로 위치 업데이트 요청을 두 번 보내서, 두 요청이 거의 동시에 서버에서 처리되는 상황을 가정해 보았습니다.

이때 두 요청을 처리하는 스레드가 모두 사용자의 이전 위치를 거의 동시에 읽어 간다면, 결과적으로 사용자가 두 개의 다른 Cell에 동시에 존재하는 데이터 불일치 상태가 발생할 가능성이 있지 않을까 싶습니다.

이렇게 되면 사용자가 이미 떠난 위치의 '띱' 알림을 받는 경험을 할 수도 있을 것 같아 의견 여쭙니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 넵 이해했습니다. 찰나의 순간에 두 요청이 섞이면 사용자가 두 위치에 모두 존재할 수도 , 두 위치에 모두 존재하지 않을 수도 있다는 말씀이시죠?

그럼 처음 위치 한번만 validate 후 위치의 삭제와 저장을 원자적으로 처리하도록 수정하겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lua script 방식 도입하여 유저 위치 데이터의 삭제 및 삽입 프로세스가 원자적으로 이루어지게 수정하였습니다!

Copy link
Contributor

@GitJIHO GitJIHO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 👍
아키텍쳐 관점에서 헷갈리실 수 있는 부분이 있으셨을 것 같은데, 전반적으로 잘 적용해 주신 것 같아 좋네요!
비즈니스 로직 관점에서는 전체 서비스측면에서 제가 아직 이해가 미흡한 부분이 있어 확실하게 장담하기는 힘들어서, 추후 적용해보면서 만약 문제가 있다면 함께 잡아갈 수 있으면 좋을 것 같습니다. 현재까지는 괜찮아 보이네요!
@RequireAuth : 인증이 필요한 메서드를 위한 어노테이션(메서드단)
@Login : 인증 파라미터 (AuthUser 도메인을 반환)
위 두개의 어노테이션 서버에 적용되어있습니다 :)

Comment on lines 13 to 19
public static GetNeighborsRequest of(UUID userId, double lat, double lng) {
return GetNeighborsRequest.builder()
.userId(userId)
.lat(lat)
.lng(lng)
.build();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 요즘 개선해나가고 있는 부분인데, 레코드타입에는 보통 빌더를 쓰는게 권장되지 않는다고 하더라구요.
팩토리 생성자는 괜찮지만 빌더는 오히려 레코드의 간편성을 해치거나 불필요한 보일러플레이트 코드가 발생할 수 있다고 하네요..!
물론 제 코드에도 아직 남아있어서 참고용으로 생각해주시면 좋을 것 같아요 ㅎㅎ,,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 넵 동의합니다!

@@ -0,0 +1,59 @@
package com.knu.ddip.location.application.scheduler;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RedisTemplate, RedisConnection등과 같은 외부 시스템 의존성이 있기 때문에
스케쥴러의 특성상 운영계층보다는 인프라스트럭처 하위가 헥사고날 관점에서 좀 더 적절한 위치가 아닐까 싶네요
스케쥴러 자체 구현체를 변경할 필요까지는 없을 것 같아 포트와 어댑터를 구분할 필요까지는 없고 현재 코드대로 위치만 변경하면 괜찮을 것 같습니다

Copy link
Contributor Author

@Dockerel Dockerel Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 넵 말씀하신대로 외부 시스템 의존하는 부분이 있어 인프라쪽에 더 잘 어울릴 것 같더라구요. 그래서 스케줄 실행하는 자체는 응용 계층에 두고 인프라단에 외부 의존성 관련 코드들을 옮겨 실행하도록 리펙터링하였습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 코드만 옮기는 방식도 좋네요 👍

Comment on lines 15 to 35
public class LocationEntity implements Persistable<String> {

@Id
private String cellId;

public static LocationEntity create(String cellId) {
return LocationEntity.builder()
.cellId(cellId)
.build();
}

@Override
public String getId() {
return cellId;
}

@Override
public boolean isNew() {
return cellId == null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cellId에 대한 자동생성 로직을 두지 않았기 때문에 하이버네이트단의 select 쿼리발생 방지를 위해 isNew를 두신게 맞을까용

Copy link
Contributor Author

@Dockerel Dockerel Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 정확합니다! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 확인해보니 벌크 삽입이 jdbc를 통해 이루어지게 구현되어 있는데 이때는 isNew 결과에 상관없이 들어간다고 하네요. 실제로 찍히는 쿼리 로그를 통해 확인해봤는데 실제로도 그렇더라구요!

jdbc 쿼리 로그 확인해보고 싶으시면

spring.datasource.url 뒤에

&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=2147483647

를 추가해주시고

logging.level.com.mysql.cj: trace

를 추가해주시면 됩니다!

Comment on lines 48 to 51
@Override
public int getBatchSize() {
return cellIds.size();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배치 사이즈를 cellIds의 개수만큼 설정하신 것 같은데, 평균적으로 몇개의 사이즈정도 나올까요? 서비스 특성상 과도하게 많아지지는 않을지 고민이네요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 셀들이 경북대로 제한되어 있어 정확하게 330개가 나옵니다. 현재는 괜찮지만 추후 서비스 확장시에는 배치사이즈에 대한 고민이 필요할 듯 하네요.

Comment on lines 40 to 53
jdbcTemplate.batchUpdate(sql,
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String cellId = cellIds.get(i);
ps.setString(1, cellId);
}

@Override
public int getBatchSize() {
return cellIds.size();
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveAll에 대한 배치처리는 넘 좋습니다 👍

@Operation(summary = "위치 갱신",
description = "위도와 경도로 현재 내 위치를 갱신한다.")
ResponseEntity<Void> updateMyLocation(
// @Login AuthenticateUser user,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RequireAuth 와 @Login 어노테이션 이미 존재합니다 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군요. 해당 어노테이션 사용해서 수정 후 커밋하겠습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 다만, 아직 전체 인증 로직에 대한 몇개의 부족한 엔드포인트가 있고 우선순위가 뒤로 밀려있어서 실제 테스트를 위해서는 이 점 고려하여 적용해주시면 감사드리겠습니다!

Comment on lines 4 to 7
# MySQL
spring.datasource.url=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true
spring.datasource.url=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
spring.datasource.username=${MYSQL_USER}
spring.datasource.password=${MYSQL_PASSWORD}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MySQL이라서 SQL를 전송하는 네트워크단에서 배치 적용이 되는군요,,! PostgreSQL에서는 하이버네이트단의 배치처리까지만 가능해서 신선하네요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 넵! 저 옵션을 켜줘야 JDBC를 통한 벌크 삽입이 작동한다고 하더라구요

Comment on lines 25 to 27
private final JdbcTemplate jdbcTemplate;

private final RedisTemplate<String, String> redisTemplate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 jdbc와 redis에 대한 의존성이 동시에 존재하는데, 나중에 리팩토링 과정에서 이 둘을 분리하여 적용한다면 좀 더 유연한 구조가 되지 않을까 싶네요 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇네요. 레디스와 JPA와 JDBC에 대한 의존을 서로 나누는 것이 좋겠네요!

Copy link
Contributor Author

@Dockerel Dockerel Aug 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 역할 기반으로 조회와 쓰기로 분리해보았습니다!

@Dockerel
Copy link
Contributor Author

수정 사항

  1. 레디스 파이프라인으로 특정 셀들에 포함된 유저들 가져오기 개선
  2. lua script로 유저 위치 삭제 / 삽입 로직 원자적으로 실행
  3. 레코드 빌더 구조 수정
  4. LocationEntity 삽입 시 Persistable 제거(jdbc를 통한 벌크 삽입이라 필요없음)
  5. @Login 적용
  6. 역할 기반 모델로 분리(조회 / 쓰기)
  7. key 생성 메서드 클래스로 분리
  8. 특정 엣지 케이스들과 유저 위치 삭제 / 삽입 동시성 테스트[2 : lua script 적용한 부분] 추가

해당 부분 수정하였습니다!

@Dockerel Dockerel requested review from GitJIHO and parknew0 August 16, 2025 12:28
Copy link
Contributor

@GitJIHO GitJIHO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전반적으로 효율성과 성능 측면에서 많은 개선이 있는 것 같아 좋네요! 👍
궁금한 부분에 대한 코멘트 몇개 남겨두었습니다.
로직적으로는 큰 문제 없어보여서 미리 approve 하겠습니다!

@@ -0,0 +1,59 @@
package com.knu.ddip.location.application.scheduler;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 코드만 옮기는 방식도 좋네요 👍

Comment on lines +40 to +90
@PostConstruct
public void init() {
saveUserLocationScript = new DefaultRedisScript<>();
saveUserLocationScript.setResultType(String.class);
saveUserLocationScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("luascript/save_user_location.lua"))
);
}

@Override
public void deleteAll() {
locationJpaRepository.deleteAllInBatch();
}

@Override
public void saveAll(List<String> cellIds) {
String sql = """
INSERT INTO locations (cell_id) VALUES (?)
""";

jdbcTemplate.batchUpdate(sql,
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String cellId = cellIds.get(i);
ps.setString(1, cellId);
}

@Override
public int getBatchSize() {
return cellIds.size();
}
});
}

@Override
public void saveUserIdByCellIdAtomic(String newCellId, boolean cellIdNotInTargetArea, String encodedUserId) {
String userIdKey = createUserIdKey(encodedUserId);
String cellIdUsersKey = createCellIdUsersKey(newCellId);
String cellIdExpiriesKey = createCellIdExpiriesKey(newCellId);

long now = System.currentTimeMillis();
long expireAt = now + TTL_SECONDS * 1000L;
int cellIdNotInTargetAreaFlag = cellIdNotInTargetArea ? 1 : 0;

redisTemplate.execute(
saveUserLocationScript,
Arrays.asList(userIdKey, cellIdExpiriesKey, cellIdUsersKey),
newCellId, encodedUserId, String.valueOf(TTL_SECONDS), String.valueOf(expireAt), String.valueOf(cellIdNotInTargetAreaFlag)
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오,, 루나스크립트로 원자적으로 실행하도록 할 수 있군요
하나 배워갑니다 💯 👍

Comment on lines +39 to +67
@Override
public List<String> findUserIdsByCellIds(List<String> targetCellIds) {
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();

if (connectionFactory == null) return null; // throw xxx

try (RedisConnection conn = connectionFactory.getConnection()) {
conn.openPipeline();

List<byte[]> keys = targetCellIds.stream()
.map(LocationKeyFactory::createCellIdUsersKey) // "cell:{id}:users" 형태
.map(k -> k.getBytes(StandardCharsets.UTF_8))
.collect(Collectors.toList());

for (byte[] key : keys) {
conn.sMembers(key);
}

List<Object> rawResults = conn.closePipeline();

return rawResults.stream()
.map(result -> (Set<byte[]>) result)
.filter(Objects::nonNull)
.flatMap(set -> set.stream()
.map(bytes -> new String(bytes, StandardCharsets.UTF_8))
)
.collect(Collectors.toList());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋네요 👍 추후에 유저가 많아질 경우에 블로킹에 대비한 처리를 도입하거나 redis자체를 클러스터형식으로 변경하여 pipeline 도입으로 인한 오버헤드를 관리할 수 있을 것 같네용

Comment on lines +16 to +22
@PutMapping
@Operation(summary = "위치 갱신",
description = "위도와 경도로 현재 내 위치를 갱신한다.")
ResponseEntity<Void> updateMyLocation(
@Login AuthUser user,
UpdateMyLocationRequest request
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RequireAuth를 메서드 혹은 클래스단에 붙여야 인터셉터에서 정상 처리를 하여 @Login어노테이션 사용이 가능해집니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군요 수정하겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RestController
@RequiredArgsConstructor
public class LocationController implements LocationApi {

    private final LocationService locationService;

    @Override
    @RequireAuth
    public ResponseEntity<Void> updateMyLocation(AuthUser user, UpdateMyLocationRequest request) {
        locationService.saveUserLocationAtomic(user.getId(), request);
        return ResponseEntity.ok().build();
    }
}

@GitJIHO 이렇게 붙혀주면 될까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다 !

Comment on lines 9 to 19
@Component
@RequiredArgsConstructor
public class GeoJsonInitializer implements ApplicationRunner {

private final LocationService locationService;

@Override
public void run(ApplicationArguments args) {
locationService.loadAndSaveGeoJsonFeatures();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 db 세팅 로직을 애플리케이션 재시작시마다 실행되도록 하신 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타겟 영역이 추후 변경되거나 확장될 수도 있고 이에 대해 모두 삭제했다가 어플리케이션 시작시 다시 삽입하는 방식이 가장 깔끔하다고 생각했습니다. 현재 데이터 크기에 대해서는 괜찮은 것 같은데 추후 타겟 영역이 너무 커지게 되면 다른 방식을 생각해보아야 할 것 같긴 하네요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 의도는 이해했습니다
다만 다중 인스턴스 환경으로 변경할 경우 CD 과정 중 하나의 인스턴스가 업데이트될 경우에도 인스턴스들이 공통으로 사용하는 DB의 영역 데이터들이 잠시 비게되어 오류가 나지 않을까 걱정이네요
실제로 aws ECS를 도입할 의향이 있어 말씀드립니다 !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군요. 일단 레디스 분산락이나 shed lock 기반으로 해결해보겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 schedule은 저번에 사용한 shedlock 기반 방식으로 해결하였고, ApplicationRunner 의 초기화는 redis 분산락 기반으로 해결하였습니다.

현재 분산락 관련 로직을 다 짤지 고민했다가 한번만 실행되는 로직은 따로 구현하는게 낫다고 생각하여

인프라단에

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisOneTimeRunner implements OneTimeRunner {

    private final RedissonClient redisson;

    @Override
    public void runOnce(String lockName, Runnable task) {
        RLock lock = redisson.getLock(lockName);
        boolean acquired = false;
        try {
            acquired = lock.tryLock(0, 30, TimeUnit.MINUTES);
            if (!acquired) {
                return;
            }
            task.run();
        } catch (InterruptedException e) {
            log.error("Interrupted while waiting for lock {}", lockName, e);
        } finally {
            if (acquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

그리고 응용 단에는

public interface OneTimeRunner {
    void runOnce(String lockName, Runnable task);
}

@Component
@RequiredArgsConstructor
public class GeoJsonInitializer implements ApplicationRunner {

    public static final String GEOJSON_INIT_LOCK_KEY = "lock:geojson:init";
    
    private final OneTimeRunner oneTimeRunner;
    private final LocationService locationService;

    @Override
    public void run(ApplicationArguments args) {
        oneTimeRunner.runOnce(
                GEOJSON_INIT_LOCK_KEY,
                () -> locationService.loadAndSaveGeoJsonFeatures()
        );
    }
}

이런식으로 적용해보았습니다

String cellIdExpiriesKey = createCellIdExpiriesKey(newCellId);

long now = System.currentTimeMillis();
long expireAt = now + TTL_SECONDS * 1000L;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비즈니스적으로 궁금한 부분입니다만,
타임아웃을 해당 시간으로 둔 이유가 뭔지 알 수 있을까요?
백그라운드에서 앱을 실행하며 위치를 갱신하는 엔드포인트를 지속하여 호출하지 않을 것이라 생각해
타임아웃 시간이 지나면 유저가 그 시간동안 앱을 접속하지 않은 것이므로 현재 유저 위치가 정확하지 않을 것이라 가정해 두신 것인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다. 일단은 사용자의 위치에 시간 제한을 두기 위한 것이긴 한데 경우에 따라 더 줄이는 것이 정확성에 더 도움이 될 것 같긴 하네요

@Dockerel Dockerel merged commit b7ce91a into main Aug 21, 2025
2 checks passed
@Dockerel Dockerel deleted the Feat/issue-#21 branch August 21, 2025 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가 및 구현하는 경우 ✅ Test Code 테스트 관련 작업을 진행하는 경우

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: S2 기반 위치 관리 기능 개발

4 participants