Skip to content

MongoDB 의존성 제거#93

Open
sejinheo wants to merge 4 commits intomainfrom
develop
Open

MongoDB 의존성 제거#93
sejinheo wants to merge 4 commits intomainfrom
develop

Conversation

@sejinheo
Copy link
Collaborator

@sejinheo sejinheo commented Jan 28, 2026

Summary by CodeRabbit

릴리스 노트

  • Refactor
    • 데이터베이스 기반 시설 현대화 완료
    • Elasticsearch 기반 검색 기능 통합 적용
    • 핵심 라이브러리 의존성 업그레이드
    • 내부 데이터 구조 및 저장소 최적화

✏️ Tip: You can customize this high-level summary in your review settings.

@sejinheo sejinheo requested a review from guhyunwoo as a code owner January 28, 2026 16:03
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

요약

MongoDB 기반의 Dropping, Playlist 엔티티와 관련 서비스를 JPA/Hibernate 기반 관계형 데이터베이스(PostgreSQL)로 마이그레이션하고, 모든 ID 타입을 String에서 Long으로 변경했습니다. 또한 docker-compose에서 MongoDB를 Elasticsearch로 교체했습니다.

변경사항

코호트 / 파일 요약
인프라 및 의존성
build.gradle, docker-compose.yaml, src/main/java/org/example/remedy/global/config/MongoConfig.java, src/main/resources/application.yml
MongoDB 스타터 의존성을 Hypersistence Utils로 교체, MongoConfig 클래스 제거, docker-compose에서 MongoDB 서비스를 Elasticsearch로 변경, JPA/Hibernate 설정 추가
Dropping 엔티티 및 리포지토리
src/main/java/org/example/remedy/domain/dropping/domain/Dropping.java, src/main/java/org/example/remedy/domain/dropping/repository/DroppingPersistenceRepository.java, src/main/java/org/example/remedy/domain/dropping/repository/DroppingRepository.java, src/main/java/org/example/remedy/domain/dropping/repository/DroppingRepositoryImpl.java
MongoDB Document에서 JPA Entity로 마이그레이션, String droppingId를 Long id로 변경, MongoRepository에서 JpaRepository로 전환, Mongo 기반 쿼리를 JPQL 기반으로 교체
Dropping DTO 및 응답 클래스
src/main/java/org/example/remedy/domain/dropping/application/dto/request/DroppingCreateRequest.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/DroppingResponse.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/MusicDroppingResponse.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/MusicDroppingSearchResponse.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/PlaylistDroppingResponse.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/PlaylistDroppingSearchResponse.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/VoteDroppingResponse.java, src/main/java/org/example/remedy/domain/dropping/application/dto/response/VoteDroppingSearchResponse.java
playlistId와 droppingId 필드 타입을 String에서 Long으로 일괄 변경
Dropping 서비스 및 매퍼
src/main/java/org/example/remedy/domain/dropping/application/service/DroppingService.java, src/main/java/org/example/remedy/domain/dropping/application/service/DroppingServiceFacade.java, src/main/java/org/example/remedy/domain/dropping/application/service/MusicDroppingService.java, src/main/java/org/example/remedy/domain/dropping/application/service/PlaylistDroppingService.java, src/main/java/org/example/remedy/domain/dropping/application/service/VoteDroppingService.java, src/main/java/org/example/remedy/domain/dropping/application/mapper/DroppingMapper.java, src/main/java/org/example/remedy/domain/dropping/application/validation/DroppingTypeValidator.java
droppingId 파라미터를 String에서 Long으로 변경, Dropping.getId() 사용으로 통일, 패턴 검증 로직 완화
Dropping 컨트롤러
src/main/java/org/example/remedy/domain/dropping/presentation/DroppingController.java
@PathVariable 타입을 String에서 Long으로 변경
Comment 서비스
src/main/java/org/example/remedy/domain/comment/application/service/CommentService.java
droppingId를 String에서 Long으로 파싱하는 헬퍼 메서드 추가, Long 기반 검증 로직으로 변경
Playlist 엔티티 및 리포지토리
src/main/java/org/example/remedy/domain/playlist/domain/Playlist.java, src/main/java/org/example/remedy/domain/playlist/repository/PlaylistPersistenceRepository.java, src/main/java/org/example/remedy/domain/playlist/repository/PlaylistRepository.java, src/main/java/org/example/remedy/domain/playlist/repository/PlaylistRepositoryImpl.java
MongoDB Document에서 JPA Entity로 마이그레이션, MongoRepository에서 JpaRepository로 전환, String id를 Long으로 변경
Playlist 서비스, DTO 및 컨트롤러
src/main/java/org/example/remedy/domain/playlist/application/service/PlaylistService.java, src/main/java/org/example/remedy/domain/playlist/application/dto/response/PlaylistDetailResponse.java, src/main/java/org/example/remedy/domain/playlist/application/dto/response/PlaylistResponse.java, src/main/java/org/example/remedy/domain/playlist/presentation/PlaylistController.java
playlistId 파라미터를 String에서 Long으로 변경
Like 엔티티, 서비스 및 리포지토리
src/main/java/org/example/remedy/domain/like/domain/Like.java, src/main/java/org/example/remedy/domain/like/application/service/LikeService.java, src/main/java/org/example/remedy/domain/like/repository/LikeRepository.java, src/main/java/org/example/remedy/domain/like/repository/LikeRepositoryImpl.java, src/main/java/org/example/remedy/domain/like/repository/LikePersistenceRepository.java
droppingId를 String에서 Long으로 변경
Like DTO, 요청/응답 및 이벤트
src/main/java/org/example/remedy/domain/like/application/dto/request/LikeRequest.java, src/main/java/org/example/remedy/domain/like/application/dto/response/MusicLikeDroppingResponse.java, src/main/java/org/example/remedy/domain/like/application/dto/response/PlaylistLikeDroppingResponse.java, src/main/java/org/example/remedy/domain/like/application/dto/response/VoteLikeDroppingResponse.java, src/main/java/org/example/remedy/domain/like/application/event/LikeCreatedEvent.java
droppingId 필드와 파라미터를 String에서 Long으로 변경, 검증 어노테이션을 @NotBlank에서 @NotNull로 변경
Like 매퍼 및 컨트롤러
src/main/java/org/example/remedy/domain/like/application/mapper/LikeMapper.java, src/main/java/org/example/remedy/domain/like/presentation/LikeController.java
droppingId를 Long으로 변경, 응답 DTO에 address 및 albumImagePath 필드 추가

예상 코드 리뷰 난이도

🎯 4 (복잡함) | ⏱️ ~60분

관련 PR

제안 리뷰어

  • guhyunwoo

🐰 스트링에서 롱으로, 몽고에서 포스트로~
엔티티들은 옷을 갈아입고 춤을 춘답니다!
저장소는 쿼리를 새로 배우고,
ID들은 숫자가 되어 더 빠르게 달려요.
마이그레이션의 모험, 오늘도 성공! 🎉✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경의 핵심을 정확히 반영합니다. MongoDB 의존성 제거는 전체 변경 사항의 주요 목표이며, 빌드 파일부터 도메인 엔티티, 저장소까지 광범위한 수정이 이를 뒷받침합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@src/main/java/org/example/remedy/domain/comment/application/service/CommentService.java`:
- Around line 118-124: parseDroppingId currently calls Long.valueOf(droppingId)
which throws NPE for null and bypasses the NumberFormatException mapping; update
parseDroppingId to first check for null or blank (e.g., droppingId == null ||
droppingId.trim().isEmpty()) and throw DroppingNotFoundException.EXCEPTION for
those cases, and keep the existing try/catch for NumberFormatException to also
throw DroppingNotFoundException.EXCEPTION.

In `@src/main/java/org/example/remedy/domain/playlist/domain/Playlist.java`:
- Around line 26-28: The songIds JSONB field can be null when loading existing
rows causing NPEs in methods like addSong, removeSong, hasSong, and
getSongIdList; fix by ensuring songIds is initialized after JPA loads the
entity—either make the field default to an empty list (private List<String>
songIds = new ArrayList<>();) or add a `@PostLoad` lifecycle method in Playlist
(e.g., postLoad()) that checks if songIds is null and sets it to new
ArrayList<>(), so all usages of songIds in
addSong/removeSong/hasSong/getSongIdList are safe.
🧹 Nitpick comments (10)
docker-compose.yaml (1)

65-78: Elasticsearch JVM 메모리 설정 및 헬스체크 추가 권장

Elasticsearch 8.x는 메모리 사용량이 많아 명시적인 JVM 힙 설정 없이는 리소스 제한 환경에서 OOM 문제가 발생할 수 있습니다. 또한, service_started 조건은 Elasticsearch가 실제로 요청을 받을 준비가 되었는지 보장하지 않으므로, 헬스체크 추가를 권장합니다.

♻️ 권장 설정 추가
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=true
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
+     - ES_JAVA_OPTS=-Xms512m -Xmx512m
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    networks:
      - remedy-network
+   healthcheck:
+     test: ["CMD-SHELL", "curl -s -u elastic:${ELASTIC_PASSWORD} http://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"]
+     interval: 30s
+     timeout: 10s
+     retries: 5

spring-boot 서비스의 depends_on도 함께 업데이트:

      elasticsearch:
-       condition: service_started
+       condition: service_healthy
src/main/java/org/example/remedy/domain/dropping/application/dto/request/DroppingCreateRequest.java (1)

21-23: playlistId 양수 제약 추가를 권장합니다

Line 22의 playlistId가 Long으로 바뀌면서 0/음수 값이 유입될 수 있습니다. 선택 값이라도 유효한 ID 범위를 보장하려면 @Positive 같은 제약을 추가하는 편이 안전합니다.

♻️ 제안 변경
-        Long playlistId,
+        `@Positive`(message = "playlistId는 양수여야 합니다")
+        Long playlistId,
src/main/java/org/example/remedy/domain/like/application/dto/request/LikeRequest.java (1)

3-6: 유효성 강화가 필요하면 양수 검증도 고려해 주세요.
현재 @NotNull만으로는 0/음수를 막지 못합니다. 도메인에서 음수가 불가라면 @Positive 또는 @Min(1) 추가를 고려해볼 수 있습니다.

src/main/java/org/example/remedy/domain/dropping/application/service/DroppingServiceFacade.java (1)

33-41: switch 문의 일관성 확인 필요

createDropping 메서드(Line 21-27)에서는 default 케이스를 통해 InvalidDroppingTypeException을 던지고 있지만, getDropping 메서드의 switch 표현식(Line 36-40)에는 default 케이스가 없습니다.

Java의 switch 표현식은 enum이 exhaustive할 경우 컴파일러가 이를 처리하지만, 향후 새로운 DroppingType이 추가될 경우 런타임 오류가 발생할 수 있습니다. 일관성을 위해 default 케이스 추가를 권장합니다.

♻️ 제안된 수정
         return switch (dropping.getDroppingType()) {
             case MUSIC -> musicDroppingService.getMusicDropping(droppingId);
             case VOTE -> voteDroppingService.getVoteDropping(droppingId, userId);
             case PLAYLIST -> playlistDroppingService.getPlaylistDropping(droppingId);
+            default -> throw InvalidDroppingTypeException.EXCEPTION;
         };
src/main/java/org/example/remedy/domain/playlist/application/service/PlaylistService.java (1)

51-62: N+1 쿼리 최적화 고려

getPlaylist 메서드에서 각 songId마다 개별 DB 조회가 발생하여 N+1 쿼리 문제가 있습니다. getMyPlaylistscreateAlbumImageMap 메서드(Line 71-79)처럼 findAllById를 사용한 배치 조회로 개선할 수 있습니다.

♻️ 제안된 수정
     public PlaylistDetailResponse getPlaylist(Long playlistId) {
         Playlist playlist = playlistRepository.findById(playlistId)
                 .orElseThrow(() -> PlaylistNotFoundException.EXCEPTION);

-        List<SongResponse> songs = playlist.getSongIdList().stream()
-                .map(songId -> songRepository.findById(songId)
-                        .map(SongMapper::toSongResponse)
-                        .orElseThrow(() -> SongNotFoundException.EXCEPTION))
-                .collect(Collectors.toList());
+        List<String> songIdList = playlist.getSongIdList();
+        List<Song> foundSongs = songRepository.findAllById(songIdList);
+        
+        if (foundSongs.size() != songIdList.size()) {
+            throw SongNotFoundException.EXCEPTION;
+        }
+        
+        Map<String, Song> songMap = foundSongs.stream()
+                .collect(Collectors.toMap(Song::getId, song -> song));
+        
+        List<SongResponse> songs = songIdList.stream()
+                .map(songMap::get)
+                .map(SongMapper::toSongResponse)
+                .collect(Collectors.toList());

         return PlaylistMapper.toPlaylistDetailResponse(playlist, songs);
     }
src/main/java/org/example/remedy/domain/playlist/domain/Playlist.java (1)

45-48: songId null 체크 추가 권장

addSong 메서드에서 songIdnull인 경우에 대한 방어 코드가 없습니다. null이 전달되면 리스트에 null이 추가될 수 있습니다.

♻️ 제안된 수정
     public void addSong(String songId) {
+        if (songId == null) return;
         if (hasSong(songId)) return;
         this.songIds.add(songId);
     }
src/main/java/org/example/remedy/domain/dropping/repository/DroppingRepositoryImpl.java (2)

15-24: @PersistenceContext@RequiredArgsConstructor 혼용 주의

@RequiredArgsConstructorfinal 필드에 대한 생성자를 생성하지만, @PersistenceContext로 주입되는 EntityManagerfinal이 아니므로 생성자 주입 대상에서 제외됩니다. 이는 의도된 동작이지만, 일관성을 위해 명시적으로 주석을 추가하거나 EntityManager도 생성자 주입 방식으로 통일하는 것을 고려해 주세요.


86-117: LocalDateTime.now() 외부 주입 권장

Line 111에서 LocalDateTime.now()를 쿼리 내부에서 직접 호출하고 있습니다. 이는 테스트 시 시간 제어가 어렵고, 메서드 호출 시점과 쿼리 실행 시점 사이에 미세한 시간 차이가 발생할 수 있습니다. 파라미터로 LocalDateTime을 받거나 Clock을 주입받는 방식을 고려해 주세요.

♻️ 제안된 수정
-    private List<Dropping> findDroppingsByAroundRadius(double longitude, double latitude, double distance) {
+    private List<Dropping> findDroppingsByAroundRadius(double longitude, double latitude, double distance, LocalDateTime now) {
         // 대략적인 위도/경도 범위 계산 (1도 ≈ 111km)
         double latOffset = distance / 111.0;
         double lonOffset = distance / (111.0 * Math.cos(Math.toRadians(latitude)));

         double minLat = latitude - latOffset;
         double maxLat = latitude + latOffset;
         double minLon = longitude - lonOffset;
         double maxLon = longitude + lonOffset;

         // JPQL을 사용한 범위 검색
         String jpql = """
             SELECT d FROM Dropping d
             WHERE d.isDeleted = false
             AND d.expiryDate > :now
             AND d.latitude BETWEEN :minLat AND :maxLat
             AND d.longitude BETWEEN :minLon AND :maxLon
             """;

         return entityManager.createQuery(jpql, Dropping.class)
-                .setParameter("now", LocalDateTime.now())
+                .setParameter("now", now)
                 .setParameter("minLat", minLat)
                 .setParameter("maxLat", maxLat)
                 .setParameter("minLon", minLon)
                 .setParameter("maxLon", maxLon)
                 .getResultList();
     }

호출부도 함께 수정:

     public void createDropping(Dropping dropping) {
         List<Dropping> nearbyDroppings = findDroppingsByAroundRadius(
                 dropping.getLongitude(),
                 dropping.getLatitude(),
-                DROPPING_CONSTRAINT_DISTANCE
+                DROPPING_CONSTRAINT_DISTANCE,
+                LocalDateTime.now()
         );
         // ...
     }
src/main/java/org/example/remedy/domain/dropping/domain/Dropping.java (2)

34-34: content 필드에 @Column 어노테이션 추가를 고려해주세요.

다른 필드들은 모두 명시적으로 @Column을 지정했는데, content만 빠져있어 일관성이 부족합니다. 의도적으로 nullable을 허용하는 것이라면 주석으로 명시하거나, 다른 필드들과 동일하게 어노테이션을 추가하면 코드 가독성이 향상됩니다.

♻️ 일관성을 위한 수정 제안
+    `@Column`(name = "content")
     private String content;

54-67: 생성자 파라미터가 많아 Builder 패턴 적용을 고려해보세요.

10개의 파라미터를 받는 생성자는 호출 시 인자 순서 실수가 발생하기 쉽습니다. Lombok의 @Builder를 활용하면 가독성과 안전성이 향상됩니다.

♻️ Builder 패턴 적용 제안
 `@Entity`
 `@Table`(name = "droppings")
 `@Getter`
 `@NoArgsConstructor`(access = AccessLevel.PROTECTED)
+@Builder
 public class Dropping {

Builder 적용 시 createdAt 기본값 로직은 @Builder.Default@PrePersist로 처리할 수 있습니다:

`@PrePersist`
protected void onCreate() {
    if (this.createdAt == null) {
        this.createdAt = LocalDateTime.now();
    }
}

Comment on lines +118 to +124
private Long parseDroppingId(String droppingId) {
try {
return Long.valueOf(droppingId);
} catch (NumberFormatException ex) {
throw DroppingNotFoundException.EXCEPTION;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

null/blank droppingId 처리 누락으로 NPE 가능 (Line 118-123).

Long.valueOf(null)NullPointerException을 발생시키므로, 현재 예외 매핑이 동작하지 않습니다. null/blank를 먼저 차단하는 것이 안전합니다.

🛠️ 수정 제안
 private Long parseDroppingId(String droppingId) {
+    if (droppingId == null || droppingId.isBlank()) {
+        throw DroppingNotFoundException.EXCEPTION;
+    }
     try {
         return Long.valueOf(droppingId);
     } catch (NumberFormatException ex) {
         throw DroppingNotFoundException.EXCEPTION;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Long parseDroppingId(String droppingId) {
try {
return Long.valueOf(droppingId);
} catch (NumberFormatException ex) {
throw DroppingNotFoundException.EXCEPTION;
}
}
private Long parseDroppingId(String droppingId) {
if (droppingId == null || droppingId.isBlank()) {
throw DroppingNotFoundException.EXCEPTION;
}
try {
return Long.valueOf(droppingId);
} catch (NumberFormatException ex) {
throw DroppingNotFoundException.EXCEPTION;
}
}
🤖 Prompt for AI Agents
In
`@src/main/java/org/example/remedy/domain/comment/application/service/CommentService.java`
around lines 118 - 124, parseDroppingId currently calls Long.valueOf(droppingId)
which throws NPE for null and bypasses the NumberFormatException mapping; update
parseDroppingId to first check for null or blank (e.g., droppingId == null ||
droppingId.trim().isEmpty()) and throw DroppingNotFoundException.EXCEPTION for
those cases, and keep the existing try/catch for NumberFormatException to also
throw DroppingNotFoundException.EXCEPTION.

Comment on lines +26 to 28
@Type(JsonBinaryType.class)
@Column(name = "song_ids", columnDefinition = "jsonb")
private List<String> songIds;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for songIds field usage and null checks
rg -n "songIds" --type java -C 3 src/main/java/org/example/remedy/domain/playlist/

Repository: TEAM-TERA/REMEDY-BACK

Length of output: 7365


🏁 Script executed:

# Look at the Playlist.java file structure
find . -name "Playlist.java" -path "*/playlist/domain/*" | head -1 | xargs wc -l

Repository: TEAM-TERA/REMEDY-BACK

Length of output: 139


🏁 Script executed:

# Read the Playlist.java file
find . -name "Playlist.java" -path "*/playlist/domain/*" | head -1 | xargs cat -n

Repository: TEAM-TERA/REMEDY-BACK

Length of output: 2212


JSONB 필드 null 값으로 인한 NullPointerException 위험

songIds 필드가 JSONB로 저장될 때, DB에 이미 null로 저장된 데이터가 있거나 직접 DB에 삽입된 경우 엔티티 로드 시 null 상태가 됩니다. 생성자의 new ArrayList<>() 초기화는 새로운 엔티티 생성 시에만 적용되며, JPA는 기존 데이터 로드 시 no-arg 생성자를 사용하므로 이 초기화가 발생하지 않습니다.

현재 addSong(), removeSong(), hasSong(), getSongIdList() 등의 메서드들이 null 체크 없이 songIds를 직접 사용하고 있어, null 값이 있는 데이터를 로드하면 NullPointerException이 발생합니다.

해결책: 필드를 기본값으로 초기화하거나(private List<String> songIds = new ArrayList<>();), @PostLoad 메서드를 추가하여 로드 후 null 값을 초기화하세요.

🤖 Prompt for AI Agents
In `@src/main/java/org/example/remedy/domain/playlist/domain/Playlist.java` around
lines 26 - 28, The songIds JSONB field can be null when loading existing rows
causing NPEs in methods like addSong, removeSong, hasSong, and getSongIdList;
fix by ensuring songIds is initialized after JPA loads the entity—either make
the field default to an empty list (private List<String> songIds = new
ArrayList<>();) or add a `@PostLoad` lifecycle method in Playlist (e.g.,
postLoad()) that checks if songIds is null and sets it to new ArrayList<>(), so
all usages of songIds in addSong/removeSong/hasSong/getSongIdList are safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant