9oormthonUNIV 스터디 프로젝트

S3를 이용한 이미지 관리[SpringBoot]

연향동큰손 2025. 4. 15. 00:19

 

프로젝트 구현 목표
1. 제목, 내용, 이미지를 첨부하여 게시글 작성(Create) API 구현
2. DB에는 제목, 내용, 이미지 URL 저장
3. 이미지는 S3에 저장

 

 

S3 버킷 생성 방법 관련 글

 

AWS S3 활용한 파일 및 이미지 업로드

S3란 무엇인가?S3는 AWS에서 제공하는 '클라우드 스토리지 서비스'이다. 쉽게 말하면 AWS에서 제공하는 파일 저장 서비스이다. S3의 사용 용도이미지나 동영상 저장 및 제공정적 웹사이트 호스팅CD

developerwoohyeon.tistory.com

 

 

 

 

Post 엔티티 생성

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    private String image;

}

 

 

<PostRepository>

import S3_9oormthonUNIV.demo.domain.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> {
}

 

 

<S3Config>

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;
    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;
    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder
                .standard() //표준 설정을 기반으로 빌더 인스턴스를 생성
                .withCredentials(new AWSStaticCredentialsProvider(credentials)) //이 AmazonS3 객체는 accessKey, secretKey를 사용해서 인증을 하게 된다.
                .withRegion(region)
                .build();
    }

}

 

AmazonS3 객체를 생성해서 Spring Bean으로 등록하는 S3Config클래스를 생성해줘야 한다.

 

 

 

<PostService>

이 클래스는 S3 버킷에 객체를 업로드하는 동작을 구현한 클래스이다.

@Slf4j
@RequiredArgsConstructor
@Component
public class PostService {

    private final AmazonS3 amazonS3;
    private final PostRepository postRepository;

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

    public String upload(MultipartFile image) {
        if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
            throw new S3Exception(ErrorCode.EMPTY_FILE_EXCEPTION);
        }
        return this.uploadImage(image);
    }

    private String uploadImage(MultipartFile image) {
        this.validateImageFileExtention(image.getOriginalFilename());
        try {
            return this.uploadImageToS3(image);
        } catch (IOException e) {
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_UPLOAD);
        }
    }

    private void validateImageFileExtention(String filename) {
        int lastDotIndex = filename.lastIndexOf(".");
        if (lastDotIndex == -1) {
            throw new S3Exception(ErrorCode.NO_FILE_EXTENTION);
        }

        String extention = filename.substring(lastDotIndex + 1).toLowerCase();
        List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");

        if (!allowedExtentionList.contains(extention)) {
            throw new S3Exception(ErrorCode.INVALID_FILE_EXTENTION);
        }
    }

    private String uploadImageToS3(MultipartFile image) throws IOException {
        String originalFilename = image.getOriginalFilename(); //원본 파일 명
        String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명

        String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //UUID를 통해 중복없이 변경된 파일 명

        log.info("[S3 업로드 시작] 원본파일명: {}, 확장자: {}, 저장될 S3 파일명: {}", originalFilename, extention, s3FileName);

        InputStream is = image.getInputStream();
        byte[] bytes = IOUtils.toByteArray(is);

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + extention); // OK: image/png
        metadata.setContentLength(bytes.length);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try{
            PutObjectRequest putObjectRequest =
                    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);
                            //.withCannedAcl(CannedAccessControlList.PublicRead); ACL (Access Control List)말고 정책으로 명시 했기 때문에 이 코드는 필요 없다.

            amazonS3.putObject(putObjectRequest); // put image to S3
        }catch (Exception e){
            log.error("[S3 업로드 실패] 에러 메시지: {}", e.getMessage(), e);
            throw new S3Exception(ErrorCode.PUT_OBJECT_EXCEPTION);
        }finally {
            byteArrayInputStream.close();
            is.close();
        }

        return amazonS3.getUrl(bucketName, s3FileName).toString();
    }

    public void deleteImageFromS3(String imageAddress){
        String key = getKeyFromImageAddress(imageAddress);
        try{
            amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
        }catch (Exception e){
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
        }
    }

    private String getKeyFromImageAddress(String imageAddress){
        try{
            URL url = new URL(imageAddress);
            String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
            return decodingKey.substring(1); // 맨 앞의 '/' 제거
        }catch (MalformedURLException | UnsupportedEncodingException e){
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
        }
    }
    public void savePost(String title, String content, MultipartFile imageFile) {
        // 1. 이미지 S3 업로드
        String imageUrl = this.upload(imageFile);

        // 2. Post 객체 생성 및 저장
        Post post = Post.builder()
                .title(title)
                .content(content)
                .image(imageUrl)
                .build();

        postRepository.save(post);
    }

    @Transactional
    public void deleteImageAndUpdatePost(Long postId) {
        // 1. 게시글 조회
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new S3Exception(ErrorCode.POST_NOT_FOUND));

        // 2. 기존 이미지 주소 가져오기
        String imageUrl = post.getImage();
        if (imageUrl != null && !imageUrl.isEmpty()) {
            // 3. S3에서 이미지 삭제
            deleteImageFromS3(imageUrl);

            // 4. DB에서 이미지 필드 초기화
            post.setImage(null);
            postRepository.save(post);
        }
    }
}

 

1. 컨트롤러에서 savePost(title,content,imageFile) 호출

 

2. 이미지 파일을 S3 버킷에 업로드 하기 위해 upload(imageFile) 호출 ( 이미지가 비어있거나 파일명이 없는 경우 예외 발생) -> uploadImage() 호출

 

3. validateImageFileExtention()을 통해 확장자가 .jpg, .jpeg, .png, .gif 중 하나인지 확인 -> uploadImageToS3() 호출

 

4.  uploadImageToS3() 호출

  • 파일 이름 생성 -> UUID + 원본 파일명 (파일명 중복 방지)
  • 이미지를 byte[]로 읽음
  • S3 업로드용 메타데이터 생성
  • PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);를 통해 업로드용 요청 객체 생성
  • amazonS3.putObject() => S3 버킷으로 업로드!
  • 이미지 URL을 리턴

5. 이미지 URL과 함께 게시글 DB에 저장