아키텍처 의사결정: 성능(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 방식 (안전성 중심) - [최종 채택 ✅]

  • 방식:

    1. 관련 데이터를 메모리에 조회(Fetch).

    2. Java 코드로 “이것은 고아 객체인가?”를 판단(Logic).

    3. 판단된 결과에 따라 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을 통해 안전하게 처리” 하는 하이브리드 방식을 통해 엔지니어링의 균형을 맞췄습니다.