본문 바로가기
서버 공부

[스프링] 이미지와 함께 게시물 작성하기

by 모선효 2024. 6. 25.

이번에는 이미지 업로드와 json 타입의 Request Dto 데이터 값을 동시에 받아 글을 작성하는 API 코드를 소개하겠습니다.

 

S3를 사용하여 이미지를 업로드하는 코드는 이전 게시물에 올려뒀습니다! 먼저 아래 글을 읽고 오는 것을 추천합니다.

2024.06.13 - [서버 공부] - [스프링] S3를 이용한 이미지 업로드

 

[스프링] S3를 이용한 이미지 업로드

사진 업로드를 할 수 있는 외주 작업을 맡게 되어 S3를 이용한 이미지 업로드 로직을 작성했습니다.더보기요구사항 - 게시물에 이미지는 필수값이 아니다.- 하나의 게시물에 올릴 수 있는 파일

hy5sun.tistory.com

 

이미지와 함께 글을 올리기 위해 Image 엔티티를 생성하여 Board를 작성하면 Board와 Image를 각각 저장해 줄 겁니다.

 

Image와 Board를 build 하고 저장할 순서는 다음과 같습니다.

1) Image Entity & Repository 생성하기

2) S3Service에서 board_id를 제외하여 Image 빌드하고, BoardService로 넘기기

 

 

코드와 함께 설명해보겠습니다.

 

1. Image Entity & Repository

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Image extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(nullable = false)
    private String originalName;

    @Column(nullable = false)
    private String savedName;

    @Column(nullable = false)
    private String imageUrl;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Board board;

    @Builder
    public Image(String originalName, String savedName, String imageUrl) {
        this.originalName = originalName;
        this.savedName = savedName;
        this.imageUrl = imageUrl;
    }

    public void setBoard(Board board) {
        this.board = board;
    }
}
public interface ImageRepository extends JpaRepository<Image, UUID> {
    Optional<List<Image>> findByBoard(Board board);
    void deleteByBoard(Board board);
}

 

Image 필드에 있는 board 값을 저장하기 위해서는 board가 먼저 생성되어야 합니다. FK값인 board_id가 아직 생성되지 않았기 때문입니다. 따라서 builder는 board를 제외하여 해줬고, setBoard 함수를 만들어 추후에 board 값을 넣을 수 있도록 했습니다.

 

찾아보니 setter는 가독성 문제로 사용하지 않는 게 좋다고 하여 builder를 2개 만들어서 진행하려고 했었는데 자꾸 제대로 작동하지 않아, builder를 사용하지는 않아도 코드의 가독성에는 문제가 생기지 않게 하기 위해 노력했습니다.

 

2. S3Service

@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {
    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    @Value("${cloud.aws.region.static}")
    private String region;

    private final AmazonS3Client amazonS3Client;
    
    public List<Image> uploadFiles(List<MultipartFile> files) {
    
    	// 빌드된 Image들을 List로 만들어서 반환
        List<Image> images = files.stream().map(file -> {
            try {
                return uploadFile(file);
            } catch (IOException e) {
                throw new BusinessException(FILE_UPLOAD_FAILED);
            }
        }).collect(Collectors.toList());

        return images;
    }

	// board를 제외하여 Image Builder
    private Image builderImage(String originalName, String savedName, String url) {
        return Image.builder().originalName(originalName).savedName(savedName).imageUrl(url).build();
    }

    public Image uploadFile(MultipartFile file) throws IOException {
        try {
            String folderDir = bucket;
            String originalName = file.getOriginalFilename();
            String savedName = createSaveName(originalName);
            String fileUrl = "https://" + bucket + ".s3." + region + ".amazonaws.com/" + savedName;

            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(file.getContentType());
            metadata.setContentLength(file.getSize());

            amazonS3Client.putObject(folderDir, savedName, file.getInputStream(), metadata);

            log.info("파일 업로드 성공: " + fileUrl);
            
            // 이미지 빌더 반환
            return builderImage(originalName, savedName, fileUrl);
        } catch (IOException e) {
            throw new BusinessException(FILE_UPLOAD_FAILED);
        }
    }

 

Image의 본래이름, 저장된 이름, URL은 S3Service에서 build 하도록 했습니다.

build된 Image들은 모두 List로 묶어서 BoardService로 반환해줄 겁니다.

 

3. BoardService

@Service
@RequiredArgsConstructor
public class BoardService {

	private final BoardRepository boardRepository;
    private final ImageRepository imageRepository;
	private final S3Service s3Service;

	@Transactional
    public BoardResponse createBoard(Member member, CreateBoardRequest req, List<MultipartFile> files) {
        List<Image> images = s3Service.uploadFiles(files);

        Board board = Board.builder()
                .title(req.getTitle())
                .content(req.getContent())
                .images(images)
                .member(member)
                .build();

        boardRepository.save(board);

        images.stream().forEach(image -> image.setBoard(board));
        imageRepository.saveAll(images);

        return BoardResponse.entityToDto(board, isLiked(board, member));
    }
}

S3Service에서 Image를 받아와 새로 생성할 Board의 image 필드에 build 해준 뒤, 저장합니다.

이제 FK 값인 board의 id가 생성됐으니 forEach문을 사용하여 image에 board를 지정해줍니다. 그리고 Image도 저장해주면 완성입니다!

 

4. BoardController

@RestController
@RequestMapping("/boards")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @PostMapping()
    @ResponseStatus(HttpStatus.CREATED)
    public CustomResponse create(@Validated @RequestPart("board") CreateBoardRequest req, @RequestPart(value = "files") List<MultipartFile> files, @Login Member member) {
        BoardResponse board = boardService.createBoard(member, req, files);
        return CustomResponse.response(HttpStatus.CREATED, "게시물을 정상적으로 작성했습니다.", board);
    }
 }

 

JSON 타입인 Request 데이터와 MultipartFile 데이터를 함께 받기 위해서는 @RequestPart를 사용해야 합니다.

만약 사진 업로드가 필수가 아니라면 아래처럼 작성해야 합니다.

@RequestPart(value = "files", required = false)

 

 

포스트맨으로 테스트 해보고 싶다면 다음과 같이 구성하여 API 요청하면 됩니다.

여기서 Text 형태 데이터의 Content-Type에 application/json을 반드시 작성해야 합니다! 

 

그럼 정상적으로 게시물이 작성되는 것을 확인할 수 있습니다

 

 

긴 글 읽어주셔서 감사합니다!

 

정말 어렵게 느껴지던 부분 중 하나였기 때문에 부족한 코드지만 다른 분들께 작은 도움이라도 됐으면 좋겠습니다!

감사합니다