도메인 주도 설계와 변경 감지 (Dirty Checking) 활용
비즈니스 로직을 구현할 때, “데이터의 상태 변경을 누가 책임질 것인가?” 는 중요한 아키텍처 결정 사항입니다. 저는 서비스 레이어의 복잡도를 낮추고 객체 지향적인 설계를 유지하기 위해 JPA의 Dirty Checking (변경 감지) 기능을 적극적으로 활용하기로 결정했습니다.
1. 구현 간 고려사항: 절차지향 vs 객체지향
알림의 ‘읽음 처리’나 ‘요청 수락/거절’ 같은 상태 변경 로직을 구현하는 방법은 크게 두 가지가 있습니다.
-
Option A (절차지향 - Transaction Script):
서비스 레이어에서 데이터를 꺼내 Setter로 값을 바꾸고, 명시적으로 repository.save()를 호출하여 저장한다.
-
Option B (객체지향 - Domain Model):
엔티티에게 “상태를 변경하라”고 명령(메시지 전송)하고, 저장 과정은 JPA에게 위임한다.
우리는 Option B를 선택했습니다. 비즈니스 로직이 엔티티 안에 응집되어야 유지보수가 쉽고, 코드가 더 직관적이기 때문입니다.
2. 구현 패턴: 비즈니스 메서드와 트랜잭션의 결합
NotificationService의 코드를 보면 save() 메서드가 보이지 않습니다. 대신 트랜잭션 범위 내에서 엔티티의 비즈니스 메서드만 호출합니다.
📝 Implementation Code (NotificationService.java)
/**
* 알림 읽음 처리 로직
* 별도의 save() 호출 없이, 트랜잭션 종료 시점에 변경 사항이 DB에 반영됨 (Dirty Checking)
*/
@Transactional
public void markAsRead(Long userId, UUID notificationUuid) {
// 1. 조회 (Snapshot 생성)
NotificationEntity notification = notificationRepository.findByUuidEntityUuid(notificationUuid)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 알림입니다."));
// 2. 권한 검증
if (!notification.getReceiver().getUserId().equals(userId)) {
throw new SecurityException("본인의 알림만 읽음 처리할 수 있습니다.");
}
// 3. 상태 변경 (Business Logic 수행 - Entity에게 메시지 전달)
// Service가 필드를 직접 수정(setRead(true))하지 않고, 행동을 명령함
notification.read();
// 4. 메서드 종료 및 트랜잭션 Commit 시점에 UPDATE 쿼리 자동 수행
}3. 설계의 이점 (호텔 룸 서비스 비유) 🏨
이 방식은 호텔 체크아웃 프로세스와 유사하게 동작하도록 설계되었습니다.
-
체크인 (Select): 객실(Entity)에 들어갈 때, 호텔 매니저(JPA Context)는 방의 초기 상태를 기록해둡니다. (Snapshot)
-
투숙 중 (Logic Execution): 손님(Developer)은 미니바 음료를 마시거나 가구를 재배치합니다. 이때 매니저에게 일일이 “나 콜라 마셨어”라고 보고(
save)하지 않습니다. 단순히 행동만 합니다. -
체크아웃 (Commit): 퇴실할 때 매니저가 처음 상태와 비교하여 변경된 내역을 확인하고, 자동으로 비용을 청구하거나 원상복구(
Update SQL)합니다.
이 설계를 통해 개발자는 **“DB에 언제 저장할까?”**라는 인프라 관점의 고민을 덜고, **“이 객체의 상태가 어떻게 변해야 하는가?”**라는 비즈니스 본질에만 집중할 수 있게 되었습니다.