토이 프로젝트 스터디 #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
- 특정 시간 이전에 생성되었으며
Post
가 null
이여서 사용되지 않는 이미지 조회
- 사용되지 않는 이미지를 조회하기 위한 메소드
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회로 변경