새소식

SpringBoot

🐸SpringBoot 파일 처리 총 정리 2탄 (static 경로에 파일 업로드/다운로드, 파일 경로, 파일 삭제) springboot + thymeleaf

  • -

 

 

 

🐸SpringBoot 파일 처리 총 정리 (파일 업로드/다운로드, 파일 경로, 파일 삭제) springboot + thymeleaf

꽤나 애를 먹인 파일 처리 부분을 완벽 정리해봤다.최대한 자세히 정리하겠지만, 이해가 안 가거나 어려운 내용이 있다면 댓글을 달아주세요!  1. 파일 경로 결정가장 먼저 파악해야 할 것은 파

lulook.tistory.com

 

위의 글에 이어 조금 더 보충하고 싶은 내용이 있어, 추가 정리를 해본다!

 

 

추가 포스팅 이유

앞선 포스팅에서 아래처럼 파일 경로를 외부 경로(디렉터리)로 하는 것이 일반적이라고 했는데, 반대로 프로젝트 내부에 업로드 파일을 올리는 경우엔 어떻게 할 수 있을지 정리해보려 한다.

 

기존 포스팅

 

 


Spring Boot 파일 구조

파일 구조를 살펴보면, 대략 이해가 될 텐데 우린 내부 디렉터리에 저장하기 위해서 static 폴더를 활용할 것이다.

참고로 나는 spring boot + gradle + thymeleaf 를 사용하고 있다.

my-spring-boot-project
│
├── build.gradle
├── settings.gradle
│
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── myproject
│   │   │               ├── MyProjectApplication.java
│   │   │               ├── controller
│   │   │               │   └── MyController.java
│   │   │               ├── service
│   │   │               │   └── MyService.java
│   │   │               ├── repository
│   │   │               │   └── MyRepository.java
│   │   │               ├── model
│   │   │               │   └── MyModel.java
│   │   │               └── config
│   │   │                   └── WebConfig.java
│   │   ├── resources
│   │   │   ├── static  (정적 파일 위치! => 파일 업로드 경로)
│   │   │   │   └── css
│   │   │   │       └── style.css
│   │   │   ├── templates
│   │   │   │   └── index.html
│   │   │   ├── application.properties
│   │   │   └── application.yml
│   ├── test
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── myproject
│   │   │               └── MyProjectApplicationTests.java
│   └── target
└── README.md

 

 


 

기본 세팅

만약 STS를 쓰고 있다면, IDE 자동 새로고침을 켜야 한다. 그래야 파일 시스템 변경 사항을 자동으로 감지할 수 있기 때문이다. 만약 이 설정을 끄면, Spring Boot 프로젝트에서 정적 파일을 업로드한 후 STS (Spring Tool Suite) Package Explorer 창을 새로고침해야 값이 뜨는 걸 볼 수 있을 것이다.

 

자동 새로고침을 끄면, 아래 사진처럼 업로드한 파일을 찾아가지 못 한다.

 

 

Window > Preferences > General > Workspace > Refresh using native hooks or polling 를 체크!

 

 


 

파일 업로드 & 삭제

외부 경로에 파일을 업로드 하는 것보다 백만 배 간단하다. 뷰 파일은 앞선 포스팅을 참조해주시고 달라지는 건 service 부분 뿐이라 해당 내용만 상세히 정리해보겠다.

 

FileVO

@Data
public class FileVO {

	int fileId;
	/*
    	중략
    	*/
	String originalName;
	String serverName;
	String filePath;
	String createDatetime;
	
}

 

Service

블로그 템플릿 때문에 코드가 스크롤 형태로 나올 텐데 가능하면, copy해서 VSCode로 확인해보길 바란다.

주석을 꼼꼼히 달아뒀으니 도움이 되길!

 

+ 앞선 포스팅과 마찬가지로 세마포머는 필요하지 않다면 쓸 필요 없다.

세마포어(Semaphore)는 프로그래밍에서 여러 스레드가 공유 자원에 접근하는 것을 제어하는 데 사용되는 동기화 도구입니다.

 

 

package com.test.service;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.UUID;
import java.util.concurrent.Semaphore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.test.dto.FileVO;
import com.test.mapper.TestMapper;

import lombok.extern.slf4j.Slf4j;


@Slf4j
@Service
public class testService {
	
	@Autowired
	private TestMapper testMapper;

	// 파일 경로 (PROJECT_PATH: 프로젝트 경로 / FOLDER_PATH: 저장할 static 경로)
	private static final String PROJECT_PATH = System.getProperty("user.dir");
	private static final String FOLDER_PATH = "/src/main/resources/static/upload";
	

	
	
	
	// =========================== 파일 업로드 로직 ===========================
	
	// 세마포어 객체 생성 (permit(1), fair(true) = 공유자원 1개, FIFO)
	private final Semaphore semaphore = new Semaphore(1, true); 
	
	public void uploadTestFiles(MultipartFile[] files) throws IllegalStateException, IOException {
		
		try {
			semaphore.acquire(); 		// 세마포어 획득
						
			for (int i=0; i<files.length; i++) {
	            MultipartFile file = files[i];

	            
				// 2) 파일 이름 변경
				String fileName = fileRename(file.getOriginalFilename());
//				log.info("file : {}" ,file);
//				log.info("folderPath : {}" ,folderPath);
//				log.info("fileName : {}" ,fileName);
//				log.info("============================");
				
				
				// 3) 이미지를 저장할 파일 객체 생성
				File uploadFile = new File(PROJECT_PATH + FOLDER_PATH + "/test", fileName);
				//throw new IllegalStateException("Illegal state occurred");
				
				
				// 4) 파일을 해당 위치에 저장
				file.transferTo(uploadFile);
				
				
				// 5) 파일 객체에 파일 정보 설정
				FileVO fileVo = new FileVO();
				
				fileVo.setFileId(fileId + i);
				fileVo.setOriginalName(file.getOriginalFilename());		// 	01.jpg
				fileVo.setServerName(fileName);							// 	240528103837555_88cf7914.jpg
				fileVo.setFilePath("/test/" + fileName);				// 	/test/240528103837555_88cf7914.jpg	
				
//				System.out.println("fileVo : " + fileVo);
				
				// 6) DB 저장
				testMapper.insertTestFile(fileVo);
			}
			
			
		} catch (InterruptedException e) {
			log.error("Semaphore acquisition interrupted : {}", e);
			Thread.currentThread().interrupt(); // 스레드 인터럽트 처리
		} catch (Exception e) {
			log.error("File data inserted Error : {}", fileVo, e);
		} finally {
			semaphore.release(); 				// 세마포어 반환
		}
		
		
	}
	
	
	
	// =========================== 파일 삭제 로직 ===========================
	
	public void deleteTestFiles(ArrayList<FileVO> files) throws IllegalStateException, IOException {
		
		try {
			semaphore.acquire(); 		// 세마포어 획득
			
			for (FileVO fileVo : files) {
				
				// 1) 파일명 
				String filePath = fileVo.getFilePath();
				String fullFilePath = PROJECT_PATH + FOLDER_PATH + filePath;
				
				File file = new File(fullFilePath);
				
				// 2) 해당 파일 존재할 경우, 삭제처리
				if (file.exists()) {
					if (!file.delete()) {
						log.warn("Failed to delete file: {}", fullFilePath);
					}
				} 
				else log.warn("File not found: {}", fullFilePath);
				
				// 3) DB에서 삭제
				testMapper.deleteTestFile(fileVo);
			}
			
			
		} catch (InterruptedException e) {
			log.error("Semaphore acquisition interrupted : {}", e);
			Thread.currentThread().interrupt(); // 스레드 인터럽트 처리
		} catch (Exception e) {
			log.error("File data inserted Error : {}", boardVo, e);
		} finally {
			semaphore.release(); 				// 세마포어 반환
		}
		
		
	}
	
	



	
	
	// 파일명 변경 메소드
	public static String fileRename(String originalFileName) {
		SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmssS");
		String date = sdf.format(new java.util.Date(System.currentTimeMillis()));

		UUID uuid = UUID.randomUUID();
		String uuidString = uuid.toString();
		String first8Characters = uuidString.substring(0, 8);

		String ext = originalFileName.substring(originalFileName.lastIndexOf("."));

//		return date + "_" + first8Characters + "_" + originalFileName;
		return date + "_" + first8Characters + ext;
	}

	
    // 날짜 폴더 생성 (not used)
	private String makeFolder(String uploadPath) {

		// 현재 날짜를 기준으로 년/월/일의 계층 구조를 가진 폴더 경로 생성
	    String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yy/MM/dd"));
	    
	    // 파일 시스템에 맞는 구분자로 변경
	    String folderPath = str.replace("/", File.separator);
	    
	    // 폴더 객체 생성
	    File uploadPathFolder = new File(uploadPath, folderPath);
	    
	    // 폴더가 존재하지 않으면 생성
	    if (!uploadPathFolder.exists()) {
	        boolean mkdirs = uploadPathFolder.mkdirs();
	        
	        // 폴더 생성 로그 출력 (optional)
	        log.info("-------------------makeFolder------------------");
	        log.info("uploadPathFolder.exists(): " + uploadPathFolder.exists());
	        log.info("mkdirs: " + mkdirs);
	    }
	    
	    return "/" + folderPath;
	}
	// =========================== 파일 업로드 로직 ===========================
	
	
	
	
	
	
	
	// ========================= 미사용 but 필요 시 사용가능 ==========================
	// 크로스 사이트 스트립트 공격을 방지 하기 위한 메소드
	public static String XSSHandling(String content) {
		if (content != null) {
			content = content.replaceAll("&", "&amp;");
			content = content.replaceAll("<", "&lt;");
			content = content.replaceAll(">", "&gt;");
			content = content.replaceAll("\"", "&quot;");
		}
		return content;
	}

	// 크로스 사이트 스트립트 해제
	public static String XSSClear(String content) {
		if (content != null) {
			content = content.replaceAll("&amp;", "&");
			content = content.replaceAll("&lt;", "<");
			content = content.replaceAll("&gt;", ">");
			content = content.replaceAll("&quot;", "\"");
		}
		return content;
	}

	// 개행문자 처리
	public static String newLineHandling(String content) {
		return content.replaceAll("(\r\n|\r|\n|\n\r)", "<br>");
	}

	// 개행문자 해제
	public static String newLineClear(String content) {
		return content.replaceAll("<br>", "\n");
	}

	

}

 

 


업로드 결과 보기

뷰에서도 크게 달라질 건 없다. 다만 따로 추가 매핑을 할 필요가 없다.

(thymeleaf 문법으로 작성됨) 

 

참고로 static 폴더 안의 파일 경로는 웹 애플리케이션의 루트 경로(/)를 기준으로 접근할 수 있다. 따라서, static/upload 폴더에 파일을 저장하는 경우, 해당 파일에 접근하려면 /upload 경로를 사용하면 된다.

// 1) 이미지 보기
<img th:src="'/upload' + ${entry.filePath}" alt="사진">

// 위 코드는 아래와 동일하다. local 서버에서 띄우면 이렇게 뜸
// <img th:src="http://localhost/upload-path/test/240529135713642_90278e8c.jpg" alt="사진">


// 2) 파일 다운로드
// 이렇게 적어주면 다운될 때 serverName이 아닌 originalName으로 다운된다.
<a th:href="'/upload' + ${entry.filePath}" th:download="${entry.originalName}" class="cursor-pointer">
    <span th:text="${entry.originalName}"></span>
</a>

 

 

값은 /static/upload/test 에 저장된다.

 

DB에는 /test/로시작하는 경로가 filePath로 저장되어 있다.

 

 

 

 


내부 경로(/static) 에 파일 업로드 시 장/단점

결론을 말하자면, 개인적으론 선호하지 않는 방식이다. 
다만 만약 1️⃣소규모의 프로젝트이거나 2️⃣파일 업로드 양이 적고 3️⃣보안이 중요하지 않다면 추천한다!

 

장점

 

1. 간편한 접근:
   - 업로드된 파일이 `static` 디렉토리에 저장되면, 웹 애플리케이션을 통해 정적 파일처럼 간편하게 접근 가능
   - 별도의 파일 서빙 설정 없이,  (`/static/upload`) 경로를 통해 쉽게 파일에 접근 가능

2. 일관된 URL 구조:
   - 모든 정적 자산을 한 곳에 모아두면 URL 구조가 일관되어 관리하기 쉬움

3. 배포의 일관성:
   - 애플리케이션과 정적 파일이 같은 배포 단위에 포함되므로 배포 시 별도의 파일 동기화 과정이 불필요

단점

1. 파일 크기 제한:
   - 업로드된 파일이 많거나 크기가 큰 경우, `static` 디렉토리에 저장하면 WAR/JAR 파일의 크기가 커져 배포와 로딩 시간이 증가

2. 보안 문제:
   - 모든 파일이 웹 서버를 통해 직접 접근 가능하므로, 민감한 파일이 노출될 위험이 있음으로 적절한 접근 제어가 필요함

 


외부 경로에 파일을 저장하는 장점과 단점

그럼 반대로 외부 경로에 파일을 저장했을 때 장/단점을 살펴보자.

장점

1. 애플리케이션과 파일 분리:
   - 정적 파일과 애플리케이션 코드를 분리하여 배포하고 관리
   - 파일 저장 공간과 애플리케이션 서버의 디스크 공간을 독립적으로 관리

2. 동적 파일 관리:
   - 대용량 파일이나 많은 수의 파일을 처리할 때 유리

3. 확장성:
   - 파일 저장을 외부 스토리지 (예: Amazon S3, Google Cloud Storage 등)로 옮기면 확장성과 성능을 개선할 수 있음

4. 보안 관리:
   - 파일 접근을 별도로 관리하고 보안 설정을 강화 가능

 

단점

1. 복잡한 설정:
   - 파일 서버를 별도로 설정하고, 애플리케이션과 연동하는 과정이 복잡
   - 외부 스토리지 서비스와의 연동 설정이 필요

2. 의존성 증가:
   - 외부 파일 시스템이나 스토리지 서비스에 대한 의존성이 증가

3. 추가 비용:
   - 외부 스토리지 서비스 사용 시 추가 비용이 발생

 

 


결론

- 내부 경로 (static 디렉토리)에 저장하는 경우 간편하지만, 파일 크기 및 수량이 많아지면 관리와 배포가 어려워질 수 있음 
- 외부 경로에 저장하면 파일 관리가 유연해지지만, 설정과 연동 작업이 추가로 필요함

 

 

 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.