이전 개발 보러 가기
https://taetae99.tistory.com/38
[Board] 벡엔드 토이 프로젝트 - 게시글 생성 기능 구현 (7)
이전 개발 단계 보러 가기https://taetae99.tistory.com/36 벡엔드 토이 프로젝트(게시판) - MemberController MockMvc를 사용하여 테스트하기목차 1. 개요 프로젝트 진행 중 컨트롤러 테스트를 진행하려고 했
taetae99.tistory.com
목차
1. 주요 변경사항
1-1. 의존 관계 수정
PostService에서 게시글을 작성할 때 회원 정보를 조회해야 했다.
기존에는 MemberService를 통해 조회했지만 불필요한 서비스 간 의존을 줄이고 명확한 책임 분리를 위해 MemberRepository를 사용하도록 변경했다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final MemberRepository memberRepository;
@Transactional
public Long write(Long memberId, String title, String content) {
Member member = memberRepository.findOne(memberId);
Post post = Post.createPost(member, title, content, member.getNickname());
postRepository.save(post);
return post.getId();
}
}
1-2. 게시글 조회 수 증가
기존 게시글 상세 보기 페이지를 가져올 때는 postService의 findOne을 호출하여 게시글의 정보를 넘겨주기만 했었다.
하지만 상세 보기 페이지를 가져올 때 viewPost를 사용하여 게시글 조회 시 조회 수가 증가하도록 코드를 변경하였다.
@GetMapping("/board/post/{postId}")
public String post(@PathVariable Long postId, @ModelAttribute CommentForm commentForm, Model model) {
Post post = postService.viewPost(postId);
model.addAttribute("post", post);
List<Comments> comments = post.getComments();
model.addAttribute("comments", comments);
return "post/postForm";
}
postService.findOne -> postService.viewPost로 변경되었다.
@Transactional
public Post viewPost(Long postId) {
Post post = postRepository.findOne(postId);
if (post == null) {
throw new PostNotFoundException("게시글이 존재하지 않습니다.");
}
post.addViewCount();
return post;
}
viewPost에서 findOne으로 게시글의 정보를 조회하고 게시글이 존재하면 조회 수를 증가시킨다.
2. 게시글 수정 기능 작성
게시글 수정, 삭제는 게시글 작성자만 가능해야 한다.
게시글 수정, 삭제 버튼은 게시글 작성자에게만 렌더링 되도록 한다.
@PostMapping("/board/post/{postId}")
public String update(@PathVariable Long postId, @AuthenticationPrincipal MemberDetail memberDetail,
@Valid PostForm postForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "post/editPostForm";
}
Long updateId = postService.update(postId,memberDetail.getMember().getId(),
postForm.getTitle(), postForm.getContent());
return "redirect:/board/post/" + updateId;
}
update는 게시물을 수정하는 기능을 담당한다.
수정 시에도 게시물 생성 시와 동일하게 PostForm을 사용하여 제목과 내용을 입력받는다.
PostForm의 유효성 검사는 다음과 같다.
@NotEmpty
@Size(max = 500)
private String title;
@NotEmpty
@Size(max = 1000)
private String content;
제목(title)의 최대 길이는 500자, 내용(content)의 최대 길이는 1000자로 제한하며 빈 값이 입력될 수 없다.
유효성 검사를 통과하면 postService의 update 메서드가 실행된다.
@Transactional
public Long update(Long postId,Long currentMemberId,String title,String content) {
Post post = postRepository.findOne(postId);
if (post == null) {
throw new PostNotFoundException("게시글이 존재하지 않습니다.");
}
if (!post.getMember().getId().equals(currentMemberId)) {
throw new UnauthorizedAccessException("게시글 삭제 권한이 없습니다.");
}
post.updatePost(title, content);
return post.getId();
}
PostService의 update 메서드이다.
수정 권한(작성자)을 확인하기 위하여 게시글의 member 정보와 update를 요청한 member를 비교한다.
updatePost를 통해 게시글의 제목과 내용을 수정하고 이는 변경 감지를 통해 값을 수정한다.
제대로 수정 작업이 완료되면 다시 게시글 상세 보기 페이지로 이동한다.
아래는 게시글 수정 흐름을 나타낸 캡쳐본이다.



3. 게시글 삭제 기능 작성
@DeleteMapping("/board/post/{postId}/delete")
public String delete(@AuthenticationPrincipal MemberDetail memberDetail, @PathVariable Long postId) {
postService.deletePost(postId,memberDetail.getMember().getId());
return "redirect:/board";
}
해당 URL로 DELETE 요청이 들어오면 게시글을 삭제하고 목록 페이지로 리다이렉트 한다.
@Transactional
public void deletePost(Long postId,Long currentMemberId) {
//게시물 검색
Post post = postRepository.findOne(postId);
if (post == null) {
throw new PostNotFoundException("게시글을 찾을 수 없습니다.");
}
if (!post.getMember().getId().equals(currentMemberId)) {
throw new UnauthorizedAccessException("게시글 삭제 권한이 없습니다.");
}
//멤버 연관관계 삭제
post.removeMember();
postRepository.delete(postId);
}
게시글을 삭제할 때 엔티티 간의 연관관계를 고려해야 한다.
아래의 Post 엔티티는 Member 및 Comments와 각각 양방향 연관관계를 맺고 있다.
그렇다면 엔티티를 삭제할 때 연관관계는 어떻게 처리해야 하는지에 중점을 두고 진행했다.
@Entity
@Getter
public class Post extends BaseTimeEntity{
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<Comments> comments = new ArrayList<>();
//연관 관계 메서드
public void setMember(Member member) {
this.member = member;
member.getPosts().add(this);
}
// 연관 관계 해제 메서드
public void removeMember() {
this.member.getPosts().remove(this);
this.member = null;
}
...
}
[ Member ↔ Post ]
게시글 생성 시에는 setMember를 사용하여 양방향 연관관계를 설정했었다.
그렇다면 게시글 삭제 시에는 removeMember를 사용하여 연관관계를 해제해야 한다.
[ Comments ↔ Post ]
댓글은 영속성 전이(CascadeType.ALL) 설정으로 인해 부모 엔티티인 게시글이 삭제되면 연관된 댓글도 함께 삭제된다.
이 경우 게시글과 댓글이 모두 제거되므로 별도로 연관 관계 해제 메서드를 호출할 필요가 없다.
아래는 삭제 요청 시 출력된 로그 중 필요한 부분을 캡처한 것이다.




아래는 게시글 삭제 흐름을 나타낸 캡쳐본이다.




4. 테스트 코드 작성
4-1. 게시글 생성에 대한 테스트
@Test
public void 게시글_작성() {
//given
String title = "오늘의 첫 게시물";
String content = "반갑습니다!";
Member member = createMember("테스터", "test@spring.com", "123456", "test");
//when
Post post = createPost(member, title, content);
Post savedPost = postService.findOne(post.getId());
//then
assertThat(savedPost.getTitle()).isEqualTo(title);
assertThat(savedPost.getContent()).isEqualTo(content);
//then-연관관계 체크
assertThat(savedPost.getMember()).isEqualTo(member);
Assertions.assertTrue(member.getPosts().contains(post));
}
- given: 게시글 작성을 위한 데이터를 입력했다.
- when: 임의의 게시글을 작성한 후 이를 검색하여 가져온다.
- then: 저장된 데이터의 제목과 내용이 given에서 주어진 데이터와 동일한 지 검사한다.
- then 연관관계: 연관관계가 제대로 설정되었는지 확인한다.
4-2. 게시글 조회 수 기능 테스트
@Test
public void 조회수_증가() {
//given
Member member = createMember("테스터", "test@spring.com", "123456", "test");
Post post = createPost(member, "조회 수 증가한다!", "얼마나 증가할까?");
int target = 54321;
//when
for (int i = 0; i < target; i++) {
postService.viewPost(post.getId());
}
Post savedPost = postService.findOne(post.getId());
//then
assertThat(savedPost.getViewCount()).isEqualTo(target);
}
- given: 임의의 게시글을 작성하고 조회 횟수를 설정한다.
- when: 해당 게시글을 지정된 횟수만큼 조회한다.
- then: 검색한 게시글의 조회수가 기대한 만큼 증가하였는지 검사한다.
4-3. 게시글 수정 테스트
@Test
public void 게시글_수정() {
//given
String title = "오늘의 첫 게시물";
String content = "반갑습니다!";
Member member = createMember("테스터", "test@spring.com", "123456", "test");
Post post = createPost(member, title, content);
//when
String modifyTitle = "두번째 게시물 입니다.";
String modifyContent = "방가방가";
Long updateId = postService.update(post.getId(), member.getId(), modifyTitle, modifyContent);
Post savedPost = postService.findOne(updateId);
//then
assertThat(savedPost.getTitle()).isEqualTo(modifyTitle);
assertThat(savedPost.getContent()).isEqualTo(modifyContent);
}
- given: 임의의 게시글을 작성한다.
- when: 수정할 값을 설정하고 update를 통해 수정한다.
- then: 검색한 게시글의 title, content를 수정 값과 비교한다.
4-4. 게시글 수정 실패 테스트(권한이 없는 회원이 수정)
@Test
public void 게시글_수정_실패() {
//given
String title = "오늘의 첫 게시물";
String content = "반갑습니다!";
Member member = createMember("테스터", "test@spring.com", "123456", "test");
String modifyTitle = "두번째 게시물 입니다.";
String modifyContent = "방가방가";
Post post = createPost(member, title, content);
//when
Member fake = createMember("가짜 테스트", "sp@spring.com", "123456", "fake");
//then
assertThatThrownBy(() -> {
postService.update(post.getId(), fake.getId(), modifyTitle, modifyContent);
}).isInstanceOf(UnauthorizedAccessException.class)
.hasMessage("게시글 삭제 권한이 없습니다.");
}
- given: 임의의 게시글을 작성하고 수정할 값을 설정한다.
- when: 권한이 없는 (게시글 작성자가 아닌) Member를 생성한다.
- then: 권한이 없는 작성자가 수정요청을 했을 때 UnauthorizedAccessException 이 발생하는지 확인한다.
4-5. 게시글 삭제 테스트
@Test
public void 게시글_삭제() {
//given
Member member = createMember("테스터", "test@spring.com", "123456", "test");
Post post = createPost(member, "오늘의 첫 게시물", "반갑습니다!");
//when
postService.deletePost(post.getId(), member.getId());
List<Post> allPosts = postService.findPosts();
//then
Assertions.assertFalse(allPosts.contains(post));
Assertions.assertFalse(member.getPosts().contains(post));
}
- given: 임의의 게시글을 작성한다.
- when: 게시글을 삭제하고 전체 게시글을 검색한다.
- then: 전체 게시글에 해당 게시글이 포함되지 않았음을 확인한다.
- then 연관관계 : 연관관계가 올바르게 해제되었는지 확인하기 위해 member의 posts가 해당 게시글을 포함하지 않음을 확인한다.
4-6. 게시글 삭제 실패 테스트(권한이 없는 회원이 삭제)
@Test
public void 게시글_삭제_실패() {
//given
Member member = createMember("테스터", "test@spring.com", "123456", "test");
Post post = createPost(member, "오늘의 첫 게시물", "반갑습니다!");
Member fake = createMember("가짜 테스트", "sp@spring.com", "123456", "fake");
//when & then
assertThatThrownBy(() -> {
postService.deletePost(post.getId(), fake.getId());
}).isInstanceOf(UnauthorizedAccessException.class)
.hasMessage("게시글 삭제 권한이 없습니다.");
//then - 연관관계 체크
Assertions.assertTrue(member.getPosts().contains(post));
assertThat(post.getMember()).isEqualTo(member);
}
- given: 임의의 게시글을 생성하고 권한이 없는 회원 정보를 생성한다.
- when&then: 게시글 삭제를 했을 때 UnauthorizedAccessException이 발생하는지 테스트한다.
- then 연관관계: 게시글 삭제가 실패했으므로 기존 연관관계가 유지되는지 확인한다.
- 1. member의 posts가 해당 게시글을 포함하는지
- 2. post가 member와 연관관계를 갖고 있는지

'프로젝트 > Board 프로젝트' 카테고리의 다른 글
| [JPA] 게시판 프로젝트 - 비동기 없이 댓글 수정 및 댓글 생성, 삭제 구현 (10) (0) | 2025.02.24 |
|---|---|
| [JPA] 게시판 프로젝트 - 게시판 페이징 구현 (9) (0) | 2025.02.15 |
| [JPA] 게시판 프로젝트 - 게시글 생성 기능 구현 (7) (4) | 2025.02.03 |
| [JPA] 게시판 프로젝트 - MemberController MockMvc를 사용하여 테스트하기 (6) (3) | 2025.01.27 |