From 5492ee09bc390ef4e77a2e08aea52134957b56f8 Mon Sep 17 00:00:00 2001 From: songhaechan Date: Wed, 19 Jun 2024 13:06:09 +0900 Subject: [PATCH 1/3] test --- .github/workflows/deploy.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 87e64e7..9f707f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -72,7 +72,7 @@ jobs: # run: docker push haechansomg/simple-board2:was02 - name: Deploy to server - uses: appleboy/ssh-action@v1.0.0 + uses: appleboy/ssh-action@v1.0.3 id: deploy env: COMPOSE: "/home/ubuntu/compose/docker-compose.yml" @@ -85,5 +85,4 @@ jobs: script: | sudo docker-compose -f $COMPOSE down --rmi all sudo docker pull haechansomg/simple-board:was01 - sudo docker-compose -f $COMPOSE up -d - sudo docker image prune -f \ No newline at end of file + sudo docker-compose -f $COMPOSE up -d \ No newline at end of file From 2a9636e79e035f5ff7ae722e30ddac07c49ff265 Mon Sep 17 00:00:00 2001 From: songhaechan Date: Fri, 5 Jul 2024 22:16:46 +0900 Subject: [PATCH 2/3] feat: MessageQueue --- .github/workflows/deploy.yml | 2 +- build.gradle | 1 + docker-compose.yml | 4 +- .../java/squad/board/BoardApplication.java | 2 +- .../apicontroller/BoardApiController.java | 36 +++-- .../board/config/AsyncThreadPoolConfig.java | 20 +++ .../java/squad/board/config/MailConfig.java | 15 ++ .../java/squad/board/config/RedisConfig.java | 20 ++- src/main/java/squad/board/dto/S3/S3Task.java | 17 ++ .../board/dto/board/ImageInfoRequest.java | 10 ++ .../squad/board/service/BoardService.java | 64 ++++---- .../squad/board/service/S3FailMailSender.java | 23 +++ .../squad/board/service/S3MessageQueue.java | 50 ++++++ .../java/squad/board/service/S3Service.java | 32 ++-- src/main/resources/application.yml | 2 +- .../resources/static/js/board/createBoard.js | 150 ++++++++++++------ src/main/resources/static/js/board/url.js | 4 +- .../templates/board/createBoard.html | 120 +++++++------- 18 files changed, 399 insertions(+), 173 deletions(-) create mode 100644 src/main/java/squad/board/config/AsyncThreadPoolConfig.java create mode 100644 src/main/java/squad/board/config/MailConfig.java create mode 100644 src/main/java/squad/board/dto/S3/S3Task.java create mode 100644 src/main/java/squad/board/service/S3FailMailSender.java create mode 100644 src/main/java/squad/board/service/S3MessageQueue.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9f707f4..02d9e60 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -85,4 +85,4 @@ jobs: script: | sudo docker-compose -f $COMPOSE down --rmi all sudo docker pull haechansomg/simple-board:was01 - sudo docker-compose -f $COMPOSE up -d \ No newline at end of file + sudo docker-compose -f $COMPOSE up -d \ No newline at end of file diff --git a/build.gradle b/build.gradle index cd7673e..7cc1544 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-quartz:2.7.5' implementation 'org.apache.commons:commons-collections4:4.0' implementation group: 'com.github.javafaker', name: 'javafaker', version: '1.0.2' + implementation 'org.springframework.boot:spring-boot-starter-mail' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2") } diff --git a/docker-compose.yml b/docker-compose.yml index 2e8b663..c2e84d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,6 @@ services: # - 9121:9121 # environment: # REDIS_ADDR: "redis:6379" - links: - - redis +# links: +# - redis # - prometheus \ No newline at end of file diff --git a/src/main/java/squad/board/BoardApplication.java b/src/main/java/squad/board/BoardApplication.java index 77e9d96..f347fd6 100644 --- a/src/main/java/squad/board/BoardApplication.java +++ b/src/main/java/squad/board/BoardApplication.java @@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.web.http.CookieSerializer; @@ -13,7 +14,6 @@ @SpringBootApplication @EnableRedisHttpSession @EnableConfigurationProperties -@EnableScheduling public class BoardApplication { public static void main(String[] args) { diff --git a/src/main/java/squad/board/apicontroller/BoardApiController.java b/src/main/java/squad/board/apicontroller/BoardApiController.java index 0086701..e455b11 100644 --- a/src/main/java/squad/board/apicontroller/BoardApiController.java +++ b/src/main/java/squad/board/apicontroller/BoardApiController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import squad.board.aop.BoardWriterAuth; @@ -21,15 +22,24 @@ public class BoardApiController { // 게시글 생성 @PostMapping(value = "/boards") public CommonIdResponse saveBoard( - @SessionAttribute Long memberId, - @Valid @RequestBody CreateBoardRequest createBoard) { + @SessionAttribute Long memberId, + @Valid @RequestBody CreateBoardRequest createBoard) { return boardService.createBoard(memberId, createBoard); } +// @PostMapping(value = "/boards", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, +// MediaType.APPLICATION_JSON_VALUE}) +// public CommonIdResponse saveBoard( +// @SessionAttribute Long memberId, +// @RequestPart CreateBoardRequest createBoard, +// @RequestPart("images") MultipartFile[] images) { +// return boardService.createBoard(memberId, createBoard, images); +// } + // 이미지 S3 전송 @PostMapping(value = "/boards/img") public ImageInfoResponse saveImg( - @RequestPart(value = "image") MultipartFile image) { + @RequestPart(value = "image") MultipartFile image) { return boardService.saveImageToS3(image); } @@ -37,7 +47,7 @@ public ImageInfoResponse saveImg( @DeleteMapping("/boards/{boardId}") @BoardWriterAuth public CommonIdResponse deleteBoard( - @PathVariable Long boardId + @PathVariable Long boardId ) { return boardService.deleteBoard(boardId); } @@ -46,8 +56,8 @@ public CommonIdResponse deleteBoard( @PatchMapping("/boards/{boardId}") @BoardWriterAuth public CommonIdResponse updateBoard( - @PathVariable Long boardId, - @Valid @RequestBody BoardUpdateRequest boardUpdateRequest + @PathVariable Long boardId, + @Valid @RequestBody BoardUpdateRequest boardUpdateRequest ) { return boardService.updateBoard(boardId, boardUpdateRequest); } @@ -55,7 +65,7 @@ public CommonIdResponse updateBoard( // 상세 게시글 조회 @GetMapping("/boards/{boardId}") public BoardDetailResponse getBoard( - @PathVariable Long boardId + @PathVariable Long boardId ) { return boardService.findOneBoard(boardId); } @@ -63,8 +73,8 @@ public BoardDetailResponse getBoard( // 전체 게시글 조회 (페이징 처리) @GetMapping("/boards") public ContentListResponse boardList( - @RequestParam Long size, - @RequestParam Long page + @RequestParam Long size, + @RequestParam Long page ) { return boardService.findBoards(size, page); } @@ -72,10 +82,10 @@ public ContentListResponse boardList( // 게시글 검색 @GetMapping("/boards/search") public ContentListResponse searchBoard( - @RequestParam String keyWord, - @RequestParam Long size, - @RequestParam Long page, - @RequestParam String searchType + @RequestParam String keyWord, + @RequestParam Long size, + @RequestParam Long page, + @RequestParam String searchType ) { return boardService.searchBoard(keyWord, size, page, searchType); } diff --git a/src/main/java/squad/board/config/AsyncThreadPoolConfig.java b/src/main/java/squad/board/config/AsyncThreadPoolConfig.java new file mode 100644 index 0000000..8cb99bb --- /dev/null +++ b/src/main/java/squad/board/config/AsyncThreadPoolConfig.java @@ -0,0 +1,20 @@ +package squad.board.config; + +import java.util.concurrent.Executor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@EnableAsync +public class AsyncThreadPoolConfig { + + @Bean + public Executor asyncThreadTaskExecutor() { + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setCorePoolSize(1); + threadPoolTaskExecutor.setMaxPoolSize(1); + return threadPoolTaskExecutor; + } +} diff --git a/src/main/java/squad/board/config/MailConfig.java b/src/main/java/squad/board/config/MailConfig.java new file mode 100644 index 0000000..8b9e288 --- /dev/null +++ b/src/main/java/squad/board/config/MailConfig.java @@ -0,0 +1,15 @@ +package squad.board.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +@Configuration +public class MailConfig { + + @Bean + public JavaMailSender javaMailSender() { + return new JavaMailSenderImpl(); + } +} diff --git a/src/main/java/squad/board/config/RedisConfig.java b/src/main/java/squad/board/config/RedisConfig.java index a1d9791..ffc3596 100644 --- a/src/main/java/squad/board/config/RedisConfig.java +++ b/src/main/java/squad/board/config/RedisConfig.java @@ -27,6 +27,18 @@ public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(redisConfiguration); } + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + // 일반적인 key:value의 경우 시리얼라이저 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + + return redisTemplate; + } + @Bean public RedisConnectionFactory redisConnectionFactoryToken() { RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(); @@ -36,14 +48,6 @@ public RedisConnectionFactory redisConnectionFactoryToken() { return new LettuceConnectionFactory(redisConfiguration); } - @Bean(name = "redisTemplate") - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory()); - redisTemplate.setDefaultSerializer(new StringRedisSerializer()); - return redisTemplate; - } - @Bean(name = "redisTemplateToken") public RedisTemplate redisTemplateToken() { RedisTemplate redisTemplate = new RedisTemplate<>(); diff --git a/src/main/java/squad/board/dto/S3/S3Task.java b/src/main/java/squad/board/dto/S3/S3Task.java new file mode 100644 index 0000000..4fd2f6f --- /dev/null +++ b/src/main/java/squad/board/dto/S3/S3Task.java @@ -0,0 +1,17 @@ +package squad.board.dto.S3; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor +@Getter +@Builder +public class S3Task { + private String imgUUID; + private Integer failCount = 0; + + public void increaseFailCount() { + this.failCount++; + } +} diff --git a/src/main/java/squad/board/dto/board/ImageInfoRequest.java b/src/main/java/squad/board/dto/board/ImageInfoRequest.java index 526f19e..4050c49 100644 --- a/src/main/java/squad/board/dto/board/ImageInfoRequest.java +++ b/src/main/java/squad/board/dto/board/ImageInfoRequest.java @@ -3,6 +3,9 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import squad.board.dto.S3.S3Task; + +import javax.management.timer.TimerMBean; @AllArgsConstructor @Getter @@ -11,4 +14,11 @@ public class ImageInfoRequest { private String imageUUID; private long imageSize; private String imageOriginalName; + + public S3Task toS3Task() { + return S3Task.builder() + .imgUUID(imageUUID) + .failCount(0) + .build(); + } } diff --git a/src/main/java/squad/board/service/BoardService.java b/src/main/java/squad/board/service/BoardService.java index 19147f4..9eec7e5 100644 --- a/src/main/java/squad/board/service/BoardService.java +++ b/src/main/java/squad/board/service/BoardService.java @@ -1,5 +1,8 @@ package squad.board.service; +import java.time.LocalDateTime; +import java.util.List; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -10,7 +13,12 @@ import squad.board.domain.board.Board; import squad.board.dto.ContentListResponse; import squad.board.dto.Pagination; -import squad.board.dto.board.*; +import squad.board.dto.board.BoardDetailResponse; +import squad.board.dto.board.BoardResponse; +import squad.board.dto.board.BoardUpdateRequest; +import squad.board.dto.board.CreateBoardRequest; +import squad.board.dto.board.ImageInfoRequest; +import squad.board.dto.board.ImageInfoResponse; import squad.board.exception.board.BoardException; import squad.board.exception.board.BoardStatus; import squad.board.exception.image.ImageException; @@ -19,9 +27,6 @@ import squad.board.repository.CommentMapper; import squad.board.repository.ImageMapper; -import java.time.LocalDateTime; -import java.util.List; - @Service @RequiredArgsConstructor @Transactional @@ -32,31 +37,27 @@ public class BoardService { private final CommentMapper commentMapper; private final ImageMapper imageMapper; private final S3Service s3Service; + private final S3MessageQueue messageQueue; private static final long MAX_IMAGE_SIZE = 5000000L; private static final int MAX_IMAGE_NAME_SIZE = 100; - private static final String TEMP_FOLDER_NAME = "tmp"; - private static final String ORIGINAL_FOLDER_NAME = "original"; private static final String IMAGE_EXTENSION_EXTRACT_REGEX = "(.png|.jpg|.jpeg)$"; - + private static final String TEMP_FOLDER_NAME = "tmp"; + public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) { // 게시글 저장 Board board = createBoard.toEntity(memberId); boardMapper.save(board); // 이미지 정보 저장 if (createBoard.isImageExist()) { - saveImageInfo(board.getBoardId(), createBoard.getImageInfo()); + imageMapper.save(createBoard.getImageInfo(), board.getBoardId()); + messageQueue.pushAll(createBoard.getImageInfo()); + s3Service.moveImageToOriginal(); } + log.info("board id = {}, successfully moved {} images", board.getBoardId(), + createBoard.getImageInfo().size()); return new CommonIdResponse(board.getBoardId()); } - private void saveImageInfo(Long boardId, List imageInfoRequests) { - imageMapper.save(imageInfoRequests, boardId); - for (ImageInfoRequest request : imageInfoRequests) { - // tmp 폴더의 이미지를 original 폴더로 이동 - s3Service.moveImageToOriginal(request.getImageUUID(), TEMP_FOLDER_NAME, ORIGINAL_FOLDER_NAME); - } - } - public ImageInfoResponse saveImageToS3(MultipartFile image) { imageValidation(image); String uuid = s3Service.saveFile(image, TEMP_FOLDER_NAME); @@ -66,7 +67,8 @@ public ImageInfoResponse saveImageToS3(MultipartFile image) { private void imageValidation(MultipartFile image) { String imageName = image.getOriginalFilename(); - if (!imageName.substring(imageName.lastIndexOf(".")).matches(IMAGE_EXTENSION_EXTRACT_REGEX)) { + if (!imageName.substring(imageName.lastIndexOf(".")) + .matches(IMAGE_EXTENSION_EXTRACT_REGEX)) { throw new ImageException(ImageStatus.INVALID_IMAGE_EXTENSION); } // 이미지 파일명 길이 제한 @@ -81,20 +83,24 @@ private void imageValidation(MultipartFile image) { } @Transactional(readOnly = true) - public ContentListResponse findBoards(Long size, Long requestPage, Long memberId) { - if (memberId == null) { + public ContentListResponse findBoards(Long size, Long requestPage, + Long memberId) { + if (memberId==null) { throw new BoardException(BoardStatus.INVALID_MEMBER_ID); } Long offset = calcOffset(requestPage, size); - Pagination boardPaging = new Pagination(requestPage, boardMapper.countBoards(memberId), size); - return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, memberId), boardPaging); + Pagination boardPaging = new Pagination(requestPage, boardMapper.countBoards(memberId), + size); + return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, memberId), + boardPaging); } @Transactional(readOnly = true) public ContentListResponse findBoards(Long size, Long requestPage) { Long offset = calcOffset(requestPage, size); Pagination boardPaging = new Pagination(requestPage, boardMapper.countBoards(null), size); - return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, null), boardPaging); + return new ContentListResponse<>(boardMapper.findAllWithNickName(size, offset, null), + boardPaging); } @Transactional(readOnly = true) @@ -118,7 +124,7 @@ public CommonIdResponse updateBoard(Long boardId, BoardUpdateRequest updateBoard List imageInfoRequests = updateBoard.getImageInfoList(); // DB에 이미지 정보 저장 if (CollectionUtils.isNotEmpty(imageInfoRequests)) { - saveImageInfo(boardId, imageInfoRequests); + imageMapper.save(imageInfoRequests, boardId); } // 기존 이미지 정보 List savedImageUuid = imageMapper.findImageUuid(boardId); @@ -139,11 +145,15 @@ private void deleteImageInfo(List deleteRequestImageUuid) { } @Transactional(readOnly = true) - public ContentListResponse searchBoard(String keyWord, Long size, Long requestPage, String searchType) { + public ContentListResponse searchBoard(String keyWord, Long size, + Long requestPage, String searchType) { Long offset = calcOffset(requestPage, size); - Pagination boardPaging = new Pagination(requestPage, boardMapper.countByKeyWord(keyWord, searchType), size); - List byKeyWord = boardMapper.findByKeyWord(keyWord, size, offset, searchType); - return new ContentListResponse<>(boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging); + Pagination boardPaging = new Pagination(requestPage, + boardMapper.countByKeyWord(keyWord, searchType), size); + List byKeyWord = boardMapper.findByKeyWord(keyWord, size, offset, + searchType); + return new ContentListResponse<>( + boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging); } private Long calcOffset(Long page, Long size) { diff --git a/src/main/java/squad/board/service/S3FailMailSender.java b/src/main/java/squad/board/service/S3FailMailSender.java new file mode 100644 index 0000000..7932bf2 --- /dev/null +++ b/src/main/java/squad/board/service/S3FailMailSender.java @@ -0,0 +1,23 @@ +package squad.board.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class S3FailMailSender { + private final JavaMailSender javaMailSender; + private static final String ADMIN_EMAIL = "bukak2019@naver.com"; + private static final String TITLE = "S3 Move Action Failed"; + private static final String IMG_UUID = "imgUUID: "; + + public void send(final String imgUUID) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(ADMIN_EMAIL); + message.setSubject(TITLE); + message.setText(IMG_UUID + imgUUID); + javaMailSender.send(message); + } +} diff --git a/src/main/java/squad/board/service/S3MessageQueue.java b/src/main/java/squad/board/service/S3MessageQueue.java new file mode 100644 index 0000000..0e38432 --- /dev/null +++ b/src/main/java/squad/board/service/S3MessageQueue.java @@ -0,0 +1,50 @@ +package squad.board.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import squad.board.dto.S3.S3Task; +import squad.board.dto.board.ImageInfoRequest; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class S3MessageQueue { + + private final RedisTemplate redisTemplate; + private final S3FailMailSender s3FailMailSender; + private static final String MESSAGE_QUEUE = "s3queue"; + + public void pushAll(List images) { + List tasks = getS3Task(images); + redisTemplate.opsForList().leftPushAll(MESSAGE_QUEUE, tasks); + } + + public void push(S3Task task) { + redisTemplate.opsForList().leftPush(MESSAGE_QUEUE, task); + } + + public S3Task pop() { + S3Task s3Task = (S3Task) redisTemplate.opsForList().rightPop(MESSAGE_QUEUE); + if (s3Task.getFailCount() >= 3) { + s3FailMailSender.send(s3Task.getImgUUID()); + } + return s3Task; + } + + public boolean isEmpty() { + if (redisTemplate.opsForList().size(MESSAGE_QUEUE)==0) { + return true; + } + return false; + } + + private List getS3Task(List images) { + return images.stream() + .map(ImageInfoRequest::toS3Task) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/squad/board/service/S3Service.java b/src/main/java/squad/board/service/S3Service.java index cad257a..e50cc59 100644 --- a/src/main/java/squad/board/service/S3Service.java +++ b/src/main/java/squad/board/service/S3Service.java @@ -1,26 +1,28 @@ package squad.board.service; +import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.*; +import com.amazonaws.services.s3.model.ObjectMetadata; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import software.amazon.awssdk.services.s3.model.Delete; -import software.amazon.awssdk.services.s3.model.ObjectIdentifier; +import squad.board.dto.S3.S3Task; import java.io.IOException; -import java.util.List; -import java.util.ListIterator; import java.util.UUID; @RequiredArgsConstructor @Component @Slf4j public class S3Service { + private final AmazonS3 s3Client; + private final S3MessageQueue messageQueue; + private static final String TEMP_FOLDER_NAME = "tmp"; + private static final String ORIGINAL_FOLDER_NAME = "original"; @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -33,16 +35,26 @@ public String saveFile(MultipartFile multipartFile, String folderName) { metadata.setContentLength(multipartFile.getSize()); metadata.setContentType(multipartFile.getContentType()); try { - s3Client.putObject(bucket, folderName + "/" + uuid, multipartFile.getInputStream(), metadata); + s3Client.putObject(bucket, folderName + "/" + uuid, multipartFile.getInputStream(), + metadata); } catch (IOException e) { log.error("Image Upload Failed"); } return uuid; } - public void moveImageToOriginal(String imageUUID, String from, String to) { - s3Client.copyObject(bucket, from + "/" + imageUUID, bucket, to + "/" + imageUUID); - s3Client.deleteObject(bucket, from + "/" + imageUUID); + @Async + public void moveImageToOriginal() { + while (messageQueue.isEmpty()) { + S3Task task = messageQueue.pop(); + try { + s3Client.copyObject(bucket, TEMP_FOLDER_NAME + "/" + task.getImgUUID(), bucket, ORIGINAL_FOLDER_NAME + "/" + task.getImgUUID()); + s3Client.deleteObject(bucket, TEMP_FOLDER_NAME + "/" + task.getImgUUID()); + } catch (AmazonServiceException e) { + task.increaseFailCount(); + messageQueue.push(task); + } + } } // 이미지 소스 반환 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9f96606..3d7808a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,3 @@ spring: profiles: - active: prod + active: dev diff --git a/src/main/resources/static/js/board/createBoard.js b/src/main/resources/static/js/board/createBoard.js index 301a2ae..2372aba 100644 --- a/src/main/resources/static/js/board/createBoard.js +++ b/src/main/resources/static/js/board/createBoard.js @@ -4,65 +4,119 @@ const contentInput = document.getElementById("editor"); const writeBtn = document.getElementById("write-btn"); const imageSelector = document.getElementById('img-selector'); let imageInfoList = []; +// const formData = new FormData(); + +// writeBtn.addEventListener("click", () => { +// +// while (contentInput.firstChild) { +// contentInput.removeChild(contentInput.firstChild); +// } +// +// const title = titleInput.value; +// const content = contentInput.innerHTML; +// const data = { +// "title": title, +// "content": content, +// "imageInfo": imageInfoList +// } +// formData.append('createBoard', +// new Blob([JSON.stringify(data)], {type: 'application/json'})); +// +// axios.post(`${homeUrl}/api/boards`, formData, { +// headers: { +// 'Content-Type': 'multipart/form-data' +// } +// }).then(response => { +// const statusCode = response.status; +// const data = response.data; +// // 게시글 작성 성공 +// if (statusCode === 200) { +// window.location.href = `/boards/${data.id}`; +// } +// }).catch(error => { +// const data = error.response.data; +// // 필드 에러 +// if (data.code === 500) { +// alert(data.fieldErrorMessage) +// } +// }) +// }) writeBtn.addEventListener("click", () => { - const title = titleInput.value; - const content = contentInput.innerHTML; - const data = { - "title": title, - "content": content, - "imageInfo": imageInfoList + const title = titleInput.value; + const content = contentInput.innerHTML; + const data = { + "title": title, + "content": content, + "imageInfo": imageInfoList + } + axios.post(`${homeUrl}/api/boards`, data + ).then(response => { + const statusCode = response.status; + const data = response.data; + // 게시글 작성 성공 + if (statusCode === 200) { + window.location.href = `/boards/${data.id}`; } - axios.post(`${homeUrl}/api/boards`, data - ).then(response => { - const statusCode = response.status; - const data = response.data; - // 게시글 작성 성공 - if (statusCode === 200) { - window.location.href = `/boards/${data.id}`; - } - }).catch(error => { - const data = error.response.data; - // 필드 에러 - if (data.code === 500) { - alert(data.fieldErrorMessage) - } - }) + }).catch(error => { + const data = error.response.data; + // 필드 에러 + if (data.code === 500) { + alert(data.fieldErrorMessage) + } + }) }) // 이미지 첨부기능 const btnImage = document.getElementById('btn-image'); btnImage.addEventListener('click', function () { - imageSelector.click(); + imageSelector.click(); }); +// imageSelector.addEventListener('change', function (e) { +// const files = e.target.files; +// formData.append("images", files[0]); +// const reader = new FileReader(); +// reader.onload = function (event) { +// // 이미지 태그를 생성하고 src 속성을 설정 +// const img = document.createElement('img'); +// img.src = event.target.result; +// img.style.maxWidth = '200px'; // 이미지 크기를 조절할 수 있습니다 +// img.style.maxHeight = '200px'; +// +// // contentInput 요소에 이미지 태그를 추가 +// contentInput.appendChild(img); +// } +// reader.readAsDataURL(files[0]); +// }); + imageSelector.addEventListener('change', function (e) { - const files = e.target.files; - const formData = new FormData(); - let imgSrc; - formData.append("image", files[0]); - axios.post(`${homeUrl}/api/boards/img`, - formData - , { - headers: { - "Content-Type": "multipart/form-data" - } + const files = e.target.files; + const formData = new FormData(); + let imgSrc; + formData.append("image", files[0]); + axios.post(`${homeUrl}/api/boards/img`, + formData + , { + headers: { + "Content-Type": "multipart/form-data" } - ).then(response => { - imgSrc = response.data.imageSrc; - imageInfoList.push( - { - imageUUID: response.data.imageUUID, - imageSize: response.data.imageSize, - imageOriginalName: response.data.imageOriginalName - }); - let img = document.createElement("img"); - img.src = imgSrc; - img.style.width = '600px'; - contentInput.appendChild(img); - }).catch(error => { - const data = error.response.data; - alert(data.message); - }) + } + ).then(response => { + imgSrc = response.data.imageSrc; + imageInfoList.push( + { + imageUUID: response.data.imageUUID, + imageSize: response.data.imageSize, + imageOriginalName: response.data.imageOriginalName + }); + let img = document.createElement("img"); + img.src = imgSrc; + img.style.width = '200px'; + contentInput.appendChild(img); + }).catch(error => { + const data = error.response.data; + alert(data.message); + }) }); diff --git a/src/main/resources/static/js/board/url.js b/src/main/resources/static/js/board/url.js index 833ca6a..a15e4eb 100644 --- a/src/main/resources/static/js/board/url.js +++ b/src/main/resources/static/js/board/url.js @@ -1,2 +1,2 @@ -const homeUrl = 'http://13.125.17.239:80' -// const homeUrl = 'http://localhost:8080' \ No newline at end of file +// const homeUrl = 'http://13.125.17.239:80' +const homeUrl = 'http://localhost:8080' \ No newline at end of file diff --git a/src/main/resources/templates/board/createBoard.html b/src/main/resources/templates/board/createBoard.html index aad7ea7..8a91d12 100644 --- a/src/main/resources/templates/board/createBoard.html +++ b/src/main/resources/templates/board/createBoard.html @@ -1,79 +1,79 @@ - + - - - Simple Board - - + + + Simple Board + +
- +
-
+
- -
- -
- - - - - - - -
-
-
- + +
+ +
+ + + + + + + +
+
+
+ -
- -
+
+
+
- +
From 5e7c136304f2bc5fe0f5a65e0550b6ea31acdaaa Mon Sep 17 00:00:00 2001 From: songhaechan Date: Sat, 6 Jul 2024 21:36:05 +0900 Subject: [PATCH 3/3] feat:normal queue, deadletter queue --- .../java/squad/board/BoardApplication.java | 3 +- .../apicontroller/BoardApiController.java | 27 +++++------ .../java/squad/board/config/RedisConfig.java | 1 - .../board/config/TaskSchedulerConfig.java | 22 +++++++++ src/main/java/squad/board/dto/S3/S3Task.java | 17 ------- .../board/dto/board/ImageInfoRequest.java | 10 ---- .../exception/ObjectMapperException.java | 4 ++ .../squad/board/service/BoardService.java | 41 ++++++++--------- .../java/squad/board/service/S3Consumer.java | 46 +++++++++++++++++++ .../board/service/S3DeadLetterQueue.java | 34 ++++++++++++++ ...3FailMailSender.java => S3MailSender.java} | 7 ++- .../squad/board/service/S3MessageQueue.java | 38 +++++---------- .../java/squad/board/service/S3Service.java | 21 ++------- 13 files changed, 160 insertions(+), 111 deletions(-) create mode 100644 src/main/java/squad/board/config/TaskSchedulerConfig.java delete mode 100644 src/main/java/squad/board/dto/S3/S3Task.java create mode 100644 src/main/java/squad/board/exception/ObjectMapperException.java create mode 100644 src/main/java/squad/board/service/S3Consumer.java create mode 100644 src/main/java/squad/board/service/S3DeadLetterQueue.java rename src/main/java/squad/board/service/{S3FailMailSender.java => S3MailSender.java} (77%) diff --git a/src/main/java/squad/board/BoardApplication.java b/src/main/java/squad/board/BoardApplication.java index f347fd6..217b6bc 100644 --- a/src/main/java/squad/board/BoardApplication.java +++ b/src/main/java/squad/board/BoardApplication.java @@ -1,11 +1,9 @@ package squad.board; -import org.quartz.*; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.web.http.CookieSerializer; @@ -14,6 +12,7 @@ @SpringBootApplication @EnableRedisHttpSession @EnableConfigurationProperties +@EnableScheduling public class BoardApplication { public static void main(String[] args) { diff --git a/src/main/java/squad/board/apicontroller/BoardApiController.java b/src/main/java/squad/board/apicontroller/BoardApiController.java index e455b11..9a54324 100644 --- a/src/main/java/squad/board/apicontroller/BoardApiController.java +++ b/src/main/java/squad/board/apicontroller/BoardApiController.java @@ -1,5 +1,6 @@ package squad.board.apicontroller; +import com.fasterxml.jackson.core.JsonProcessingException; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -22,8 +23,8 @@ public class BoardApiController { // 게시글 생성 @PostMapping(value = "/boards") public CommonIdResponse saveBoard( - @SessionAttribute Long memberId, - @Valid @RequestBody CreateBoardRequest createBoard) { + @SessionAttribute Long memberId, + @Valid @RequestBody CreateBoardRequest createBoard) throws JsonProcessingException { return boardService.createBoard(memberId, createBoard); } @@ -39,7 +40,7 @@ public CommonIdResponse saveBoard( // 이미지 S3 전송 @PostMapping(value = "/boards/img") public ImageInfoResponse saveImg( - @RequestPart(value = "image") MultipartFile image) { + @RequestPart(value = "image") MultipartFile image) { return boardService.saveImageToS3(image); } @@ -47,7 +48,7 @@ public ImageInfoResponse saveImg( @DeleteMapping("/boards/{boardId}") @BoardWriterAuth public CommonIdResponse deleteBoard( - @PathVariable Long boardId + @PathVariable Long boardId ) { return boardService.deleteBoard(boardId); } @@ -56,8 +57,8 @@ public CommonIdResponse deleteBoard( @PatchMapping("/boards/{boardId}") @BoardWriterAuth public CommonIdResponse updateBoard( - @PathVariable Long boardId, - @Valid @RequestBody BoardUpdateRequest boardUpdateRequest + @PathVariable Long boardId, + @Valid @RequestBody BoardUpdateRequest boardUpdateRequest ) { return boardService.updateBoard(boardId, boardUpdateRequest); } @@ -65,7 +66,7 @@ public CommonIdResponse updateBoard( // 상세 게시글 조회 @GetMapping("/boards/{boardId}") public BoardDetailResponse getBoard( - @PathVariable Long boardId + @PathVariable Long boardId ) { return boardService.findOneBoard(boardId); } @@ -73,8 +74,8 @@ public BoardDetailResponse getBoard( // 전체 게시글 조회 (페이징 처리) @GetMapping("/boards") public ContentListResponse boardList( - @RequestParam Long size, - @RequestParam Long page + @RequestParam Long size, + @RequestParam Long page ) { return boardService.findBoards(size, page); } @@ -82,10 +83,10 @@ public ContentListResponse boardList( // 게시글 검색 @GetMapping("/boards/search") public ContentListResponse searchBoard( - @RequestParam String keyWord, - @RequestParam Long size, - @RequestParam Long page, - @RequestParam String searchType + @RequestParam String keyWord, + @RequestParam Long size, + @RequestParam Long page, + @RequestParam String searchType ) { return boardService.searchBoard(keyWord, size, page, searchType); } diff --git a/src/main/java/squad/board/config/RedisConfig.java b/src/main/java/squad/board/config/RedisConfig.java index ffc3596..6fba2a4 100644 --- a/src/main/java/squad/board/config/RedisConfig.java +++ b/src/main/java/squad/board/config/RedisConfig.java @@ -32,7 +32,6 @@ public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); - // 일반적인 key:value의 경우 시리얼라이저 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); diff --git a/src/main/java/squad/board/config/TaskSchedulerConfig.java b/src/main/java/squad/board/config/TaskSchedulerConfig.java new file mode 100644 index 0000000..814b241 --- /dev/null +++ b/src/main/java/squad/board/config/TaskSchedulerConfig.java @@ -0,0 +1,22 @@ +package squad.board.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@Configuration +public class TaskSchedulerConfig implements SchedulingConfigurer { + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskScheduler()); + } + + @Bean + public Executor taskScheduler() { + return Executors.newScheduledThreadPool(2); + } +} \ No newline at end of file diff --git a/src/main/java/squad/board/dto/S3/S3Task.java b/src/main/java/squad/board/dto/S3/S3Task.java deleted file mode 100644 index 4fd2f6f..0000000 --- a/src/main/java/squad/board/dto/S3/S3Task.java +++ /dev/null @@ -1,17 +0,0 @@ -package squad.board.dto.S3; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -@AllArgsConstructor -@Getter -@Builder -public class S3Task { - private String imgUUID; - private Integer failCount = 0; - - public void increaseFailCount() { - this.failCount++; - } -} diff --git a/src/main/java/squad/board/dto/board/ImageInfoRequest.java b/src/main/java/squad/board/dto/board/ImageInfoRequest.java index 4050c49..526f19e 100644 --- a/src/main/java/squad/board/dto/board/ImageInfoRequest.java +++ b/src/main/java/squad/board/dto/board/ImageInfoRequest.java @@ -3,9 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; -import squad.board.dto.S3.S3Task; - -import javax.management.timer.TimerMBean; @AllArgsConstructor @Getter @@ -14,11 +11,4 @@ public class ImageInfoRequest { private String imageUUID; private long imageSize; private String imageOriginalName; - - public S3Task toS3Task() { - return S3Task.builder() - .imgUUID(imageUUID) - .failCount(0) - .build(); - } } diff --git a/src/main/java/squad/board/exception/ObjectMapperException.java b/src/main/java/squad/board/exception/ObjectMapperException.java new file mode 100644 index 0000000..464a0ba --- /dev/null +++ b/src/main/java/squad/board/exception/ObjectMapperException.java @@ -0,0 +1,4 @@ +package squad.board.exception; + +public class ObjectMapperException extends IllegalStateException { +} diff --git a/src/main/java/squad/board/service/BoardService.java b/src/main/java/squad/board/service/BoardService.java index 9eec7e5..d6bd904 100644 --- a/src/main/java/squad/board/service/BoardService.java +++ b/src/main/java/squad/board/service/BoardService.java @@ -1,8 +1,6 @@ package squad.board.service; -import java.time.LocalDateTime; -import java.util.List; - +import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -13,12 +11,7 @@ import squad.board.domain.board.Board; import squad.board.dto.ContentListResponse; import squad.board.dto.Pagination; -import squad.board.dto.board.BoardDetailResponse; -import squad.board.dto.board.BoardResponse; -import squad.board.dto.board.BoardUpdateRequest; -import squad.board.dto.board.CreateBoardRequest; -import squad.board.dto.board.ImageInfoRequest; -import squad.board.dto.board.ImageInfoResponse; +import squad.board.dto.board.*; import squad.board.exception.board.BoardException; import squad.board.exception.board.BoardStatus; import squad.board.exception.image.ImageException; @@ -27,34 +20,38 @@ import squad.board.repository.CommentMapper; import squad.board.repository.ImageMapper; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor @Transactional @Slf4j public class BoardService { + private static final long MAX_IMAGE_SIZE = 5000000L; + private static final int MAX_IMAGE_NAME_SIZE = 100; + private static final String IMAGE_EXTENSION_EXTRACT_REGEX = "(.png|.jpg|.jpeg)$"; + private static final String TEMP_FOLDER_NAME = "tmp"; private final BoardMapper boardMapper; private final CommentMapper commentMapper; private final ImageMapper imageMapper; private final S3Service s3Service; private final S3MessageQueue messageQueue; - private static final long MAX_IMAGE_SIZE = 5000000L; - private static final int MAX_IMAGE_NAME_SIZE = 100; - private static final String IMAGE_EXTENSION_EXTRACT_REGEX = "(.png|.jpg|.jpeg)$"; - private static final String TEMP_FOLDER_NAME = "tmp"; + private final S3DeadLetterQueue deadLetterQueue; - public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) { + public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) throws JsonProcessingException { // 게시글 저장 Board board = createBoard.toEntity(memberId); boardMapper.save(board); // 이미지 정보 저장 if (createBoard.isImageExist()) { imageMapper.save(createBoard.getImageInfo(), board.getBoardId()); - messageQueue.pushAll(createBoard.getImageInfo()); - s3Service.moveImageToOriginal(); + deadLetterQueue.pushAll(createBoard.getImageInfo().stream() + .map(ImageInfoRequest::getImageUUID) + .toList()); } - log.info("board id = {}, successfully moved {} images", board.getBoardId(), - createBoard.getImageInfo().size()); return new CommonIdResponse(board.getBoardId()); } @@ -95,6 +92,10 @@ public ContentListResponse findBoards(Long size, Long requestPage boardPaging); } + private Long calcOffset(Long page, Long size) { + return (page - 1) * size; + } + @Transactional(readOnly = true) public ContentListResponse findBoards(Long size, Long requestPage) { Long offset = calcOffset(requestPage, size); @@ -155,8 +156,4 @@ public ContentListResponse searchBoard(String keyWord, Long size, return new ContentListResponse<>( boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging); } - - private Long calcOffset(Long page, Long size) { - return (page - 1) * size; - } } diff --git a/src/main/java/squad/board/service/S3Consumer.java b/src/main/java/squad/board/service/S3Consumer.java new file mode 100644 index 0000000..4c0c624 --- /dev/null +++ b/src/main/java/squad/board/service/S3Consumer.java @@ -0,0 +1,46 @@ +package squad.board.service; + +import com.amazonaws.AmazonServiceException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class S3Consumer { + + private final S3MessageQueue messageQueue; + private final S3DeadLetterQueue deadLetterQueue; + private final S3Service s3Service; + private final S3MailSender s3MailSender; + private final String MAIL_CONTENT = "The dead letter queue has exceeded its maximum allowable size."; + +// @Scheduled(fixedDelay = 1000) +// public void normalConsume() { +// while (!messageQueue.isEmpty()) { +// String uuid = messageQueue.pop(); +// try { +// s3Service.moveImageToOriginal(uuid); +// } catch (AmazonServiceException e) { +// deadLetterQueue.push(uuid); +// } +// } +// } + + @Scheduled(fixedDelay = 2000) + public void deadLetterConsume() { + if (deadLetterQueue.isRateLimit()) { + s3MailSender.send(MAIL_CONTENT); + } + List uuids = new ArrayList<>(); + while (!deadLetterQueue.isEmpty()) { + uuids.add(deadLetterQueue.pop()); + } + messageQueue.pushAll(uuids); + } +} diff --git a/src/main/java/squad/board/service/S3DeadLetterQueue.java b/src/main/java/squad/board/service/S3DeadLetterQueue.java new file mode 100644 index 0000000..b9155f3 --- /dev/null +++ b/src/main/java/squad/board/service/S3DeadLetterQueue.java @@ -0,0 +1,34 @@ +package squad.board.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class S3DeadLetterQueue { + + private static final String MESSAGE_QUEUE = "s3_dead_letter"; + private static final int EMPTY = 0; + private static final int LIMIT = 3; + private final RedisTemplate redisTemplate; + + public void pushAll(List imageUUID) { + for (String uuid : imageUUID) + redisTemplate.opsForList().leftPushAll(MESSAGE_QUEUE, uuid); + } + + public String pop() { + return (String) redisTemplate.opsForList().rightPop(MESSAGE_QUEUE); + } + + public boolean isRateLimit() { + return redisTemplate.opsForList().size(MESSAGE_QUEUE) > LIMIT; + } + + public boolean isEmpty() { + return redisTemplate.opsForList().size(MESSAGE_QUEUE)==EMPTY; + } +} diff --git a/src/main/java/squad/board/service/S3FailMailSender.java b/src/main/java/squad/board/service/S3MailSender.java similarity index 77% rename from src/main/java/squad/board/service/S3FailMailSender.java rename to src/main/java/squad/board/service/S3MailSender.java index 7932bf2..5b4d6a7 100644 --- a/src/main/java/squad/board/service/S3FailMailSender.java +++ b/src/main/java/squad/board/service/S3MailSender.java @@ -7,17 +7,16 @@ @Component @RequiredArgsConstructor -public class S3FailMailSender { +public class S3MailSender { private final JavaMailSender javaMailSender; private static final String ADMIN_EMAIL = "bukak2019@naver.com"; private static final String TITLE = "S3 Move Action Failed"; - private static final String IMG_UUID = "imgUUID: "; - public void send(final String imgUUID) { + public void send(final String content) { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(ADMIN_EMAIL); message.setSubject(TITLE); - message.setText(IMG_UUID + imgUUID); + message.setText(content); javaMailSender.send(message); } } diff --git a/src/main/java/squad/board/service/S3MessageQueue.java b/src/main/java/squad/board/service/S3MessageQueue.java index 0e38432..7c3ae6e 100644 --- a/src/main/java/squad/board/service/S3MessageQueue.java +++ b/src/main/java/squad/board/service/S3MessageQueue.java @@ -3,48 +3,34 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import squad.board.dto.S3.S3Task; -import squad.board.dto.board.ImageInfoRequest; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class S3MessageQueue { - private final RedisTemplate redisTemplate; - private final S3FailMailSender s3FailMailSender; private static final String MESSAGE_QUEUE = "s3queue"; + private final RedisTemplate redisTemplate; + private final S3MailSender s3FailMailSender; - public void pushAll(List images) { - List tasks = getS3Task(images); - redisTemplate.opsForList().leftPushAll(MESSAGE_QUEUE, tasks); - } - public void push(S3Task task) { - redisTemplate.opsForList().leftPush(MESSAGE_QUEUE, task); + public void pushAll(List uuids) { + for (String uuid : uuids) { + redisTemplate.opsForList().leftPush(MESSAGE_QUEUE, uuid); + } } - public S3Task pop() { - S3Task s3Task = (S3Task) redisTemplate.opsForList().rightPop(MESSAGE_QUEUE); - if (s3Task.getFailCount() >= 3) { - s3FailMailSender.send(s3Task.getImgUUID()); - } - return s3Task; + public void push(String imageUUID) { + redisTemplate.opsForList().leftPush(MESSAGE_QUEUE, imageUUID); } - public boolean isEmpty() { - if (redisTemplate.opsForList().size(MESSAGE_QUEUE)==0) { - return true; - } - return false; + public String pop() { + return (String) redisTemplate.opsForList().rightPop(MESSAGE_QUEUE); } - private List getS3Task(List images) { - return images.stream() - .map(ImageInfoRequest::toS3Task) - .collect(Collectors.toList()); + public boolean isEmpty() { + return redisTemplate.opsForList().size(MESSAGE_QUEUE)==0; } } diff --git a/src/main/java/squad/board/service/S3Service.java b/src/main/java/squad/board/service/S3Service.java index e50cc59..614cfef 100644 --- a/src/main/java/squad/board/service/S3Service.java +++ b/src/main/java/squad/board/service/S3Service.java @@ -1,6 +1,5 @@ package squad.board.service; -import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import lombok.RequiredArgsConstructor; @@ -9,7 +8,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import squad.board.dto.S3.S3Task; import java.io.IOException; import java.util.UUID; @@ -19,11 +17,10 @@ @Slf4j public class S3Service { - private final AmazonS3 s3Client; - private final S3MessageQueue messageQueue; private static final String TEMP_FOLDER_NAME = "tmp"; private static final String ORIGINAL_FOLDER_NAME = "original"; - + private final AmazonS3 s3Client; + private final S3MessageQueue messageQueue; @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -44,17 +41,9 @@ public String saveFile(MultipartFile multipartFile, String folderName) { } @Async - public void moveImageToOriginal() { - while (messageQueue.isEmpty()) { - S3Task task = messageQueue.pop(); - try { - s3Client.copyObject(bucket, TEMP_FOLDER_NAME + "/" + task.getImgUUID(), bucket, ORIGINAL_FOLDER_NAME + "/" + task.getImgUUID()); - s3Client.deleteObject(bucket, TEMP_FOLDER_NAME + "/" + task.getImgUUID()); - } catch (AmazonServiceException e) { - task.increaseFailCount(); - messageQueue.push(task); - } - } + public void moveImageToOriginal(String uuid) { + s3Client.copyObject(bucket, TEMP_FOLDER_NAME + "/" + uuid, bucket, ORIGINAL_FOLDER_NAME + "/" + uuid); + s3Client.deleteObject(bucket, TEMP_FOLDER_NAME + "/" + uuid); } // 이미지 소스 반환