STATUS 를 무시하지 말자

김태윤·2025년 12월 10일

개발 노트

목록 보기
1/6

문제 상황

문서 업로드 시 발생하는 예외를 어떻게 처리할 것인가?

기존 구조


다음과 같은 문서 업로드 페이지에서, 문서 업로드를 하면

아래와 같이 업로드 한 문서 조회가 가능하다.

이 과정에서, 서버(Minio) -> VectorDB (Milvus) -> DB (Mysql) 에 순차적으로 저장하는 과정을 거치게 된다.

발생한 이슈


하지만 만약 Minio 에서 오류가 난다면? 혹은 Milvus에서 오류가 난다면?

파일 업로드를 Rollback 해야 하나? 아니면 이미 업로드 된 파일들을 다시 삭제해줘야 하나?

하는 고민들을 했다.

고려해본 해결책

  1. 예외 발생 시 Rollback
  2. status 컬럼 추가

방법 1. 예외 발생 시 Rollback


만약 VectorDB 저장 단계에서 예외가 발생한다고 하자.

그럴 경우 앞서 성공적으로 완료된 Minio 업로드를 Rollback 해주어야 한다.

이를 코드로 구현한다면 다음과 같은 Try-Catch 기반의 흐름을 갖게 된다.


def upload_pipeline(file, metadata):
    uploaded_file_path = None
    vector_id = None

    try:
        # 1단계: Minio 업로드
        uploaded_file_path = minio_client.upload(file)
        
        # 2단계: Milvus 벡터 저장
        vector = convert_to_vector(file)
        vector_id = milvus_client.insert(vector)

        # 3단계: MySQL 메타데이터 저장 (최종)
        db_repo.save(metadata, uploaded_file_path, vector_id)
        
        return "Success"

    except Exception as e:
        # 예외 발생 시: 역순으로 보상 트랜잭션(Rollback) 수행
        
        log.error(f"Pipeline failed: {e}")

        # Milvus에 데이터가 들어갔다면 삭제
        if vector_id:
            milvus_client.delete(vector_id)
            log.info("Rollback: Milvus data deleted")

        # Minio에 파일이 올라갔다면 삭제
        if uploaded_file_path:
            minio_client.delete(uploaded_file_path)
            log.info("Rollback: Minio file deleted")
            
        raise e # 클라이언트에게 에러 전파

장점
1. 구현이 직관적이고 빠름
2. 코드 레벨에서 제어가 가능하다.
3. 동기식 처리이므로 클라이언트에게 성공/실패 응답을 줄 수 있다.

단점
1. 만약 롤백 실패 시에도 오류가 발생한다면? 또 같은 상황이 발생한다.
2. Embedding 과정이 보통 약 1분 정도 소요되는데, 그 시간동안 사용자가 기다려야 한다.

방법 2. DB 확장 ( 상태 컬럼 추가 )


방법 1 (Rollback) 으로 하더라도, 여전히 같은 상황이 발생한다는 점과,
기존에 성공적으로 완료한 작업들도 다 날려버려야 한다는 점,
그리고 어디서 오류가 발생했는지에 대한 기록이 남지 않는다는 아쉬움이 있었다.

그래서 데이터베이스를 확장하여, 상태값을 저장하는 방식을 생각했다.

[변경 사항]
1. File 테이블에 Status 컬럼 추가 (Enum : Pending, Success, Failed)
2. 파일 업로드 시 Flow 변경 ( DB insert Pending -> minio -> milvus -> DB update Success)

해결책 채택

Status 를 관리하자

결론적으로, DB 확장하는 방법을 선택했다.
코드 레벨에서 수정할 수 있는 방법 1과 달리,
테이블 구조를 변경과 Entitiy 객체를 수정해야 한다는 부담이 있었지만,

다음과 같은 장점으로 선택하게 되었다.

  1. 오류가 발생한 위치를 기록할 수 있다
    어디서 문제가 발생했는지 로그 확인 전, UI를 통해 확인할 수 있다는 장점이 있음
  1. 장기적으로 확장성이 좋음
    Failed 상태인 데이터를 Retry 하거나, Scheduling 으로 삭제하거나 등의 다양한 방법을 선택할 수 있음
    또한 업로드를 비동기로 처리할 때 Pending 중임을 알릴 수도 있기 때문에,
    비동기 처리도 가능하다.

문제 상황 2

파일 삭제 시 정합성의 오류

하지만 또 다른 문제가 발생했다.
"파일을 다운로드를 했는데 404 에러라고 떠. 확인해줘"

원인을 파악해보니, RDB에는 File Data가 남아있지만, Minio or VectorDB에서 존재하지 않아서 생기는 문제였다.

일전에 Mysql 서버가 다운된 적이 있었는데, 그 때 파일 삭제 요청을 했을 때,

Minio / Vector DB에서는 삭제가 되었지만, Mysql 에서는 삭제가 되지 않은 것 같았다.

즉, 파일 삭제 시 정합성 을 지켜주어야 하는 오류였다.

임시 해결책 (Hotfix)

파일 삭제 순서 변경

기존에는 Minio와 Vector DB 에서 삭제를 완료하고, 최종적으로 Mysql 에서 삭제를 하도록 하는 순서였다.

하지만 일단 임시로, Mysql 에서 먼저 지우고, 그 후에 Minio 와 Vector DB에서 삭제하도록 순서를 바꾸었다.

그 결과, 파일을 삭제하면 RDB 에서 가장 먼저 삭제되므로,
특정 부분에서 오류가 발생하더라도 사용자에게는 존재하지 않는 값이 노출이 되는 상황을 막을 수 있었다.

하지만 근본적인 해결책은 아니었다.
File Storage 나 Vector DB 에서 오류가 발생한다면,
RDB 에서 추적이 안되는 쓰레기 데이터가 생긴다는 더욱 치명적인 문제가 남아있었다.

따라서 해당 구조에 대한 개선이 필요했다.

Status 컬럼 사용

이 때, 이전에 확장했던 Status 를 쓰면 어떨까? 라는 생각이 들었다.

현재는 PENDING, FAILED, SUCCESSED 만 관리하고 있지만, 이를 확장해

삭제 요청 시에 DELETING, 삭제 완료 시에 DELETED 를 기록한다면,

Soft Delete + 올바른 삭제가 되지 않은 값들을 RETRY 가 가능하다.

배운 점

사실 Status Column 을 추가하는 것은 어렵지 않은 아이디어다.

하지만 DB 설계 시에 굳이 필요할까? 싶어서 크게 고려하지 않은 내용이었는데,

이번 경험으로 Status 컬럼이 좀 더 남다르게 느껴질 것 같다.

DB 설계 등등 을 진행하면서 항상 어려운 것이,

어디까지가 지금 단계에서 필요한 설계이고, 어디부터가 Over Engineering 인지,

그 간격을 파악하는 것이 어렵다.

개발 기간과 시스템의 규모, 예상 사용자 수 등을 고려하면서 적절한 판단을 내리는 것은 어려운 만큼 중요하다.

이번 프로젝트에서 느낀 이러한 경험들이 쌓이면서,
적절한 판단을 내릴 수 있는, 경험에서 나오는 판단력을 기르는 것이 중요할 것 같다.

0개의 댓글