Spring Boot 게시판 JPA 연관관계 매핑으로 글 작성자만 수정, 삭제 가능하게 하기
by coco3o이전엔 글 작성시 닉네임과 사용자 닉네임을 비교 후 일치하면 게시글 수정 및 삭제가 가능하도록 구현 했었는데,
이 방법은 문제가 있었다.
기존 닉네임에서 다른 닉네임으로 변경할 경우 기존 닉네임으로 작성한 게시글들은 수정 및 삭제를 할 수 없게 된다.
그래서 생각한 방법이 연관관계 매핑을 통해 User의 id(PK)를 Posts의 FK로 두어 id 값으로 비교해 수정 및 삭제가 가능하도록 변경하는 방법이다.
※ JPA 연관관계 매핑 자세히 알아보기 링크
1. Posts
Posts와 User는 단방향 관계를 가진다.
@NoArgsConstructor @AllArgsConstructor @Builder @Getter @Entity public class Posts extends TimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(length = 500, nullable = false) private String title; @Column(columnDefinition = "TEXT", nullable = false) private String content; private String writer; @Column(columnDefinition = "integer default 0") private int view; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; /* 게시글 수정 */ public void update(String title, String content) { this.title = title; this.content = content; } }
@ManyToOne(fetch = FetchType.LAZY)
: User 입장에선 Posts와 다대일 관계이므로 @ManyToOne이 된다.
@JoinColumn(name = "user_id")
: 외래 키 매핑을 위해 JoinColumn을 사용한다.
Posts 엔티티는 User 엔티티의 id 필드를 "user_id"라는 이름으로 외래 키를 가진다.
※ @OneToMany의 기본 Fetch 전략은 LAZY(지연 로딩)
@ManyToOne의 기본 Fetch 전략은 EAGER(즉시 로딩)이다.
EAGER 전략을 사용하면 필요하지 않은 쿼리도 JPA에서 함께 조회하기 때문에 N+1 문제를 야기할 수 있어,
Fetch 전략을 LAZY(지연 로딩)로 설정했다.
※ 즉시로딩과 지연로딩 알아보기 ( 링크 )
각 Fetch 전략을 사용했을 때 JPA는 다음과 같은 쿼리문을 수행하는 것을 볼 수 있다.


2. Dto
PostsRequestDto
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class PostsRequestDto { private Long id; private String title; private String writer; private String content; private String createdDate, modifiedDate; private int view; private User user; /* Dto -> Entity */ public Posts toEntity() { Posts posts = Posts.builder() .id(id) .title(title) .writer(writer) .content(content) .view(0) .user(user) .build(); return posts; } }
3. Repository
UserRepository
public interface UserRepository extends JpaRepository<User, Long> { /* Security */ Optional<User> findByUsername(String username); /* OAuth */ Optional<User> findByEmail(String email); /* user GET */ User findByNickname(String nickname); /* 중복인 경우 true, 중복되지 않은경우 false 리턴 */ boolean existsByUsername(String username); boolean existsByNickname(String nickname); boolean existsByEmail(String email); }
4. RestController
PostsApiController
@RequestMapping("/api") @RequiredArgsConstructor @RestController public class PostsApiController { private final PostsService postsService; /* CREATE */ @PostMapping("/posts") public ResponseEntity save(@RequestBody PostsRequestDto dto, @LoginUser UserSessionDto userSessionDto) { return ResponseEntity.ok(postsService.save(userSessionDto.getNickname(), dto)); } .... }
@LoginUser UserSessionDto userSessionDto
: 현재 사용중인 User의 세션정보를 담고 있는 클래스
5. Service
PostsService
@RequiredArgsConstructor @Service public class PostsService { private final PostsRepository postsRepository; private final UserRepository userRepository; /* CREATE */ @Transactional public Long save(String nickname, PostsRequestDto dto) { User user = userRepository.findByNickname(nickname); dto.setUser(user); Posts posts = dto.toEntity(); postsRepository.save(posts); return posts.getId(); } .... }
User의 id(PK)를 Posts의 user_id(FK)에 저장하기 위해 User 정보를 가져와 Posts에 저장한다.
6. IndexController
PostsIndexController
@Controller @RequiredArgsConstructor public class PostsIndexController { private final PostsService postsService; .... @GetMapping("/posts/read/{id}") public String read(@PathVariable Long id, @LoginUser UserSessionDto user, Model model) { PostsResponseDto dto = postsService.findById(id); if (user != null) { model.addAttribute("user", user.getNickname()); /*게시글 작성자 본인인지 확인*/ if (dto.getUserId().equals(user.getId())) { model.addAttribute("writer", true); } } postsService.updateView(id); // views ++ model.addAttribute("posts", dto); return "posts/posts-read"; } .... }
7. Mustache
posts-read.mustache
{{>layout/header}} <br/> <div id="posts_list"> <div class="col-md-12"> <form class="card"> <div class="card-header d-flex justify-content-between"> <label for="id">번호 : {{posts.id}}</label> <input type="hidden" id="id" value="{{posts.id}}"> {{! label 연결 }} <label for="createdDate">{{posts.createdDate}}</label> </div> <div class="card-header d-flex justify-content-between"> <label for="writer">작성자 : {{posts.writer}}</label> <label for="view"><i class=" bi bi-eye-fill"> {{posts.view}}</i></label> </div> <div class="card-body"> <label for="title">제목</label> <input type="text" class="form-control" id="title" value="{{posts.title}}" readonly> <br/> <label for="content">내용</label> <textarea rows="5" class="form-control" id="content" readonly>{{posts.content}}</textarea> </div> </form> {{! Buttons }} {{#user}} <a href="/" role="button" class="btn btn-info bi bi-arrow-return-left"> 목록</a> {{#writer}} <a href="/posts/update/{{id}}" role="button" class="btn btn-primary bi bi-pencil-square"> 수정</a> <button type="button" onclick="" id="btn-delete" class="btn btn-danger bi bi-trash"> 삭제</button> {{/writer}} {{/user}} {{^user}} <a href="/" role="button" class="btn btn-info bi bi-arrow-return-left"> 목록</a> {{/user}} </div> </div> {{>layout/footer}}
8. 결과 확인
8-1. 테스트용 계정 및 게시글 생성


8-2. 글 작성자만 수정 및 삭제 가능


8-3. 닉네임 변경 전에 작성한 게시글 변경 후 수정 및 삭제 가능여부 확인

이전에는 글 작성 후 닉네임 변경시 수정 및 삭제가 불가능했지만, 이제 가능해졌다.
블로그의 정보
슬기로운 개발생활
coco3o