1. yml 추가
spring:
servlet:
multipart:
enabled: true # 파일 업로드 활성화
max-file-size: 10MB # 개별 파일 최대 크기
max-request-size: 10MB # 전체 요청 최대 크기

2. User 엔티티에 컬럼 추가 (사용자 프로필 파일 이름)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
private String email;
@CreationTimestamp
private Timestamp createdAt;
//@Column(nullable = false)
private String profileImage;
...
2-1. h2-console확

3. join-form.mustach
{{> layout/header}}
<div class="container p-5">
<!--
파일 업로드를 위한 form 태그 설정
enctype="multipart/form-data : 파일 형식을 포함한 MIME 타입을 의미한다
즉, 이 속성 없이는 파일이 서버로 전송되지 않는다 !!!
일반 텍스트 데이터는 - application/x-www-form-urlencoded
파일 데이터는 - multipart/form-data 필요 하다.
-->
<div class="card">
<div class="card-header"><b>회원가입을 해주세요</b></div>
<div class="card-body">
<form action="/join" method="post" enctype="multipart/form-data">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username">
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password">
</div>
<div class="mb-3">
<input type="email" class="form-control" placeholder="Enter email" name="email">
</div>
<div class="mb-3">
<label for="profileImage" class="form-label">프로필 사진(선택사항)</label>
<input type="file" class="form-control" id="profileImage" name="profileImage" accept="image/*">
<small class="form-text text-muted mt-1">이미지 파일만 업로드 가능합니다(JPG, PNG, GIF)</small>
</div>
<button type="submit" class="btn btn-primary form-control">회원가입</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
3-1. joinProc 확인
// 회원가입 기능 요청
// http://localhost:8080/join
@PostMapping("/join")
public String joinProc(UserRequest.JoinDTO joinDTO) {
joinDTO.validate();
userService.회원가입(joinDTO);
return "redirect:/login";
}
4. UserRequest.JoinDTO 에서 MultipartFile 멤버 할당 확인
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
// MultipartFile - Spring 에서 파일 업로드를 처리하기 위한 인터페이스
// 우리 프로젝트에서는 선택 사항이라 회원 가입시 null 또는 empty 상태가 될 수 있음
private MultipartFile profileImage;
public void validate() {
if(username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("사용자명을 입력해주세요");
}
if(password == null || password.trim().isEmpty()) {
throw new IllegalArgumentException("비밀번호를 입력해주세요");
}
if(email == null || email.trim().isEmpty()) {
throw new IllegalArgumentException("이메일을 입력해주세요");
}
if(email.contains("@") == false) {
throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다");
}
}
// JoinDTO 를 User 타입으로 변환 시키는 기능
public User toEntity(String profileImageFileName) {
return User.builder()
.username(this.username)
.password(this.password)
.email(this.email)
// DB에 MultipartFile 를 저장할 수 없다 (파일 이름만 저장할 예정)
.profileImage(profileImageFileName)
.build();
}
} // end of inner class
5. User 엔티티 생성자 수정 - profileImage 생성자에 설계
package org.example.demo_ssr_v1_1.user;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
// 엔티티 화면 보고 설계해 보세요.
@NoArgsConstructor
@Data
@Table(name = "user_tb")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
private String email;
@CreationTimestamp
private Timestamp createdAt;
//@Column(nullable = false)
private String profileImage; // 추가
@Builder
public User(Long id, String username, String password,
String email, Timestamp createdAt, String profileImage) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.createdAt = createdAt;
this.profileImage = profileImage; // 추가
}
// 회원정보 수정 비즈니스 로직 추가
// 추후 DTO 설계
public void update(UserRequest.UpdateDTO updateDTO) {
// 유효성 검사
updateDTO.validate();
this.password = updateDTO.getPassword();
// 더티 체킹 (변경 감지)
// 트랜잭션이 끝나면 자동으로 update 쿼리 진행
}
// 회원 정보 소유자 확인 로직
public boolean isOwner(Long userId) {
return this.id.equals(userId);
}
}

FileUtil 유틸 클래스 만들어서 나중에 재활용 하기
package org.example.demo_ssr_v1_1._core.utils;
import org.springframework.web.multipart.MultipartFile;
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 {
// 프로젝트 루트 폴더 아래에 images/ 폴더를 생성할 예정 (프로필 이미지만 넣을 예정)
public static final String IMAGES_DIR = "images/";
private static String saveFile(MultipartFile file) throws IOException {
return saveFile(file, IMAGES_DIR);
}
public static String saveFile(MultipartFile file, String uploadDir) throws IOException {
// 1. 유효성 검사
if (file == null || file.isEmpty()) {
return null; // 파일이 없으면 null (왜 선택사항이므로 에러 아님)
}
// 2. 업로드 디렉토리 생성
// Path : 파일 시스템 경로를 나타내는 객체
// paths.get() : 문자열 경로를 Path 객체로 변환 시켜주는 메서드
Path uploadPath = Paths.get(IMAGES_DIR); // new Path() 내부에서
// 디렉토리가 있으면 새로운 폴더를 생성하지 않고 없으면 자동 생성
if (!Files.exists(uploadPath)) {
// 디렉토리 생성인데 상위 까지 알아서 다 만들어 줌
// Root/Image/user/a/aaa.png
Files.createDirectories(uploadPath);
}
// 3. 원본 파일명 가져오기
// getOriginalFilename() -> 사용자가 입력한 파일 이름
String originalFilename = file.getOriginalFilename();
if(originalFilename == null || originalFilename.isEmpty()) {
throw new IOException("파일명이 없습니다");
}
// 4. UUID를 사용한 고유한 파일명 생성
// 왜 UUID 를 사용하나요?
// 사용자들은 같은 이름에 파일명을 서버에 저장 시키고자 할 수 있다. 그럼 원본 파일 사라짐
String uuid = UUID.randomUUID().toString(); // 12kjhsdkjfhas934-sadkjhasdfk
String savedFileName = uuid + "_" + originalFilename;
// 결과 예시 : 123123asdf_abc.png
// 5. 파일을 디스크(물리적 저장 장치)에 저장
Path filePath = uploadPath.resolve(savedFileName);
// 실제 파일 생성
Files.copy(file.getInputStream(), filePath);
return savedFileName;
}
// 유효성 검사 기능
public static boolean isImageFile(MultipartFile file) {
// 파일 이미지가 없으면 이미지가 아님
if(file == null || file.isEmpty()) {
return false;
}
// Content-Type 가져오기
// 예시 : "image/jpg", "image/png", "image/gif"
String contentType = file.getContentType();
// Content-Type 가 image/ 로 시작하는지 확인
return contentType != null && contentType.startsWith("image/");
}
}
UserService
@Transactional
public User 회원가입(UserRequest.JoinDTO joinDTO) {
// 1. 사용자명 중복 체크
if(userRepository.findByUsername(joinDTO.getUsername()).isPresent()) {
// isPresent -> 있으면 true 반환 , 없으면 false 반환
throw new Exception400("이미 존재하는 사용자 이름입니다");
}
// User 엔티티에 저장할 때는 String 이어야 하고 null 값도 가질 수 있음
String profileImageFileName = null;
// 2. 회원 가입시 파일이 넘어 왔는 확인
if(joinDTO.getProfileImage() != null) {
// 2.1 유효성 검사 (이미지 파일 이어야 함)
try {
if(!FileUtil.isImageFile(joinDTO.getProfileImage())) {
throw new Exception400("이미지 파일만 업로드 가능합니다");
}
profileImageFileName = FileUtil.saveFile(joinDTO.getProfileImage());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
User user = joinDTO.toEntity(profileImageFileName);
return userRepository.save(user);
}
결과 확인

'개인 피드백 > 백엔드' 카테고리의 다른 글
| 프로필 이미지 - 조회, 수정, 삭제 기능 추가 (0) | 2025.12.25 |
|---|---|
| 세션 인터셉터 만들기 (0) | 2025.12.24 |
| Spring 코드 순서의 중요성(자식 <-> 부모) (0) | 2025.11.19 |