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 계층 구조는 문자열 정렬인데, 문자열은 오름차순 정렬 시 10이 2보다 앞쪽에 오게 되는 등 자릿수가 바뀌면 숫자와 정렬 방향이 바뀌는 현상이 생김
▪
이를 해결하기 위해선 왼쪽 패딩을 넣어줘야 함 (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자리 정도면 충분할 것으로 사료됨