슬기로운 개발생활

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는 다음과 같은 쿼리문을 수행하는 것을 볼 수 있다.

EAGER
LAZY

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

활동하기