최원종의 개발 블로그

V10-4 프로필 이미지 수정 하기 본문

Spring boot 입문

V10-4 프로필 이미지 수정 하기

chl6698 2026. 5. 22. 16:49

4단계 프로필 이미지 수정 하기

회원정보 수정화면에서 프로필 이미지 수정하기


update-form.mustache ( 회원 정보 수정 화면 코드 추가)⬇️

더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <div class="card">
        <div class="card-header"><b>회원정보수정</b></div>
        <div class="card-body">

            <!--  프로필 이미지  start  -->
            <div class="text-center mb-4">
                {{#user.profileImage}}
                    <img class="rounded-circle" src="/images/{{user.profileImage}}" alt="프로필이미지"
                         style="width:200px; height:200px"/>
                    <div class="mt-2">
                        <form action="/user/profile-image/delete" method="post">
                            <button type="submit" class="btn btn-sm btn-danger"
                                    onclick="return confirm('프로필 사진을 삭제할까요?')">프로필 사진 삭제</button>
                        </form>
                    </div>
                {{/user.profileImage}}
                {{^user.profileImage}}
                    <div class="rounded-circle bg-secondary d-inline-flex
                                justify-content-center align-items-center border"
                         style="width:200px; height:200px;">
                        <span>프로필 사진 없음</span>
                    </div>
                {{/user.profileImage}}
            </div>
            <!--  프로필 이미지   end -->

            <!--  이미지 업로드 시 반드시   multipart/form-data 선언   -->
            <form action="/user/update" method="post" enctype="multipart/form-data">
                <div class="mb-3">
                    <input type="text" class="form-control" name="username" value="{{user.username}}" disabled>
                </div>
                <div class="mb-3">
                    <input type="password" class="form-control" name="password" value="" >
                </div>
                <div class="mb-3">
                    <input type="text" class="form-control" name="email" value="{{user.email}}" disabled>
                </div>

                <!--  프로필 이미지 등록 필드(선택 사항)  -->
                <div class="mb-3">
                    <label for="profileImage" class="form-label">프로필 사진(선택사항)</label>
                    <input type="file" id="profileImage" class="form-control" name="profileImage" accept="image/*"  >
                    <small class="form-text text-muted">이미지 파일만 업로드 가능합니다(JPG, PNG, GIF 등)</small>
                    <small class="form-text text-muted d-block mt-1">⭐ 프로필 사진을 등록하지 않아도 회원 가입 가능⭐ </small>
                </div>

                <button class="btn btn-primary form-control">회원정보수정</button>
            </form>
        </div>
    </div>
</div>
{{> layout/footer}}

UserController (비밀번호 기존 사용 처리, UpdateDTO 코드 수정)

코드 요약

방어적 비밀번호 보존 정책 : 수정 폼에서 비밀번호를 입력하지 않았을 경우(null/공백),
세션의 기존 비밀번호를 DTO에 채워넣어 데이터 유실 방지

DTO 자체 유효성 검증 발동 : 서비스 레이어로 진입하기 전 updateDTO.validate()를 직접 실행하여
데이터 형식을 1차적으로 꼼꼼히 필터링

세션 기반 식별자 바인딩 : 외부 파라미터 변조 공격을 차단하기 위해 
세션에서 추출한 안전한 고유 식별자(sessionUser.getId())를 서비스에 전달

영속성 컨텍스트 결과 세션 반영 : UserService가 리턴한 
최신 수정 유저 객체(updateUser)를 세션 가방에 덮어씌워 브라우저 상태를 즉시 동기화

PRG 패턴을 통한 화면 전환 : 정보 수정 처리가 완료된 후
메인 페이지(/)로 redirect 처리하여 브라우저 새로고침 시 중복 수정 요청을 완벽 차단

UserController 코드

    // 회원 정보 수정 기능 요청
    @PostMapping("/user/update")
    public String updateProc(UserRequest.UpdateDTO updateDTO, HttpSession session) {
        // 회원 정보 수정 요청시 기본 비밀번호 null 이고 프로필 이미지만 수정 요청
        User sessionUser = (User) session.getAttribute(Define.SESSION_USER);
        // 프로필 이미지 변경 요청이 왔을 때 기존에 비밀번호 저장
        if(updateDTO.getPassword() == null || updateDTO.getPassword().isBlank()) {
            updateDTO.setPassword(sessionUser.getPassword());
        }
        updateDTO.validate();
        User updateUser = userService.회원정보수정(sessionUser.getId(), updateDTO);

        session.setAttribute(Define.SESSION_USER, updateUser);
        return "redirect:/";
    }

 

UserRequest.UpdateDTO 에 MultipartFile 타입 추가(파일 업로드 처리)

@Data
public static class UpdateDTO {

    private String password;
    private MultipartFile profileImage;

    public void validate() {
        if(password == null || password.isBlank()) {
            throw new IllegalArgumentException("비밀번호는 필수 입니다");
        }
        if (password.length() < 4) {
            throw new IllegalArgumentException("비밀번호는 4자 이상이어야 합니다");
        }
    }
}

UserService 수정 (FileUtil 클래스 활용)

코드 요약

UserService와 FileUtil의 역할 분담 : FileUtil은 IoC 등록 없이 순수 파일 제어(static)만 담당하고,
UserService는 비즈니스 로직과 트랜잭션을 관리함

MIME 타입 및 유효성 1차 검문 : FileUtil.isImageFile()을 통해 
업로드된 파일이 실제 이미지(image/*)가 맞는지 체크하여 허가되지 않은 파일 형식을 차단

선 저장 후 삭제 시퀀스 : 신규 파일을 UUID 파일명으로 디스크에 안전하게 저장 완료한 후,
성공했을 때만 기존 옛날 파일을 삭제하여 데이터 유실 방지

선택적 수정 대응 (데이터 보존) : 사용자가 새 이미지를 첨부하지 않았을 경우(null),
else 분기를 통해 기존 엔티티가 가진 파일명을 그대로 유지

JPA 더티 체킹 자동 반영 : 수정 완료 후 repository.save() 호출 없이,
@Transactional 안에서 엔티티 객체의 값만 변경하면 트랜잭션 종료 시 수정 쿼리 자동 실행

UserService 코드

/**
 * 사용자 정보 수정 처리 (프로필 업데이트)
 * @param id  (User PK)
 * @param updateDTO (사용자가 요청한 데이터)
 * @return User
 */
@Transactional
public User 회원정보수정(Integer id, UserRequest.UpdateDTO updateDTO) {
    log.info("회원정보 서비스 시작");
    User userEntity = userRepository.findById(id).orElseThrow(
            () -> new Exception404("사용자 정보를 찾을 수 없습니다"));

    // 프로필 이미지 처리 (사용자가 이미지를 보냈다면)
    String uuidImageFileName = null;
    if (updateDTO.getProfileImage() != null && !updateDTO.getProfileImage().isEmpty()) {
        // 새 프로필 정보 수정 요청
        // 1. 기존에 프로필 사진이 있다면 삭제하고 새로 저장 (디스트), (DB 수정)
        // 2. 기존에는 프로필 이미지가 null 인 경우
        String oldProfileImage = userEntity.getProfileImage(); // null , 기존 이미지 명
        //String newProfileImage = updateDTO.getProfileImage().getOriginalFilename();
        if(!FileUtil.isImageFile(updateDTO.getProfileImage())) {
            throw new Exception400("이미지 파일만 업로드 가능합니다");
        }
        // 신규 이미지 저장
        try {
            uuidImageFileName = FileUtil.saveFile(updateDTO.getProfileImage(), FileUtil.IMAGES_DIR);
            // 기존 이미 삭제 처리 (있다면)
            if(oldProfileImage != null) {
                FileUtil.deleteFile(oldProfileImage, FileUtil.IMAGES_DIR);
            }

        } catch (IOException e) {
            throw new Exception500("프로필 이미지 파일 저장 실패");
        }
    }
    // 더티 체킹 활용
    userEntity.update(updateDTO, uuidImageFileName); // null, 새로운 이미지 명
    return userEntity;
}