처누

[MyBatis] 사진 저장/불러오기 기능(MultipartFile) 본문

스터디

[MyBatis] 사진 저장/불러오기 기능(MultipartFile)

처누 2024. 11. 30. 15:11

관통 프로젝트 진행 중에 게시글 등록 시 사진을 같이 등록해야하는 기능을 구현해야했다.

이번 기회를 통해 사진을 저장하는 기능은 완벽하게 짚고 넘어가보자.

 

GPT한테 사진 저장하는 방법에 대해 모든 방법을 알려달라고 물어본 결과 아래와 같은 답변을 줬다.

 

1. 서버 디스크에 저장

 업로드된 파일을 서버의 로컬 디스크에 저장하는 방법

 

구현 흐름

  1. 클라이언트에서 파일 업로드 요청 전송 (예: multipart/form-data)
  2. 서버에서 파일을 특정 디렉토리에 저장.
  3. 저장된 파일의 경로나 URL을 DB에 저장.

 

장점

  1. 구현이 간단하고 추가적인 외부 서비스가 필요 없음.
  2. 빠른 저장 및 접근 가능

 

단점

  1. 서버 디스크 용량 제한 문제
  2. 서버를 여러 대 사용하는 환경(분산 서버)에서는 파일 동기화가 어려움.
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
    String uploadDir = "/uploads";
    String filePath = uploadDir + "/" + file.getOriginalFilename();
    file.transferTo(new File(filePath));
    return ResponseEntity.ok("File saved at: " + filePath);
}

 

2. DB에 저장

이미지 바이너리 데이터(예: BLOB)로 변환하여 DB에 저장하는 방법

 

구현 흐름

  1. 업로드된 파일을 바이트 배열로 변환.
  2. 변환된 데이터를 DB의 BLOB 또는 BYTEA 컬럼에 저장.

 

장점

  1. 데이터와 파일의 일관성을 유지할 수 있음.
  2. 백업 및 복구가 용이함

 

단점

  1. DB의 크기가 커질수록 성능 저하 발생
  2. 파일 입출력(IO)이 느릴 수 있음.
@Lob
@Column(name = "image_data")
private byte[] imageData;

public void saveImage(MultipartFile file) {
    ImageEntity image = new ImageEntity();
    image.setImageData(file.getBytes());
    imageRepository.save(image);
}

 

3. 클라우드 스토리지 사용(AWS S3, Google Cloud Strage 등)

업로드된 파일을 클라우드 스토리지에 저장하고 URL을 관리하는 방식

 

구현 흐름

  1. 업로드된 파일을 서버에서 클라우드 스토리지로 전송
  2. 클라우드 스토리지가 반환하는 URL을 DB에 저장

 

장점

  1. 대규모 파일 저장에 적합
  2. 분산 서버 환경에서 파일 관리가 쉬움
  3. 높은 확장성과 가용성 제공

 

단점

  1. 설정과 초기 구현이 다소 복잡
  2. 클라우드 서비스 사용 비용 발생
AmazonS3 s3Client = AmazonS3ClientBuilder.standard().build();
s3Client.putObject(new PutObjectRequest("my-bucket", "image.jpg", file.getInputStream(), null));

 

4. CDN(Content Delivery Network)과 연계

이미지를 업로드한 후 CDN을 통해 전송 속도를 최적화 함

 

구현 흐름

  1. 이미지를 클라우드 스토리지에 업로드
  2. CDN에서 제공하는 URL을 프론트엔드에 반환
  3. 사용자는 CDN을 통해 빠르게 파일에 접근

 

장점

  1. 이미지 로딩 속도가 빠름.
  2. 트래픽 부하를 효과적으로 분산

 

단점

  1. 클라우드 스토리지와 연계가 필요
  2. CDN 설정 비용 발생

 

5. Base64 인코딩 후 전송

이미지를 Base64로 인코딩하여 JSON 데이터로 전송하고, 서버에서 디코딩 후 저장

 

구현 흐름

  1. 프론트엔드에서 이미지를 Base65로 변환해 API 요청에 포함
  2. 서버에서 Base64 데이터를 디코딩하여 저장

 

장점

  1. API 요청 하나로 이미지를 포함하여 전송 가능
  2. 별도의 파일 관리 필요 없음.

 

단점

  1. 파일 크기가 커질수록 요청 크기 증가
  2. 메모리 및 CPU 사용량 증가
String base64Image = "data:image/png;base64,...";
byte[] decodedBytes = Base64.getDecoder().decode(base64Image.split(",")[1]);
Files.write(Paths.get("/uploads/image.png"), decodedBytes);

 


위 방법 중 진행하고 있던 프로젝트는 소규모 프로젝트이었기에 1번 방법(서버 디스크에 저장)을 사용하기로 했다.

 

게시글 등록 시 사용자가 여러장의 사진을 등록할 수 있도록 기능을 구현했다.

@PostMapping(value = "/write", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<?> createReview(
            @RequestPart("newReview") ReviewDto reviewDto,
            @RequestPart(value = "uploadedPhotos", required = false) List<MultipartFile> images)
consumes이란?
- 이 API가 어떤 Content-Type의 요청 데이터를 처리할 수 있는지를 지정함.
- MediaType.MULTIPART_FORM_DATA_VALUE는 멀티파트 형식의 요청을 처리한다는 것을 명시함.

 

멀티파트 형식이란?

 - 파일과 데이터를 동시에 전송할 때 사용하는 HTTP 요청 방식

 - Content-Type: multipart/form-data 헤더를 포함하여, 데이터를 여러 부분(part)으로 나누어 전송함.

 - 각 파트에는 파일 데이터, 텍스트 데이터 등의 정보가 포함될 수 있음.

 

프론트에서 보낸 이미지 파일을 경로로 저장하기 위해서 ImageUtil.class를 따로 생성해 파일 경로를 반환해주도록 구현했다.

//application.properties
file.upload-dir=C:/uploads/
spring.web.resources.static-locations=file:///C:/uploads/
//ImageUtil.class
@Component
public class ImageUtil {
    @Value("${file.upload-dir}")
    private String uploadDir;
}

 

디렉토리는 c:/uploads/에 저장했다. 사용자 ID별 디렉토리(/uploads/{userId}/)에 저장하면 좋았겠지만 일단 통합 폴더에 저장하기로 결정했다.

 

//ImageUtil.class
@Component
public class ImageUtil {
	
    @Value("${file.upload-dir}")
    private String uploadDir;
    
    public String saveImage(MultipartFile image) throws IOException {
        String fileName = UUID.randomUUID() + "_" + image.getOriginalFilename();
        String filePath = uploadDir + fileName;
    }
}

파일 저장 시 원본 이름 대신 고유 식별자(UUID)를 사용

 

//ImageUtil.class
@Component
public class ImageUtil {
    public String saveImage(MultipartFile image) throws IOException {
        File dest = new File(filePath);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        image.transferTo(dest);
        return filePath;
    }
}

 

클라이언트가 업로드한 파일 데이터를 지정된 경로(dest)에 전송하고 저장, transferTo 메서드는 파일의 스트림을 처리하고, 임시 파일을 관리하는 작업을 자동으로 처리함.

 

//Controller.class
reviewService.uploadImage(id, photoPath);

 

서비스 로직을 통해 이미지 저장

 

이미지 저장 후 DB 내 경로 저장 완료

 

DB에서 사진 경로만 저장하는 테이블을 따로 생성해서 관리했다.

위 처럼 구현하고 사진 경로를 저장하면 아래와 같이 저장이된다.


사진 경로를 불러오는 과정에서 사진 저장 경로에 http://localhost:8080이 계속 추가되는 오류가 발생했다.

DB에서 사진 경로를 불러온 후 웹에서 보여주기 위해 http://localhost:8080 해당 경로를 사진 경로 앞에 붙이다보니 계속해서 오류가 발생했던 것이다.

백엔드에서 Dto를 반환할 때 이미 경로에 http://localhost:8080가 포함되어 있다면 그대로 사용하고, 그렇지 않으면 추가하여 반환하도록 했다.

private List<ReviewResponseDto> makeReviewsDto(List<ReviewDto> findReviews) {
    List<String> absolutePhotoUrls = findPhotosByPostId.stream()
            .map(photoPath -> {
                // 경로 수정 (C:/uploads/ -> /uploads/)
                String relativePath = photoPath.replace("C:/uploads/", "/uploads/");

                // 이미 localhost:8080이 포함되어 있다면 그대로 사용, 그렇지 않으면 추가
                return relativePath.startsWith(BASE_URL) ? relativePath : BASE_URL + relativePath;
            })
            .toList();
    }

 

'스터디' 카테고리의 다른 글

[Algorith] 조합  (0) 2024.02.25
[Clean Code] 7장 - 오류 처리  (0) 2024.01.20
[Clean Code] 5장 - 형식 맞추기  (0) 2024.01.15
[Clean Code] 3장 - 함수  (1) 2024.01.15
[Clean Code] 2장 - 의미 있는 이름  (1) 2024.01.14