[개인 프로젝트] 이미지 처리 방법에 대한 고민

Turtle·2024년 9월 10일
0

개인 프로젝트 기록

목록 보기
13/15

👉문제 상황

데이터베이스(DB)에 이미지를 직접 저장하는 것은 권장되지 않는 방법이다. 그 이유는 다음과 같다.

  1. 성능 저하 : 이미지 데이터는 용량이 크기 때문에 DB 쿼리 속도를 현저하게 떨어뜨린다. 특히 많은 이미지를 조회하거나 복잡한 쿼리를 실행할 때 성능 문제가 심각해질 수 있다.
  2. 디스크 I/O 부하 증가 : 이미지 데이터를 DB에 저장하게 되면 디스크 I/O 부하가 증가하여 전체 시스템 성능에 영향을 미친다.
  3. DB 용량 부족 : 이미지 데이터를 시간이 지날수록 증가하기에 제한적인 DB 용량을 빠르게 소모한다. 이는 DB 관리 비용의 증가로 이어진다.
  4. 이미지 데이터는 동일한 크기와 형식을 가지므로 수평적 확장이 어렵다.

구글링을 해봤더니 대부분 S3와 같은 객체 저장소를 별도로 활용하는 방법이 많았다.

AWS S3를 사용하는 이유는 아래와 같다.

S3는 Amazon Simple Storage Service의 약자로, 대용량 파일을 안전하고 저렴하게 저장하기 위한 클라우드 스토리지 서비스를 말한다. 이미지와 같은 비정형 데이터를 저장하는 데 매우 효율적이며, 다음과 같은 장점이 있다.

  1. 무제한 저장 공간 : 필요에 따라 저장 공간을 쉽게 확장할 수 있다.
  2. 높은 가용성 : 여러 지역에 분산되어 있어 안정적인 서비스 제공이 가능하다.
  3. 빠른 데이터 전송 : CDN(Content Delivery Network)과 연동하여 전 세계 어디서든 빠르게 데이터에 접근할 수 있다.
  4. 사용한 만큼만 비용을 지불하면 되므로 효율적이다.
  5. 버전 관리, 라이프 사이클 관리 등 다양한 기능을 제공하여 데이터 관리를 효율적으로 할 수 있다.

데이터베이스(DB)에 이미지를 직접 저장하는 것은 권장되지 않는 방법이다. 그 이유는 다음과 같다.

❓사용자에게 보여지는 이미지는 사용자가 업로드한 파일명인가? 아니면 서버에 저장된 경로인가?

사용자에게 보여지는 이미지는 사용자가 업로드한 파일명이 아니라 서버에서 생성된 URL이다.

만약 A, B 두 사용자가 존재하는 상황에서 A가 a라는 이름의 이미지를 저장하고 B 역시 a라는 이름의 이미지를 저장한다고 가정하자.
※ 문제 상황을 만들기 위해 같은 이름을 가지나 이미지는 다르다고 가정한다.

  1. 파일 덮어쓰기 문제
  • 서버에서 파일을 저장하는 경로가 동일하다면 나중에 업로드된 파일이 이전 파일을 덮어쓸 수 있는 문제가 발생한다. 그렇게 되면 순서에 따라 이미지가 바뀌는 문제가 발생한다.
  • 덮어쓰기로 인해 원본 이미지 데이터가 손실될 수 있다.
  1. 파일 관리 어려움
  • 동일한 파일명을 가진 파일들이 섞여 있어 어떤 파일이 어떤 사용자의 것인지 구분하기 어렵다.
  • 파일 관리가 어려워지면 백업 및 복원 시 문제가 발생할 가능성이 높아진다.

❓그렇다면 서버는 어떤 방식으로 문제를 해결할까?

  1. 고유한 파일명 생성
  • 업로드 시 파일명에 고유한 값(UUID 등등...)을 추가하여 파일명이 중복되지 않도록 만든다.

→ 데이터 정합성도 지키면서 효율적으로 데이터를 관리하기 위해 S3 객체 저장소와 DB를 같이 활용하기로 했다.
→ S3 버킷 생성과 관련된 부분은 남기지 않겠다.

♻️개선 코드

cloud:
  aws:
    s3:
      bucketName: S3 버킷명
    credentials:
      access-key: 액세스 키
      secret-key: 비밀 키
    region:
      static: 리전
    stack:
      auto: false
@Configuration
public class AwsS3Config {
	
    // 액세스 키
	@Value("${cloud.aws.credentials.access-key}")
	private String accessKey;
	
    // 비밀 키
	@Value("${cloud.aws.credentials.secret-key}")
	private String secretKey;

	// S3 리전
	@Value("${cloud.aws.region.static}")
	private String region;

	@Bean
	public AmazonS3Client amazonS3Client() {
		AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
		return (AmazonS3Client) AmazonS3ClientBuilder
				.standard()
				.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
				.withRegion(region)
				.build();
	}
}
@Slf4j
@RequiredArgsConstructor
@Service
public class AwsS3Uploader {

	private final AmazonS3Client amazonS3Client;

	@Value("${cloud.aws.s3.bucketName}")
	private String bucket;

	public String uploadFile(MultipartFile multipartFile) throws IOException {
		File convertedFile = convertMultiPartFileToFile(multipartFile);
		String fileName = UUID.randomUUID().toString();
		String fileUrl = uploadToS3(convertedFile, fileName);
		convertedFile.delete();
		return fileUrl;
	}

	private File convertMultiPartFileToFile(MultipartFile file) throws IOException {
		File convertedFile = new File(file.getOriginalFilename());
		try (FileOutputStream fos = new FileOutputStream(convertedFile)) {
			fos.write(file.getBytes());
		}
		return convertedFile;
	}

	// S3 이미지 업로드
	private String uploadToS3(File file, String fileName) {
		amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, file)
				.withCannedAcl(CannedAccessControlList.PublicRead));
		return amazonS3Client.getUrl(bucket, fileName).toString();
	}

	// S3 이미지 삭제(데이터 정합성)
	public void deleteFile(String fileName) {
		try {
			if (amazonS3Client.doesObjectExist(bucket, fileName)) {
				amazonS3Client.deleteObject(bucket, fileName);
				log.info("파일 삭제가 정상적으로 처리되었습니다.={}", fileName);
			} else {
				log.warn("해당 파일이 존재하지 않습니다.={}", fileName);
			}
		} catch (Exception e) {
			throw new RuntimeException("S3 파일 삭제 실패", e);
		}
	}
}

0개의 댓글