최원종의 개발 블로그

V10-3 프로필 이미지 삭제 하기 본문

Spring boot 입문

V10-3 프로필 이미지 삭제 하기

chl6698 2026. 5. 22. 16:40

3단계 프로필 이미지 삭제 하기


FileUtil (파일 삭제 기능 만들기)

코드 요약

물리적 업로드 경로 단일화 : IMAGES_DIR 경로를 C:\\upload로 고정하여
WebMvcConfig의 물리 리소스 위치와 1대1 매핑 구조 확립

가상 핸들러와의 완벽한 연계 : FileUtil이 C:\\upload에 저장한 원본 파일을 
WebMvcConfig가 /images/** 라는 가짜 주소로 변환하여 외부 노출 수행

NIO Path 및 Files 자원 처리 : 파일 복사(copy) 및 물리 삭제(delete) 시 
실제 지정된 C:\\upload 디렉토리를 타겟으로 명확히 연산 유도

확장자 필터링 기반 보안 적용 : Content-Type 헤더가 image/로 시작하는지 검증하여 
악성 실행 파일이나 문서 파일의 서버 유입 원천 차단

오류 추적 및 트랜잭션 전파 : 디스크 쓰기 에러 발생 시 
부모 레이어인 UserService로 IOException을 전파하여 안전한 롤백 흐름 지원

FileUtil  파일 삭제하는 기능 코드

 // 2. 파일 삭제 하는 기능
    public static void deleteFile(String fileName, String uploadDir) throws IOException {
        if (fileName == null || fileName.isEmpty()) {
            return;
        }
        // Path  -> C://upload/xxx_a.png
        Path filePath = Paths.get(uploadDir, fileName);
        if(Files.exists(filePath)) {
            // 정확한 폴더 경로 존재 확인, 파일명 기준으로 파일이 존재 한다면
            Files.delete(filePath); // 실제 폴더에서 파일 삭제 됨.
        }

    }

FileUtil 전체코드⬇️

더보기
package com.tenco.blog._core.util;

import com.tenco.blog._core.errors.Exception400;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;

// IoC 안함 (파일 기능 처리에만 동작할 수 있도록 static 메서드로 구현할 예정)
public class FileUtil {

    // 업로드될 파일 경로를 미지 상수로 지정
    public static final String IMAGES_DIR = "C:\\upload";

    // 1. 파일 저장 하는 기능
    public static String saveFile(MultipartFile file, String uploadDir) throws IOException {
        // 1단계 : 파일 유효성 검사 - 파일이 없거나 크기가 0이면 오류
        if (file == null || file.isEmpty()) {
            return  null; // 프로필 이미지 업로드는 선택 사항 임.
        }

        // 2단계 : 파일 업로드 경로 생성 (존재 여부 확인)
        // Path : 파일 시스템 경로를 나타내는 객체
        // Path.get() : 문자열 경로를 Path 객체로 변환해주는 객체
        Path uploadPath = Paths.get(IMAGES_DIR);
        // Files.exists() : 파일/디텍토리 존재 여부 확인
        if(Files.exists(uploadPath) == false) {
            // 현재 서버 컴퓨터에 images/* 없는 상태
            Files.createDirectories(uploadPath); // 상위 폴더까지 자동 생성 해 줌
        }

        // 3단계 : 원본 파일 이름 가져오기
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null || originalFilename.isBlank()) {
            throw new Exception400("파일명이 없습니다");
        }

        // 4단계 : UUID를 사용한 고유 파일명 생성
        String uuid = UUID.randomUUID().toString(); // 난수 발생
        String savedFileName = uuid + "_" + originalFilename;
        // 예) "12334123-123e-123_a.png 파일명으로 재 생성 됨.

        // 5단계 : 메모리상에 존재하는 파일 데이터를 로컬 컴퓨터(디스크)에 저장
        // 5.1 - 파일폴더경로 + 재생성한파일이름 ---> 정확한 위치에 파일이 생성 됨
        //  예 : images/123-2322-123_a.png
        Path filePath = uploadPath.resolve(savedFileName);

        Files.copy(file.getInputStream(), filePath);
        return savedFileName;
    }

    // 2. 파일 삭제 하는 기능
    public static void deleteFile(String fileName, String uploadDir) throws IOException {
        if (fileName == null || fileName.isEmpty()) {
            return;
        }
        // Path  -> C://upload/xxx_a.png
        Path filePath = Paths.get(uploadDir, fileName);
        if(Files.exists(filePath)) {
            // 정확한 폴더 경로 존재 확인, 파일명 기준으로 파일이 존재 한다면
            Files.delete(filePath); // 실제 폴더에서 파일 삭제 됨.
        }

    }


    // 3. 편의 기능 만들 예정 (이미지 파일이 맞는지 확인)
    public static boolean isImageFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            return false;
        }
        // pdf, hwp <-- 막아 줘야 한다.
        String contentType = file.getContentType(); // image/png, image/jpg, application/pdf
        boolean isImage = contentType.startsWith("image/");
        return isImage;
    }

}

Define (상수 데이터 관리 클래스 생성)

파일위치

package com.tenco.blog._core.util;

public class Define {
    public final static String SESSION_USER = "sessionUser";
}

UserController  (파일 삭제 요청 URL 맵핑 및 세션 동기화)

@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI 처리
public class UserController {

    private final UserService userService;

    // 프로필 이미지 삭제 요청
    @PostMapping("/user/profile-image/delete")
    public String deleteProfileImage(HttpSession session) {

        User sessionUser = (User) session.getAttribute(Define.SESSION_USER);
        // 프로필 이미지 삭제
        User updateUser = userService.프로필이미지삭제(sessionUser.getId());
        // 세션에 저장되어 있던 프로필이미지 삭제 후 세션 동기화 처리
        session.setAttribute(Define.SESSION_USER, updateUser);
        return "redirect:/user/detail";
    }
   // ... 생략  
  }

UserService (프로필이미지삭제 메서드 생성)

코드 요약

조회 및 인가 이중 방어선 : findById로 영속화된 유저 엔티티의 ID와 요청 인자의 ID를 대조하여
변조된 접근을 403 예외로 원천 차단

물리 자원 상태 사전 검증 : 엔티티 내부의 profileImage 주소가 존재할 때만 
삭제 로직을 발동시켜 무의미한 I/O 연산 최소화

FileUtil을 통한 디스크 정리 : FileUtil.deleteFile을 호출하여
C:\\upload 경로에 방치될 수 있는 물리적 파일 찌꺼기를 완전 청소

물리 제어 예외 분리 수용 : 파일 삭제 중 발생하는 IOException을 try-catch로 
별도 수집하여 로컬 에러 로그를 남기고 시스템 연속성 유지

1차 캐시 수정 및 더티 체킹 : 엔티티 필드를 null로 변경하면
@Transactional에 의해 메서드 종료 시점에 DB 수정 쿼리가 자동 실행

UserService코드

    @Transactional
public User 프로필이미지삭제(Integer id) {
    // 1. 정보 조회
    User userEntity = userRepository.findById(id).orElseThrow(
            () -> new Exception404("사용자를 찾을 수 없습니다")
    );
    // 2. 인가 처리
    if(userEntity.getId().equals(id) == false) {
        throw new Exception403("프로필 이미지 삭제 권한 없음");
    }

    // 3. 이미지가 등록되어 있으면 삭제 처리
    String profileImage = userEntity.getProfileImage();
    if(profileImage != null && !profileImage.isEmpty()) {
        // 내 서버 컴퓨터에 저장된(C://upload) 파일 삭제
        try {
            FileUtil.deleteFile(profileImage, FileUtil.IMAGES_DIR);
        } catch (IOException e) {
            System.err.println("프로필 이미지 삭제시 오류 발생 " + e.getMessage());
        }
    }
    // 1차 캐쉬에 저장된 User 정보 수정 - 트랜잭션이 종료 되면 반영(더티 체킹)
    userEntity.setProfileImage(null);
    return userEntity;
}