Board와 Post (2)

Minseo Kang·2023년 10월 6일
0
post-thumbnail

개요


지난 글에서는 Board의 구현에 대해 작성하였고, 이번 글에서는 Board와 Post의 관계를 고려하여 Post를 구현하는 것을 작성해보고자 한다.




Post(게시글) 구현


1. model 패키지에 PostDto 클래스를 작성한다.

  • 다음과 같은 필드들이 존재한다.
private Long id;
private String title;
private String content;
private String writer;
private String password;
private Long boardId;
  • Post(게시글)은 어떤 Board(게시판)에 속해있는지 알고 있어야 하기 때문에 boardId를 FK로 갖는다고 생각하면 된다.
  • getter, setter, 기본 생성자, 모든 필드에 대한 생성자, toString을 작성한다.

2. repository 패키지에 PostRepository 인터페이스를 작성한다.

  • 인터페이스를 기반으로 이후 세부 구현체를 작성한다.
  • 다음과 같이 작성하였다.
public interface PostRepository {
    PostDto create(Long boardId, PostDto dto);
    PostDto read(Long boardId, Long postId);
    Collection<PostDto> readAll(Long boardId);
    boolean update(Long boardId, Long postId, PostDto dto);
    boolean delete(Long boardId, Long postId, String password);
}

3. repository 패키지에 PostRepository의 구현체인 InMemoryPostRepository 클래스를 작성한다.

  • InMemoryBoardRepository와 마찬가지로 실제 DB에 연결되어 프로젝트가 돌아가는 것이 아니라 로컬에서 실행되므로 클래스 명에 InMemory를 붙였다.
  • 작성한 코드는 다음과 같다.
@Repository
public class InMemoryPostRepository implements PostRepository {

    private final BoardRepository boardRepository;
    private final Map<Long, PostDto> memory = new HashMap<>();
    private Long lastIndex = 0L;

    public InMemoryPostRepository(@Autowired BoardRepository boardRepository) {
        this.boardRepository = boardRepository;
    }

    @Override
    public PostDto create(Long boardId, PostDto dto) {
        BoardDto boardDto = this.boardRepository.read(boardId);
        if (boardDto == null) return null;
        dto.setBoardId(boardId);
        lastIndex++;
        dto.setId(lastIndex);
        memory.put(lastIndex, dto);
        return dto;
    }

    @Override
    public PostDto read(Long boardId, Long postId) {
        PostDto postDto = memory.getOrDefault(postId, null);
        if(postDto == null) {
            return null;
        }
        else if (!Objects.equals(postDto.getBoardId(), boardId)) {
            return null;
        }
        return postDto;
    }

    @Override
    public Collection<PostDto> readAll(Long boardId) {
        if(boardRepository.read(boardId) == null) return null;
        Collection<PostDto> postList = new ArrayList<>();
        memory.forEach((postId, postDto) -> {
            if (Objects.equals(postDto.getBoardId(), boardId))
                postList.add(postDto);
        });
        return postList;
    }

    @Override
    public boolean update(Long boardId, Long postId, PostDto dto) {
        PostDto targetPost = memory.getOrDefault(postId, null);
        if(targetPost == null) {
            return false;
        }
        else if(!Objects.equals(targetPost.getBoardId(), boardId)) {
            return false;
        }
        else if(!Objects.equals(targetPost.getPassword(), dto.getPassword())) {
            return false;
        }
        targetPost.setTitle(
                dto.getTitle() == null ? targetPost.getTitle() : dto.getTitle());
        targetPost.setContent(
                dto.getContent() == null ? targetPost.getContent() : dto.getContent());
        return true;
    }

    @Override
    public boolean delete(Long boardId, Long postId, String password) {
        PostDto targetPost = memory.getOrDefault(postId, null);
        if (targetPost == null) {
            return false;
        }
        else if(!Objects.equals(targetPost.getBoardId(), boardId)) {
            return false;
        }
        else if(!Objects.equals(targetPost.getPassword(), password)) {
            return false;
        }
        memory.remove(postId);
        return true;
    }

}
  • InMemoryPostRepository 클래스를 루트 컨테이너에 빈(Bean) 객체로 생성해주기 위한 @Repository 어노테이션을 붙인다.
  • 인터페이스를 구현함을 명시하기 위해 implements PostRepository를 작성한다.
  • Post(게시글)은 Board(게시판)에 대한 정보를 알고 있어야 하기 때문에 boardRepository 변수를 가지고 있다. 생성자에서 주입받아 사용한다.
  • lastIndex 변수는 PK인 id를 위한 변수이다.
  • memory 변수는 Long 타입을 key로, PostDto 타입을 value로 갖는다. Long 형의 데이터 타입(PK)로 PostDto를 조회하는 등의 행위를 함을 알 수 있다.

구현된 메소드를 하나씩 살펴보자.

Board보다 로직이 까다로운데, Board에 대한 검증이 있어서 그렇다.

  • create : 매개변수로 들어온 boardId 값이 존재하는지 검증하고, 존재한다면 해당 boardId를 세팅하고 인덱스 값을 증가시켜 memory에 dto를 저장한다. 존재하지 않는다면 null을 리턴한다.

  • read : Map의 getOrDefault 메소드를 이용해 PostDto를 반환한다. 검증은 두 단계를 거치는데, 첫 번째는 매개변수로 받은 postId 값을 통해 postDto가 존재하는지 확인한다. 존재하지 않으면 null을 리턴한다. 두 번째는 매개변수로 받은 boardId가 postId로 찾은 Post의 boardId와 동일한지 확인한다. 동일하지 않다면 null을 리턴한다. 두 경우에 걸리지 않는다면 postDto를 반환한다.

  • readAll : 매개변수로 전달받은 boardId에 해당하는 모든 post를 Collection에 담아서 반환한다. boardId가 null 이라면 null을 반환한다. 그렇지 않다면 빈 ArrayList를 만들고, forEach를 이용해 boardId에 대한 검증을 하고 검증이 정상적으로 되었다면 postList에 postDto를 추가한다.

  • update : 매개변수로 전달받은 postId로 post를 찾는다. post가 존재하지 않으면 false를 리턴한다. postId로 찾은 boardId와 매개변수로 전달받은 boardId가 일치하지 않는다면 false를 리턴한다. postId로 찾은 비밀번호와 dto에 담긴 비밀번호가 일치하지 않으면 false를 리턴한다. false에 걸리지 않는다면, 제목과 내용이 null이 아닌 경우에 한해서 업데이트를 진행하고 true를 리턴한다.

  • delete : update와 유사하기 때문에 생략한다.


4. controller 패키지에 PostController 클래스를 작성한다.

  • 작성한 코드는 다음과 같다.
@RestController
@RequestMapping("board/{boardId}/post")
public class PostController {

    private static final Logger logger = LoggerFactory.getLogger(PostController.class);
    private final PostRepository postRepository;

    public PostController(@Autowired PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @PostMapping
    public ResponseEntity<PostDto> createPost(@PathVariable("boardId") Long boardId,
                                              @RequestBody PostDto dto) {
        PostDto result = this.postRepository.create(boardId, dto);
        return ResponseEntity.ok(result.passwordMasked());
    }

    @GetMapping("{postId}")
    public ResponseEntity<PostDto> readPost(@PathVariable("boardId") Long boardId,
                                            @PathVariable("postId") Long postId) {
        PostDto postDto = this.postRepository.read(boardId, postId);
        if (postDto == null) return ResponseEntity.notFound().build();
        else return ResponseEntity.ok(postDto.passwordMasked());
    }

    @GetMapping ResponseEntity<Collection<PostDto>> readPostAll(@PathVariable("boardId") Long boardId) {
        Collection<PostDto> postList = this.postRepository.readAll(boardId);
        if(postList == null) return ResponseEntity.notFound().build();
        else return ResponseEntity.ok(postList);
    }

    @PutMapping("{postId}")
    public ResponseEntity<?> updatePost(@PathVariable("boardId") Long boardId,
                                        @PathVariable("postId") Long postId,
                                        @RequestBody PostDto dto) {
        if (!postRepository.update(boardId, postId, dto))
            return ResponseEntity.notFound().build();
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping("{postId}")
    public ResponseEntity<?> deletePost(@PathVariable("boardId") Long boardId,
                                        @PathVariable("postId") Long postId,
                                        @RequestParam("password") String password) {
        if (!postRepository.delete(boardId, postId, password))
            return ResponseEntity.notFound().build();
        return ResponseEntity.noContent().build();
    }

}
  • @RequestMapping("board/{boardId}/post") 이렇게 작성한 이유는, Post가 어떤 Board에 속해있는지 나타내기 위함이다.
  • PostRepository를 final로 선언하고 생성자에서 주입받아 사용한다.
  • 로직은 크게 어렵지 않다. 하지만 특이한 부분이 하나 있는데, 바로 passwordMasked()이다.

passwordMasked()

  • return ResponseEntity.ok()와 같이 작성하면 응답에 항상 비밀번호가 노출된다.
  • 해당 문제를 해결하기 위해 PostDto 클래스에 메소드를 하나 만든다.
public PostDto passwordMasked() {
        return new PostDto(
                this.id,
                this.title,
                this.content,
                this.writer,
                "****",
                this.boardId
        );
    }
  • 이렇게 작성하여 해당 메소드를 사용하면 응답으로 비밀번호는 항상 **** 로 나오게 된다.



정리


연관관계를 가지는 객체의 CRUD 로직을 작성할 때는 추가적인 검증 과정이 필요함을 깨달았다. 이전에 했던 프로젝트에서는 이와 같은 검증 과정을 철저하게 작성하지 않았는데, API를 리뉴얼하는 겸 해당 내용을 반영해서 구멍 없는 로직을 작성해야겠다.

0개의 댓글