이번에 시간부터 게시글 기능을 구현해 보도록 하겠습니다. 먼저 게시글 기능을 위한 Entity 설계부터 해보겠습니다.
Entity 생성
Post
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends EntityDate {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
@Lob
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Category category;
@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Image> images = new ArrayList<>();
public Post(String title, String content, Member member, Category category, List<Image> images) {
this.title = title;
this.content = content;
this.member = member;
this.category = category;
addImages(images);
}
private void addImages(List<Image> added) {
added.stream().forEach(i -> {
images.add(i);
i.initPost(this);
});
}
}
주요 내용에 대해서만 설명하겠씁니다.
- 필드는 다음과 같습니다.
- title
- content
- member
- category
- images
- @Lob : 변수가 큰 객체임을 나타냄
- 게시글에는 image가 있는데 여러개의 image를 가질 수 있다.
Image
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Image {
@Id @GeneratedValue
private Long id;
private String uniqueName;
private String originName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Post post;
private final static String supportedExtension[] = {"jpg", "jpeg", "gif", "bmp", "png"};
public Image(String originName) {
this.uniqueName = generateUniqueName(extractExtension(originName));
this.originName = originName;
}
public void initPost(Post post){
if(this.post == null){
this.post = post;
}
}
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));
}
}
- generateUniqueName : UUID를 사용하여 고유한 파일 이름을 생성
- extractExtension: originName에서 확장자를 추출
- isSupportedFormat: 지원 가능한 이미지 확장자인지 확인
- initPsot : Imgae는 Post와 관계를 가지며, 이 메서드를 통해 관련 Post Entity 초기화 할 수 있다.
Image Test
Image Entity에는 uniqueName관련 기능이 들어가기 때문에 테스트를 진행하겠습니다.
class ImageTest {
@Test
public void createImageTest(){
//given
String validExtension = "JPEG";
//when //then
createImageWithOriginName("image." + validExtension);
}
@Test
public void createImageExceptionByUnsupportedFormatTest(){
//given
String invalidExtension = "invalid";
//when //then
assertThatThrownBy(()->createImageWithOriginName("image." + invalidExtension)).isInstanceOf(UnsupportedImageFormatException.class);
}
@Test
void createImageExceptionByNoneExtensionTest() {
// given
String originName = "image";
// when, then
assertThatThrownBy(() -> createImageWithOriginName(originName))
.isInstanceOf(UnsupportedImageFormatException.class);
}
@Test
void initPostTest() {
// given
Image image = createImage();
// when
Post post = createPost();
image.initPost(post);
System.out.println("post = " + post);
System.out.println("image.getPost() = " + image.getPost());
// then
assertThat(image.getPost()).isSameAs(post);
}
@Test
void initPostNotChangedTest() {
// given
Image image = createImage();
image.initPost(createPost());
System.out.println("image.getPost() = " + image.getPost());
// when
Post post = createPost();
image.initPost(post);
System.out.println("post = " + post);
System.out.println("image.getPost() = " + image.getPost());
// then
assertThat(image.getPost()).isNotSameAs(post);
}
}
Factory
Post와 Image 에 대해서 생성 메서드들을 미리 만들어 두겠습니다.
PostFactory
public class PostFactory {
public static Post createPost() {
return createPost(createMember(), createCategory());
}
public static Post createPost(Member member, Category category) {
return new Post("title", "content", member, category, List.of()); }
public static Post createPostWithImages(Member member, Category category, List<Image> images) {
return new Post("title", "content", member, category, images);
}
public static Post createPostWithImages(List<Image> images) {
return new Post("title", "content", createMember(), createCategory(), images);
}
}
ImageFactory
public class ImageFactory {
public static Image createImage() {
return new Image("origin_filename.jpg");
}
public static Image createImageWithOriginName(String originName) {
return new Image(originName);
}
}
Repository 생성
JpaRepository를 상속받는 PostRepository와 ImageRepository를 생성하겠습니다.
PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
}
ImageRepository
public interface ImageRepository extends JpaRepository<Image, Long> {
}
Post Repository 테스트
테스트는 이전 테스트들과 동일하기 때문에 설명은 생략하겠습니다.
@DataJpaTest
class PostRepositoryTest {
@Autowired PostRepository postRepository;
@Autowired ImageRepository imageRepository;
@Autowired MemberRepository memberRepository;
@Autowired CategoryRepository categoryRepository;
@PersistenceContext EntityManager em;
Member member;
Category category;
@BeforeEach
void beforeEach(){
member = memberRepository.save(createMember());
category = categoryRepository.save(createCategory());
}
@Test
public void createAndReadTest(){
//given
Post post = postRepository.save(createPost(member, category));
clear();
//when
Post foundPost = postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new);
//then
assertThat(foundPost.getId()).isEqualTo(post.getId());
assertThat(foundPost.getTitle()).isEqualTo(post.getTitle());
}
@Test
public void deleteTest(){
//given
Post post = postRepository.save(createPost(member, category));
clear();
//when
postRepository.deleteById(post.getId());
clear();
//then
assertThatThrownBy(()->postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new)).isInstanceOf(PostNotFoundException.class);
}
@Test
public void createCascadeImageTest(){
//given
Post post = postRepository.save(createPostWithImages(member, category, List.of(createImage(), createImage())));
clear();
//when
Post foundPost = postRepository.findById(post.getId()).orElseThrow(PostNotFoundException::new);
//then
List<Image> images = foundPost.getImages();
for (Image image : images) {
assertThat(image.getOriginName()).isEqualTo("origin_filename.jpg");
}
}
@Test
void deleteCascadeImageTest() { // 이미지도 연쇄적으로 제거되는지 검증
// given
Post post = postRepository.save(createPostWithImages(member, category, List.of(createImage(), createImage())));
clear();
// when
postRepository.deleteById(post.getId());
clear();
// then
List<Image> images = imageRepository.findAll();
assertThat(images.size()).isZero();
}
@Test
void deleteCascadeByMemberTest() { // Member가 삭제되었을 때 연쇄적으로 Post도 삭제되는지 검증
// given
postRepository.save(createPostWithImages(member, category, List.of(createImage(), createImage())));
clear();
// when
memberRepository.deleteById(member.getId());
clear();
// then
List<Post> result = postRepository.findAll();
Assertions.assertThat(result.size()).isZero();
}
@Test
void deleteCascadeByCategoryTest() { // Category가 삭제되었을 때 연쇄적으로 Post도 삭제되는지 검증
// given
postRepository.save(createPostWithImages(member, category, List.of(createImage(), createImage())));
clear();
// when
categoryRepository.deleteById(category.getId());
clear();
// then
List<Post> result = postRepository.findAll();
assertThat(result.size()).isZero();
}
void clear(){
em.flush();
em.clear();
}
}
궁금한신점이나 잘못된 부분이 있으면 자유롭게 댓글 달아주세요.
github : https://github.com/jaeyeon423/spring_board_api
'spring > 게시판 api' 카테고리의 다른 글
Spring boot 게시판 API 서버 제작 (20) - 게시글 - 조회 (0) | 2023.03.04 |
---|---|
Spring boot 게시판 API 서버 제작 (19) - 게시글 - 생성 (0) | 2023.03.03 |
Spring boot 게시판 API 서버 제작 (17) - Member <- Entity Graph 적용 (0) | 2023.02.27 |
Spring boot 게시판 API 서버 제작 (16) - 중간 정리 & 카테고리 API (0) | 2023.02.27 |
Spring boot 게시판 API 서버 제작 (15) - 게시판 - 카테고리 - 웹 계층 구현 (0) | 2023.02.26 |