게시글 생성 + 파일 업로드(SpringBoot + JPA + AWS S3)
이번 글에서는 게시글 생성 + 파일 업로드(Aws S3)를 다룰 것이다. 🕌
- Post
@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
@EntityListeners(AuditListener.class)
public class Post implements Auditable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "post_id")
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, length = 1000)
private String content;
@Embedded
private TimeEntity timeEntity;
@ColumnDefault("0")
@Column(name = "view_count",nullable = false)
private Integer viewCount;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = LAZY,cascade = CascadeType.PERSIST)
@JoinColumn(name = "post_category_id")
private PostCategory postCategory;
@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<PostLike> postLikes = new ArrayList<>();
@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<Scrap> scraps = new ArrayList<>();
@OneToMany(mappedBy = "post", orphanRemoval = true)
private List<PostImage> postImages = new ArrayList<>();
@Override
public void setTimeEntity(TimeEntity timeEntity) {
this.timeEntity = timeEntity;
}
@Builder
public Post(String title, String content, Integer viewCount, User user, List<Comment> comments, PostCategory postCategory) {
this.title = title;
this.content = content;
this.viewCount = viewCount;
this.user = user;
this.comments = comments;
this.postCategory = postCategory;
}
/**
* 생성 메서드
*/
public static Post createPost(String title, String content, User user, PostCategory postCategory){
return Post.builder()
.title(title)
.content(content)
.user(user)
.postCategory(postCategory)
.build();
}
public void updatePost(String title, String content) {
this.title = title;
this.content = content;
}
/**
* 초기화 값이 DB 에 추가되지 않는 오류가 있어서
* persist 하기 전에 초기화
*/
@PrePersist
public void prePersistCount(){
this.viewCount = this.viewCount == null ? 0 : this.viewCount;
}
}
-
Post
엔티티는 위와 같고 자세한 설명은 넘어가겠다. -
먼저 게시글을 생성 하려면 제목, 내용, 카테고리 이름, 이미지파일을 프론트에서 넘겨받아야 한다.
- PostRequest
@ApiModel(description = "게시글 생성 요청 데이터 모델")
@Getter
@Setter
@NoArgsConstructor
public class PostRequest {
@ApiModelProperty(value = "게시글 제목", example = "모아모아 화이팅!", required = true)
private String title;
@ApiModelProperty(value = "게시글 내용", example = "무야호", required = true)
private String content;
@ApiModelProperty(value = "커뮤니티 게시글의 카테고리 이름", example = "모아모아", required = true)
private String categoryName;
@ApiModelProperty(value = "이미지 파일", required = false)
private List<MultipartFile> imageFiles = new ArrayList<>();
}
- 이미지 파일은
List
로 여러 개의 파일을 받을 것이다.
- PostController
@ApiOperation(value = "게시글 생성", notes = "Form Data 값을 받아와서 글을 생성하는 API",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "해당 게시글이 정상적으로 생성된 경우"),
@ApiResponse(responseCode = "404", description = "회원의 Id를 찾지 못한 경우")
})
@PostMapping
public PostCreateResponse createPost(@ModelAttribute PostRequest request){
return postService.createPost(request);
}
-
필자는
Swagger
를 사용해서API
문서화를 하고 있다. -
이미지 파일을 전송할 때는
FormData
방식으로 전달을 해야 하고, Json 타입의 제목, 내용, 카테고리 이름을 같이 보내기 때문에@RequestPart
를 사용해서 받을 수 있으나,Swagger
로 명확하게 볼 수 없기 때문에@ModelAttribut
를 사용했다.
- PostService
@Transactional
public PostCreateResponse createPost(PostRequest postRequest) {
PostCategory postCategory = postCategoryRepository.findByCategoryName(postRequest.getCategoryName())
.orElseGet(() -> PostCategory.createCategory(postRequest.getCategoryName()));
User user = userUtil.findCurrent();
Post post = postRepository.save(Post.createPost(postRequest.getTitle(), postRequest.getContent(), user, postCategory));
List<String> postImages = uploadPostImages(postRequest, post);
return new PostCreateResponse(post.getId(), "게시글 작성이 완료되었습니다.", postImages);
}
-
먼저 위에
postCategoryRepository.findByCategoryName()
은 게시글의 카테고리가 존재하는지 확인하고 없다면 카테고리를 새로 생성해서 가져오는 로직인데 위의 코드는 다시 변경을 할 예정이다.(DB
에서 미리 만들어놓고 사용 예정) -
여기서 중요한 점은 필자는
JWT
를 사용하고 있어서 사용자의 정보를 따로 넘겨주지는 않고,HttpHeader
에서AccessToken
을 받아서 그걸로 사용자에 대한 정보를 검증을하고, 검증된 결과를 SecurityContext에 담아주기 때문에userUtil.findCurrent()
메서드로 유저에 대한 정보를 받아올 수 있다. -
Request
로부터 받아온 값들을PostRepository.save()
로 게시글을 생성한다.
uploadImages()
private List<String> uploadPostImages(PostRequest postRequest, Post post) {
return postRequest.getImageFiles().stream()
.map(image -> s3Uploader.upload(image, "post"))
.map(url -> createPostImage(post, url))
.map(postImage -> postImage.getImageUrl())
.collect(Collectors.toList());
}
createPostImage()
private PostImage createPostImage(Post post, String url) {
return postImageRepository.save(PostImage.builder()
.imageUrl(url)
.storeFilename(StringUtils.getFilename(url))
.post(post)
.build());
}
- 먼저 request로 받아온 이미지 파일들을
stream()
을 사용해서 미리 설정한S3
경로에 저장한 후PostImage
엔티티를 만들어 주고, 그 이미지의 경로를List
로 반환하게 된다.
- PostImage
@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
@EntityListeners(AuditListener.class)
public class PostImage implements Auditable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "post_image_id")
private Long id;
private String imageUrl;
private String storeFilename;
@Embedded
private TimeEntity timeEntity;
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
@Builder
public PostImage(String imageUrl, String storeFilename, Post post) {
this.imageUrl = imageUrl;
this.storeFilename = storeFilename;
this.post = post;
}
@Override
public void setTimeEntity(TimeEntity timeEntity) {
this.timeEntity = timeEntity;
}
}
Postman 테스트
-
@ModelAttribute
로 지정을 해 주었다면 위와같이form-data
방식으로 key + value 방식으로 보내주어야한다. -
파일의 Content Type = multiparty/form-data
-
나머지는
Json
방식으로 보내기 때문에 위와같이 설정을 했다.
결과 응답
게시글 수정 🏡
- PostUpdateRequest
@ApiModel(description = "게시글 수정 요청 데이터 모델")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PostUpdateRequest {
@ApiModelProperty(value = "게시글 PK", example = "1", required = true)
@NotNull
private Long postId;
@ApiModelProperty(value = "게시글 제목", example = "모아모아 화이팅!")
private String title;
@ApiModelProperty(value = "게시글 내용", example = "무야호")
private String content;
@ApiModelProperty(value = "삭제한 이미지 경로를 제외한 남아있는 게시글 이미지 경로")
private List<String> saveImageUrl = new ArrayList<>();
@ApiModelProperty(value = "게시글 이미지", required = false)
private List<MultipartFile> imageFiles = new ArrayList<>();
}
- 수정을 할 때는 위와 같이 삭제한 이미지 경로를 제외한 남아있는 게시글 이미지의 경로를
List
로 보내주어야 한다.
PostController
@ApiOperation(value = "게시글 수정", notes = "Request Body 값을 받아와서 글을 수정하는 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "해당 게시글이 정상적으로 수정된 경우"),
@ApiResponse(responseCode = "404", description = "회원 OR 게시글의 Id를 찾지 못한 경우")
})
@PatchMapping
public PostUpdateResponse updatePost(@ModelAttribute PostUpdateRequest request) {
return postService.updatePost(request);
}
postService
@Transactional
public PostUpdateResponse updatePost(PostUpdateRequest request) {
User user = userUtil.findCurrent();
Post post = postRepository.findByIdAndUser(request.getPostId(), user.getId())
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_POST));
validateDeletedImages(request);
uploadPostImages(request, post);
List<String> saveImages = getSaveImages(request);
post.updatePost(request.getTitle(), request.getContent());
return new PostUpdateResponse(post.getId(), "게시글 변경이 완료되었습니다.", saveImages);
}
- 게시글 수정은 게시글 생성보다 더 까다롭게 흘러간다.
-
유저 정보를 찾는다
-
유저와 게시글의 관계를 찾는다(수정할 권한이 있는지)
-
Request
로 받아온 이미지 경로랑 저장 되어있던 이미지 경로랑 일치하지 않는다면 모두 삭제 -
파일을 추가할 경우
S3
에 업로드 및PostImage
생성 -
PostImage
테이블에 저장 되어있는 이미지 경로를 추출 -
게시글의 변경사항을 JPA의
변경감지
기능을 이용하여update
validateDeletedImages()
/**
* @Request로 받아온 이미지 경로랑 저장 되어있던 이미지 경로랑 일치하지 않는다면 모두 삭제
*/
private void validateDeletedImages(PostUpdateRequest request) {
postImageRepository.findBySavedImageUrl(request.getPostId()).stream()
.filter(image -> !request.getSaveImageUrl().stream().anyMatch(Predicate.isEqual(image.getImageUrl())))
.forEach(url -> {
postImageRepository.delete(url);
s3Uploader.deleteImage(url.getImageUrl());
});
}
stream
을 사용해서request
로 받아온 이미지 경로랑PostImage
테이블에 저장 되어있는 경로랑 일치하는지 검증을 한 후 일치하지 않는다면 모두 삭제하는 로직이다.
uploadPostImages()
/**
* S3에 업로드 및 PostImage 생성
*/
private void uploadPostImages(PostUpdateRequest request, Post post) {
request.getImageFiles()
.stream()
.forEach(file -> {
String url = s3Uploader.upload(file, "post");
createPostImage(post, url);
});
}
getSaveImages()
/**
* PostImage 테이블에 저장 되어있는 이미지 경로를 추출
*/
private List<String> getSaveImages(PostUpdateRequest request) {
return postImageRepository.findBySavedImageUrl(request.getPostId())
.stream()
.map(image -> image.getImageUrl())
.collect(Collectors.toList());
}
Postman 테스트
- 기존에 파일을 2개를 저장해놓은 상태이고, 이미지 경로를 하나만 넣어서 하나만 삭제를하고, 2개의 파일을 더 넣을것이다 그러면 총 3개의 이미지경로가 반환이 되어야한다.
결과 화면
S3에 대한 설정 및 로직들은 다른 블로그에 많이 올라와있으니 참고하시길.. 🍎
Author And Source
이 문제에 관하여(게시글 생성 + 파일 업로드(SpringBoot + JPA + AWS S3)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@do-hoon/게시글-생성-파일-업로드SpringBoot-JPA-AWS-S3저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)