Skip to content

Conversation

@dnzp75
Copy link
Collaborator

@dnzp75 dnzp75 commented Apr 6, 2025

📌 PR 제목

지도 내 다이어리 클러스터 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터)

✨ 변경 사항

1. MySQL & PostgreSQL 하이버네이트 분리 설정 추가

왜 Config를 분리해야 했는가?
Spring에서 기본적으로 하나의 DataSource, EntityManagerFactory, TransactionManager를 사용하는 단일 DB 환경을 전제로 구성
하지만 이 프로젝트처럼 두 개의 서로 다른 RDBMS (MySQL + PostgreSQL) 을 사용할 경우, 다음과 같은 문제들이 발생

  • SQL Dialect 충돌
    MySQL은 MySQL8Dialect, PostgreSQL은 PostgreSQLDialect를 사용해야 하는데, 단일 설정으로 실행하면 하이버네이트가 한 쪽에만 맞춰져, 다른 쪽 쿼리는 오류 발생

  • Entity 스캔 범위 충돌
    Spring JPA는 @EntityScan, @EnableJpaRepositories 설정에서 지정된 패키지의 엔티티와 레포지토리를 모두 하나의 EntityManager로 등록하려 한다.
    PostgreSQL용 Entity를 MySQL의 EntityManager에 등록하려 하면 매핑 오류 혹은 쿼리 실행 오류 발생

따라서 각 DB에 알맞게 등록되기 위해 Config 통해 분리

행정구역 데이터는 PostgreSQL에 MULTIPOLYGON 형태로 저장되어있음

2. 지도 내 다이어리 클러스터 조회 API 구현

사용자가 보고 있는 지도 범위 내에 존재하는 다이어리 개수를 줌 레벨에 따라 클러스터링된 형태로 반환

  • 줌레벨 로직
    • zoom ≤ 10 : 시/도 단위 클러스터링
    • zoom ≥ 11 : 시/군/구 단위 클러스터링
    • zoom ≥ 14 : 클러스터링 OFF, 개별 다이어리 반환 예정 (후속 예정)
  • API 응답 구조
    areaName, areaId, lat, lon, diaryCount 형식으로 클러스터 정보 반환

3. PostGIS 기반 PostgreSQL을 Docker로 실행하도록 구성

  • 비용 절감을 위해 RDS 대신 EC2 내부 Docker로 처리
  • 초기 데이터는 init.sql을 통해 수동 세팅

서버 실행 전 해당 DB가 들어있는 Docker를 Compose 해야 서버 실행 시 에러가 나지 않습니다.

🧑‍💻 팀원 실행 가이드

  1. docker/init.sql 파일은 수동으로 받아서 넣어야 합니다
  2. 프로젝트 루트에서 아래 명령 실행
📁 [프로젝트 루트]/
├── docker/
│   └── init.sql    ← 👈 따로 전달받아 여기에 직접 넣어야 함
├── docker-compose.yml
docker-compose up -d

🚀 배포 가이드

  1. 최초 배포 시:
    init.sql 파일 EC2에 복사
    docker-compose up -d 실행 후 서버 구동
  2. 이후 배포 시:
    docker-compose up -d만 실행 (volume 삭제 금지 ❌)

🔍 기능 구현하면서 고민했던 사항

1. 비즈니스 로직 흐름

기존에는 다음과 같은 방식으로 다이어리 클러스터를 계산

  1. 지도 사각형 범위 내의 시/군/구 리스트 조회
  2. 범위 내 다이어리 리스트 조회
  3. 각 다이어리마다 시/군/구 포함 여부를 확인
  4. 시/군/구 별로 다이어리 수 카운트 집계

단점: 다이어리 수가 많아지면 시/군/구 × 다이어리 만큼 반복 검사를 수행하므로 성능 저하가 발생

→ 이에 대한 해결 방안으로, 다음과 같은 방식으로 리팩터링:

  • SidoAreasDiaryCount, SiggAreasDiaryCount 테이블 생성
  • 다이어리 생성 시, 요청 좌표로 해당 지역(Sido/Sigg)을 판별
  • 판별된 지역의 count 값을 +1 증가 후 저장
  • 클러스터 조회 시에는 해당 count 테이블만 조회하여 빠르게 응답 가능

조회 API는 매우 자주 호출되는 반면, 생성은 드물게 발생하므로 생성 시 비용을 약간 감수하고 조회 성능을 최적화하는 방향으로 결정

2. 공간 데이터 다루기 위한 DB선택

  • PostgreSQL 사용 이유
    지도에서 행정구역(시/도, 시/군/구) 단위로 공간 데이터를 다뤄야했다. 예를 들어, 사용자가 지도에서 특정 범위를 보고 있을 때, 그 사각형 범위 안에 있는 시/군/구 혹은 해당 지역 안에 속한 다이어리의 개수를 클러스터링 형태로 제공해야 했다.
    단순 위도/경도 between 조건으로는 한계가 있었고, 아래와 같은 공간 연산이 필요했었다:

    • 특정 시군구가 사용자의 지도 사각형 범위와 겹치는지ST_Intersects
    • 다이어리 좌표가 특정 행정구역 안에 포함되는지ST_Contains
  • MySQL도 공간 데이터 다룰 수 있지만, 아래와 같은 단점들이 존재했다.

    1. 공간 연산 기능의 한계

      • MySQL에도 ST_Contains, ST_Within 등의 함수가 존재하긴 하나, 복잡한 도형 구조(예: MULTIPOLYGON, GEOMETRYCOLLECTION)를 다루는 데 있어 불안정하거나 오류가 자주 발생
      • 공간 인덱스 사용도 제한적이라 성능 저하가 심각하다.
    2. SHP / GeoJSON 등 외부 공간 데이터 Import의 어려움

      • 공공 데이터에서 제공하는 SHP 파일을 MySQL에 직접 넣기 어려움
      • 반면 PostgreSQL은 shp2pgsql 툴을 통해 바로 import 가능
  • PostgreSQL + PostGIS 도입 이후 가능해진 것

예시 쿼리 – 지도 사각형 범위 안에 포함된 시군구 검색:

SELECT *
FROM sigg_areas
WHERE ST_Intersects(
  geom,
  ST_MakeEnvelope(:west, :south, :east, :north, 4326)
);
  • 위 쿼리를 통해 사용자가 보고 있는 지도 범위 내의 행정구역만 정확히 필터링 가능
  • 각 다이어리 좌표에 대해 어느 행정구역에 속해있는지도 정확하게 파악 가능

PostGIS는 GIST 인덱스를 통해 수천 개의 행정구역도 빠르게 연산 가능

MULTIPOLYGON 같은 복잡한 도형도 정확하게 다룸

✅ 체크리스트

  • 코드가 정상적으로 동작하는지 확인
  • 관련 테스트 코드 작성 및 통과 여부 확인
  • 문서화(README 등) 필요 여부 확인 및 반영
  • 리뷰어가 알아야 할 사항 추가 설명

📸 스크린샷 (선택)

📌 참고 사항

  • 코드 리팩토링 진행중
  • 클러스터링 OFF, 개별 다이어리 반환 API 구현 예정

📌 리뷰 요구 사항

  • 고민했던 내용에 있어서 잘못된 부분이 있는지?
  • 코드 리뷰

@dnzp75 dnzp75 added the enhancement New feature or request label Apr 6, 2025
@github-actions
Copy link

github-actions bot commented Apr 6, 2025

Claude의 전체 변경사항 및 관련 파일에 대한 리뷰:

개선된 사항:

  • 다중 데이터베이스 구성 (MySQL과 PostgreSQL)을 위한 설정 클래스 추가
  • 지도 기능을 위한 새로운 도메인 (map) 추가 및 관련 엔티티, 레포지토리, 서비스 구현
  • 다이어리 생성 시 지역별 다이어리 카운트 증가 로직 추가

주요 이슈:

  1. 트랜잭션 관리:

    • 제안: MapService의 increaseRegionDiaryCount 메서드에 @transactional 어노테이션 추가
    @Transactional
    public void increaseRegionDiaryCount(Double lat, Double lon) {
        // 기존 코드
    }
  2. 예외 처리:

    • 제안: MapService에서 지역을 찾지 못했을 때 NotFoundRegionException 발생
    public void increaseRegionDiaryCount(Double lat, Double lon) {
        sidoAreasRepository.findRegionByLatLon(lat, lon)
            .flatMap(sido -> sidoAreasDiaryCountRepository.findById(sido.getId()))
            .orElseThrow(() -> new NotFoundRegionException("시/도 지역을 찾을 수 없습니다."));
        
        // 시군구에 대해서도 동일하게 적용
    }
  3. 성능 최적화:

    • 제안: getDiaryClusters 메서드에서 zoom 레벨에 따른 분기를 Repository 레벨로 이동
    @Query(value = """
        SELECT CASE WHEN :zoom <= 10 THEN s.id ELSE sg.gid END AS id,
               CASE WHEN :zoom <= 10 THEN s.name ELSE sg.sgg_nm END AS name,
               CASE WHEN :zoom <= 10 THEN s.lat ELSE sg.lat END AS lat,
               CASE WHEN :zoom <= 10 THEN s.lon ELSE sg.lon END AS lon,
               CASE WHEN :zoom <= 10 THEN COALESCE(sc.diary_count, 0) ELSE COALESCE(sgc.diary_count, 0) END AS diaryCount
        FROM sido_areas s
        LEFT JOIN sido_areas_diary_count sc ON s.id = sc.id
        LEFT JOIN sigg_areas sg ON ST_Contains(s.geom, sg.geom)
        LEFT JOIN sigg_areas_diary_count sgc ON sg.gid = sgc.id
        WHERE (CASE WHEN :zoom <= 10 THEN s.lat ELSE sg.lat END) BETWEEN :south AND :north
          AND (CASE WHEN :zoom <= 10 THEN s.lon ELSE sg.lon END) BETWEEN :west AND :east
    """, nativeQuery = true)
    List<AreaClusterProjection> findAreaClusters(@Param("zoom") int zoom, @Param("south") double south, @Param("north") double north, @Param("west") double west, @Param("east") double east);

관련 파일에 대한 영향 분석:

  • DiaryService: MapService 의존성 추가로 인해 다이어리 생성 시 지역 정보 업데이트 로직 변경
  • MapController: 새로운 엔드포인트로 인해 API 문서 업데이트 필요
  • 테스트 코드: MapService와 DiaryService의 변경으로 인해 관련 테스트 케이스 수정 및 추가 필요

전반적인 의견:
코드 변경은 새로운 지도 기능 추가와 다중 데이터베이스 지원을 위한 것으로 보입니다. 전반적으로 잘 구조화되어 있지만, 트랜잭션 관리와 예외 처리에 대한 부분을 좀 더 강화하면 좋을 것 같습니다. 또한, 성능 최적화를 위해 일부 쿼리를 개선할 수 있을 것 같습니다.

@dnzp75 dnzp75 self-assigned this Apr 6, 2025
@sonarqubecloud
Copy link

sonarqubecloud bot commented Apr 7, 2025

@dnzp75 dnzp75 changed the title 지도 내 다이어리 클러스터 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) 지도 범위 내 클러스터 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) Apr 7, 2025
@dnzp75 dnzp75 changed the title 지도 범위 내 클러스터 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) 지도 범위 내 다이어리 클러스터 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) Apr 7, 2025
@dnzp75 dnzp75 changed the title 지도 범위 내 다이어리 클러스터 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) 지도 범위 내 클러스터 다이어리 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) Apr 7, 2025
@dnzp75 dnzp75 changed the title 지도 범위 내 클러스터 다이어리 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) 지도 범위 내 클러스터형 다이어리 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) Apr 7, 2025
@dnzp75 dnzp75 changed the title 지도 범위 내 클러스터형 다이어리 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) 지도 범위 내 클러스터형 공개 다이어리 조회 API 구현 (줌레벨 기준 시/도, 시군구 클러스터) Apr 7, 2025
@dnzp75 dnzp75 merged commit d6d4f6c into develop Apr 8, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants