도메인에 의존하지 않는 고아 파일 정리 시스템
문제
기존 파일 첨부 기능은 단순한 방식으로 구현되어 있었다.
파일명을 난수로 생성한 뒤 S3-compatible 저장소에 업로드하고, 서비스에서는 생성된 public URL만 본문에서 참조했다.
파일을 업로드하고, 반환된 URL을 본문에 넣으면 끝나는 구조였다.
하지만 이 방법에는 다음과 같은 고아 파일 문제가 있다.
게시글을 수정하면서 이미지가 제거되어도 파일이 저장소에 남는다.
임시 저장 글을 여러 번 수정한 뒤 최종 저장하면, 중간에 사용했다가 제거된 파일이 저장소에 남는다.
글을 삭제하더라도 본문에서 참조하던 파일이 저장소에 남는다.
본문에서는 더 이상 사용하지 않지만 저장소에는 계속 남아 있는 파일이 생기는 것이다.
이 문제를 해결하기 위해 파일 정리 시스템이 필요해졌다.
첫 번째 접근
처음 떠올린 방법은 도메인마다 파일 메타데이터를 관리하는 것이었다.
파일명, 저장 위치, 참조 상태를 별도 테이블에 저장하고, 파일이 더 이상 필요 없어지는 시점에 상태를 DETACHED로 변경한다.
그리고 정기적으로 파일 정리 시스템을 배치로 실행해서 메타데이터가 DETACHED인 파일에 대해서 저장소로부터 삭제하는 것이다.
구조는 단순하다.
파일 업로드 시 메타데이터를 저장한다.
도메인에서 파일을 더 이상 참조하지 않으면 상태를 변경한다.
배치가 해당 파일을 삭제한다.
하지만 이 방식은 서비스가 늘어날수록 관리 부담이 커진다.
첫 번째 접근의 한계
이 방식은 구조는 단순하지만, 서비스가 늘어날수록 관리 부담이 커진다.
운영 중인 서비스에서는 여러 도메인에서 파일을 사용하고 있다. 게시글, 발음교정, 첨부 문서처럼 여러 도메인에서 파일 메타데이터 저장 로직을 추가해야 한다. 또한 어느 시점에 DETACHED로 변경하는지도 서비스마다 다르다.
결국 파일 정리 시스템이 도메인 구조를 알아야 한다.
문서화 부담도 커진다. 각 서비스마다 파일 관리 방식이 생기고, 그 방식이 변경될 때마다 문서를 수정해야 한다. 한 번 만든 로직을 계속 기억하고 관리해야 하는 구조가 된다.
그래서 파일 정리 시스템이 도메인 구조를 모르는 방향이 필요했다.
두 번째 접근
필요한 것은 파일 정리 시스템이 각 서비스의 도메인 내부 구조를 모르는 것이다. 대신 다음의 정보만을 받도록 약속한다.
도메인이 최종적으로 어떤 파일을 가지게 되는가.
서비스 입장에서는 도메인이 현재 참조하는 최종 파일 목록을 전달하고, 파일 정리 시스템은 이 정보를 받기만 하면 된다. 이렇게 하면 위의 간단한 해결책에서 발생하는 관리 문제가 완화된다.
파일 정리 시스템은 다음의 기능을 갖는다.
도메인의 파일 목록을 전달받는다.
도메인의 기존 파일 목록과 전달 받은 파일 목록을 비교한다.
전달 받은 파일 목록에 없는 파일을 정리 대상으로 판단한다.
이벤트 기반 모델
도메인 모델이 저장될 때, 해당 도메인이 최종적으로 참조하는 파일 목록을 이벤트 메시지로 전달하는 방식을 사용했다.
예를 들어 게시글이 저장될 때 다음과 같은 이벤트 메시지를 발행한다.
{
"resourceType": "BLOG_POST",
"resourceId": "21",
"bucket": "blog",
"basePath": "public/draft/21/",
"objects": [
"a.png",
"b.png",
"c.png"
]
}이 이벤트 메시지의 의미는 단순하다.
public/draft/21/ 경로 아래에서 현재 게시글이 최종적으로 참조하는 파일은 a.png, b.png, c.png라는 것이다. basePath는 반드시 하나의 리소스 단위로 격리되어야 한다. 여러 리소스가 같은 경로를 공유하면, 한 리소스의 정리 이벤트가 다른 리소스의 파일을 삭제할 수 있다.
파일 정리 시스템은 이 이벤트 메시지를 받으면 저장소의 실제 파일 목록을 조회했다.
저장소의 실제 파일 목록
- a.png
- b.png
- c.png
- d.png
- e.png그리고 이벤트 메시지로 전달받은 파일 목록과 비교했다.
도메인이 참조하는 파일 목록
- a.png
- b.png
- c.png이 경우 d.png, e.png는 더 이상 도메인이 참조하지 않는 파일로 판단할 수 있었다.
정리 대상 파일
- d.png
- e.png이 방식의 장점은 파일 정리 시스템이 도메인별 삭제 조건을 알 필요가 없다는 점이다.
파일 정리 시스템은 게시글이 수정되었는지, 댓글이 삭제되었는지, 프로필 이미지가 교체되었는지 알지 못한다.
그저 특정 리소스가 현재 참조해야 하는 최종 파일 목록만 알고 있으면 된다.
마주친 문제점과 해결
이벤트 메시지 순서 문제
도메인 모델에 수정 요청이 동시에 발생하면 이벤트 메시지도 여러 개 생성되어 순서가 뒤바뀌면서 정합성 문제가 발생할 수 있다. 이런 경우에는 이벤트 메시지에 도메인 모델에 대한 버전 정보를 추가로 전달하고 파일 정리 시스템은 도메인 DB를 직접 조회하지 않는다. 대신 이벤트 메시지에 포함된 버전 값을 기준으로 별 마지막 처리 버전을 자체 저장소에 기록한다. 이후 더 낮거나 같은 버전의 이벤트 메시지가 도착하면 이미 처리된 이벤트로 보고 무시한다.
서비스별 이벤트 메시지 규격 문제
서비스별로 도메인 모델이 다르기 때문에 이벤트 메시지 규격이 서로 다르다. 다른 서비스가 전달하는 이벤트 메시지를 핸들링하는 과정이 필요하게 되는데, 이렇게 되면 파일 정리 시스템이 다시 서비스에 의존적이게 된다.
처음에는 중간에 별도의 이벤트 메시지 필터 시스템을 두는 방법도 생각할 수 있었다. 그렇지만 결국 서비스별 모델을 이해해야 하는 문제는 여전히 남아 오버헤드만 발생시키는 것이라는 생각이 들었다. 대신 각 서비스가 공통된 규격의 이벤트 메시지를 발생시키도록 했다.
서비스가 이벤트 규격 메시지에 의존하는 문제가 생겼다고 볼 수 있지만, 의존 방향이 달라지고 하나의 규격만 따르면 되기 때문에 복잡도가 훨씬 내려간다고 생각한다.
이렇게 하면 서비스는 공통 메시지 규격에 의존하지만, 파일 정리 시스템은 개별 서비스의 도메인 구조에 의존하지 않는다.
새로운 서비스가 추가되더라도 공통 이벤트 규격만 맞추면 파일 정리 시스템의 처리 로직은 변경하지 않아도 된다.
정리
이 구조에서 중요한 기준은 다음과 같다.
파일 정리 시스템은 도메인 내부 구조를 이해할 필요가 없다.
서비스는 도메인의 최종 파일 목록만 전달한다.
파일 정리 시스템은 도메인별 마지막 처리 버전을 저장한다.
이벤트 메시지 순서가 뒤바뀌어도 오래된 이벤트는 무시한다.
서비스는 파일 정리용 공통 메시지 규격에 맞춰 이벤트를 발행한다.
파일 정리 시스템은 서비스별 도메인 이벤트를 직접 해석하지 않는다.
basePath는 도메인 단위로 격리되어야 한다.
결국 이 설계는 의존성을 완전히 없애는 구조가 아니다.
대신 의존성의 방향을 조정하는 구조다.
파일 정리 시스템이 각 서비스의 도메인 구조에 의존하지 않도록 하고, 각 서비스가 파일 정리용 공통 메시지 계약에 의존하도록 만들었다.
그 결과 파일 정리 시스템은 서비스 도메인 내부 구조에 의존하지 않고, 저장소 기준으로 고아 파일을 정리할 수 있게 되었다.