개주 훈련일지/🏋️ 전집중 호흡 훈련

스프링부트 이미지 업로드 경로 설정: 상대경로 vs 절대경로, 장단점과 추천 구조

lshfood2 2026. 2. 7. 22:00

[ 파일 업로드에서 경로 설정이 중요한 이유 ]

스프링부트에서 이미지 파일을 업로드할 때

가장 자주 겪는 문제가 있다.

  • 내 컴퓨터에서는 업로드/미리보기가 된다.
  • 팀원 컴퓨터에서는 업로드가
    실패하거나 미리보기가 404가 난다.
  • 실행 방식(IDE 실행, jar 실행, war 배포)이
    바뀌면 저장 위치가 달라진다.

원인은 대부분 '파일이 저장되는 실제 경로'와

'브라우저에서 접근하는 URL 매핑'이

환경마다 달라지기 때문이다.

 

특히 팀 프로젝트에서는

상대경로/절대경로 선택이 결과에 직접 영향을 준다.


1. 업로드 경로를 잡는 방법 2가지

1-1) 상대경로 방식(프로젝트 기준)

예시

  • uploads/profile_temp
  • uploads/anime
  • uploads/newsThumb

이 방식은 '실행 기준 디렉토리' 아래에

uploads 폴더를 만들고 그 안에 파일을 저장한다.

 

장점

  • 팀원이 프로젝트만 받아도 같은 구조로 동작하기 쉽다.
  • Windows/Mac/Linux 등 OS에 덜 종속된다.
  • 기본값으로 두기 좋다.

주의점

  • 실제 저장 위치는 실행 방식에 따라 달라질 수 있다.
    (working directory 영향)
  • 업로드 폴더를 깃에
    실수로 올리지 않도록 관리가 필요하다.

1-2) 절대경로 방식(컴퓨터/서버 고정 경로)

예시

  • D:/DoNotUse2/animaletest/uploads/profile_temp
  • /var/app/uploads/profile_temp (운영 서버)

이 방식은 특정 디스크/폴더를 고정 저장소로 쓰는 방식이다.

 

장점

  • 저장 위치가 항상 고정이라 관리/디버깅이 쉽다.
  • 프로젝트 폴더와 업로드 파일을 분리할 수 있다.
  • 운영 서버 구조(예: /var/app/uploads)에 맞추기 쉽다.

주의점

  • 다른 PC에 D: 드라이브가 없거나 폴더가 없으면 바로 실패한다.
  • OS가 바뀌면 경로 자체가 성립하지 않는다.
  • 팀원마다 같은 절대경로를 강제하기가 어렵다
    (권한/폴더 생성/드라이브 구성 차이).

2. 상대경로 vs 절대경로 장단점 비교

항목 상대경로(uploads/...) 절대경로(D:/..., /var/...)
팀원 PC에서 바로 실행 유리 불리
(PC마다 경로 다름)
OS 독립성 유리 불리
저장 위치 고정 불리
(실행 기준에 영향)
유리
(항상 동일)
운영 배포 적합성 케이스에 따라 다름
(컨테이너면 위험)
유리
관리 난이도 낮음
(기본값으로 사용)
중간~높음
(환경별 설정 필요)
흔한 실패 저장 위치가 예상과 다름,
URL 매핑 불일치
경로 없음,
권한 없음

3. 업로드 저장 경로와 URL 매핑(ResourceHandler)

이미지를 업로드한 뒤 브라우저에서 바로

보이게 하려면 다음 3개가 일치해야 한다.

  • 저장 경로
    (파일이 실제 저장되는 파일시스템 위치)
  • 접근 URL(/uploads/...)
  • ResourceHandler 매핑
    (스프링이 URL을 어떤 폴더로 연결하는지)

예를 들어 저장은 uploads/profile_temp에 했는데,

ResourceHandler가 D:/.../uploads만 바라보고 있으면

업로드는 성공해도 미리보기 URL은 404가 된다.


4. 추천 구조: 업로드 루트(root-dir) 1개로 통일하고 환경별로 오버라이드

팀 프로젝트에서 가장 덜 깨지는 방식은 이 구조다.

  • 코드에는 특정 경로를 하드코딩하지 않는다.
  • 업로드 루트 경로를 설정으로만 관리한다.
    (app.upload.root-dir)
  • 기본값은 상대경로 uploads로 둔다.
  • 개인 PC나 운영 서버는 설정으로
    절대경로를 오버라이드한다.

5. 설정 및 코드 예시

5-1) application.properties (기본/공유)

# 업로드 루트(기본은 프로젝트 기준 상대경로)
app.upload.root-dir=uploads

# 파일 업로드 크기 제한(예시)
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=7MB

 

개인 PC에서만 절대경로를 쓰고 싶다면

깃에 올리지 않는 설정 파일로 분리

  • application-local.properties
app.upload.root-dir=D:/DoNotUse2/animaletest/uploads

 

5-2) WebConfig (정적 접근 URL 매핑)

현재 /uploads/** 로 접근 가능하게 만들고,

실제 폴더는 app.upload.root-dir로 결정한다.

package fourcheetah.animale.web.config;

import java.nio.file.Path;
import java.nio.file.Paths;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Value("${app.upload.root-dir:uploads}")
    private String uploadRootDir;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        Path root = Paths.get(uploadRootDir).toAbsolutePath().normalize();

        registry.addResourceHandler("/uploads/**")
                .addResourceLocations(root.toUri().toString());
    }
}

이렇게 하면 URL은 항상 /uploads/... 로 통일되고,

실제 폴더만 환경에 따라 바뀐다.

 

5-3) 업로드 저장 코드(프로필 임시 업로드 예시)

핵심 포인트

  • uploadRootDir 기준으로
    하위 폴더(profile_temp)를 만든다.
  • 저장 전에 Files.createDirectories로 폴더 없으면 생성
  • 확장자를 유지한다.
    (브라우저 표시/캐시/처리에 유리)
  • 반환 URL을 /uploads/...로 통일한다.
    (ResourceHandler와 일치)
@Value("${app.upload.root-dir:uploads}")
private String uploadRootDir;

@PostMapping(value="/member/profile/upload", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
public Map<String, Object> upload(@RequestParam("profileImageFile") MultipartFile file,
                                  HttpSession session) {

    Map<String, Object> res = new HashMap<>();

    Integer memberId = (Integer) session.getAttribute("memberId");
    if (memberId == null) {
        res.put("result", "FAIL");
        res.put("errorMessage", "로그인이 필요합니다.");
        return res;
    }

    try {
        if (file == null || file.isEmpty()) {
            res.put("result", "FAIL");
            res.put("errorMessage", "파일이 없습니다.");
            return res;
        }

        String ct = file.getContentType();
        if (ct == null || !ct.startsWith("image/")) {
            res.put("result", "FAIL");
            res.put("errorMessage", "이미지 파일만 업로드 가능합니다.");
            return res;
        }

        String ext = switch (ct) {
            case "image/jpeg" -> ".jpg";
            case "image/png"  -> ".png";
            case "image/webp" -> ".webp";
            default -> "";
        };
        if (ext.isEmpty()) {
            res.put("result", "FAIL");
            res.put("errorMessage", "지원하지 않는 이미지 형식입니다.");
            return res;
        }

        Path dir = Paths.get(uploadRootDir, "profile_temp").toAbsolutePath().normalize();
        Files.createDirectories(dir);

        String token = UUID.randomUUID().toString().replace("-", "") + ext;
        Path savePath = dir.resolve(token);

        file.transferTo(savePath.toFile());

        String url = "/uploads/profile_temp/" + token;

        res.put("result", "SUCCESS");
        res.put("temporaryProfileImageToken", token);
        res.put("temporaryProfileImageUrl", url);
        return res;

    } catch (Exception e) {
        res.put("result", "FAIL");
        res.put("errorMessage", "업로드 중 오류가 발생했습니다.");
        return res;
    }
}

6. 운영 및 팀 프로젝트에서 자주 놓치는 체크리스트

1) uploads 폴더는 깃에 올리지 않는다

/uploads/

 

2) 개인 설정 파일도 깃에 올리지 않는다.

(로컬 절대경로가 들어갈 수 있음)

/src/main/resources/application-local.properties

 

3) 저장 경로가 실제로 어디인지

한 번은 로그로 확인한다. (상대경로 혼란 방지)

System.out.println(Paths.get(uploadRootDir).toAbsolutePath().normalize());

 

4) 임시 업로드(profile_temp)는 만료/삭제 정책이 필요하다

- 1일 지난 파일 정리 배치,  최종 저장 완료 후 임시 파일 삭제

 

5) 권한 문제 확인(운영 서버)
절대경로를 쓰면 해당 디렉토리에

쓰기 권한이 없어서 실패하는 경우가 많다.


[ 마무리 ]

  • 상대경로는 팀 프로젝트에서
    기본값으로 사용하기 좋다.
  • 절대경로는 운영 서버처럼
    저장 위치 고정이 필요한 환경에서 유리하다.
  • 코드에 경로를 박지 않고, 설정으로만(app.upload.root-dir)
    경로를 관리하면 환경에 따른 실패가 크게 줄어든다.
  • 저장 경로와 /uploads/** ResourceHandler 매핑이
    일치해야 브라우저 미리보기 404를 피할 수 있다.