아키텍처 의사결정: 성능(Speed)과 데이터 무결성(Stability)의 트레이드 오프
엔지니어링은 언제나 트레이드 오프(Trade-off)의 연속입니다. doeatfit의 회원 탈퇴 프로세스를 설계하면서, 우리는 “극한의 성능” 과 “완벽한 데이터 무결성” 사이에서 중요한 의사 결정을 내려야 했습니다.
1. 딜레마: 복잡한 연관 관계와 고아 객체
알림 시스템에서 사용자는 ‘보낸 사람(Sender)‘일 수도 있고 ‘받은 사람(Receiver)‘일 수도 있습니다. 회원이 탈퇴할 때 발생할 수 있는 시나리오는 복잡합니다.
-
Case 1: 내가 받은 알림은 삭제되어야 한다.
-
Case 2: 내가 보낸 알림은 상대방이 아직 읽지 않았을 수 있으므로 남겨두되, ‘보낸 사람’ 정보만
NULL처리해야 한다. -
Case 3 (Edge Case): 만약 주고받은 양쪽 사용자가 모두 탈퇴했다면? 그 알림은 누구에게도 필요 없는 ‘고아 객체’가 되므로 삭제되어야 한다.
2. 선택지 비교
Option A: 순수 SQL Bulk 연산 (속도 중심)
-
방식: 복잡한
WHERE조건절을 가진 SQL을 직접 실행. -
장점: 매우 빠름. 메모리 사용량 거의 없음.
-
위험요소: ‘Case 3’와 같은 복잡한 로직을 SQL에 담기 어려움. A와 B가 동시에 탈퇴하는 **동시성 상황(Race Condition)**에서 데이터가 꼬이거나 고아 데이터가 남을 위험이 큼.
Option B: Hybrid 방식 (안전성 중심) - [최종 채택 ✅]
-
방식:
-
관련 데이터를 메모리에 조회(
Fetch). -
Java 코드로 “이것은 고아 객체인가?”를 판단(
Logic). -
판단된 결과에 따라 Batch 삭제 또는 Dirty Checking 수행(
Action).
-
3. 핵심 트레이드 오프 분석
| 구분 | Bulk 연산 (Option A) | Hybrid 방식 (Option B) |
|---|---|---|
| 성능 (속도) | 압도적으로 빠름 • 단 3번의 쿼리로 완료 | 상대적으로 느림 • N개의 데이터 로드 (SELECT) • 변경된 수만큼 UPDATE 발생 |
| 데이터 정합성 | 위험성 존재 • 동시성 문제에 취약 • 비즈니스 로직이 SQL에 파편화됨 | 매우 높음 • 트랜잭션 내 일관성 보장 • 자바 코드로 복잡한 조건 완벽 제어 |
| 유지보수성 | 낮음 • 로직 변경 시 SQL 수정 필요 | 높음 • 엔티티 메서드( isOrphan)만 수정하면 됨 |
4. 구현 상세: Hybrid 방식 (NotificationService.java)
우리는 Option B를 선택했습니다. 탈퇴는 빈번한 이벤트가 아니므로, 약간의 성능 비용을 지불하고 데이터가 오염되지 않는 안전함을 사는 것이 합리적이기 때문입니다. 단, 삭제 성능을 보완하기 위해 deleteAllInBatch를 활용했습니다.
@Transactional
public void cleanAllRelatedNotifications(Long userId) {
// 1. [Fetch] 메모리 로딩: 연관된 모든 알림을 한 번에 가져옴 (Fetch Join)
List<NotificationEntity> relatedNotifications =
notificationRepository.findAllByReceiverUserIdOrSenderUserId(userId, userId);
List<NotificationEntity> notificationsToDelete = new ArrayList<>();
// 2. [Logic] 비즈니스 로직 수행 (Java 레벨)
for (NotificationEntity notification : relatedNotifications) {
// 관계 끊기 (Dirty Checking 대상)
if (isReceiver(notification, userId)) notification.severReceiver();
if (isSender(notification, userId)) notification.severSender();
// 고아 객체 판단: Java 메서드로 명확하게 정의
if (notification.isOrphan()) {
notificationsToDelete.add(notification);
}
}
// 3. [Action] 삭제는 Batch로 처리하여 성능 최적화
if (!notificationsToDelete.isEmpty()) {
// DELETE FROM notification WHERE id IN (...)
notificationRepository.deleteAllInBatch(notificationsToDelete);
}
}🎯 결론
이 설계는 Java의 논리적 표현력(안전성) 과 JPA의 Batch 기능(성능) 을 결합한 형태입니다.
단순히 “속도”만을 쫓는 것이 아니라, “삭제(Delete)는 Bulk 연산으로 최적화” 하되, “복잡한 관계 정리(Update)는 Dirty Checking을 통해 안전하게 처리” 하는 하이브리드 방식을 통해 엔지니어링의 균형을 맞췄습니다.