게시판 설계(1)

강서진·2023년 12월 14일
0

Language: Java 17
Framework: Spring Boot 3.2.0
DBMS: MySQL 8
DB Library: JPA

  • 회원가입이나 로그인 기능은 없으며, 이름, 비밀번호(4자리), 이메일을 입력하여 게시글을 작성하고, 답변을 달 수 있는 게시판을 만들어본다.
  • 게시판에 이름을 입력하여 생성할 수 있다. 원하는 게시판에 포스트를 쓸 수 있으며, 포스트를 열람할 때, 답변이 있을 경우 이를 함께 반환한다.
  • status 칼럼이 존재하며, 포스트나 답변을 삭제할 경우 이 status가 REGISTERED에서 UNREGISTERED로 바뀌며 검색 쿼리에서 제외된다.

MySQL Workbench에 있는 툴로 ERD를 그릴 수 있다.

게시판을 저장할 board 테이블,
하나의 board에 올라갈 여러 게시글을 담는 post 테이블(1:N),
하나의 post에 달릴 여러 댓글을 담는 reply 테이블을 만든다(1:N).

ERD를 완성한 후 export-forward engineer sql create script로 SQL 문을 만들 수 있다. 이 때 foreign key와 foreign key index 생성을 하지 않도록 옵션을 체크한다.

현업에서는 Foreign key를 연결해두면 테이블 수정에 제약이 많이 걸리기 때문에, 설정해두지 않고 id 칼럼을 따로 파두었다가 join을 사용한다고도 한다. 하여 반드시 foreign key를 사용하는 건 아니라는 것을 알아둘 필요가 있다.


API Endpoint

Api 엔드포인트는 API 호출을 수신하는 API 연결의 끝을 말한다.

글 작성 기능을 먼저 만들어본다. 게시판을 생성할 board 패키지 내부에 db, controller, model, service 패키지를 생성하고, BoardEntity를 만든다.

// BoardEntity

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name="board")
public class BoardEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String boardName;
    private String status;
}

Lombok으로 기본적인 getter, setter, toString(), 생성자 메서드들을 만들어준다. @Entity 애너테이션으로 어떤 테이블과 연결되는지 명시해주었다.

다음으로 db에 JPARepository를 상속한 인터페이스 BoardRepository를 만든다.

// BoardRepository

public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}

이 BoardRepository는 JpaRepository를 상속함으로써 JPA가 구현해둔 쿼리 메서드들을 사용할 수 있게 해준다. 이 BoardRepository를 사용해 DB와 상호작용을 하는 BoardService를 만든다.

// BoardService

@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;

    public BoardEntity create(BoardRequest boardRequest){
        BoardEntity entity = BoardEntity.builder()
                .id(1L)
                .boardName(boardRequest.getBoardName())
                .status("REGISTERED")
                .build();
        return boardRepository.save(entity);
    }

}

서비스 애너테이션을 붙이고, 자동으로 BoardRepository를 주입해주는 RequiredArgsConstructor를 붙여준다. BoardRepository는 처음 생성된 후로 변경이 없는 객체기 때문에 final로 선언한다.
BoardEntity를 생성하는 create() 메서드는 프론트에서 들어왔다고 가정하는 BoardRequest를 받는다. BoardRequest에서 필요한 정보를 가지고 BoardEntity를 build하고, 이 entity를 boardRepository의 save 메서드로 board 테이블에 저장한다.

BoardRequest 클래스를 model 패키지에 생성한다. 필수로 입력받아야 하는 내용인 게시판 이름만 가지고 있으며, 공백일 수 없으므로 @NotBlank 검증 애너테이션을 달고 있다.

// BoardRequest

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class BoardRequest {

    @NotBlank
    private String boardName;
}

BoardRequest 형태로 요청이 들어오면 이를 받아 BoardEntity를 생성하고 그 응답을 내갈 BoardApiController를 controller 패키지에 만든다.

// BoardApiController

@RestController
@RequestMapping("/api/board")
@RequiredArgsConstructor
public class BoardApiController {

    private final BoardService boardService;

    @PostMapping("")
    public BoardEntity create(@Valid @RequestBody BoardRequest boardRequest){
        return boardService.create(boardRequest);
    }
}

post와 reply도 마찬가지로 비슷한 구조를 가진다.
그러나 post에서는 한 게시판에 여러 post가 존재할 수 있기 때문에 포스트 1개만 보려면 PostViewRequest 클래스가 하나 더 필요하다.

// PostViewRequest

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class PostViewRequest {
    @NotNull
    private Long postId;

    @NotBlank
    @Size(min=4,max=4)
    private String password;
}

PostService에서는 PostViewRequest를 받아 password가 일치하면 status가 REGISTERED인 포스트 중에서 해당하는 id를 가진 포스트를 반환한다.
처음에는 findById를 사용했으나, PostRepository에서 JPA 쿼리 메서드를 생성하여 사용하였다.

// PostRespository

// select * from post where id = ? and status = ? order by id desc limit 1
    public Optional<PostEntity> findFirstByIdAndStatusOrderByIdDesc(Long id, String status);
// PostService view()

	public PostEntity view(PostViewRequest postViewRequest){
//        return postRepository.findById(postViewRequest.getPostId())
        return postRepository.findFirstByIdAndStatusOrderByIdDesc(postViewRequest.getPostId(), "REGISTERED")
                .map( it -> {
                    if (!it.getPassword().equals(postViewRequest.getPassword())){
                        String format = "비밀번호가 일치하지 않습니다. %s vs %s";
                        throw new RuntimeException(String.format(format, it.getPassword(), postViewRequest.getPassword()));
                    }

                    List<ReplyEntity> replyList = replyService.findAllByPostId(it.getId());
                    // reply도 함께 반환
                    it.setReplyList(replyList);

                    return it;
                }).orElseThrow(
                        () -> {
                            return new RuntimeException("포스트가 존재하지 않습니다. "+postViewRequest.getPostId());
                        }
                );
    }

replyList 부분은 reply 부분에서 다시 돌아와서 보면 된다.

만약 비밀번호가 일치하지 않거나 해당 id의 포스트가 존재하지 않으면 RuntimeException을 보낸다.
delete도 마찬가지로 PostViewRequest를 사용한다. 만약 비밀번호가 일치하면 해당하는 id를 가진 post의 status를 UNREGISTERED로 바꾼다.

	public void delete(PostViewRequest postViewRequest) {
        postRepository.findById(postViewRequest.getPostId())
                .map( it -> {
                    if (!it.getPassword().equals(postViewRequest.getPassword())){
                        String format = "비밀번호가 일치하지 않습니다. %s vs %s";
                        throw new RuntimeException(String.format(format, it.getPassword(), postViewRequest.getPassword()));
                    }

                    it.setStatus("UNREGISTERED");
                    postRepository.save(it);
                    return it;
                }).orElseThrow(
                        () -> {
                            return new RuntimeException("포스트가 존재하지 않습니다. "+postViewRequest.getPostId());
                        }
                );
    }

reply 쪽의 로직도 비슷하게 완성하다보면, post에서 view로 게시글을 열람할 때 reply도 함께 반환하기 위해 PostEntity에 추가할 부분이 생긴다.

	// column으로 인식하지 않도록
    @Transient
    // 빈 리스트 - view 호출 시 추가
    private List<ReplyEntity> replyList = List.of();

view가 호출될 때 reply 테이블에서 해당하는 post_id를 가지고 status가 REGISTERED인 reply를 리스트로 반환하는 쿼리메서드를 ReplyRepository에 작성한다. ReplyService에서 이 쿼리메서드를 사용한 메서드를 만들어두고, PostService에서 ReplyService를 객체로 생성하여 view 메서드에 replyList 부분을 추가하여 사용한다.

연관관계 설정은 다음에 해보기로 한다.

0개의 댓글