문제 상황
PlaylistTrack의 순서 변경 기능을 구현하면서, 초기에는 트랙 순서를 변경할 때마다 전체 트랙의 position을 재정렬하는 방식을 사용하였다.
private void reorderPositions(List<PlaylistTrack> playlistTracks) {
for (int i = 0; i < playlistTracks.size(); i++) {
playlistTracks.get(i).moveToTemporaryPosition(-(i + 1));
}
playlistTrackRepository.flush();
for (int i = 0; i < playlistTracks.size(); i++) {
playlistTracks.get(i).updatePosition(i + 1);
}
playlistTrackRepository.flush();
}
이 방식은 (playlist_id, position) 유니크 제약으로 인해 순서 변경 과정에서 발생할 수 있는 충돌을 방지하기 위해 도입되었다.
원인 분석
해당 구조는 기능적으로는 문제가 없었지만, 다음과 같은 한계를 가지고 있었다.
- 트랙 하나만 이동해도 전체 트랙이 update됨
- 데이터 규모가 커질수록 불필요한 DB 부하 증가
- 실제로는 일부 구간만 영향을 받음에도 전체 재정렬 수행
- 유니크 제약 충돌을 피하기 위해 향상
- 항상 음수 변환 → 재정렬 과정이 필요
즉, 변경 범위 대비 과도한 update가 발생하는 구조였다.
해결 방향
전체 재정렬 방식을 유지하는 대신,
순서 변경 시 실제 영향받는 구간만 갱신하는 방식으로 개선하였다.
설계 기준은 다음과 같다.
- position은 기존과 동일하게 정수 기반 유지
- 이동 대상 기준으로 영향 범위만 계산
- 해당 범위의 트랙만 position 변경
- 유니크 제약 충돌 방지를 위해 영향 범위에 대해서만 임시 음수 position 적용
해결 과정
1. 부분 재정렬 방식 도입
전체 재정렬 대신 이동 구간만 처리하도록 변경하였다.
if (oldPosition < newPosition) {
moveDown(playlistId, target, oldPosition, newPosition);
} else {
moveUp(playlistId, target, oldPosition, newPosition);
}
예를 들어
- 2 → 4로 이동하는 경우
- 3, 4 위치 트랙만 이동
- target만 최종 위치로 이동
전체가 아닌 영향 범위만 갱신
2. 유니크 제약 충돌 처리 범위 축소
기존에는 전체 트랙을 음수로 변경했지만, 개선 후에는 영향 범위만 처리하도록 수정하였다.
target.moveToTemporaryPosition(-oldPosition);
for (PlaylistTrack track : affectedTracks) {
track.moveToTemporaryPosition(-track.getPosition());
}
playlistTrackRepository.flush();
3. position 재할당 로직 개선
이동 방향에 따라 영향을 받는 트랙만 position을 조정하도록 변경하였다.
for (PlaylistTrack track : affectedTracks) {
int originalPosition = -track.getPosition();
track.updatePosition(originalPosition - 1);
}
target.updatePosition(newPosition);
"전체 정렬"이 아닌 영향 범위 기반 업데이트
4. 삭제 로직 개선
삭제 로직 역시 동일한 방식으로 개선하였다.
List<PlaylistTrack> affectedTracks =
playlistTrackRepository.findByPlaylistIdAndPositionGreaterThanOrderByPositionAsc(
playlistId, deletedPosition
);
for (PlaylistTrack track : affectedTracks) {
track.updatePosition(track.getPosition() - 1);
}
삭제 이후에도 전체 재정렬 제거
5. Repository 쿼리 분리
부분 재정렬을 위해 필요한 범위만 조회하도록 쿼리를 분리하였다.
findByPlaylistIdAndPositionBetweenOrderByPositionAsc
findByPlaylistIdAndPositionGreaterThanOrderByPositionAsc
전체 조회 제거 → 효율성 개선
결과
기존 구조와 비교하면 다음과 같은 개선이 이루어졌다.
| 구분 | 기존 | 개선 |
| 재정렬 방식 | 전체 재정렬 | 부분 재정렬 |
| DB update | 전체 | 영향 범위 |
| 성능 | 데이터 증가 시 비효율 | 확장성 개선 |
| 충돌 처리 | 전체 음수 처리 | 부분 음수 처리 |
느낀 점
이번 개선을 통해 단순한 정렬 로직이라고 생각했던 문제가, 실제로는 DB 제약, 성능, 그리고 설계가 함께 얽힌 문제라는 것을 알게 되었다.
초기에는 "동작하는 코드"에 집중하여 전체 재정렬 방식으로 구현했지만, 문제를 다시 바라보면서 변경 범위 대비 과도한 작업이 발생하고 있다는 점을 인지하게 되었다.
이후 로직을 개선하는 과정에서 단순히 기능을 구현하는 것을 넘어,
- 어떤 데이터가 실제로 영향을 받는지
- 불필요한 update를 어떻게 줄일 수 있는지
- DB 제약 조건을 고려한 안정적인 처리 방식은 무엇인지
를 함께 고민하게 되었다.
그 결과, 전체 재정렬 대신 영향 범위만 갱신하는 방식으로 구조를 개선할 수 있었고, 보다 효율적인 데이터 처리 방식에 대해 고민해볼 수 있었다.
앞으로도 기능 구현에 그치지 않고, 데이터 흐름과 변경 범위를 기준으로 설계를 고민하는 방식을 지속적으로 적용해보고자 한다.
'Fivefy 프로젝트 > 트러블슈팅' 카테고리의 다른 글
| Top N 제한을 DB로 이동하며 조회 성능 개선한 경험 (0) | 2026.04.28 |
|---|---|
| 인기 차트 생성 로직 리팩토링과 조회 흐름 정리 (0) | 2026.04.28 |
| Soft Delete 구조에서 DB 유니크 제약을 통한 데이터 무결성 보완 (0) | 2026.04.22 |
| Soft Delete 기반 플레이리스트 정책 개선 (1) | 2026.04.22 |
| 유효 재생 기준 적용과 Projection 기반 집계 구조 개선 과정 (0) | 2026.04.21 |