Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test #36

Merged
merged 3 commits into from
Jul 6, 2024
Merged

test #36

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
# run: docker push haechansomg/simple-board2:was02

- name: Deploy to server
uses: appleboy/[email protected].0
uses: appleboy/[email protected].3
id: deploy
env:
COMPOSE: "/home/ubuntu/compose/docker-compose.yml"
Expand All @@ -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
sudo docker-compose -f $COMPOSE up -d
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ services:
# - 9121:9121
# environment:
# REDIS_ADDR: "redis:6379"
links:
- redis
# links:
# - redis
# - prometheus
1 change: 0 additions & 1 deletion src/main/java/squad/board/BoardApplication.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package squad.board;

import org.quartz.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/squad/board/apicontroller/BoardApiController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package squad.board.apicontroller;

import com.fasterxml.jackson.core.JsonProcessingException;
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;
Expand All @@ -22,10 +24,19 @@ public class BoardApiController {
@PostMapping(value = "/boards")
public CommonIdResponse saveBoard(
@SessionAttribute Long memberId,
@Valid @RequestBody CreateBoardRequest createBoard) {
@Valid @RequestBody CreateBoardRequest createBoard) throws JsonProcessingException {
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(
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/squad/board/config/AsyncThreadPoolConfig.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
15 changes: 15 additions & 0 deletions src/main/java/squad/board/config/MailConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
19 changes: 11 additions & 8 deletions src/main/java/squad/board/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisConfiguration);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
}

@Bean
public RedisConnectionFactory redisConnectionFactoryToken() {
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration();
Expand All @@ -36,14 +47,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<>();
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/squad/board/config/TaskSchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package squad.board.exception;

public class ObjectMapperException extends IllegalStateException {
}
69 changes: 38 additions & 31 deletions src/main/java/squad/board/service/BoardService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package squad.board.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
Expand All @@ -21,42 +22,39 @@

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 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)$";

public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) {
private final S3MessageQueue messageQueue;
private final S3DeadLetterQueue deadLetterQueue;

public CommonIdResponse createBoard(Long memberId, CreateBoardRequest createBoard) throws JsonProcessingException {
// 게시글 저장
Board board = createBoard.toEntity(memberId);
boardMapper.save(board);
// 이미지 정보 저장
if (createBoard.isImageExist()) {
saveImageInfo(board.getBoardId(), createBoard.getImageInfo());
imageMapper.save(createBoard.getImageInfo(), board.getBoardId());
deadLetterQueue.pushAll(createBoard.getImageInfo().stream()
.map(ImageInfoRequest::getImageUUID)
.toList());
}
return new CommonIdResponse(board.getBoardId());
}

private void saveImageInfo(Long boardId, List<ImageInfoRequest> 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);
Expand All @@ -66,7 +64,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);
}
// 이미지 파일명 길이 제한
Expand All @@ -81,20 +80,28 @@ private void imageValidation(MultipartFile image) {
}

@Transactional(readOnly = true)
public ContentListResponse<BoardResponse> findBoards(Long size, Long requestPage, Long memberId) {
if (memberId == null) {
public ContentListResponse<BoardResponse> 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);
}

private Long calcOffset(Long page, Long size) {
return (page - 1) * size;
}

@Transactional(readOnly = true)
public ContentListResponse<BoardResponse> 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)
Expand All @@ -118,7 +125,7 @@ public CommonIdResponse updateBoard(Long boardId, BoardUpdateRequest updateBoard
List<ImageInfoRequest> imageInfoRequests = updateBoard.getImageInfoList();
// DB에 이미지 정보 저장
if (CollectionUtils.isNotEmpty(imageInfoRequests)) {
saveImageInfo(boardId, imageInfoRequests);
imageMapper.save(imageInfoRequests, boardId);
}
// 기존 이미지 정보
List<String> savedImageUuid = imageMapper.findImageUuid(boardId);
Expand All @@ -139,14 +146,14 @@ private void deleteImageInfo(List<String> deleteRequestImageUuid) {
}

@Transactional(readOnly = true)
public ContentListResponse<BoardResponse> searchBoard(String keyWord, Long size, Long requestPage, String searchType) {
public ContentListResponse<BoardResponse> 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<BoardResponse> byKeyWord = boardMapper.findByKeyWord(keyWord, size, offset, searchType);
return new ContentListResponse<>(boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging);
}

private Long calcOffset(Long page, Long size) {
return (page - 1) * size;
Pagination boardPaging = new Pagination(requestPage,
boardMapper.countByKeyWord(keyWord, searchType), size);
List<BoardResponse> byKeyWord = boardMapper.findByKeyWord(keyWord, size, offset,
searchType);
return new ContentListResponse<>(
boardMapper.findByKeyWord(keyWord, size, offset, searchType), boardPaging);
}
}
46 changes: 46 additions & 0 deletions src/main/java/squad/board/service/S3Consumer.java
Original file line number Diff line number Diff line change
@@ -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<String> uuids = new ArrayList<>();
while (!deadLetterQueue.isEmpty()) {
uuids.add(deadLetterQueue.pop());
}
messageQueue.pushAll(uuids);
}
}
34 changes: 34 additions & 0 deletions src/main/java/squad/board/service/S3DeadLetterQueue.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> redisTemplate;

public void pushAll(List<String> 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;
}
}
Loading
Loading