Java

[SpringBoot] 이미지(파일) 저장

lucy1215 2024. 7. 30. 23:06
728x90
반응형

 

최근 SpringBoot에서 이미지(파일)를 불러와 저장할 때 어떻게 했었더라?라는 생각이 들었다.

위 생각이 드는 것과 동시에 이미지 파일을 업로드 하는 과정을 나 스스로 설명을 못한다는 것이 까먹었다는 증거이다.

 

따라서 이번에는 가장 개발의 가장 기본이자 필수인 이미지 파일 저장 과정을 다시 해보면서 복습하려고 한다.

 

 

 

이미지 파일의 저장에는 대표적인 방법에는 2가지 있다.

 

1. 이미지 자체는 DB에 저장하는 방식 (BLOB 형식 그대로 사용)

BLOB (Binary Large Object) 방식은 이진 형식의 데이터를 직접 DB에 저장하는 방법이다.

파일 자체가 DB에 저장되므로 별도의 경로나 URL이 필요없다.

 

장점

  • 백업 및 복구가 용이하다.
  • 데이터의 일관성을 유지한다.
  • 파일 접근을 강력하게 제어 가능하다.
  • 파일 보안을 강화한다.

단점

  • DB의 크기가 빠르게 증가할 우려가 있다.
  • 백업 및 복구 작업이 복잡해질 수 있다.
  • DB I/O 작업이 더 많아질 수 있다.
  • 성능 저하 문제가 발생할 수 있다.

 

 

2. 경로 저장 방식으로 DB에 간접적으로 저장하는 방식 (파일 시스템에 저장)

이미지는 파일 시스템에 저장하고 DB에는 해당 파일의 URL이나 파일 이름만 저장하는 방법이다.

 

보통 파일 시스템은 DB에 비해 값이 저렴하므로 같은 용량의 이미지를 저장할 때 DB에 비해 경제적이며 DB에 이미지를 저장하는 경우보다 작업을 빠르게 처리할 수 있다. => 유동적이다.

 

단점은 저장소가 요구된다는 점과 이미지의 주소를 알고 있는 누구나 이미지에 접근할 수 있기 때문에 대부분 추가적인 보안 처리가 필요하다.

 

 

 

 

2가지 방식 중 주로 파일 시스템에 저장하는 방식을 택하여 이미지를 저장했었다.

DB에 직접 저장하는 것보다 파일 시스템에 저장하는 것이 경제적이며, 성능면에서도 좋기 때문이다.

파일 시스템에 저장하는 과정을 진행해 보도록 하겠다.

 

 

 

 

개발 환경

  • SpringBoot 3.3.2
  • Java 17
  • h2 DataBase

유저와 유저의 프로필 사진을 저장하기 위한 DB 구조를 작성하였다.

 

  1. 이미지 업로드 Table - MemberImage
  2. MultipartFile 인터페이스로 구현
  3. 유저와 이미지는 일대일(1:1) 관계

 

DB 구조와 같이 SpringBoot에서 각각의 엔티티를 작성하도록 하겠다.

 

 

 

엔티티 생성


Member 엔티티 생성

유저 정보를 저장하는 엔티티이다. MemberImage 필드를 일대일로 참조한다.

  • 유저가 삭제되면 이미지도 삭제되어야 하므로 영속성 전이를 설정해 준다. CascadeType.ALL
  • 고아 객체 관리를 위해 orphanRemoval을 true로 설정한다.
import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "member")
public class Member {

    @Id
    @Column(name = "user_id")
    private Long userId;

    @NotEmpty
    @Column(name = "user_pw")
    private String userPw;

    @NotEmpty
    @Column(name = "name")
    private String name;

    @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
    private MemberImage memberImage;


    public Member(Long userId, String userPw, String name, MemberImage memberImage) {
        this.userId = userId;
        this.userPw = userPw;
        this.name = name;
    }
}

 

 

 

 

MemberImage 엔티티 생성

이미지를 저장할 엔티티이다. Member 엔티티를 일대일로 참조한다.

import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "member_image")
public class MemberImage {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "file_num")
    private Long fileNum;

    @OneToOne
    @JoinColumn(name = "user_id", nullable = false)
    private Member member;

    @NotEmpty
    @Column(name = "file_name")
    private String fileName;

    public MemberImage(Member member, String fileName) {
        this.member = member;
        this.fileName = fileName;
    }
}

 

 

 

 

Repository 인터페이스 생성


MemberRepository 생성

  • Member 객체를 저장하는 saveMember() 메서드
  • Member 객체를 찾는 finMember() 메서드
import com.example.demo.domain.Member;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public void saveMember(Member member) {
        em.persist(member);
    }

    public Member findMember(Long userId) {
        return em.find(Member.class, userId);
    }

}

 

 

 

MemberImageRepository 생성

  • MemberImage 객체를 저장하는 saveImage() 메서드
import com.example.demo.domain.MemberImage;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class MemberImageRepository {

    private final EntityManager em;

    public void saveImage(MemberImage memberImage) {
        em.persist(memberImage);
    }
}

 

 

 

 

Service 생성


MemberImageService 생성

  • 이미지 파일을 저장하는 uploadImage() 메서드
  • db에 저장할 이미지 파일 경로를 생성하는 saveImage() 메서드
import com.example.demo.domain.Member;
import com.example.demo.domain.MemberImage;
import com.example.demo.repository.MemberImageRepository;
import com.example.demo.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class MemberImageService {

    private final MemberRepository memberRepository;
    private final MemberImageRepository memberImageRepository;

    // 이미지 파일을 저장하는 메서드
    public void uploadImage(Member member, MultipartFile image) {
        try {
            // 이미지 파일 저장을 위한 경로 설정
            String uploadsDir = "src/main/resources/static/uploads/images/";

            // 이미지 파일 경로를 저장
            String dbFilePath = saveImage(image, uploadsDir);

            // MemberImage 엔티티 생성 및 저장
            MemberImage memberImage = new MemberImage(member, dbFilePath);
            memberImageRepository.saveImage(memberImage);

        } catch (IOException e) {
            // 파일 저장 중 오류가 발생한 경우 처리
            e.printStackTrace();
        }

    }

    public String saveImage(MultipartFile image, String uploadsDir) throws IOException{

        //파일 이름 생성
        String fileName = UUID.randomUUID().toString().replace("-", "") + "_" + image.getOriginalFilename();

        //실제 파일이 저장될 경로
        String filePath = uploadsDir + fileName;

        // DB에 저장할 경로 문자열
        String dbFilePath = "/uploads/images/" + fileName;

        Path path = Paths.get(filePath); // Path 객체 생성
        Files.createDirectories(path.getParent()); // 디렉토리 생성
        Files.write(path, image.getBytes()); // 디렉토리에 파일 저장

        return dbFilePath;
    }
}

 

 

 

MemberService 생성

  • Member 객체, 이미지 파일을 저장하는 join() 메서드
import com.example.demo.domain.Member;
import com.example.demo.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class MemberService {

    private final MemberRepository memberRepository;
    private final MemberImageService memberImageService;

    public Long join(Member member, MultipartFile image) {

        memberRepository.saveMember(member);
        memberImageService.uploadImage(member,image);

        return member.getUserId();
    }

    public Member fineOne(Long userId) {
        return memberRepository.findMember(userId);
    }
    
}

 

 

 

Controller 생성


MemberController 생성

import com.example.demo.domain.Member;
import com.example.demo.service.MemberService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@Slf4j
public class MemberController {

    private final MemberService memberService;

    @PostMapping("members/new")
    public ResponseEntity<String> create(@Valid @RequestParam("image")MultipartFile image, @ModelAttribute Member member) {
        Long userId = memberService.join(member, image);

        return ResponseEntity.status(HttpStatus.CREATED).body("유저 등록 완료. Id : " + userId);
    }
}

 

 

 

확인


Postman을 이용해 유저를 생성하는 엔드포인트에 FormData 형식으로 사진과 필요한 필드를 전송하면

"유저 등록 완료. Id : 1" 같이 등록 완료 반환값이 나오는 것을 확인할 수 있다.

 

 

 

이후, DB를 확인해 보면

MEMBER 테이블과 MEMBER_IMAGE 테이블에 알맞게 저장되어 있는 것을 확인할 수 있다.

 

 

 

또한, 이미지를 저장하려는 위치인 /resources/static/uploads/images 경로가 생성되었고 해당 경로에 이미지가 추가된 것을 확인할 수 있다.

반응형

'Java' 카테고리의 다른 글

[JPA] JPA란?  (0) 2024.07.28
[Java/Spring Boot] Java로 웹 크롤링 해보기 (Naver 날씨)  (0) 2023.02.22