최원종의 개발 블로그

V10-5 운영체제 별 경로 설정 본문

Spring boot 입문

V10-5 운영체제 별 경로 설정

chl6698 2026. 5. 22. 17:00

5단계 운영체제 별 경로 설정

FileUtil 핵심 코드 분석

public static final String IMAGES_DIR = 
Paths.get(System.getProperty("user.home"), "blog_uploads").toString();
JVM 시스템 프로퍼티에서 사용자 홈 디렉토리를 동적으로 긁어온 뒤 blog_uploads 폴더를 결합하는 방식을 채택함으로써,
윈도우에서는 C:\Users\유저명\blog_uploads, 
맥에서는 /Users/유저명/blog_uploads로 자동 가공되는 크로스 플랫폼 설계

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 {

    // 업로드될 파일 경로를 미지 상수로 지정
    // System.getProperty("user.home")을 사용해서 OS 상관없이
    // 사용자 홈 경로를 동적으로 설정해서 가져 옴
    // 예) window : C:\Users\사용명\blog_uploads
    // 예) Mac : /Users/사용자명/blog_uploads
    public static final String IMAGES_DIR = Paths.get(System
            .getProperty("user.home"), "blog_uploads").toString();

    // 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;
    }

}

WebMvcConfig (addResourceHandlers 수정)

수정된 핵심 코드

FileUtil과 WebMvcConfig의 완전한 물리 경로 동기화
String externalPath = Paths.get(FileUtil.IMAGES_DIR).toString();
registry.addResourceHandler("/images/**")
		// file: 추가 하기
        .addResourceLocations("file:" + externalPath);
이유: 이전 코드의 문제점은 파일 저장 유틸리티가 바라보는 실제 폴더(C:\\upload)와
설정 파일이 바라보는 폴더 위치를 개발자가 각각 수동으로 짝을 맞춰줘야 했기 때문에 
한쪽을 바꾸고 다른 쪽을 놓치면 화면에 엑스박스가 뜨는 현상이 빈번함.

수정된 코드는 FileUtil.IMAGES_DIR 상수를 그대로 긁어와
접두사인 file:과 동적으로 문자열 결합을 수행함. 
이로써 파일 저장 장소가 맥 환경이나 특정 서버 환경에 맞추어
어디로 바뀌든 간에 웹 가상화 리소스 위치가 자동으로 연동되어 
추적되는 강력한 유지보수성을 확보.

WebMvcConfig 코드 ⬇️

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

import com.tenco.blog._core.interceptor.LoginInterceptor;
import com.tenco.blog._core.interceptor.SessionInterceptor;
import com.tenco.blog._core.util.FileUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.file.Paths;

// 자바 코드로 스프링 부트 설정 파일을 다둘 수 있다.

// @Component
@Configuration // IoC 대상 - 하나 이상의 IoC 처리를 하고 싶을 때 사용 한다.
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired // DI 처리
    private LoginInterceptor loginInterceptor;
    @Autowired // DI 처리
    private SessionInterceptor sessionInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 화면에 SessionUser 정보를 내려줄 사용 됨. 
        registry.addInterceptor(sessionInterceptor)
                .addPathPatterns("/**"); // 모든 URL 요청서 동작 함


        // 인증 처리 인터셉터 동작 함
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/board/**", "/user/**", "/reply/**")
                .excludePathPatterns(
                    // 로그인 관련(인증이 필요 없는 페이지)
                    "/login-form",  // 로그인 화면 요청 시
                    "/join-form",   // 회원 가입 화면 요청 시
                    "/logout", // 로그아웃

                    // 게시글 조회 관련 (인증 없이도 볼 수 있는 페이지)
                    "/board/list",  // 게시글 목록 화면 요청 시
                    "/"          ,  // 메인 페이지
                    "/index"          ,  // 메인 페이지
                    "/board/{id:\\d+}", // 게시글 상세보기( 숫자 ID만 허용)

                    // 정적 리소스 (CSS, JS, 이미지 등)
                    "/css/**",          // CSS 파일 제외
                    "/js/**",           // JS 파일 제외
                    "/images/**",      //  이미지 파일 제외
                    "/favicon.ico",    // 파비콘 제외

                    // H2 데이터베시스 콘솔( 개발 환경용)
                    "/h2-console/**"    // H2 콘솔 접근
                );
    }

    // 정적 리소스 핸들러 설정
    // 외부 사용자가 내 서버 컴퓨터에 특정 경로를 바로 확인을 할 수 있게 한다면
    // 보안상 취약 할 수 있습니다. ( 사용자에게는 가짜 경로를 보여주고 내부에서는
    // 실정 경로를 찾을 수 있도록 처리 하는 기법(보안상)
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
       String externalPath = Paths.get(FileUtil.IMAGES_DIR).toString();
       registry.addResourceHandler("/images/**")
                // file: 추가 하기
               .addResourceLocations("file:" + externalPath);
    }
}

yml 파일 수정 코드⬇️

더보기
server:
  servlet:
    encoding:
      charset: utf-8
      force: true
  # 파일 업로드 설정
    multipart:
      enabled: true  # 파일 업로드 활성화
      max-file-size: 50MB  # 단일 파일 최대 크기 설정 (1MB)
      max-request-size: 100MB # 전체 요청(Request) 최대 크기 설정
  port: 8080



logging:
  level:
    root: INFO #모든 라이브러리는 INFO 이상만 출력
    com.tenco: DEBUG # 내 프로젝트는 DEBUG 이상 모두 출력

spring:
  mustache:
    servlet:
      # 머스태치 템플릿 엔진에서 request 객체와 세션 객체에 접근할 수 있도록 허용하는 설정 추가
      expose-session-attributes: false
      expose-request-attributes: true


  #데이터베이스 연결 설정 (MySQL)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/myblog?serverTimezone=Asia/Seoul
    username: root
    password: root
  #    driver-class-name: org.h2.Driver
  #    url: jdbc:h2:mem:test
  #    username: sa
  #    password:
  h2:
    console:
      enabled: true

  #      초기 데이터 설정
  # SQL 초기화 설정
  sql:
    init:
      #mode: always  # 외부 DB(MySQL)에서도 실행되도록 설정
      mode: never  # 절대 실행 안됨
      data-locations:
        - classpath:db/data.sql

  jpa:
    # Open Session In View -> false 설정
    # true (기본 값) -> 뷰 렌더링까지 DB 세션 유지 됨(LAZY 로딩 가능)
    # false : 트랜잭션 종료시 DB 세션 종료 ( Controller, View LAZY 로딩 불가)
    # false 사용시 Service 필요한 데이터를 모두 조회하고 응답시 DTO 설계 방식으로 처리 한다.
    open-in-view: false
    hibernate:
      # create 애플리케이션 시작시 테이블 새로 생성
      # 기존 데이터는 모드 삭제됨 (개발용)
      ddl-auto: update
    #SQL 쿼리를 콘솔에 출력 (개발용)
    show-sql: true
    properties:
      hibernate:
        # SQL 쿼리를 보기 좋게 포맷팅
        format_sql: true
        # N + 1 문제를 완화 하기위한 완충 장치 설정
        # 기본 WHERE 절에서 in 절(in 쿼리로 변경 해준다)
        default_batch_fetch_size: 100
    # data.sql 파일을 하이버네티트 초기화 이후에 실행

    # data.sql 파일을 Hibernate 초기화 이후에 실행
    defer-datasource-initialization: true