CRUD의 함정: 간단한 업데이트가 숨은 위험
카테고리
프로그래밍/소프트웨어 개발
서브카테고리
웹 개발
대상자
소프트웨어 개발자, 특히 Ruby on Rails 또는 ORM 기반의 백엔드 개발자
난이도: 중간 (데이터베이스 트랜잭션, 콜백 관리, 로깅 전략 이해 필요)
핵심 요약
- 레이스 컨디션으로 인한 데이터 손실 예방:
with_lock
또는 비관적 동시성 제어 사용 - 변경 이력 추적 위해:
AuditLog
또는 CDC(Change Data Capture) 도구(예: Debezium) 활용 - 업데이트 시 부작용 방지: 콜백 대신 이벤트 기반 아키텍처로 분리
- 부분 업데이트 시
update_columns
또는assign_attributes
사용 (전체 필드 업데이트 방지)
섹션별 세부 요약
1. 레이스 컨디션과 데이터 손실
user.update!(balance: balance + 50)
와 같은 read-modify-write 패턴은 다중 스레드 환경에서 데이터 손실 발생- 예: 스레드 A와 B가 동일한
balance
필드를 동시에 업데이트 시 최종 값이 130 (예상: 180)으로 오버라이드 - 해결책:
with_lock
블록 사용 (비관적 동시성 제어)version
필드를 사용한 비관적 동시성 제어 (예:update!(balance: ..., version: user.version)
)
2. 로깅 부족과 이력 추적
- 대부분의 앱은 변경 이력 로깅을 하지 않거나, 로그가 기한이 있는 경우 발생
- 예:
UPDATE orders SET status = 'refunded' WHERE id = 123;
실행 시 변경 원인, 시점, 사용자 정보 누락 - 해결책:
after_update
콜백으로AuditLog
생성 (예:AuditLog.create!(action: "update", old_values: previous_changes, user: Current.user)
)- CDC 도구 (예: Debezium) 사용: 데이터베이스 변경을 Kafka로 스트리밍
3. 콜백 기반 업데이트의 부작용
after_update
콜백은 다중 서비스 호출로 인해 예상치 못한 부작용 발생 (예:send_welcome_email
,update_search_index
,notify_admin
동시 실행)- 해결책:
- 명시적 워크플로우 사용 (예:
UserActivator.new(user).call
) - 이벤트 기반 분리:
EventBus.publish(UserActivated.new(user_id: user.id))
4. 부분 업데이트 vs 전체 필드 업데이트
user.save!
는 모든 변경된 필드를 업데이트 (예:name
변경 시email
변경 중인 스레드와 충돌 가능성)- 해결책:
update_columns(name: "Alice")
(콜백 무시, 특정 필드만 업데이트)assign_attributes(name: "Alice")
+save(only: [:name])
(업데이트 대상 필드 명시)
결론
- CRUD는 "최종 쓰기 승리" 전략이 필요한 경우에만 사용 (예: 관리자 대시보드, 프로토타입)
- 단계적 개선 전략:
- 1개 모델에 감사 로깅 추가
- 1개 콜백을 서비스로 대체
- 이벤트 기반 아키텍처 전환 전에 현재 시스템의 문제점 측정
> 핵심 팁: update!
대신 update_columns
또는 assign_attributes
사용, 이벤트 기반 분리로 콜백 중복 제거