Techit 10th 4th

Huisu·2023년 6월 22일
0

Techit

목록 보기
21/42
post-thumbnail

Tree

Binary Search Tree

탐색 작업을 효율적으로 하기 위한 자료 구조이다. 모든 노드의 데이터가 서로 다른 이진 트리이다. 만약 어느 노드에게 자식이 있는 경우, 왼쪽 자식의 데이터는 부모보다 작고 오른쪽 자식의 데이터는 부모보다 크다. 중위 순회를 하게 되면 왼쪽 → 루트 → 오른쪽 순서기 때문에 오름차순으로 정렬된 값이 나온다.

이진 트리의 탐색에 대해 알아 보자. 루트 노드와 탐색 데이터를 비교한다. 데이터가 더 작을 경우 왼쪽 서브 트리로 가고 더 클 경우 오른쪽 서브 트리로 간다. 이때의 시간 복잡도는 O(h)O(h) ~ O(logN)O(logN)이다. 편향 이진 트리의 경우 순차 탐색과 다를 바 없기 때문에 O(N)O(N)이다.

Insert

BST에 새로운 데이터를 추가할 경우, 탐색의 과정과 비슷하게 진행합니다. 루트 노드에서 부터 비교를 시작한다.

  • 루트 노드가 삽입할 데이터와 동일한 경우, BST에 동일한 데이터를 입력하지 못하므로 삽입할 수 없다.
  • 루트 노드의 데이터가 삽입하고자 하는 데이터보다 큰 경우, 오른쪽 서브트리로 이동한다.
  • 루트 노드의 데이터가 삽입하고자 하는 데이터보다 작은 경우, 왼쪽 서브트리로 이동한다.

서브트리로 이동하다가, 이동할 서브트리 루트노드가 없는 경우, 해당 위치에 데이터를 삽입한다.

Delete

우선 삭제할 대상 데이터를 가지고 있는 노드까지 이동합니다. 이후 해당 노드를 삭제하면서, 이진 트리의 구조를 유지하기 위해 자식 노드들 중 하나의 데이터를 루트 노드의 위치에 삽입해야 gks다.

  • 삭제할 데이터를 가진 노드까지 이동한다.
  • BST에서 루트 노드를 기준으로 왼쪽 서브트리의 모든 데이터는 루트 노드의 데이터보다 작아야 하며, 오른쪽 서브트리의 모든 데이터는 루트 노드보다 커야 한다.
  • 삭제하고자 하는 노드에 자식이 하나일 경우, 해당 자식 노드와 부모 노드를 연결한다.
  • 노드가 두개일 경우, 둘 중 하나를 선택해서 진행한다.
    • 왼쪽 서브트리의 값 중 제일 큰 데이터를 루트 노드에 할당한다. 이때 이 데이터는 단말 노드에 저장되어 있으므로, 해당 단말노드를 삭제한다.
    • 오른쪽 서브트리의 값 중 제일 작은 데이터를 루트 노드에 할당한다. 이때 이 데이터는 단말 노드에 저장되어 있으므로, 해당 단말노드를 삭제한다.

REST

GET /articles/{articleID}/comments/

게시글의 댓글을 전체 조회하는 API를 만들어 보자.

@GetMapping
    public List<CommentDto> readAll(
            @PathVariable("articleId") Long articleId
    ){
        return commentService.readCommentAll(articleId);
    }
public List<CommentDto> readCommentAll(Long articleId) {
        List<CommentEntity> CommentEntities =
                commentRepository.findAllByArticleId(articleId);
        List<CommentDto> commentDtos = new ArrayList<>();
        for (CommentEntity entity:
             CommentEntities) {
            commentDtos.add(CommentDto.fromEntity(entity));
        }
        return commentDtos;
    }

잘 나오는 것을 확인할 수 있다.

PUT /articles/{articleID}/comments/{commentId}

게시글의 댓글을 수정는 API를 만들어 보자. 이때 기능에서 articleId를 직접적으로 사용하지는 않지만 수정하고자 하는 댓글이 지정한 게시글에 있는지 확인할 목적으로 articleID를 첨부한다.

// TODO 게시글 댓글 수정
    // PUT /articles/{articleID}/comments/{commentId}
    @PutMapping("{commentId")
    public CommentDto update(
            @PathVariable("articleId") Long articleId,
            @PathVariable("commentId") Long commentId,
            @RequestBody CommentDto dto
    ) {
        return commentService.updateComment(articleId, commentId, dto);
    }
// TODO 게시글 댓글 수정
    // 수정하고자 하는 댓글이 지정한 게시글에 있는지 확인할 목적으로 articleID를 첨부한다.
    public CommentDto updateComment(
            Long articleId,
            Long commentId,
            CommentDto dto
    ) {
        // 요청한 댓글이 존재하는지
        Optional<CommentEntity> optionalComment
                = commentRepository.findById(commentId);
        
        // 존재하지 않으면 예외 발생
        if(optionalComment.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        
        // 아니라면 로직 진행
        CommentEntity comment = optionalComment.get();
        
        // 대상 댓글이 대상 게시글의 댓글이 맞는지 확인
        if(!articleId.equals(comment.getArticleId()))
            // 요청한 두 자원의 일치가 없기 때문에 BAD REQUEST로 응답 (400)
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
        
        comment.setContent(dto.getContent());
        comment.setWriter(dto.getWriter());
        return CommentDto.fromEntity(commentRepository.save(comment));
    }

결과가 잘 나오는 것을 확인할 수 있다.

@Data Annotation은 무분별한 Setter를 만들기 때문에 바뀌지 않아야 하는 데이터도 변경을 만들어낼 수 있다. 따라서 영향을 받을 만한 상황에서는 잘 사용하지 않는다.

DELETE /articles/{articleID}/comments/{commentID}

게시글의 댓글을 삭제하는 API를 만들어 보자.

public void deleteComment(
            Long articleId,
            Long commentId
    ) {
        Optional<CommentEntity> commentEntity
                = commentRepository.findById(commentId);
        if(commentEntity.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);

        CommentEntity comment = commentEntity.get();

        if(!articleId.equals(comment.getArticleId()))
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);

        commentRepository.deleteById(commentId);
    }
// TODO 게시글 댓글 삭제
    // DELETE /articles/{articleID}/comments/{commentID}
    @DeleteMapping("/{commentId}")
    public void delete(
            @PathVariable("articleId") Long articleId,
            @PathVariable("commentId") Long commentId
    ) {
        commentService.deleteComment(articleId, commentId);
    }

JOIN

JOIN

Comment 테이블에 articleId를 넣어 주는 것은 좋은 테이블 설계 방법이 아니다. orm의 기능 중 좋은 기능을 해치는 방법이다. entity 자체도 내부에 정의할 수 있기 때문이다. 관심사의 분리를 해치기 때문이다. 댓글 관련된 기능이고 게시글이 존재하는지 유무를 댓글 서비스에서 관리하는 것은 관심사 분리가 되지 않은 것이다.

@Data
@Entity
@Table(name = "comments")
public class CommentEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //private Long articleId;
    private String writer;
    private String content;
    @ManyToOne
    private ArticleEntity article;
}

위 어노테이션처럼 쓰면 애초에 article이 없는 경우 댓글이 생성되지 않게 된다.

Query Parameter

RequestParam

url에서 ? 뒤에 오는 값들을 Query Parameter라고 한다. 즉 동적인 데이터를 받아 오기 위해 사용한다. 먼저 @RequestParam을 살펴보자. html에서 form의 submit을 전송하면 받던 값이다. form에서 제출 버튼을 누르면 입력값들을 ? 뒤에 묶어 url로 build한 뒤 RequestParam으로 읽어 온다. 즉 Query Parameter를 읽어 오기 위한 Annotation이다. url을 인식해서 값을 읽는지 확인해 보기 위해 Controller를 하나 만들어 보자.

@GetMapping("/path")
    public Map<String, Object> queryParams(
        @RequestParam("query") String query,
        @RequestParam("limit") Integer limit
    ) {
        log.info("query: " + query);
        log.info("limit: " + limit);
        Map<String, Object> response = new HashMap<>();
        response.put("query", query);
        response.put("limit", limit);
        return response;
    }

Postman을 통해 잘 출력되는 것을 확인할 수 있다. 주로 검색, 카테고리로 묶은 분류 등과 같은 기능을 만들 때 주로 사용된다. url 자체는 원래 있던 자원을 재활용하는 경우가 많다. deaultValue로 기본값 설정이 가능하고, required로 필수 유무를 설정할 수 있다. url에서는 .이나 / 같은 문자가 url 구분을 위해 사용되고 있다. 하지만 이를 url 자원 자체로 활용하고 싶을 때는 url encoding을 참조하면 된다.

Pagination

조회할 데이터의 개수가 많을 때 조회되는 데이터의 갯수를 한정시켜 페이지 단위로 나누는 기법이다. 조회할 데이터의 갯수가 줄어들기 때문에 성능 향상을 꾀할 수 있고, 사용자가 데이터를 확인하는 과정에서 확인해야 하는 데이터를 줄여 UX가 더욱 깔끔해진다. SQL 문에서 LIMIT으로 개수를 제한하고 OFFSET을 통해 몇 개를 건너뛸 것인지를 조정하는 법과 같다.

articles 중 사용자의 조건에 따라 20개씩 나눠서 제공하는 기능을 생각해 보자. ID가 큰 순서대로 최상위 20개를 반환하는 메소드를 JPA Interface에 작성한다.

public interface ArticleRepository 
        extends JpaRepository<ArticleEntity, Long> {
    List<ArticleEntity> findTop20ByOrderByIdDesc();
}

그렇다면 다음 페이지는 어떻게 볼까? Id가 특정값보다 작은 데이터 중 큰 순서대로 최상위 20개로 하는 함수명을 정해 주면 된다.

public interface ArticleRepository
        extends JpaRepository<ArticleEntity, Long> {
    List<ArticleEntity> findTop20ByOrderByIdDesc();
    List<ArticleEntity> findTop20ByIdLessThanOrderByIdDesc(Long id);
}

먼저 findTop20ByOrderByIdDesc() 를 활용하는 서비스를 구현하면 다음과 같다.

public List<ArticleDto> readArticlePages() {
        List<ArticleDto> articleDtos = new ArrayList<>();
        for (ArticleEntity entity:
             repository.findTop20ByOrderByIdDesc()) {
            articleDtos.add(ArticleDto.fromEntity(entity));
        }
        return articleDtos;
    }

이후 테스트를 위해 Controller를 만들어 준다.

@GetMapping("/page-test")
    public List<ArticleDto> readPageTest() {
        return service.readArticlePages();
    }

딱 20개만 잘 페이징되는 것을 확인할 수 있다. 이런 JPA Query Method 방식은 현업에서는 잘 사용하지 않는다.

JPA의 PagingAndSortingRepository 인터페이스를 활용하면 repository를 건드릴 필요가 없다. 이를 사용하기 위해서는 Pageable 객체가 필요하다. PagingAndSortingRepository 메소드에 전달하기 위한 용도로 사용된다. 조회하고 싶은 페이지의 정보를 담는 객체라고 보면 된다.

public List<ArticleDto> readArticlePages() {
        Pageable pageable = PageRequest.of(0, 20);
    }

위처럼 선언할 수 있는데, 20개씩 데이터를 나눌 때 0 번쨰 페이지를 달라고 요청하는 Pageable이다.

public Page<ArticleEntity> readArticlePages() {
        Pageable pageable = PageRequest.of(0, 20);
        Page<ArticleEntity> articleEntityPage =
                repository.findAll(pageable);
       return articleEntityPage;
    }

더욱 간단한 코드로 구현할 수 있다.

Postman을 확인해 보면 Page 객체로 잘 돌려받은 것을 확인할 수 있다. 이때 Entity가 아닌 Dto를 보여 주고 싶을 때는 foreach문을 사용해 DtoList로 바꿔 주면 된다.

public List<ArticleDto> readArticlePages() {
        Pageable pageable = PageRequest.of(0, 20);
        Page<ArticleEntity> articleEntityPage =
                repository.findAll(pageable);

        List<ArticleDto> articleDtoList = new ArrayList<>();
        for (ArticleEntity entity:
             articleEntityPage) {
            articleDtoList.add(ArticleDto.fromEntity(entity));
        }

       return articleDtoList;
    }

이때 정렬 순서를 다르게 하고 싶다면 Pageable의 세 번째 인자로 sort를 전달하면 된다.

public List<ArticleDto> readArticlePages() {
        Pageable pageable = PageRequest.of(
                0, 
                20, 
                **Sort.by("id").descending())**;
        
        Page<ArticleEntity> articleEntityPage =
                repository.findAll(pageable);

        List<ArticleDto> articleDtoList = new ArrayList<>();
        for (ArticleEntity entity:
             articleEntityPage) {
            articleDtoList.add(ArticleDto.fromEntity(entity));
        }

       return articleDtoList;
    }

이때 Pageable 객체에 나오는 메타 정보를 더욱 활용해 보자.

map()

전달받은 함수를 각 원소의 인자로 전달한 결과를 다시 모아서 Stream으로 반환해 주는 게 Streammap이었다면 Pagemap은 해당 결과를 Page로 반환해 준다. 아래의 map 기능을 활용해 EntityDto로 바꾼 Page를 반환해 주는 것이다.

public Page<ArticleDto> readArticlePaged() {
        Pageable pageable = PageRequest.of(
                0,
                20,
                Sort.by("id").descending());

        Page<ArticleEntity> articleEntityPage =
                repository.findAll(pageable);

        **Page<ArticleDto> articleDtoPage =
                articleEntityPage.map(ArticleDto::fromEntity);**
        return articleDtoPage;
    }

Postman으로 확인한 결과는 다음과 같다.

  • 결과값
    {
        "content": [
            {
                "id": 100,
                "writer": "Chantale Reese",
                "title": "lobortis tellus justo",
                "content": "nisl arcu iaculis enim, sit amet ornare lectus justo eu arcu. Morbi sit amet massa. Quisque porttitor eros nec tellus. Nunc lectus pede, ultrices a, auctor"
            },
            {
                "id": 99,
                "writer": "Selma Hudson",
                "title": "rhoncus id,",
                "content": "semper erat, in consectetuer ipsum nunc id enim. Curabitur massa. Vestibulum accumsan neque et nunc. Quisque ornare tortor at risus. Nunc ac sem ut dolor dapibus gravida. Aliquam tincidunt,"
            },
            {
                "id": 98,
                "writer": "Sigourney Copeland",
                "title": "dignissim magna",
                "content": "lacus. Etiam bibendum fermentum metus. Aenean sed pede nec ante blandit viverra. Donec tempus, lorem fringilla ornare placerat, orci lacus vestibulum lorem, sit amet ultricies sem"
            },
            {
                "id": 97,
                "writer": "Karleigh Poole",
                "title": "pede. Praesent eu",
                "content": "interdum. Curabitur dictum. Phasellus in felis. Nulla tempor augue ac ipsum. Phasellus vitae mauris sit amet lorem semper auctor. Mauris vel turpis. Aliquam adipiscing lobortis risus."
            },
            {
                "id": 96,
                "writer": "Unity Romero",
                "title": "magna nec",
                "content": "magna. Cras convallis convallis dolor. Quisque tincidunt pede ac urna. Ut tincidunt vehicula risus. Nulla eget metus eu erat semper rutrum."
            },
            {
                "id": 95,
                "writer": "Fuller Beasley",
                "title": "eu tellus",
                "content": "dolor. Donec fringilla. Donec feugiat metus sit amet ante. Vivamus non lorem vitae odio sagittis semper. Nam tempor diam dictum sapien. Aenean massa. Integer vitae nibh."
            },
            {
                "id": 94,
                "writer": "Ima Orr",
                "title": "egestas a,",
                "content": "Sed neque. Sed eget lacus. Mauris non dui nec urna suscipit nonummy. Fusce fermentum fermentum arcu. Vestibulum ante ipsum primis in faucibus orci luctus"
            },
            {
                "id": 93,
                "writer": "Venus Frazier",
                "title": "luctus",
                "content": "interdum. Nunc sollicitudin commodo ipsum. Suspendisse non leo. Vivamus nibh dolor, nonummy ac, feugiat non, lobortis quis, pede. Suspendisse dui. Fusce diam nunc,"
            },
            {
                "id": 92,
                "writer": "Hall Carey",
                "title": "in, hendrerit consectetuer, cursus",
                "content": "Ut nec urna et arcu imperdiet ullamcorper. Duis at lacus. Quisque purus sapien, gravida non, sollicitudin a, malesuada id, erat. Etiam vestibulum massa rutrum magna. Cras convallis convallis"
            },
            {
                "id": 91,
                "writer": "Phyllis Velez",
                "title": "enim. Etiam",
                "content": "Proin sed turpis nec mauris blandit mattis. Cras eget nisi dictum augue malesuada malesuada. Integer id magna et ipsum cursus vestibulum. Mauris magna. Duis dignissim tempor arcu. Vestibulum ut"
            },
            {
                "id": 90,
                "writer": "Gavin Bradley",
                "title": "non",
                "content": "arcu et pede. Nunc sed orci lobortis augue scelerisque mollis. Phasellus libero mauris, aliquam eu, accumsan sed, facilisis vitae, orci."
            },
            {
                "id": 89,
                "writer": "Brittany Crane",
                "title": "lorem, luctus ut, pellentesque",
                "content": "Morbi non sapien molestie orci tincidunt adipiscing. Mauris molestie pharetra nibh. Aliquam ornare, libero at auctor ullamcorper, nisl arcu iaculis enim, sit"
            },
            {
                "id": 88,
                "writer": "Walter Vang",
                "title": "amet luctus",
                "content": "eu sem. Pellentesque ut ipsum ac mi eleifend egestas. Sed pharetra, felis eget varius ultrices, mauris ipsum porta elit, a feugiat tellus"
            },
            {
                "id": 87,
                "writer": "MacKenzie Slater",
                "title": "eu dui.",
                "content": "nulla at sem molestie sodales. Mauris blandit enim consequat purus. Maecenas libero est, congue a, aliquet vel, vulputate eu, odio. Phasellus at augue id ante dictum cursus. Nunc mauris"
            },
            {
                "id": 86,
                "writer": "Barrett Dickerson",
                "title": "elit. Etiam",
                "content": "penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean eget magna. Suspendisse tristique neque venenatis lacus. Etiam bibendum fermentum metus."
            },
            {
                "id": 85,
                "writer": "Buckminster Gutierrez",
                "title": "consequat auctor, nunc",
                "content": "dui quis accumsan convallis, ante lectus convallis est, vitae sodales nisi magna sed dui. Fusce aliquam, enim nec tempus scelerisque, lorem ipsum sodales purus, in molestie tortor"
            },
            {
                "id": 84,
                "writer": "Clarke Higgins",
                "title": "pellentesque, tellus sem",
                "content": "Sed neque. Sed eget lacus. Mauris non dui nec urna suscipit nonummy. Fusce fermentum fermentum arcu. Vestibulum ante ipsum primis in faucibus orci luctus et"
            },
            {
                "id": 83,
                "writer": "Reagan O'brien",
                "title": "Duis elementum, dui quis",
                "content": "imperdiet dictum magna. Ut tincidunt orci quis lectus. Nullam suscipit, est ac facilisis facilisis, magna tellus faucibus leo, in lobortis tellus justo"
            },
            {
                "id": 82,
                "writer": "Erasmus Benson",
                "title": "malesuada fames ac turpis",
                "content": "amet massa. Quisque porttitor eros nec tellus. Nunc lectus pede, ultrices a, auctor non, feugiat nec, diam. Duis mi enim, condimentum"
            },
            {
                "id": 81,
                "writer": "Gisela Haley",
                "title": "mattis. Integer eu",
                "content": "euismod et, commodo at, libero. Morbi accumsan laoreet ipsum. Curabitur consequat, lectus sit amet luctus vulputate, nisi sem semper erat, in consectetuer ipsum nunc id enim. Curabitur massa."
            }
        ],
        "pageable": {
            "sort": {
                "empty": false,
                "unsorted": false,
                "sorted": true
            },
            "offset": 0,
            "pageNumber": 0,
            "pageSize": 20,
            "paged": true,
            "unpaged": false
        },
        "last": false,
        "totalPages": 5,
        "totalElements": 100,
        "first": true,
        "size": 20,
        "number": 0,
        "sort": {
            "empty": false,
            "unsorted": false,
            "sorted": true
        },
        "numberOfElements": 20,
        "empty": false
    }

이후 원래 쓰던 readAll 메소드를 수정한다. url은 자원이다. 조회하는 데이터는 여전히 articleDto이기 때문에 /article을 유지하고, 기타 데이터는 Query Parameter로 전달한다. 자원 값에 따라 사용자가 원하는 개수만큼의 데이터를 볼 수 있도록 해 보자.

// GET /articles?page=3&size=20
    @GetMapping
    public Page<ArticleDto> readAll(
            @RequestParam("page") Integer page,
            @RequestParam("size") Integer size
    ) {
        return service.readArticlePages(page, size);
    }
public Page<ArticleDto> readArticlePages(Integer page, Integer size) {
        Pageable pageable = PageRequest.of(
                page,
                size,
                Sort.by("id").descending());

        Page<ArticleEntity> articleEntityPage =
                repository.findAll(pageable);

        Page<ArticleDto> articleDtoPage =
                articleEntityPage.map(ArticleDto::fromEntity);
        return articleDtoPage;
    }

페이지 사이즈와 페이지 넘버를 고정해 두는 하드 코딩에서 사용자의 요청에 따라 다양하고 어울리는 값을 반환할 수 있도록 변주를 준 것이다.

특정 문자열이 제목에 포함되어 있는지를 기준으로 간단한 검색 기능을 구현해 보자. 말에서 SELECT를 기반으로 데이터 조회가 이루어질 것을 예상할 수 있다.

어느 서비스에서 구현하느냐에 따라 Search URL의 자유도는 넓다고 볼 수 있다. 새로운 /search라고 자원을 만들 수도 있다. 이후 어떤 문자열을 필요로 하는지는 RequestParam으로 작성하며 이때 검색어는 필수이다. JPA Repository에 추가해서 단순하게 구현할 수도 있지만 JPA Repository PagingAndSortingRepository를 사용하여 Pageable로 구현하는 방법도 있다.

먼저 JPA Repository를 사용하는 방법은 다음과 같이 작성할 수 있다.

public interface ArticleRepository
        extends JpaRepository<ArticleEntity, Long> {
    List<ArticleEntity> findTop20ByOrderByIdDesc();
    List<ArticleEntity> findTop20ByIdLessThanOrderByIdDesc(Long id);
    **Page<ArticleEntity> findAllByTitleContains(String query);**
}

이후 Service에 검색 기능을 구현해 준다.

public Page<ArticleDto> search(
            String query,
            Integer pageNumber
    ) {
        Pageable pageable = PageRequest.of(
                pageNumber,
                20,
                Sort.by("id").descending());
        return repository.findAllByTitleContains(query, pageable)
                .map(ArticleDto::fromEntity);
    }

다음에 Controller와 연결하면 마무리된다.

@GetMapping("/search")
    public Page<ArticleDto> search(
            @RequestParam("query") String query,
            @RequestParam(value = "page", defaultValue = "0")
            Integer pageNumber
     ) {
        return service.search(query, pageNumber);
    }

Postman을 통해 잘 출력되는 것을 확인할 수 있다.

0개의 댓글