์์ต๊ธฐ๊ฐ 4์ฃผ ๋์ '๊ฒ์ํ ๋ง๋ค๊ธฐ' ๊ณผ์ ๋ฅผ ๋ถ์ฌ๋ฐ์๋ค. ์ด ๊ธ์ ๊ณผ์ ๋ฅผ ์งํํ๋ฉฐ ๋๋ 1๋ฌ์ด๋ผ๋ ๊ธด ์๊ฐ ๋์ ๋ฌด์์ ๋ฐฐ์ ๋์ง ์ ๋ฆฌํ๊ณ ์ ํ๋ค.
ํ๋ก์ ํธ ๊ธฐ์ ์คํ
- ํ๋ก ํธ์๋: ํผ๋ธ๋ฆฌ์ฑ์ JavaScript๋ฅผ ๋ง๋ถ์ฌ Ajax ์์ฒญ
- ๋ฐฑ์๋: Spring Boot, Spring Security, JDBC Template, Thymeleaf
- DB: MySQL
- ๋น๋: Maven
- API ๋ฌธ์: Rest Docs
๊ตฌํํ ๊ธฐ๋ฅ์ ํฌ๊ฒ ๋ค์๊ณผ ๊ฐ๋ค.
- ์ฌ์ฉ์, ๊ฒ์๊ธ, ๋๊ธ/๋ต๊ธ, ๊ด๋ฆฌ์ CRUD
- ๊ฒ์๊ธ ํ์ด์ง, ๊ฒ์, ์ถ๋ ฅ ๊ฑด์ ์กฐ๊ฑด
๋์์ 1๊ณผ 2๋ ๊ฒฐ๊ณผ๋ฌผ์ด๋ค.
๋์์ 2์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ ๊ด๋ฆฌ์ ๊ถํ์ ๊ฐ์ง ์ฌ์ฉ์๋ก ๋ค๋ฅธ ์ฌ์ฉ์์ ๋ชจ๋ ๋๊ธ/๋ต๊ธ์ ์ญ์ ํ ์ ์์ง๋ง, ์์ ์ ๋ถ๊ฐํ๋ค.
1. ์ฝ๋ฉ์ ์์ ์ด ์๋ ๋ฌธ์ ํด๊ฒฐ์ ์๋จ
โ๋๊ธ๊ณผ ๋ต๊ธ์ด ๊ฐ๋ฅํ๊ฒ ํด์ฃผ์ธ์.โ ๊ณผ์ ์ ์๊ตฌ์ฌํญ์ ๊ฐ๋จํ์ง๋ง, ๋๋ ์ฌ๊ธฐ์ โ๋ฌดํ Depth ๋๋๊ธโ์ ๊ตฌํํด๋ณด๊ณ ์ถ์๋ค. ๊ตฌํํ๊ธฐ ๊ฐ์ฅ ์ด๋ ค์ธ ๊ฒ ๊ฐ์๊ธฐ ๋๋ฌธ์ด์๋ค.
๊ทธ๋์ ๋๊ธ ํ ์ด๋ธ์ parent_id ์ปฌ๋ผ์ ์ถ๊ฐํ๊ณ , ํด๋น ๊ฒ์๊ธ์ ํฌํจ๋ ๋๊ธ์ ๊ฐ์ ธ์จ ๋ค ์ ํ๋ฆฌ์ผ์ด์ ๋จ์์ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ๋ง๋ ํ ์ฌ๊ท์ ์ผ๋ก ํ์ํ์ฌ ๋๋๊ธ์ ๊ตฌํํ๋ค.
Map<Integer, List<CommentResponse>> commentTreebyParentId = comments.stream()
.collect(Collectors.groupingBy(
rep -> rep.replyParentId() == null ? TREE_ROOT : rep.replyParentId(),
LinkedHashMap::new,
Collectors.toList()
));
private static void visitCommentTree(List<CommentTreeResponse> treeResult, Map<Integer, List<CommentResponse>> byParentId, List<CommentResponse> comments, int depth) {
if (comments == null) return; // ๋ค์ ๋
ธ๋ ์์ผ๋ฉด ์ข
๋ฃ
comments.forEach(each -> {
treeResult.add(CommentTreeResponse.of(each, depth));
visitCommentTree(treeResult, byParentId, byParentId.get(each.commentId()), depth + 1);
});
}
๊ทธ๋ ๊ฒ ๊ธฐ๋ฅ์ ์์ฑํ ํ, "๋ฌดํ Depth ๋ก ๋๊ธ์ ๋ง๋ค์๋๋ฐ ๊ด์ฐฎ๊ฒ ์ฃ ?" ๋ผ๊ณ ๋ฌผ์ด๋ดค๋ค.
โ์ด ๊ฒ์ํ์ ๋ฌดํ Depth์ ๋๋๊ธ์ด ์ ๋ง ํ์ํ๊ฐ์?โ. ๊ทธ์ ์์ผ ๋๋ 'ํ๊ณ ์ถ์ ๋๋ก' ๊ตฌํํ์ง, ์ค์ ๋ก ํ์ํ ๊ฒ์ ๊ตฌํํ์ง๋ ์์๋ค๋ ๊ฒ์ ๊นจ๋ฌ์๋ค.
์๊ตฌ์ฌํญ์ด ๋ชจํธํ๋๋งํผ ์์จ์ฑ์ด ์ปธ์ง๋ง, ๋๋ "์๊ตฌ์ฌํญ์ด ๋ณ๊ฒฝ๋๋๋ผ๋ ์ ์ฐํ๊ฒ ๋ณ๊ฒฝ๋๋ ์ฝ๋"๊ฐ ์๋ ํก์ฌ "ํ๊ต ๊ณผ์ "๋ฅผ ๋ง๋ค์๊ตฌ๋๋ฅผ ๊นจ๋ฌ์๋ค. ์ฆ, ๊ณผ์ ๋ฅผ ์ํ๊ธฐ ์ํด ๊ฐ์ฅ ์ด๋ ค์ด ๊ฒ์ ํํ ๊ฒ์ด์๊ณ , ๋ณธ์ง์ ์๊ณ ์์๋ค.
์๊ตฌ์ฌํญ์ผ๋ก โ๋ต๊ธ์ด 2 depth์์ผ๋ฉด ์ข๊ฒ ์ด์โ๋ก ๋ฐ๋๋ค๋ฉด, ์ด ๋ฌดํ Depth๋ ์ ์ง๋ณด์์ฑ๊ณผ ํ์ ์ธก๋ฉด์์ ๋ถํ์ํ ๊ตฌํ์ด์๋ค.
ํ์ ๋ต๊ธ์ด 1 depth ์ด๋๋ก ๋ฆฌํฉํ ๋ง ํ์๊ณ , depth๊ฐ ๋ ์ปค์ง ์ ์๊ฒ(ํ์ฅ์ฑ์ ๊ณ ๋ คํด) ์ถ๊ฐ๋ก ๋๊ธ ํ ์ด๋ธ์ depth ์ปฌ๋ผ์ ์ถ๊ฐํ๋ฉฐ ๋ง๋ฌด๋ฆฌ ํ๋ค. ์ฝ๋ฉ์ ์์ ์ด ์๋ ๋ฌธ์ ํด๊ฒฐ์ ์๋จ์ด์๊ณ , ๋์ ๋ง์๊ฐ์ง์ ๋ฐ๊พธ๊ฒ ๋ ๊ฒฝํ์ด์๋ค.

2. ๊ธฐ์ด ์ฒด๋ ฅ CS
ํ์ฌ ๋ด๋ถ๋ง์ ๊ฒ์ํ์ ๋ฐฐํฌํ๋ฉด์ ์ฌ๋ฌ ์ค๋ฅ๋ฅผ ํด๊ฒฐํ๊ณ , ๋ด๋ถ๋ง ๊ตฌ์กฐ์ ๋ํด ์ค๋ช ์ ๋ฃ๋ ๊ณผ์ ์์ ๋คํธ์ํฌ ์ง์์ด ํฐ ๋์์ด ๋์๋ค. ๋ํ, ํ์ฌ์ ์ฃผ๋ ฅ ์ ํ์ด Elasticsearch ๊ธฐ๋ฐ์ด๋ผ ์ด๋ฅผ ํ์ต ์ค์ธ๋ฐ, Elasticsearch์ JVM ํ ๋ฉ๋ชจ๋ฆฌ/GC ํ๋, ๋์คํฌ ๊ตฌ์กฐ, ๊ณ ๊ฐ์ฉ์ฑ์ ์ํ ํด๋ฌ์คํฐ ๊ตฌ์ฑ ๋ฑ ๊ธฐ์ ์ ์์ฉํ๋ ค๋ฉด ์ข ํฉ์ ์ธ CS ์ง์์ด ์๊ตฌ๋์๋ค. ์ด ๊ณผ์ ์์, ํ์ผ์์คํ ์ ๋ํด ๋ถ์กฑํจ์ ๋๊ปด ๊ณต๋ถํ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๊ฒ ๋์๋ค.
3. ๊ฐ๋ ๊ฐ์ฒด๋ ๋ฌด์์ธ๊ฐ?
ํ ํ๋ฉด(๋์์ 1)์ ๊ตฌ์ฑํ๋ค ๋ณด๋ ์ ์ ์ฝ๋๊ฐ ๋ณต์กํด์ก๋ค. ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ ์ฝ๋๋ก ํํํ๋ฉด์๋, ์ ์ง๋ณด์ ๊ฐ๋ฅํ ๊ตฌ์กฐ๋ก ๋ง๋ค๊ณ ์ถ์๋ค. ๊ทธ๋์ ์ ๋ฏธ๋๋์ โ์ํํธ์จ์ด๋ฅผ ์ง์ ์ฑ์ฅ์ํค๋ ๋ฐฉ๋ฒโ์, '๊ฐ๋ ๊ฐ์ฒด' ๋ฅผ ๋์ ํ๊ณ ๋ถํ์ํ import ๋ฅผ ์ ๊ฑฐํ๋ ค ๋ ธ๋ ฅ ํ์ง๋ง ์ ์๋ฟ์ง ์์๋ค. ํนํ ๋ทฐ(View) ์์ญ์ด ๋ณต์กํ ๋ ๊ฐ๋ ๊ฐ์ฒด๋ฅผ ์ด๋ป๊ฒ ํ์ฉํด์ผ ํ๋์ง๊ฐ ์ ๋ ์ค๋ฅด์ง ์์๋ค. ์์ผ๋ก๋ ๊ณ์ ๊ณต๋ถํ๋ฉด์, ์ธ์ ๊ฐ ๊ฐ๋ ๊ฐ์ฒด์ฒ๋ผ ์ ์ง๋ณด์์ ํ์ฅ์ด ์ฌ์ด, ์ง์์ ์ผ๋ก ์ฑ์ฅํ ์ ์๋ ์ฝ๋๋ฅผ ๋ง๋ค ์ ์๊ธฐ๋ฅผ ๋ฐ๋๋ค.
4. Maven ๋น๋ ํด ํ์ต
์ด์ ์ ์งํํ๋ ํ๋ก์ ํธ์์๋ Gradle์ ์ฌ์ฉํด Rest Docs๋ฅผ ๋ฐฐํฌํ ๊ฒฝํ์ด ์์ด โ๋ Rest Docs ํ ์ค ์์!โ๋ผ๊ณ ์๊ฐํ์ง๋ง, ๋ง์ Maven์ผ๋ก ํ๋ ค๋ ๋ฏ์ค์๋ค. ๊ทธ๋์ ์ด๋ฒ์๋ Maven ๋ฌธ๋ฒ๊ณผ Asciidoctor ๋ฌธ๋ฒ์ ๊ณต์ ๋ฌธ์ ์ค์ฌ์ผ๋ก ํ์ตํ๋ค. ๊ฐ์์ ์์กดํ์ง ์๊ณ , ์ง์ ์ค์ ํ๊ณ ๊ณต์๋ฌธ์๋ฅผ ์ฝ๊ณ ์ ์ฉํ๊ธฐ ์ํด ๋ ธ๋ ฅํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ Maven pom์์ฑ๊ฐ๋ค๊ณผ Asciidoctor ์ ๋ํด ํ์ํ ๊ฐ๋ค์ custom ํ ์ ์๊ฒ ๋์๋ค.
5. ํ์ด์ง ๊ฐ์ฒด ๊ตฌํํ๊ธฐ
์์ JDBC Template์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ํ์ด์ง ์ ๋ณด๋ฅผ ๋ด๋ Page ๊ฐ์ฒด๋ฅผ ์ง์ ๊ตฌํํด์ผ ํ๊ณ , ์๋์ ๊ฐ์ ์ฝ๋๊ฐ ์ฒ์ ๋ฒ์ ์ด์๋ค.
public class Page
public static Page of(int totalPages, int currentPage) {
if (totalPages == 0) {
return empty(); ๋น์ฆ๋์ค ์๊ตฌ์ฌํญ์ด ๋ด๋ถ์ ์จ๊ฒจ์ ธ ์์
}
return new Page(
...
);
}
private static Page empty(){
...
}
์ฒ์์ ์ด ๋ฐฉ์์ด ๊ฐ๋จํด ๋ณด์์ง๋ง, ๋ฌธ์ ๋ Page๋ฅผ ์์ฑํ๋ ์ชฝ(Service)์์ Page.of(0, 0)์ฒ๋ผ '0'์ด๋ผ๋ ๋งค์ง ๋๋ฒ๋ฅผ ์ง์ ๋ค๋ค์ผ ํ๋ค๋ ์ ์ด๋ค. ํ์ง๋ง ์ด '0'์ ์๋น์ค์ ์ฑ
์์ด ์๋์๋ค.
โํ์ด์ง๊ฐ ์๋คโ๋ ์๋ฏธ๋ Page ๊ฐ์ฒด์ ์ฑ
์์ด์ด์ผ ํ๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ๋์ ๋ฆฌํฉํ ๋ง์ ํตํด Page.empty() ๋ฉ์๋๋ฅผ ๋ณ๋๋ก ์ ์ํ๊ณ , ๋ด๋ถ์ ์์ NO_PAGE๋ฅผ ์ ์ธ์ ํตํด SRP๋ฅผ ์ค์ํ๊ณ ์ ๋
ธ๋ ฅํ๋ค.
private static final int NO_PAGE = 0;
public static Page empty() {
return new Page(NO_PAGE, NO_PAGE, false, false, NO_PAGE, NO_PAGE, false, false);
}
public static Page of(int totalPages, int currentPage) {
return new Page(
totalPages,
currentPage,
hasPrevPage(currentPage), // '<' ๋ชจ์
hasNextPage(totalPages, currentPage), // '>' ๋ชจ์
getStartPageOfCurrentBlock(currentPage), // 10n + 1 ํ์ด์ง
getLastPageOfCurrentBlock(totalPages, currentPage), // 10(n+1)ํ์ด์ง
hasPrevBlock(currentPage), // '<<' ๋ชจ์
hasNextBlock(totalPages, currentPage)); // '>>' ๋ชจ์
}
6. ๊ทธ ์ธ์ ๊ฒ๋ค
JavaScript๋ฅผ ๊ณต๋ถํ๋ฉด์ ์์ฐ์ค๋ฝ๊ฒ HTML ๊ตฌ์กฐ์๋ ์ต์ํด์ก๊ณ , ์ ๋ฐ์ ์ผ๋ก ๊ฐ๋ฐ์ ๋๊ตฌ ์ฌ์ฉ์๋ ์ต์ํด์ก๋ค. ์ด๋ฏธ ํ๋์ ์ธ์ด๋ฅผ ์๊ณ ์๋ ์ํ์์ ์๋ก์ด ์ธ์ด๋ฅผ ๋ฐฐ์ฐ๋ ๊ฒ์ด ํฌ๊ฒ ์ด๋ ต์ง๋ ์๋ค๋ ์ ๋ ์ฒด๊ฐํ ์ ์์๋ค. ๋ค๋ง, ์ค๋ณต ์์ด ๊ฐ๋ ์ฑ ์ข์ JavaScript๋ฅผ ์์ฑํ๊ธฐ๊น์ง๋ ์์ง ๊ฐ ๊ธธ์ด ๋ฉ๋ค.
Security์ ๋ด๋ถ ๋์ ์๋ฆฌ๋ฅผ ์ดํดํ ๋ค ์ฌ์ฉํ๊ณ ์ถ์์ง๋ง, ์๊ฐ์ด ๋ถ์กฑํด ์ผ๋จ์ ํ์ํ ๋ถ๋ถ๋ง ์ฐพ์์ ์ ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค. ๋ด๋ถ ๋์ ์๋ฆฌ๋ฅผ ์ค์ํ๊ฒ ์๊ฐํ๊ณ ์ด๋ฅผ ์๊ณ ์ ํ๋๋ฐ, ์ด๋ฒ์๋ ์๊ฐ์ด ๋ถ์กฑํด์ ๊ตฌํ์ ๋จผ์ ํด์ผํ๋ค. ๋ด๋ถ ์๋ฆฌ๋ฅผ ์๊ณ ์ฌ์ฉํ๋ฉด ์ฝ๋์ ํ์ง์ด ๋ฌ๋ผ์ง๋ค๊ณ ์๊ฐํ์ง๋ง ๊ฐ๋ฐ ์๋๊ฐ ๋๋ ค์ง๋ค. ์ญ์ ๊ฐ๋ฐ์ trade-off ์ ์ฐ์์ด๋ค.
์์ JdbcTemplate์ ์ฌ์ฉํ๋ฉฐ DB ํ ์ด๋ธ์ ํ๋๋ช ์ด ๋ฐ๋ ๋๋ง๋ค ๋ชจ๋ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ ํด์ผ ํ๋ ๋ถํธํจ์ด ์์๋ค.
๊ฒ์ํ์ ๊ตฌํํ๋ฉด์ ๋ง์ ๊ณ ๋ฏผ์ ํ๊ณ , ๊ทธ ๊ณผ์ ๋ค์ ๊ธ์ ๋ด๊ณ ์ถ์์ง๋ง ๋ค ๋ด์ง ๋ชปํด ์์ฌ์๋ ๋จ๊ณ , ๋ง์ ์ฐ๊ณ ๋๋ ๋ณ ๊ฒ ์๋๊ฒ ๊ฐ๊ธฐ๋ ํ๋ค.. ใ ๊ทธ๋๋ ์ ๋ฆฌ๋ผ๋ ํด์ผ ๋์ค์ ํํ๋ฅผ ์ํจ์ ์๊ธฐ์ ์ ๋ฆฌ~