Search

[Java, DB] 무한 계층형 게시판 만들기 - tree path 이용

Last update: @3/24/2023

계층형 게시판 구현 방법

답글을 달 수 있는 계층형 게시판을 만들고자 함
OracleDB라면 connect by, start with를 쓰면 된다고 하는데 Oracle이 아닌 경우 쉽지 않아 보였음
OracleDB 이외의 DB에서 계층형 게시판을 구현하는 여러가지 방법을 찾아봄
재귀 쿼리 이용
소수점 이용
컬럼을 여러개 이용
함수 이용
원글 id를 1000씩 늘리고 답글에는 그 사잇값 주기 등등
여러 방법이 있지만 쿼리가 복잡하거나 다른 레코드도 같이 업데이트 해줘야 하는 등 마음에 드는 게 없었음
나름 방법을 생각한 것이 id를 이용해 tree 계층 구조를 표현해 정렬하는 것
아래와 같은 구조가 있다면 1, 1-1, 1-2, 1-1-1과 같은 계층 구조를 저장한 컬럼을 만들어 정렬하면 될 것 같음
글 1 ㄴ 답글 1-1 ㄴ 답글 1-1-1 ㄴ 답글 1-2
Plain Text
복사
이렇게 하면 복잡한 쿼리도 필요 없고, 무한 답글도 가능해보임
어찌어찌 구현을 해놓고 보니 생각보다 잘 작동하는데, 문제가 생김
tree 계층 구조는 문자열 정렬인데, 문자열은 오름차순 정렬 시 102보다 앞쪽에 오게 되는 등 자릿수가 바뀌면 숫자와 정렬 방향이 바뀌는 현상이 생김
이를 해결하기 위해선 왼쪽 패딩을 넣어줘야 함 (010, 002처럼)
하지만 패딩을 넣으면 정렬용 컬럼이 지나치게 길어지게 됨(000000002/000000010/…)
이를 해결하기 위해 id를 더 높은 진수로 변환해 문자열 길이를 줄여주기로 함

구현

Spring Boot, JPA, MySQL을 사용했으나 스펙과는 무관한 방법이기 때문에 모든 코드를 적지는 않음

DB 컬럼 추가

게시판 관행에 따라 원글을 기준으로 작성일(또는 id) 내림차순 정렬을 하고, 원글의 답글은 오름차순 정렬을 함(즉, 원글은 최신순 답글은 등록순)
따라서 테이블에 원글 id인 root컬럼, 계층 구조를 표현한 treePath 컬럼 총 두 컬럼을 추가함

Service 계층 로직

@Transactional public Long addPost(PostForm postForm, User writer) throws IOException { Post post = new Post(); post.setUser(writer); post.setTitle(postForm.getTitle()); post.setContent(postForm.getContent()); // 게시글 저장 (id 획득) postRepository.save(post); // treePath 생성 if (parentTreePath == null || parentTreePath.equals("")) { parentTreePath = "/"; } else { parentTreePath += "/"; } // id를 36진수로 변환 String hexaTriDecimalId = Long.toString(post.getId(), Character.MAX_RADIX); // 패딩 추가 String hexaTriDecimalIdPadded = String.format("%6s", hexaTriDecimalId).replace(" ", "0"); String treePath = parentTreePath + hexaTriDecimalIdPadded; // root id를 10진수로 파싱 Long root = Long.parseLong(treePath.split("/")[1], Character.MAX_RADIX); post.setTreePath(treePath); post.setRoot(root); //JPA 의해 엔티티 자동 업데이트 return post.getId(); }
Java
복사
답글 작성 시 부모의 트리 경로인 parentTreePath가 넘어오고, 원글이라면 넘어오지 않음
parentTreePath/를 붙이고 10진수 id를 36진수(hexaTriDecimalId)로 바꾼 후 treePath에 붙여줌
36진수는 숫자 표현에 0~9, a~z를 사용하는, 자바에서 제공하는 가장 큰 진수 변환임(Character.MAX_RADIX == 36)
root는 root id의 36진수 문자열을 10진수로 파싱해서 설정해줌
들여쓰기 로직
//들여쓰기 long depth = postListDTO.getTreePath().chars() .filter(c -> c == '/').count() - 1; StringBuilder newTitle = new StringBuilder(); if (depth >= 1) { for (int i = 0; i < depth; i++) { newTitle.append(" "); } newTitle.append("ㄴ "); newTitle.append(postListDTO.getTitle()); postListDTO.setTitle(newTitle.toString()); }
Java
복사
/ 개수를 세어 1을 빼준 것이 들여쓰기 레벨이 됨
뷰 페이지 글 제목 태그에 아래처럼 CSS 설정을 해줘야 띄어쓰기를 통한 들여쓰기가 적용됨
style="white-space: pre;"
HTML
복사
또는 아래처럼 템플릿 엔진으로 padding, margin 등 이용
th:style="|padding-left:${postListDTO.depth * 5}%|"
HTML
복사

DB 쿼리

SELECT * FROM post ORDER BY root DESC, tree_path;
SQL
복사
DB에서 딱히 처리할 것도 없고, 쿼리가 간단해서 좋음
DB 의존성도 없음

결과

게시판 화면
DB 화면
패딩을 넣어줘서 자릿수에 따른 정렬 역전이 일어나지 않음
tree_path 컬럼의 길이를 255byte로 정하면 6자리로 패딩을 줄 때 36 depth까지 가능함
컬럼 자료형을 BLOB 등으로 바꾸면 사실상 무한 들여쓰기가 가능해짐
하지만 게시판 이용자들이 36 depth를 넘길 때까지 논쟁을 벌인다면 따로 현실 세계에서 토론 자리를 마련해주는 것으로 하고, 컬럼 길이를 적당하게 설정하는 것이 좋겠음

패딩 크기

예상되는 최대 id값을 36진수로 변환했을 때의 길이까지 패딩을 줘야함
그렇다면 패딩 크기를 얼마로 줘야할까?
Long형의 최대치는 10진수로 19자리인데, 이를 36진수로 압축하면 13자리로 줄일 수 있음
더 현실적으로, 36진수 6자리면 게시물 21억 개까지 가능함
웹사이트 게시글이 21억개를 넘겨 DB 오류가 날 정도면 나는 아마 부자가 되어있을 것이기 때문에 문제가 되지 않음. 그때 가서 더 훌륭한 개발자를 고용해 문제를 해결하면 되기 때문임
따라서 6자리 정도면 충분할 것으로 사료됨

References