토이 프로젝트 스터디 #13

appti·2022년 6월 25일
0

토이 프로젝트 스터디 #13

  • 스터디 진행 날짜 : 6/25
  • 스터디 작업 날짜 : 6/23 ~ 6/25

토이 프로젝트 진행 사항

  • 이미지 관련 코드 작성
  • AWS S3 + Cloudfront 적용

내용

  • 이미지 업로드 방식
    • Velog 처럼 글 작성과는 별도로 이미지를 업로드 하는 방식
      • 업로드만 하고 사용하지 않는 이미지 처리 필요
      • 단 아직 글을 작성하느라 실제 DB에 반영되지 않을 수도 있으므로, 특정 시간 이후 사용되지 않은 이미지만 처리 대상이 되어야 함

Entity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Image extends BaseTimeEntity {

    private final static String supportedExtension[] = {"jpg", "jpeg", "gif", "bmp", "png"};

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

    @Column(nullable = false)
    private String uniqueName;

    @Column(nullable = false)
    private String originName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    public Image(String originName) {
        this.uniqueName = generateUniqueName(extractExtension(originName));
        this.originName = originName;
    }

    public void initPost(Post post) {
        if (this.post == null) {
            this.post = post;
        }
    }

    public void removePost() {
        this.post.getImages().remove(this);
        this.post = null;
    }

    private String generateUniqueName(String extension) {
        return UUID.randomUUID().toString() + "." + extension;
    }

    private String extractExtension(String originName) {
        try {
            String ext = originName.substring(originName.lastIndexOf(".") + 1);
            if (isSupportedFormat(ext)) {
                return ext;
            }
        } catch (StringIndexOutOfBoundsException e) {}
        throw new UnsupportedImageFormatException();
    }

    private boolean isSupportedFormat(String ext) {
        return Arrays.stream(supportedExtension).anyMatch(e -> e.equalsIgnoreCase(ext));
    }

}
  • 실제 파일의 이름과 유니크한 파일의 이름을 구분
  • 나중에 Post에서 사용하지 않은 이미지를 구분하기 위해 Cascade 옵션을 부여하지 않음
  • 생성자에서 유니크한 파일 이름을 생성
    • extractExtension() 메소드를 통해 해당 파일을 이미지로 사용할 수 있는지 확인

Repository

public interface ImageRepository extends JpaRepository<Image, Long> {
}
  • findByCreatedDateLessThanAndPostIsNull
    • 특정 시간 이전에 생성되었으며 Postnull이여서 사용되지 않는 이미지 조회
    • 사용되지 않는 이미지를 조회하기 위한 메소드

Service

@Service
@Transactional
@RequiredArgsConstructor
public class ImageService {

    private final ImageRepository imageRepository;
    private final ImageUtils imageUtils;

    public ImagePathResponse getImagePath(Long imageId) {
        return convert(
                imageUtils.getImageFilePath(
                        imageRepository.findById(imageId).orElseThrow(ImageNotFoundException::new)));
    }

    public CreateImageResponse saveImages(CreateImageRequest request) {
        List<Long> imageIds = new ArrayList<>();

        IntStream.range(0, request.getImages().size()).forEach(
                index -> {
                    Image saveImage =
                            imageRepository.save(
                                    ImageEntityConverter.convert(request.getImages().get(index).getOriginalFilename()));
                    imageIds.add(saveImage.getId());
                    imageUtils.upload(request.getImages().get(index), saveImage.getUniqueName());
                }
        );

        return convert(imageIds);
    }

    public DeleteImageResponse deleteImage(DeleteImageRequest request) {
        Image image = imageRepository.findById(request.getImageId()).orElseThrow(ImageNotFoundException::new);
        image.removePost();

        return convert(image.getId());
    }
}
  • imageUtils
    • 로컬에서 실행할 때에는 로컬 경로에 이미지 저장
    • 배포 환경에서 실행할 때에는 AWS S3에 이미지 저장

ImageUtils

public class LocalImageUtils implements ImageUtils {

    private static String PATH = "/";

    @Override
    public void upload(MultipartFile file, String uniqueName) {
        if (file.isEmpty()) {
            return ;
        }

        try {
            file.transferTo(new File(PATH + uniqueName));
        }
        catch (IOException e) {
            throw new CannotUploadImageException(e);
        }
    }

    @Override
    public String getImageFilePath(Image image) {
        return PATH + image.getUniqueName();
    }
}
@RequiredArgsConstructor
public class S3ImageUtils implements ImageUtils {

    @Value("${cloud.aws.cloudfront.url}")
    private String url;

    private final AmazonS3Client amazonS3Client;

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

    @Override
    public void upload(MultipartFile file, String uniqueName) {
        try {
            amazonS3Client.putObject(bucket, uniqueName, file.getInputStream(), generateObjectMetaData(file));
        } catch (IOException e) {
            throw new CannotUploadImageException(e);
        }
    }

    @Override
    public String getImageFilePath(Image image) {
        return url + "/" + image.getUniqueName();
    }

    private ObjectMetadata generateObjectMetaData(MultipartFile file) {
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(file.getSize());
        objectMetadata.setContentType(file.getContentType());
        return objectMetadata;
    }

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/images")
public class ImageController {

    private final ImageService imageService;

    @PostMapping("/upload")
    public ResponseEntity<CreateImageResponse> uploadImages(@Validated @ModelAttribute CreateImageRequest request) {
        return new ResponseEntity(imageService.saveImages(request), HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity getImagePath(@PathVariable Long id) {
        return new ResponseEntity(imageService.getImagePath(id), HttpStatus.OK);
    }

    @DeleteMapping("/delete")
    public ResponseEntity deleteImages(DeleteImageRequest request) {
        return new ResponseEntity(imageService.deleteImage(request), HttpStatus.OK);
    }
}

스터디 내용

  • 스터디 시간대 변경
    • 매주 화목토 -> 매주 월금
    • 알고리즘 및 이력서, 면접 준비 등 취업 준비를 위해 주 3회에서 주 2회로 변경
profile
안녕하세요

0개의 댓글