Home Pick Place - 공통 파일 업로드 기능 리팩토링
Post
Cancel

Pick Place - 공통 파일 업로드 기능 리팩토링

🚀 공통 파일 업로드 기능 리팩토링 - 사용자 정보 추가 및 리팩토링

공통 파일 시스템 사용자 정보 추가 및 리팩토링


📅 개발 기간

  • 시작: 2026.05.25
  • 종료: 2026.05.25
  • 총 소요 시간: 3.5H

🎯 개발 배경 (Why)

  • 회원 기능 개발이 추가됨에 따라 파일 업로드 시 사용자 정보를 함께 저장할 필요가 생김.
  • 기존 파일 업로드 로직은 신규 업로드와 기존 파일 추가 업로드가 하나의 메서드에서 처리되고 있어 흐름이 명확하지 않음.
  • 파일 업로드, 수정, 삭제, 다운로드 로직이 하나의 서비스에 길게 작성되어 있어 가독성이 떨어지고 유지보수가 어려움.
  • 최근 학습 중인 클린 코드 관점을 적용하여 메서드 책임을 분리하고, 읽기 쉬운 구조로 개선하고자 함.

🧩 요구사항 (Requirements)

✔️ 기능 요구사항

  • 파일 업로드 시 어떤 사용자가 업로드했는지 추적할 수 있어야 한다.
  • 파일 수정은 기존 UUID가 존재하는 경우에만 가능해야 한다.
  • 파일 수정 시 기존 파일 묶음의 마지막 순번 이후부터 파일 순번이 증가해야 한다.
  • 파일 수정 시 현재 요청한 사용자가 해당 파일 묶음에 접근할 권한이 있는지 검증해야 한다.
  • 파일 삭제 시 요청한 사용자와 파일 소유자가 동일한지 권한 검증이 필요하다.
  • 파일 다운로드 시 DB에 요청한 파일 데이터가 모두 존재하는지 검증해야 한다.
  • 단일 파일 다운로드와 다중 파일 ZIP 다운로드를 구분해서 처리해야 한다.
  • ZIP 다운로드 시 동일한 원본 파일명이 존재할 경우 중복되지 않도록 파일명을 보정해야 한다.

⚙️ 비기능 요구사항

  • 읽기 쉬운 코드 구조를 지향한다.
  • 하나의 메서드가 너무 많은 책임을 가지지 않도록 메서드를 분리한다.
  • 파일 시스템 작업과 DB 작업의 흐름을 명확하게 분리한다.
  • 예외 상황에서 가능한 한 명확한 커스텀 예외를 발생시킨다.
  • 단위 테스트를 통해 주요 성공/실패 케이스를 검증한다.

🛠️ 구현 내용 (Implementation)

🔹 핵심 로직

파일 업로드 시

  1. 새로운 UUID 생성
    • 신규 업로드는 항상 새로운 UUID를 생성한다.
    • 같은 업로드 요청으로 저장되는 파일들은 하나의 UUID를 공유한다.
    • 파일 순번은 1부터 시작한다.
  2. UUID 기반 디렉터리 생성
    • 설정된 파일 저장 경로 하위에 UUID 디렉터리를 생성한다.
    • 디렉터리가 이미 존재하면 그대로 사용한다.
  3. 파일 메타데이터 생성
    • 원본 파일명
    • 저장 파일 경로
    • 파일 확장자
    • UUID
    • 파일 순번
    • 파일 타입
  4. 확장자 검증
    • 허용된 확장자인지 검증한다.
    • 허용되지 않은 확장자가 포함되어 있으면 예외를 발생시킨다.
  5. 로컬 디스크에 물리 파일 저장

  6. DB에 파일 메타데이터 저장
    • 파일 테이블에 원본 파일명, 저장 경로, 확장자, UUID, 순번, 업로드 사용자 정보를 저장한다.
    • 파일 타입이 PHOTO라면 사진 파일 매핑 테이블에 저장한다.
    • 파일 타입이 USER라면 회원 파일 매핑 테이블에 저장한다.
    • 파일 테이블 저장과 매핑 테이블 저장은 하나의 트랜잭션으로 처리한다.

파일 삭제 시

  1. UUID 검증
    • UUID가 null이거나 빈 값이면 예외를 발생시킨다.
    • UUID 형식이 올바르지 않으면 예외를 발생시킨다.
  2. 기존 파일 목록 조회
    • 전달받은 UUID로 기존 파일 목록을 조회한다.
    • 기존 파일이 존재하지 않으면 수정할 수 없는 요청으로 판단한다.
  3. 권한 검증
    • 기존 파일의 소유자와 현재 요청 사용자가 동일한지 검증한다.
    • 다른 사용자의 파일 묶음에 파일을 추가할 수 없도록 방지한다.
  4. 파일 순번 계산
    • 기존 파일 목록 중 가장 큰 fileSeq를 찾는다.
    • 새로 추가되는 파일은 max(fileSeq) + 1부터 순번을 부여한다.
  5. 파일 저장
    • 이후 흐름은 신규 업로드와 동일하게 처리한다.
    • 물리 파일 저장 후 파일 메타데이터와 타입별 매핑 데이터를 저장한다.

파일 삭제 시

  1. 삭제 대상 파일 조회
    • UUID와 파일 순번 목록을 기준으로 삭제할 파일 목록을 조회한다.
    • 삭제할 파일이 존재하지 않으면 별도 예외 없이 종료한다.
  2. 권한 검증
    • 현재 요청 사용자와 파일 소유자가 동일한지 검증한다.
    • 소유자가 다르면 삭제 예외를 발생시킨다.
  3. 물리 파일 삭제
    • DB에 저장된 파일 경로를 기준으로 실제 파일을 삭제한다.
    • 파일이 이미 존재하지 않는 경우에도 삭제 흐름이 실패하지 않도록 deleteIfExists를 사용한다.
  4. DB 데이터 삭제
    • 회원 파일 매핑 테이블에서 삭제한다.
    • 사진 파일 매핑 테이블에서 삭제한다.
    • 파일 테이블의 메타데이터를 삭제한다.
    • 매핑 테이블 삭제와 파일 테이블 삭제는 하나의 트랜잭션으로 처리한다.

파일 다운로드 시

  1. 다운로드 대상 파일 조회
    • UUID와 파일 순번 목록을 기준으로 다운로드할 파일 목록을 조회한다.
  2. 요청 파일 개수 검증
    • 조회된 파일이 없으면 파일 없음 예외를 발생시킨다.
    • 요청한 파일 개수와 실제 조회된 파일 개수가 다르면 다운로드 불가 예외를 발생시킨다.
  3. 단일 파일 다운로드
    • 다운로드 대상 파일이 1개라면 일반 파일 다운로드로 처리한다.
    • 실제 파일이 존재하는지 검증한다.
    • 원본 파일명으로 다운로드되도록 Content-Disposition 헤더를 설정한다.
    • Content-Typeapplication/octet-stream으로 설정한다.
  4. ZIP 파일 다운로드
    • 다운로드 대상 파일이 2개 이상이라면 ZIP 파일로 묶어서 다운로드한다.
    • 임시 ZIP 파일을 생성한다.
    • 각 파일을 ZIP Entry로 추가한다.
    • 같은 원본 파일명이 존재하면 파일명(1).확장자 형태로 중복 파일명을 처리한다.
    • Content-Typeapplication/zip으로 설정한다.

🔹 주요 코드 전략

1. 업로드와 수정 로직 분리

기존에는 하나의 업로드 메서드에서 UUID 존재 여부에 따라 신규 업로드와 수정 업로드를 함께 처리했다. 리팩토링 후에는 다음과 같이 역할을 분리했다.

  • upload(): 신규 파일 업로드
  • update(): 기존 UUID에 파일 추가 업로드

이를 통해 메서드의 의도가 명확해졌고, UUID 검증과 기존 파일 존재 여부 검증을 수정 로직에만 집중시킬 수 있었다.

2. 파일 메타데이터 객체 도입

파일 저장에 필요한 값들을 서비스 내부에서 각각 조립하지 않고 FileMetadata 객체로 묶었다. 이를 통해 다음 값들을 하나의 흐름으로 관리할 수 있게 되었다.

  • 원본 파일명
  • 파일 확장자
  • 저장 파일 경로
  • UUID
  • 파일 순번
  • 파일 타입
  • MultipartFile

서비스 코드는 파일 저장 흐름에 집중하고, 파일명/경로/확장자 계산은 별도 객체가 담당하도록 분리했다.

3. 사용자 정보 저장

파일 업로드 시 UserInfoProvider를 통해 현재 로그인 사용자를 조회하고, 파일 메타데이터에 사용자 정보를 함께 저장했다. 이를 통해 파일별 소유자를 추적할 수 있게 되었고, 삭제와 수정 시 권한 검증을 수행할 수 있는 기반을 마련했다.

4. 권한 검증 로직 추가

파일 수정과 삭제 시 현재 요청 사용자가 해당 파일의 소유자인지 검증하도록 했다.

1
2
3
private boolean hasAuth(File file) {
    return userInfoProvider.getUserId().equals(file.getUser().getUserId());
}

이를 통해 다른 사용자의 파일을 수정하거나 삭제하지 못하도록 방어했다.

5. 다운로드 로직 분리

다운로드는 단일 파일 다운로드와 ZIP 다운로드로 분리했다.

  • downloadFileSingle(): 단일 파일 다운로드
  • downloadFileZip(): 여러 파일 ZIP 다운로드
  • makeZipFile(): ZIP 파일 생성
  • writeZipStream(): ZIP Entry에 파일 데이터 쓰기
  • generateUniqueFilename(): ZIP 내부 중복 파일명 처리

기존보다 다운로드 로직의 흐름이 명확해졌고, 각 메서드가 담당하는 역할도 작아졌다.

6. 파일 존재 여부 검증 공통화

실제 파일이 존재하지 않거나, 파일 경로가 디렉터리인 경우 다운로드할 수 없도록 검증 로직을 공통화했다.

1
2
3
4
5
private void checkFilePath(Path path) {
    if (!Files.exists(path) || !Files.isRegularFile(path)) {
        throw new FileException(ERR_FILE_NOT_EXIST, path + "에 파일이 존재하지 않습니다.");
    }
}

💡 회고 (Retrospective)

👍 잘한 점

  • 신규 업로드와 기존 파일 추가 업로드를 분리하여 메서드의 의도를 명확히 함.
  • 회원 기능 추가에 맞춰 파일 업로드 시 사용자 정보를 저장하도록 개선함.
  • 파일 삭제와 수정 시 사용자 권한 검증을 추가하여 보안적으로 더 안전한 구조로 개선함.
  • 기존보다 메서드별 책임이 작아져 코드를 읽고 수정하기 쉬워짐.

👎 아쉬운 점

  • 파일 시스템 작업 특성상 try-catch 구조가 여러 곳에 반복되어 코드가 완전히 깔끔해지지는 않았다.

🔥 개선할 점

  • DB 저장 실패 시 이미 저장된 물리 파일을 정리하는 보상 처리 로직을 추가해야 한다.
  • ZIP 다운로드 시 생성되는 임시 파일을 주기적으로 삭제하는 스케줄러를 추가해야 한다.
  • 파일 서비스의 메서드가 분리됨에 따라 클래스 분리도 고려해 볼 수 있을 것 같다.

This post is licensed under CC BY 4.0 by the author.