devkobe24.com
AWS
Algorithm
2024
Architecture
Archive
AWS_archive
CPP_DS
CS_archive
DataStructure
Database
HackTheSwift
Java_archive
Leet-Code
MySQL
Network_archive
OS
Post
Read English Book
SQL_archive
Spring & Spring Boots
TIL
Web
Backend Development
CS
2024
2025
Code Review
DB
Data Structure
Development tools and environments
English
Interview
Java
Java多識
Java
Math
Network
2024
Others
SQL
2024
Server
Spring
Troubleshooting
Home
Contact
Copyright © 2024 |
Yankos
Home
> Backend Development
Now Loading ...
Backend Development
📚[Backend Development] Covering Index
“📚[Backend Development] Covering Index.” 📝 Intro Covering Index(커버링 인덱스)는 쿼리 실행 시, 인덱스만으로 필요한 데이터를 모두 충족하는 경우를 의미합니다. 즉, 테이블(원본 데이터)에 접근하지 않고 인덱스만으로 결과를 가져올 수 있는 인덱스입니다. ✅1️⃣ Covering Index의 핵심 개념. 1️⃣ 인덱스 스캔(Index Scan)만으로 필요한 모든 데이터를 조회. 일반적으로 인덱스를 사용해도 일부 컬럼만 검색한 후, 추가적으로 테이블에서 데이터를 가져와야 하는 경우가 있음. 하지만 Covering Index는 필요한 모든 데이터가 인덱스에 포함되어 있어, 테이블 조회를 생략할 수 있음. 2️⃣ 쿼리 성능 최적화. 디스크 I/O 감소 : 테이블을 읽지 않으므로 I/O 비용 절감. 쿼리 속도 향상 : 인덱스만 조회하면 되므로 실행 속도가 빨라짐. Random I/O 감소 : 테이블 데이터 접근이 필요 없으므로 불필요한 I/O를 최소화함. ✅2️⃣ Covering Index 동작 방식. 💎 예제 테이블 (MySQL 기준): CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100), email VARCHAR(100), age INT, city VARCHAR(100) ); 💎 일반적인 인덱스 사용 (테이블 조회 필요) SELECT name FROM users WHERE age = 30; age 컬럼에 인덱스가 있어도, name 컬럼을 가져오기 위해 테이블 조회(데이터 페이지 접근)가 필요함. 💎 Covering Index 사용 CREATE INDEX idx_users_age_name ON users (age, name); 인덱스(idx_users_age_name)가 age와 name 컬럼을 모두 포함하므로, 테이블을 조회할 필요 없음. 인덱스에서 직접 데이터를 가져오므로 쿼리 속도가 크게 향상됨. ✅3️⃣ Covering Index 확인 방법 (MySQL) 쿼리가 Covering Index를 사용하는지 확인하려면 EXPLAIN 명령어를 사용하면 됩니다. EXPLAIN SELECT name FROM users WHERE age = 30; 📌 Extra 컬럼에 “Using index”가 표시되면 Covering Index가 적용된 것 입니다. 🚀 결론 ✅ Covering Index는 테이블을 조회하지 않고, 인덱스만으로 데이터를 가져올 수 있는 최적화 기법 ✅ 디스크 I/O와 Random I/O를 줄여 성능을 크게 향상시킬 수 있음 ✅ 인덱스 크기가 커질 수 있으므로, 적절한 컬럼만 포함하여 생성하는 것이 중요.
Backend Development
· 2025-03-20
📚[Backend Development] 대표적인 인덱스 자료구조
“📚[Backend Development] SQL에서 INDEX” 📝 Intro. 인덱스는 데이터베이스에서 검색 성능을 최적화하기 위해 사용되며, 데이터의 특성과 쿼리 유형에 따라 다양한 자료구조가 활용됩니다. ✅1️⃣ B+ Tree (Balanced Plus Tree) ✅ 개념 대부분의 관계형 데이터베이스(RDBMS)에서 기본적으로 사용하는 인덱스 구조. B-Tree의 확장형으로, 리프 노트(Leaf Node)에만 데이터를 저장하며 범위 검색이 빠름. 균형 트리(Balanced Tree) 구조로, 검색,삽입,삭제 연산이 $O(log N)$. 순차적 검색(ORDER BY, RANGE QUERY)에 최적화됨. ✅ 특징 📌 검색, 삽입, 삭제 모두 $O(log N)$ 📌 ORDER BY, BETWEEN 검색이 빠름 📌 데이터가 정렬된 상태로 유지됨 ✅ 사용 예시 CREATE INDEX idx_user_name ON users(name); SELECT * FROM users WHERE name LIKE 'A%'; LIKE ‘A%’ 같은 접듀서 검색(범위 검색) 시 최적화됨. ✅ 사용 DBMS 📌 MySQL (InnoDB) 📌 PostgreSQL 📌 Oracle 📌 SQL Server ✅2️⃣ Hash Index ✅ 개념 정확한 값 검색(Equality Search)에 최적화된 해시 테이블 기반 인덱스. WHERE column = ‘value’ 같은 조건에 최적화. 순차적 검색이나 범위 검색을 지원하지 않음. ✅ 특징. 📌 검색이 O(1)에 가까움 (해시 충돌이 없을 경우) 📌 WHERE 절의 = 연산에 최적 📌 ORDER BY, 범위 검색 불가능 ✅ 사용 예시 CREATE INDEX idx_email_hash USING HASH ON users(email); SELECT * FROM users WHERE email = 'user@example.com'; 이메일 주소 검색 같은 정확한 값 조회에 적합. ✅ 사용 DBMS 📌 MySQL (MEMORY Engine) 📌 PostgreSQL 📌 Redis 📌 DynamoDB ✅3️⃣ LSM Tree (Log-Structured Merge Tree) ✅ 개념 쓰기 성능 최적화를 위한 NoSQL 및 시계열 데이터베이스(TSDB)에서 많이 사용됨 데이터를 메모리에 먼저 저장 후, 일정 크기가 되면 디스크에 병합(Merge)하는 방식. 읽기 성능이 상대적으로 낮지만, 쓰기(Writes) 성능이 뛰어남. ✅ 특징 📌 쓰기 성능이 뛰어남 (배치 삽입/삭제 최적화) 📌 NoSQL, 로그 데이터, 시계열 데이터에 최적 📌 랜덤 읽기가 느리면, 병합(Compaction) 비용 발생 ✅ 사용 예시 Cassandra, RocksDB, LevelDB에서 기본 인덱스로 사용됨 ✅ 사용 DBMS 📌 Apache Cassandra 📌 RocksDB 📌 LevelDB 📌 Amazon DynamoDB ✅4️⃣ R-Tree (Rectangle Tree) ✅ 개념 공간(Spatial) 데이터 검색에 최적화된 트리 구조 위도/경도, 좌표 정보(GIS 데이터)를 검색할 때 주로 사용됨 2D, 3D 범위 검색(WHERE lat BETWEEN … AND lon BETWEEN …)에 유리. ✅ 특징 📌 위치 기반 검색(GIS) 최적화 📌 범위 검색(RANGE QUERY) 가능 📌 이진 트리가 아닌 공간 분할 트리 구조 ✅ 사용 예시 CREATE INDEX idx_location ON locatioons USING GIST (geom); SELECT * FROM locations WHERE ST_Within(geom, ST_GeomFromText('POLYGON((...))')); PostGIS, Oracle Spatial에서 사용됨 ✅ 사용 DBMS 📌 PostgreSQL (PostGIS) 📌 Oracle Spatial 📌 MySQL (GIS 가능) ✅5️⃣ Bitmap Index ✅ 개념 중복 값이 많은 컬럼(성별, 상태값 등)에 최적화된 인덱스. 각 값에 대해 비트맵(Bit Map)으로 저장하여 검색 속도를 향상. 데이터 웨어하우스, OLAP(Online Analytical Processing)에서 주로 사용. ✅ 특징 📌 중복이 많은 데이터에 최적 📌 저장 공간이 적게 들고, 빠른 검색 가능 📌 쓰기(INSERT/UPDATE/DELETE) 성능이 낮음 ✅ 사용 예시 CREATE BITMAP INDEX idx_status ON orders(status); SELECT * FROM orders WHERE status = 'completed'; 상태(status) 컬럼처럼 중복이 많은 데이터 검색 시 빠름. ✅ 사용 DBMS 📌 Oracle 📌 PostgreSQL 📌 ClickHouse ✅6️⃣ Skip List ✅ 개념 연결 리스트 기반 인덱스 구조로, 정렬된 데이터를 빠르게 검색. Redis의 Sorted Set(순위 랭킹)에서 사용됨. ✅ 특징 📌 B+Tree보다 간단한 구조 📌 읽기/쓰기 속도가 균형적 📌 Redis에서 순위 기반 랭킹(Key-Value 저장소)에 사용 ✅ 사용 예시 (Redis) redisTemplate.opsForZSet().add("ranking", "user1", 100); redisTemplate.opsForZSet().add("ranking", "user2", 150); 점수 기반 랭킹을 빠르게 검색 가능. ✅ 사용 DBMS 📌 Redis 📌 Apache Ignite ✅7️⃣ 결론 ✅ 관계형 데이터베이스(RDBMS)에서 가장 많이 사용하는 인덱스 ➞ B+ Tree ✅ 빠른 키-값 조회(Hash 검색)에 적합한 인덱스 ➞ Hash Index ✅ 쓰기 성능 최적화 (NoSQL, 로그 데이터) ➞ LSM Tree ✅ 공간 데이터(GIS, 위치 정보) 검색 ➞ R-Tree ✅ 중복 데이터(성별, 상태값) 검색 최적화 ➞ Bitmap Index ✅ 순위 랭킹, Redis Sorted Set ➞ Skip List
Backend Development
· 2025-03-19
📚[Backend Development] 가장 중요한 두 가지 인덱스 유형.
“📚[Backend Development] 가장 중요한 두 가지 인덱스 유형.” 📝 Intro 데이터베이스에서 Index는 검색 성능을 최적화하는 핵심 요소입니다. 그 중에서 Clustered Index(클러스터형 인덱스)와 Secondary Index(보조 인덱스, Non-Clustered Index)는 가장 중요한 두 가지 인덱스 유형입니다. ✅1️⃣ Clustered Index (클러스터형 인덱스) ✅ 개념 테이블의 데이터를 물리적으로 정렬하는 인덱스. 한 테이블에 하나만 존재할 수 있음. 기본 키(Primary Key) 에 자동으로 생성됨. 데이터 자체가 인덱스 트리(B+ Tree) 구조로 저장됨. ✅ 특징 ✅ 데이터 자체가 정렬된 형태로 저장됨. ✅ Primary Key(기본 키)에 자동 생성됨. ✅ 범위 검색(BETWEEN, ORDER BY)이 빠름. ✅ 테이블당 하나만 존재. ✅ 예제 CREATE TABLE users ( id INT PRIMARY KEY, -- 자동으로 Clusterd Index 생성 name VARCHAR(100), age INT ); 위 테이블에서 id는 기본 키(Primary Key) 이므로 자동으로 Clustered Index가 생성됩니다. 즉, 테이블의 데이터가 id 값을 기준으로 정렬된 상태로 저장됩니다. ✅2️⃣ Secondary Index (보조 인덱스, Non-Clustered Index) ✅ 개념 Clustered Index와 별개로 추가적인 검색 속도를 높이기 위해 사용하는 인덱스. 데이터와 별도로 저장되며, Clustered Index의 값(Primary Key)을 참조. 한 테이블에 여러 개 생성 가능. ✅ 특징 ✅ 데이터 정렬에는 영향을 주지 않음 ✅ 테이블당 여러 개 생성 가능 ✅ Clustered Index를 기반으로 추가적인 검색 속도 향상 ✅ 범위 검색보다는 특정 값 검색(WHERE 조건)에 적합 ✅ 예제 CREATE INDEX idx_users_name ON users(name); name 컬럼에 보조 인덱스(Secondary Index)를 생성하여 이름 검색 속도를 최적화. ✅3️⃣ Clustered Index vs Secondary Index 비교 구분 Clustered Index Secondary Index (Non-Clustered Index) 정렬 방식 데이터 자체가 인덱스 트리에 정렬됨 데이터 정렬에 영향을 주지 않음 저장 방식 데이터 자체가 인덱스 노드에 저장됨 인덱스에 Primary Key를 저장 후 데이터 참조 속도 범위 검색(BETWEEN, ORDER BY) 최적 특정 값 검색(WHERE) 최적 개수 한 테이블에 하나만 존재 여러 개 존재 가능 예제 PRIMARY KEY (id) CREATE INDEX idx_name ON users(name) ✅4️⃣ Clustered Index & Secondary Index 검색 과정. 📌 Clustered Index 검색 과정 SELECT * FROM users WHERE id = 100; Clustered Index는 데이터 자체가 정렬된 상태이므로, id = 100을 바로 찾을 수 있음. B+ Tree에서 한 번의 검색으로 데이터까지 도달 → 빠른 조회 속도. 📌 Secondary Index 검색 과정 SELECT * FROM users WHERE name = 'Alice'; name 컬럼은 Secondary Index이므로, 먼저 보조 인덱스를 검색한 후, Primary Key 값을 찾아 Clustered Index에서 데이터 검색. “Secondary Index ➞ Clustered Index” 두 단계 검색 과정이 필요하여 Clustered Index보다 속도가 약간 느림. ✅5️⃣ 언제 Clustered Index & Secondary Index를 사용해야 할까? ✅ Clustered Index 추천 PRIMARY KEY와 같이 데이터 정렬이 중요한 경우. ORDER BY, BETWEEN, RANGE QUER를 자주 사용해야 하는 경우. ✅ Secondary Index 추천 특정 컬럼을 WHERE 조건으로 자주 검색해야 할 때. JOIN 또는 GROUP BY 연산이 많은 경우. 보조적인 검색 속도를 높이고 싶을 때. 📌 결론. ✅ Clustered Index는 데이터 자체를 정렬하여 저장하며, Primary Key에 자동으로 생성됨 ✅ Secondary Index는 추가적인 검색 최적화를 위해 사용되며, 테이블당 여러 개 생성 가능 ✅ 범위 검색(ORDER BY, BETWEEN)은 Clustered Index가 유리 ✅ 특정 컬럼 검색(WHERE 조건)은 Secondary Index가 유리.
Backend Development
· 2025-03-19
📚[Backend Development] SQL에서 INDEX
“📚[Backend Development] SQL에서 INDEX” 📝 Intro. 인덱스(INDEX)는 데이터베이스에서 검색 성능을 최적화하기 위해 사용하는 자료구조입니다. 마치 책의 목차(Table of Contents)처럼, 테이블에서 데이터를 더 빠르게 찾을 수 있도록 돕는 기능입니다. ✅1️⃣ 인덱스의 역할 ✅ 검색 속도 향상 ➞ WHERE, ORDER BY, JOIN 시 빠르게 데이터 조회 가능 ✅ 데이터 정렬 최적화 ➞ ORDER BY 연산 시 정렬 속도 향상 ✅ 중복 방지 ➞ UNIQUE INDEX를 사용하여 중복 데이터 삽입 방지 📝 예제 (인덱스가 없는 경우) SELECT * FROM users WHERE email = 'example@email.com'; 데이터베이스는 모든 행을 하나씩 검사(Full Table Scan) 해야 함 데이터가 많을수록 검색 속도가 느려짐 📝 예제 (인덱스가 있는 경우) CREATE INDEX idx_email ON users(email); SELECT * FROM users WHERE email = 'example@email.com'; 이진 탐색(Binary Search)을 통해 빠르게 검색 가능 Full Table Scan을 방지하고 인덱스를 활용하여 검색 속도 향상 ✅2️⃣ 인덱스의 종류 1️⃣ 기본(B-Tree) 인덱스 가장 일반적인 인덱스, B-Tree(Balanced Tree) 구조 사용 검색, 정렬, 범위 조회에 최적화 CREATE INDEX idx_name ON users(name); 2️⃣ UNIQUE 인덱스 중복방지를 위한 인덱스 (중복된 값 삽입 불가) CREATE UNIQUE INDEX idx_unique_email ON users(email); INSERT INTO users (id, email) VALUES (1, 'test@email.com'); INSERT INTO users (id, email) VALUES (2, 'test@email.com'); -- ❌ 오류 발생 (중복) 3️⃣ 복합(Composite) 인덱스 두 개 이상의 컬럼을 결합하여 인덱스를 생성 검색 조건이 여러 개일 때 유용 CREATE INDEX idx_name_email ON users(name, email); SELECT * FROM users WHERE name = 'Alice' AND email = 'alice@email.com'; 4️⃣ FULLTEXT 인덱스 (MySQL 전용) 긴 텍스트 데이터(TEXT, VARCHAR)에서 단어 검색이 필요할 때 사용 LIKE '%keyword%'보다 훨씬 빠른 검색 가능 CREATE FULLTEXT INDEX idx_content ON posts(content); SELECT * FROM posts WHERE MATCH(content) AGAINST ('database'); 5️⃣ HASH 인덱스 정확한 일치검색(Equality Search)에 최적화됨 범위 검색에는 적합하지 않음 CREATE INDEX idx_hash_email USING HASH ON users(email); SELECT * FROM users WHERE email = 'test@email.com'; -- ✅ 빠름 SELECT * FROM users WHERE email = LIKE 'test%'; -- ❌ 느림 (HASH 인덱스는 범위 검색 지원 안 함) ✅3️⃣ 인덱스의 성능 고려 사항 ✅ 인덱스는 빠른 검색을 제공하지만, 무조건 많이 만든다고 좋은 것은 아닙니다. ✅ 인덱스가 많을수록 삽입(INSERT), 수정(UPDATE), 삭제(DELETE) 연산 속도가 느려집니다. ✅ 자주 사용하는 조회 쿼리에 맞춰 필요한 인덱스만 생성하는 것이 중요합니다 ✅4️⃣ 인덱스 최적화 및 활용 📌 실행 계획(EXPLAIN) 확인 인덱스를 잘 활용하는지 확인하려면 EXPLAIN 명령어를 사용하면 됩니다. EXPLAIN SELECT * FROM users WHERE email = 'test@email.com'; Using index ➞ 인덱스를 사용하여 최적화된 쿼리 Using full table scan ➞ 테이블 전체 검색 (느림) ✅5️⃣ 결론 ✅ 인덱스는 데이터 검색 속도를 최적화하는 핵심 도구 ✅ 너무 많은 인덱스는 오히려 성능을 저하시킬 수 있음 ✅ 조회 성능을 높이려면 EXPLAIN을 사용하여 인덱스 최적화
Backend Development
· 2025-03-18
📚[Backend Development] 대규모 데이터에서 게시글 목록 조회가 복잡한 이유
“📚[Backend Development] 대규모 데이터에서 게시글 목록 조회가 복잡한 이유 📝 Intro 대규모 데이터에서 게시글 목록 조회가 복잡한 이유는 여러 가지가 있습니다. 일반적으로 소규모 데이터베이스에서는 단순한 SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 같은 쿼리로 쉽게 해결할 수 있지만, 수백만~수억 개의 데이터를 다뤄야 하는 시스템에서는 여러 복잡한 문제가 발생합니다. ✅1️⃣ 대규모 데이터에서 게시글 목록 조회가 복잡한 이유 1️⃣ 데이터의 양이 방대함 게시글이 많아질수록 ORDER BY와 LIMIT 연산의 부담이 커짐 인덱스를 사용하더라도 디스크의 I/O 비용이 증가하고 캐싱이 어렵게 됨 📝 예제 쿼리 SELECT * FROM posts ORDER BY created_at DESC LIMIT 10; ❌ 문제점 ORDER BY create_at DESC 실행 시 모든 게시글을 정렬해야 함 (데이터가 클수록 느려짐) 최신 게시글이 많으면 새로운 데이터가 계속 추가되며 인덱스가 자주 변경됨 2️⃣ 페이지네이션 (Pagination) 성능 저하 게시판은 보통 페이지네이션을 지원해야 합니다. 즉, LIMIT과 OFFSET을 사용해 특정 페이지의 게시글을 조회해야 하는데, 대규모 데이터에서는 OFFSET이 클수록 성능이 저하됩니다. SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 1000000; ❌ 문제점 OFFSET 1000000을 사용하면 앞의 100만 개 데이터를 스캔하고 버린 후 그 다음 10개를 반환합니다. 데이터가 많을수록 불필요한 연산이 많아지고 성능이 급격히 저하됩니다. ✅ 해결 방법 Keyset pagination (Seek 방식) 활용 ➞ OFFSET 없이 WHERE 조건을 활용 LIMIT을 활용해 이전 조회된 마지막 ID를 기준으로 다음 데이터를 가져오기 SELECT * FROM posts WHERE created_at < '2024-03-11 10:00:00' ORDER BY created_at DESC LIMIT 10; 3️⃣ 인덱스 사용 최적화 문제 📌 왜 인덱스만으로 해결되지 않을까? 인덱스를 사용하면 정렬이 빨라지지만, 데이터가 많을 경우에도 여전히 디스크 I/O 비용이 증가 새로운 게시글이 추가될 때마다 B-Tree 인덱스가 갱신되며 성능에 부담을 줌 ✅ 해결 방법 커버링 인덱스 (Covering Index) 활용 ➞ SELECT에 포함된 모든 컬럼을 인덱스에 미리 포함 파티셔닝 (Partitioning) ➞ 날짜별 테이블 분리 (posts_202403 같은 테이블) CREATE INDEX idx_posts_created ON posts(created_at, title, content); 4️⃣ 트래픽 분산 문제 게시글 목록은 대부분 서비스에서 조회 트래픽이 많고, 쓰기 트래픽도 꾸준히 발생하는 구조입니다. 즉, 읽기(READ)가 많지만, 동시에 새로운 게시글이 추가되며 정렬 순서가 계속 바뀝니다. ❌ 문제점 캐시 사용이 어렵다 ➞ 새로운 게시글이 추가되면 정렬 순서가 바뀌어 기존 캐시 무효화됨 동시성이 증가하면 DB 부하가 커짐 ➞ 많은 유저가 같은 목록을 요청하면 DB 부하 증가 ✅ 해결 방법 1️⃣ 캐싱 (Redis, Memcached) 활용 최신 게시글 목록을 Redis에 저장하고, 데이터가 변경될 때만 업데이트. redisTemplate.opsForValue().set("latest_posts", posts, Duration.ofSeconds(60)); 2️⃣ 읽기 전용 데이터베이스(Read Replica) 활용 Master-Slave Replication을 사용하여 읽기 트래픽을 Slave DB로 분산 SELECT * FROM posts ORDER BY created_at DESC LIMIT 10; -- 이 쿼리를 Slave DB에서 실행하여 Master DB 부하 감소 3️⃣ CQRS 패턴 적용 게시글 조회와 생성/수정을 다른 DB로 분리 (읽기 전용 DB, 쓰기 전용 DB) 5️⃣ JOIN 연산 성능 문제 게시글 목록을 조회할 때 보통 작성자 정보, 좋아요 수, 댓글 수 등을 함께 조회해야 합니다. 즉, JOIN을 수행해야 하는데, 데이터가 많을수록 성능이 저하됩니다. SELECT p.id, p.title, p.content, u.name, COUNT(c.id) as comment_count FROM posts p LEFT JOIN users u ON p.user_id = u.id LEFT JOIN comments c ON p.id = c.post_id GROUP BY p.id, u.name ORDER BY p.created_at DESC LIMIT 10; ❌ 문제점 JOIN을 수행할 때 데이터가 많으면 메모리와 CPU가 필요 GROUP BY를 수행하면 정렬 및 집계 연산이 필요하여 성능이 더 느려짐 ✅ 해결 방법 NoSQL(예: Redis, Elasticsearch) 캐시 활용 ➞ 좋아요 수, 댓글 수는 미리 저장해둠 CQRS 패턴 적용 ➞ 별도 테이블에 미리 계산된 카운트 값을 저장 SELECT p.id, p.title, p.title, u.name, p.comment_count FROM posts p LEFT JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC LIMIT 10; p.comment_count는 미리 계산된 값이므로 JOIN을 줄여 성능을 개선할 수 있습니다. ✅2️⃣ 대규모 게시글 조회 시 최적화 방법 1️⃣ Keyset Pagination 사용 SELECT * FROM posts WHERE created_at < '2024-03-11 10:00:00' ORDER BY created_at DESC LIMIT 10; OFFSET 없이 이전 조회된 마지막 ID를 기준으로 다음 데이터를 가져오는 방식 2️⃣ 캐싱 활용 (Redis, Elasticsearch) redisTemplate.opsForValue().set(CACHE_KEY, posts, Duration.ofSeconds(60)) 최신 게시글을 캐시에 저장하여 DB 부하를 줄임 3️⃣ 읽기 전용 DB(Read Replica) 활용 SELECT * FROM posts ORDER BY created_at DESC LIMIT 10; 읽기 트래픽을 Replica DB로 분산하여 성능 최적화 4️⃣ 미리 계산된 값 사용 SELECT p.id, p.title, p.content, u.name, p.comment_count FROM posts p LEFT JOIN users u ON p.user_id = u.id ORDER BY p.created_at DESC LIMIT 10; 댓글 개수, 좋아요 수 등을 미리 계산된 값으로 저장하여 JOIN 연산 줄이기 ✅3️⃣ 결론 ✅ 대규모 데이터에서 게시글 목록 조회가 복잡한 이유는? 데이터 양이 많아 ORDER BY LIMIT가 비효율적 OFFSET이 클수록 성능 저하 (페이징 문제) JOIN 연산이 많을수록 성능 저하 쓰기 트래픽이 많아지면 인덱스 관리 부담 증가 캐시가 자주 무효와되어 DB 부하가 커짐 ✅ 최적화 방법 1️⃣ Keyset Pagination ➞ OFFSET 대신 마지막 ID 기반 조회 2️⃣ Redis, Elasticsearch 캐싱 ➞ 최신 데이터 미리 저장 3️⃣ 읽기 전용 DB(Read Replica) 활용 ➞ 조회 트래픽 분산 4️⃣ 미리 계산된 데이터 활용 ➞ JOIN 최소화
Backend Development
· 2025-03-17
📚[Backend Development] 페이징 방식.
“📚[Backend Development] 페이징 방식.” 📝 Intro. 대규모 데이터에서 게시글 목록을 조회할 때 효율적인 페이징(Pagination) 처리는 성능 최적화의 핵심입니다. 데이터가 많아질수록 OFFSET 방식의 성능 저하가 발생하므로, Keyset Pagination(Seek 방식) 등의 최적화 기법을 적용하는 것이 중요합니다. ✅1️⃣ 페이징 방식 비교. 대규모 데이터를 조회할 때 사용할 수 있는 대표적인 페이징 방법은 다음과 같습니다. 방식 장점 단점 적용 예제 OFFSET + LIMIT 간단한 구현, SQL 표준 지원 OFFSET이 커질수록 성능 저하 블로그, 게시판 Keyset Pagination (Seek 방식) 성능 최적화, 인덱스 효율적 사용 특정 컬럼(정렬 기준)이 필요 뉴스 피드, 타임라인 Cursor 기반 페이징 유저 맞춤형 데이터 최적화 구현이 복잡 페이스북, 인스타그램 Redis 캐싱 활용 조회 속도 향상 데이터 동기화 문제 발생 가능 인기 게시글 목록 ✅2️⃣ 기본적인 OFFSET + LIMIT 방식 📌 개념 OFFSET을 사용하여 특정 위치부터 LIMIT 개수만큼 데이터를 조회하는 방식 일반적인 게시판에서 많이 사용하지만, 대규모 데이터에서는 OFFSET 값이 커질수록 성능이 저하됨 📝 SQL 예제 SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 10000; OFFSET 10000은 앞의 10,000개 행을 스캔한 후 버리고, 그 다음 10개만 가져옴. 데이터가 많아질수록 성능이 급격히 저하됨 → “페이징이 깊어질수록 느려지는 문제 발생” 📝 Java (Spring Data JPA) 예제 Pageable pageable = PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, "createdAt")); Page<Post> posts - postRepository.findAll(pageable); ❌ 문제점 OFFSET이 크면 불필요한 데이터 검색으로 인해 성능 저하 발생 인덱스가 있어도 데이터를 읽고 버리는 비용(I/O 비용)이 발생 ✅3️⃣ Keyset Pagination (Seek 방식) 📌 개념 OFFSET을 사용하지 않고, 마지막 조회한 게시글의 ID(또는 created_at)을 기준으로 다음 데이터를 가져오는 방식 “마지막 조회된 데이터의 키를 기억하고, 그 이후 데이터를 조회” 📝 SQL 예제 SELECT * FROM posts WHERE created_at < '2024-03-11 12:00:00' ORDER BY created_at DESC LIMIT 10; created_at을 기준으로 이전 페이지의 마지막 게시글 시간보다 작은 데이터만 조회 데이터 양이 많아도 OFFSET이 없으므로 빠르게 조회 가능 📝 Java (Spring Data JPA) 예제 public List<Post> getNextPage(LocalDateTime lastCreatedAt, int limit) { return postRepository.findByCreatedAtBeforeOrderByCreatedAtDesc(lastCreatedAt, PageRequest.of(0, limit)); } lastCreatedAt을 기준으로 다음 게시글을 조회 LIMIT을 적용하여 페이징 처리 ✅ 장점. ✅ OFFSET이 없으므로 속도가 빠름 ✅ 인덱스를 활용하여 빠른 검색 가능 ✅ 데이터가 많아도 성능이 일정하게 유지됨 ❌ 단점. ❌ created_at 또는 id가 반드시 있어야 함 ❌ ORDER BY를 위한 적절한 인덱스 설정 필요 ✅4️⃣ Cursor 기반 페이징. 📌 개념 페이스북, 인스타그램 같은 SNS에서 사용하는 방식 이전 페이지의 마지막 ID(또는 created_at)를 클라이언트에서 저장하고, 이를 이용해 다음 데이터를 조회 Keyset Pagination과 유사하지만, API에서 cursor 값을 반환하고 이를 사용 📝 SQL 예제. SELECT * FROM posts WHERE id > 1050 ORDER BY id ASC LIMIT 10; id가 1050보다 큰 게시글을 가져옴 ORDER BY id ASC를 사용하여 오름차순 정렬 📝 Java (Spring Data JPA) 예제 public List<Post> getNextPosts(Long lastPostId, int limit) { return postRepository.findByIdGreaterThanOrderByIdAsc(lastPostId, PageRequest.of(0, limit)); } 클라이언트에서 이전 페이지의 마지막 id 값을 저장하고 이를 이용하여 다음 데이터를 조회 ✅ 장점 ✅ Keyset Pagination과 마찬가지로 OFFSET 없이 성능 최적화 ✅ 페이스북, 인스타그램 같은 무한 스크롤(Scroll) UI에 적합 ✅ API 응답에서 cursor(마지막 id) 값을 포함하여 다음 요청에 사용 가능 ❌ 단점 ❌ 클라이언트에서 cursor(마지막 id) 값을 저장해야 함 ❌ 특정 필드(id, created_at) 기준으로 정렬해야 하므로 복잡한 정렬이 어려움. ✅5️⃣ Redis를 활용한 캐싱 페이징 📌 개념 인기 게시글, 조회수가 많은 데이터는 DB에서 직접 조회하지 않고 Redis에 저장하여 빠르게 제공 일정 시간마다(예: 5분) 인기 게시글 목록을 업데이트 ZSET(Sorted Set)을 사용하여 정렬된 데이터를 빠르게 조회 📝 Java(Spring + Redis) 예제 @Service public class PostCacheService { private static final String CACHE_KEY = "latest_posts"; @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private PostRepository postRepository; public List<Post> getLatestPosts() { List<Post> cachedPosts = (List<Post>) redisTemplate.opsForValue().get(CACHE_KEY); if (cachedPosts != null) { return cachedPosts; } // 캐시가 없으면 DB에서 조회 후 Redis에 저장 List<Post> posts = postRepository.findTop10ByOrderByCreatedAtDesc(); redisTemplate.opsForValue().set(CACHE_KEY, posts, Duration.ofSeconds(60)); // 60초 캐싱 return posts; } } ✅ 장점 ✅ 인기 게시글을 빠르게 조회 가능 ✅ DB 부하를 줄이고, 응답 속도 향상 ❌ 단점 ❌ 실시간 최신 데이터가 필요할 경우 캐시 동기화 문제가 발생 ✅6️⃣ 최적의 페이징 방식 선택 페이징 방식 성능 장점 단점 사용 사례 OFFSET + LIMIT ❌ 느림 간단한 구현 데이터 많아지면 성능 저하 기본적인 페이지네이션 Keyset Pagination ✅ 빠름 인덱스 최적화, 성능 일정 특정 정렬 필드 필요 뉴스 피드, 블로그 Cursor 기반 ✅ 빠름 SNS, 무한 스크롤 최적 클라이언트에서 cursor 저장 필요 인스타그램, 페이스북 Redis 캐싱 🚀 매우 빠름 인기 게시글 빠른 조회 실시간 데이터 반영 어려움 인기 게시글, 랭킹 ✅7️⃣ 결론 ✅ 대규모 데이터에서 OFFSET 방식은 비효율적 ✅ Keyset Pagination(Seek 방식)이 성능 최적화에 유리 ✅ Cursor 방식은 SNS나 무한 스크롤에 적합 ✅ Redis를 활용하여 캐싱하면 조회 속도를 극대화할 수 있음
Backend Development
· 2025-03-17
📚[Backend Development] 샤딩(Sharding)
“📚[Backend Development] 샤딩(Sharding)” ✅1️⃣ 샤딩(Sharding)이란? 샤딩은 하나의 데이터베이스를 여러 개의 노드(서버)로 나누어 저장하는 기술입니다. 즉, 데이터를 여러 개의 작은 데이터베이스(샤드, Shard)로 분할하여 저장하고, 분산된 데이터베이스를 하나의 시스템처럼 동작하도록 만드는 방법입니다. 💡 샤딩의 목적. ✅ 수평 확장(Scale-Out) : 서버를 추가하여 성능을 확장할 수 있습니다. ✅ 데이터 처리 속도 향상 : 특정 샤드에서만 데이터를 처리하므로 성능 향상. ✅ 부하 분산(Load Balancing) : 트래픽을 여러 서버에 분산할 수 있습니다. ✅2️⃣ 샤딩의 종류. 샤딩 방법에는 여러 가지 방식이 있으며, 대표적으로 다음과 같은 방식들이 있습니다. 1️⃣ 범위 샤딩 (Range Sharding) 📌 개념 데이터를 특정 범위에 따라 나누어 저장하는 방식 예를 들어, user_id 또는 날짜(date)를 기준으로 일정 범위별로 나누는 방법 ✅ 장점 설계가 단순하고 직관적임 특정 범위의 데이터를 조회할 때 빠름 ❌ 단점 특정 샤드에 부하가 집중될 수 있음 (Hotspot 문제) 데이터가 불균형하게 분포될 가능성이 있음 📝 예제 -- Shard 1 (User ID 1 ~ 10000) INSERT INTO users_shard_1 VALUES (1001, 'Alice'); -- Shard 1 (User ID 10001 ~ 20000) INSERT INTO users_shard_2 VALUES (15001, 'Bob') 🙋♂️ 사용 사례 사용자 ID 범위별 샤딩 1~10만번 유저 → Shard 1 10만~20만 유저 ➞ Shard 2 날짜 기준 샤딩 2023년 데이터 ➞ Shard 1 2024년 데이터 ➞ Shard 2 2️⃣ 해시 샤딩 (Hash Sharding) 📌 개념 데이터를 특정 키(예: user_id)에 해시 함수를 적용하여 샤드에 배정하는 방식 예: Shard = Hash(user_id) % 3 (3개의 샤드가 있을 경우) ✅ 장점 균등한 데이터 분배 가능 (Hotspot 문제 해결) 특정 샤드에 데이터가 집중되지 않음 ❌ 단점 특정 샤드에 범위 검색(Range Query)이 어려움 조인(Join) 연산이 어려움 📝 예제 -- 해시 함수 적용 (user_id % 3) INSERT INTO users_shard_1 VALUES (1001, 'Alice'); -- 1001 % 3 = 1번 샤드 INSERT INTO users_shard_2 VALUES (15001, 'Bob'); -- 15001 % 3 = 2번 샤드 INSERT INTO users_shard_0 VALUES (23001, 'Charlie'); -- 23001 % 3 = 0번 샤드 🙋♂️ 사용 사례 랜덤한 데이터 분산이 필요한 경우 (예: 유저 로그인 정보, 세션 데이터) 트래픽 균형 유지가 중요한 시스템 (예: 글로벌 서비스, 결제 시스템) 3️⃣ 리스트 샤딩 (List Sharding) 📌 개념 특정 기준(예: 국가, 지역, 언어 등)에 따라 데이터를 나누어 저장하는 방식 예시 Korea 데이터 ➞ Shard 1 USA 데이터 ➞ Shard 2 ✅ 장점 특정 그룹의 데이터를 빠르게 검색할 수 있음 특정 지역/카테고리에 최적화 가능 ❌ 단점 특정 샤드가 과부하될 가능성이 있음 (예: Korea는 100만 명, Brazil은 10만 명) 📝 예제 -- Korean Users ➞ Shard 1 INSERT INTO users_korea VALUES (1001, 'Alice', 'Korea'); -- USA Users ➞ Shard 2 INSERT INTO users_usa VALUES (2001, 'Bob', 'USA'); 🙋♂️ 사용 사례 국가별 사용자 데이터 저장 (예: 한국 ➞ Shard 1, 미국 ➞ Shard 2) 지역별 상품 데이터 저장 (예: 서울 ➞ Shard 1, 부산 ➞ Shard 2) 4️⃣ 동적 샤딩 (Dynamic Sharding) 📌 개념 사용자의 수요에 따라 자동으로 샤드를 확장하는 방식 기존 샤딩 방법과 달리 사전에 샤드를 미리 정의하지 않음 데이터가 증가하면 자동으로 새로운 샤드 추가 ✅ 장점 데이터 양이 늘어나도 쉽게 확장 가능 (Auto-Scaling) 특정 샤드가 과부하될 경우 자동으로 데이터 재분배 가능 ❌ 단점 데이터 이동 (Rebalancing) 시 오버헤드 발생 구현이 복잡하고, 추가적인 관리 시스템 필요 📝 예제 CockroachDB, Google Spanner, TiDB 같은 시스템에서 자동으로 동적 샤딩을 지원 🙋♂️ 사용 사례 클라우드 기반 확장형 데이터베이스 (예: AWS Aurora, Google Spanner) 데이터가 빠르게 증가하는 대규모 시스템 ✅3️⃣ 결론 ✅ 샤딩은 시스템 성능 향상과 확장성을 위한 필수 기술 ✅ 해시 샤딩은 균등한 데이터 분배에 유리하지만, 범위 검색이 어려움 ✅ 범위 샤딩은 특정 범위 검색에 유리하지만, Hotspot 문제가 발생할 수 있음 ✅ 클라우드 기반에서는 동적 샤딩이 점점 중요해지고 있음
Backend Development
· 2025-03-16
📚[Backend Development] 분산 관계형 데이터베이스(DRDB, Distributed Relational Database)
“📚[Backend Development] 분산 관계형 데이터베이스(DRDB, Distributed Relational Database)” ✅1️⃣ 분산 관계형 데이터베이스란? 📌1️⃣ 개념. 분산 관계형 데이터베이스(DRDB, Distributed Relational Database, DRDB)는 하나의 데이터베이스 시스템이 여러 개의 서버(또는 노드)에 분산되어 저장되고 운영되는 관계형 데이터베이스(RDBMS) 시스템을 의미합니다. 즉, 데이터를 하나의 중앙 서버에 저장하는 것이 아니라, 여러 개의 서버에 나누어 저장하고 관리하는 방식입니다. 📌2️⃣ 왜 분산 관계형 데이터베이스를 사용하는가? ✅ 고가용성(High Availability) ➞ 특정 서버가 장애가 나더라도 다른 서버가 대신 동작 가능 ✅ 확장성(Scalability) ➞ 데이터 양이 증가해도 서버를 추가하여 확장 가능 ✅ 성능 향상(Performance Improvement) ➞ 데이터 읽기/쓰기 성능을 높일 수 있음 ✅ 지연시간 감소(Latency Reduction) ➞ 사용자와 가까운 서버에서 데이터를 제공 가능 📌3️⃣ 분산 관계형 데이터베이스의 특징. 💎 CAP 이론 : 분산 시스템은 일관성(Consistency), 가용성(Availability), 네트워크 분할 내성(Partition Tolerance) 중 두 가지만 보장할 수 있음 💎 ACID 보장 : 관계형 데이터베이스 특성상 트랜잭션의 무결성(Atomicity, Consistency, Isolation, Durability, ACID)을 보장해야 함 💎 데이터 분산(Sharding, Replication) : 데이터를 여러 서버에 나누어 저장해야 함 ✅2️⃣ 분산 관계형 데이터 베이스 아키텍처 분산 관계형 데이터베이스를 구성하는 주요 요소들은 다음과 같습니다. 📌1️⃣ 주요 컴포넌트 1️⃣ 데이터 노트(Data Nodes) 데이터를 저장하는 서버(물리적 또는 가상 머신) 여러 개의 노드로 구성되며, 각 노드는 데이터의 일부를 저장 예: MySQL, Cluster, CockroachDB의 노드 2️⃣ 쿼리 노드(Query Nodes) 클라이언트의 요청을 받아 데이터 노드로 전달 분산된 데이터에서 SQL 쿼리를 실행하고 결과를 조합 예: MySQL, Proxy, Vitess, TiDB 3️⃣ 메타데이터 노드(Metadata Nodes) 분산 데이터베이스의 전체 구조 및 데이터 위치 정보를 저장 데이터 노드 간 트랜잭션을 조율하고 데이터 정합성을 유지 4️⃣ 로드 밸런서(Load Balancer) 클라이언트 요청을 여러 데이터 노드로 분산하여 부하를 줄임 예: HAProxy, MySQL Router ✅3️⃣ 분산 관계형 데이터베이스의 데이터 분산 방식 📌1️⃣ 샤딩(Sharding) 📌 개념 샤딩은 데이터를 여러 개의 노드(서버)에 나누어 저장하는 기법입니다. 즉, 하나의 테이블을 여러 개의 작은 테이블로 분할하여 서로 다른 서버에 분산 저장하는 방식입니다. ✅ 장점 트래픽이 증가해도 서버를 추가하여 성능 확장이 가능 (수평 확장, Scale-Out) 특정 샤드에서만 데이터를 처리하므로 빠른 조회 가능 읽기/쓰기 성능이 향상됨 ❌ 단점 샤드 키(Shard Key) 설계가 잘못되면 특정 샤드에 부하가 집중될 수 있음 데이터 연관성(Consistency) 유지가 어려움 조인(Join) 연산이 어려워짐 📝 예제 사용자 데이터를 ID 기준으로 샤딩하는 경우 User ID Name Server (Shard) 1001 Alice Shard 1 1002 Bob Shard 2 1003 Charlie Shard 3 샤딩 전략으로는 범위 샤딩(Range Sharding), 해시 샤딩(Hash Sharding), 동적 샤딩(Dynamic Sharding) 등이 있습니다. 📌2️⃣ 레플리케이션(Replication) 📌 개념 레플리케이션은 데이터를 여러 개의 서버에 복제하여 저장하는 방식입니다. 이 방식은 데이터를 항상 여러 개의 서버에 보관하여 장애 복구(High Availability)와 읽기 성능(Read Performance) 향상을 목표로 합니다. ✅ 장점 한 서버에 장애가 발생해도 다른 복제 서버에서 데이터를 제공할 수 있음 (Failover) 읽기(Read) 성능을 개선할 수 있음 (읽기 요청을 여러 서버로 분산 가능) ❌ 단점 실시간 데이터 동기화가 어려울 수 있음 여러 개의 복제본을 유지해야 하므로 저장 공간이 많이 필요함 📝 예제 Master-Slave Replication (MySQL 기준) 1.Primary(마스터) : 데이터의 쓰기(Write) 작업을 처리 2.Replica(슬레이브) : Primary에서 변경된 데이터를 복제하여 읽기(Read) 처리 전담 Primary(마스터) ---> Replica 1(슬레이브) ---> Replica 2(슬레이브) 📌3️⃣ 분산 트랜잭션 관리(Distributed Transactions) 분산 데이터베이스에서는 여러 개의 노드에서 동시에 트랜잭션을 처리해야 하기 때문에 일관성을 유지하는 것이 중요합니다. 이를 해결하는 대표적인 방법이 2PC (Two-Phase Commit) 프로토콜) 입니다. 📌 Two-Phase Commit (2PC) 1️⃣ Prepare Phase : 모든 노드에 “트랜잭션을 커밋할 준비가 되었는가?”를 물어봄 2️⃣ Commit Phase : 모든 노드가 “OK”를 응답하면 실제 커밋 수행 💎 장점 : 데이터 일관성을 보장 💎 단점 : 느린 성능 (네트워크 지연 발생 가능) ✅4️⃣ 대표적인 분산 관계형 데이터베이스 시스템 DBMS 특징 사용 사례 Google Spanner 글로벌 트랜잭션 지원, 높은 확장성 Google, YouTube CookroachDB PostgreSQL 호환, 자동 샤딩 지원 금융, e-commerce MySQL Cluster 실시간 데이터 처리, 트랜잭션 지원 텔레콤, 게임 서버 Vitess MySQL 기반 샤딩 지원 YouTube, Slack TiDB MySQL 호환, Auto-Scaling 지원 핀테크, 빅데이터 ✅5️⃣ 분산 관계형 데이터베이스 설계 시 고려할 점 ✅ 샤딩 키(Shard Key) 선정 ➞ 특정 샤드에 부하가 몰리지 않도록 설계 ✅ 읽기/쓰기 부하 분산 ➞ 레플리케이션을 활용하여 읽기 성능 최적화 ✅ 트랜잭션 관리 ➞ 2PC, Saga 패턴 등을 활용하여 데이터 정합성 유지 ✅ 데이터 일관성(Consistency) vs 가용성(Availability) 선택 ➞ CAP 이론을 고려한 설계 필요 ✅6️⃣ 결론 ✅ 분산 관계형 데이터베이스는 고가용성, 확장성, 성능 향상을 위해 사용됨 ✅ 샤딩(Sharding)과 레플리케이션(Replication)이 주요 개념 ✅ 트랜잭션 관리(2PC, Saga 패턴)가 중요 ✅ 대표적인 시스템: Google Spanner, CockroachDB, MySQL Cluster, TiDB
Backend Development
· 2025-03-15
📚[Backend Development] 시스템 아키텍처란?
“📚[Backend Development] 시스템 아키텍처란?” ✅1️⃣ 시스템 아키텍처란? 📌1️⃣ 시스템 아키텍처의 개념. 시스템 아키텍처는 소프트웨어 시스템을 설계하고 구성하는 구조를 의미합니다. 쉽게 말해, 하나의 소프트웨어가 어떻게 구성되고, 데이터가 어떻게 흐르며, 성능과 확장성을 어떻게 고려할지 결정하는 과정입니다. 📌2️⃣ 시스템 아키텍처의 역할. 확장성(Scalability) : 사용자가 증가해도 성능이 유지되도록 설계 가용성(Availability) : 장애 발생 시에도 지속적으로 운영 가능하도록 설계 보안(Security) : 데이터 보호 및 인증, 권한 관리 유지보수성(Maintainability) : 코드 수정 및 기능 추가가 쉽게 가능하도록 구조 설계 성능(Performance) : 빠르게 동작하도록 최적화 ✅2️⃣ 시스템 아키텍처의 주요 구성 요소, 시스템은 여러 컴포넌트로 구성되며, 각각의 역할이 다릅니다. 📌1️⃣ 클라이언트(Client) 사용자가 직접 조작하는 부분 (웹 브라우저, 모바일 앱) 요청을 서버에 전달하고 결과를 표시하는 역할 예: React, Vue.js, Android, iOS 📌2️⃣ API 게이트웨이 (API Gateway) 클라이언트 요청을 내부 서비스로 라우팅하는 역할 인증, 로드 밸런싱, 캐싱등의 시능 수행 예: Kong, Nginx, AWS API Gatewat 📌3️⃣ 애플리케이션 서버 (Application Server) 비즈니스 로직을 처리하는 핵심 컴포넌트 REST API 또는 GraphQL을 통해 데이터를 제공 예: Spring Boot, Node.js, Django, FastAPI 📌4️⃣ 데이터베이스 (Database) 데이터를 저장하고 조회하는 역할 RDBMS (MySQL, PostgreSQL) vs NoSQL (MongoDB, Cassandra) 📌5️⃣ 캐시 서버 (Cache Server) 자주 사용되는 데이터를 빠르게 제공하는 역할 예: Redis, Memcached 📌6️⃣ 메시지 큐 (Message Queue, MQ) 비동기 이벤트 처리를 위한 시스템 예: Kafka, RabbitMQ, AWS SQS 📌7️⃣ 로드 밸런서 (Load Balancer) 요청을 여러 서버로 분산하여 부하를 줄이는 역할 예: Nginx, HAProxy, AWS ELB 📌8️⃣ 모니터링 시스템 (Monitoring System) 서버 상태, 트래핑, 장애 발생 감지 예: Prometheus, Grafana, AWS CloudWatch ✅3️⃣ 주요 시스템 아키텍처 패턴 📌1️⃣ Monolithic Architecture (모놀리식 아키텍처) 📌 특징 하나의 애플리케이션이 모든 기능을 포함하는 구조 모든 코드가 하나의 프로젝트로 구성됨 ✅ 장점 개발 초기 단계에서 간단한 구조로 빠르게 개발 가능 배포가 단순함 ❌ 단점 하나의 기능 변경 시 전체 시스템을 다시 배포해야 함 특정 기능만 확장하기 어렵고, 트래픽 증가에 취약함 📝 예제 전통적인 웹 애플리케이션 (Spring Boot + MySQL) 📌2️⃣ Microservices Architecture (마이크로서비스 아키텍처, MSA) 📌 특징 애플리케이션을 여러 개의 작은 서비스로 분리하여 개발 각 서비스가 독립적으로 배포 가능 서비스 간 통신은 API (REST, gRPC) 또는 메시지 큐(Kafka)로 이루어짐 ✅ 장점 개별 서비스 확장이 가능하여 성능 최적화가 쉬움 특정 서비스만 업데이트 가능하여 유지보수가 쉬움 ❌ 단점 서비스 간 통신 비용이 증가하여 네트워크 성능 저하 가능 각 서비스 간 데이터 일관성 유지가 어려움 📝 예제 Netflix, Uber, 카카오, 토스 같은 대규모 서비스에서 사용됨 📌3️⃣ Layerd Architecture (계층형 아키텍처) 📌 특징 애플리케이션을 계층(Layer)별로 나누어 관리하는 방식 일반적으로 3-Tier 또는 4-Tier 구조로 설계됨 📌 3-Tier 구조 1️⃣ Presentation Layer (프레젠테이션 계층) ➞ UI & 클라이언트 2️⃣ Application Layer (애플리케이션 계층) ➞ 비즈니스 로직 3️⃣ Data Layer (데이터 계층) ➞ 데이터 저장 및 조회 ✅ 장점 역할 분리가 명확하여 유지보수성이 뛰어남 재사용 가능한 코드 구조 ❌ 단점 다층 구조로 인해 응답 속도가 느려질 수 있음 📌4️⃣ Event-Driven Architecture (이벤트 기반 아키텍처) 📌 특징 이벤트가 발생하면 특정 서비스에서 이를 처리하는 방식 메시지 큐(Kafka, RabbitMQ)를 활용하여 비동기 처리를 함 ✅ 장점 비동기 방식으로 트래픽을 분산할 수 있어 성능 최적화 가능 각 서비스가 독립적으로 작동하여 장애 발생 시 영향이 적음 ❌ 단점 이벤트 흐름을 추적하기 어려워 디버깅이 어려움 메시지 지연(latency)이 발생할 수 있음 📝 예제 주문 시스템: 주문이 발생하면 Order Service에서 Payment Service로 이벤트 전송 ✅4️⃣ 확장 가능한 시스템 설계 원칙. 📌1️⃣ Scal-Up vs Scale-Out Scale-Up : 서버의 성능을 업그레이드 (RAM, CPU 추가) Scalce-Out : 서버 개수를 늘려 부하를 분산 (로드 밸런서 활용) 📌2️⃣ 데이터베이스 최적화 샤딩(Sharding) : 데이터베이스를 여러 개로 나누어 저장 레플리케이션(Replication) : 동일한 데이터를 여러 서버에 복제하여 읽기 성능 향상 📌3️⃣ 로드 밸런싱 (Load Balancing) Round Robin : 서버에 순차적으로 요청 전달 Least Connections : 현재 접속자가 가장 적은 서버로 요청 전달 📌4️⃣ 장애 복구 (Fault Tolerance) Failover : 장애 발생 시 대체 서버로 자동 전환 Circuit Breaker : 일정 횟수 이상 실패하면 서비스 차단 ✅5️⃣ 결론 시스템 아키텍처는 소프트웨어의 성능과 확장성, 유지보수성을 결정하는 중요한 요소입니다. 초반에는 모놀리식 아키텍처로 빠르게 개발 트래픽이 증가하면 마이크로서비스 아케텍처로 확장 비동기 처리가 필요하면 이벤트 기반 아키텍처 도입
Backend Development
· 2025-03-14
📚[Backend Development] 대규모 시스템 아키텍처 핵심 요소
“📚[Backend Development] 대규모 시스템 아키텍처 핵심 요소” ✅1️⃣ 로드 밸런서 (Load Balancer) 1️⃣ 로드 밸런서란? 로드 밸런서 (Load Balancer)는 클라이언트의 요청을 여러 서버로 분산하여 부하를 줄이고, 장애가 발생한 서버를 감지하여 트래픽을 자동으로 다른 서버로 우회하는 역할을 합니다. 2️⃣ 로드 밸런서 종류 L4 (Network Layer) 로드 밸런서 : IP 주소와 포트 기반으로 트래픽을 분산 (예: AWS NLB, HAProxy) L7 (Application Layer) 로드 밸런서 : HTTP 요청을 분석하여 특정 URL이나 쿠키 정보 기반으로 분산 (예: Nginx, AWS ALB, Traefix) 3️⃣ 로드 밸런싱 방식 Round Robin : 서버에 순차적으로 요청을 분배 Least Connections : 현재 접속자가 가장 적은 서버로 분배 IP Hashing : 클라이언트의 IP를 기반으로 특정 서버로 항상 연결 (세션 유지 필요할 때 사용) Weighted Round Robin : 서버 성능에 따라 가중치를 두고 트래픽을 분배 4️⃣ 예제 AWS ALB(Application Load Balancer) 설정 시, 특정 URL 경로에 따라 다른 서버 그룹으로 요청을 보낼 수 있음. https://example.com/api/* ➞ API 서버 그룹 https://example.com/images/* ➞ 이미지 서버 그룹 ✅2️⃣ 웹 서버(Web Server)와 애플리케이션 서버(Application Server) 1️⃣ 웹 서버(Web Server)란? 웹 서버는 정적 콘텐츠(HTML, CSS, JavaScript)를 제공하는 역할을 하며, 대표적으로 Nginxm Apache가 있습니다. 2️⃣ 애플리케이션 서버(Application Server)란? 애플리케이션 서버는 비즈니스 로직을 처리하는 서버, 보통 Spring Boot, Django, Express 같은 프레임워크를 사용합니다. 3️⃣ 웹 서버(Web Server) + 애플리케이션 서버(Application Server) 연동 방식. Reverse Proxy : 웹 서버(Nginx)가 클라이언트 요청을 받고, 내부 애플리케이션 서버(Spring Boot)로 전달 예제 lcation /api/ { proxy_pass http://localhost:8080/; } ✅3️⃣ 데이터베이스 (Database, DB) 1️⃣ 데이터베이스 종류. 📌1️⃣ RDBMS (Relational Database Management System) MySQL, PostgreSQL, Oracle 데이터 정합성(ACID 보장)이 중요할 때 사용 📌2️⃣ NoSQL MongoDB, Cassandra, Redis 트래픽이 많고 빠른 읽기/쓰기 성능이 필요한 경우 사용 2️⃣ 데이터 분산 전략 샤딩(Sharding) : 데이터를 여러 서버에 나눠서 저장 레플리케이션(Replication) : 데이터를 여러 서버에 복제하여 장애 대비 3️⃣ 예제 MySQL Master-Slave Replication: Master DB: 쓰기 작업 처리 Slave DB: 읽기 작업 처리 ✅4️⃣ 캐시 시스템 (Cache System) 1️⃣ 캐시(Cache)란? 자주 사용하는 데이터를 빠르게 제공하기 위해 메모리에 저장하는 기술 2️⃣ 캐시 저장소 종류 📌1️⃣ 메모리 캐시. Redis Memcached 📌2️⃣ CDN 캐시. Cloudflare AWS CloudFront 3️⃣ 캐시 정책 TTL(Time-To-Live) : 일정 시간이 지나면 캐시 삭제 LRU(Least Recently Used) : 가장 오래 사용되지 않은 데이터부터 삭제 4️⃣ 예제 Spring Boot + Redis 캐시 적용 @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { return userRepository.findById(userId).orElse(null); } ✅5️⃣ 메시지 큐 (Message Queue, MQ) 1️⃣ 메시지 큐란? 비동기 처리를 위해 메시지를 저장하고 전달하는 시스템 2️⃣ 메시지 큐 종류. Kafka : 대량의 데이터 처리 가능, 로그 처리, 실시간 스트리밍 지원 RabbitMQ : 빠른 메시지 큐잉, 트랜잭션 처리 가능 AWS SQS : 관리형 메시지 큐 서비스 3️⃣ 예제. Spring Boot + Kafka @KafakaListener(topics = "order-topic", groupId = "order-group") public void consume(String message) { System.out.println("Received Message: " + message); } ✅6️⃣ CDN (Content Delivery Network) 1️⃣ CDN이란? 정적 콘텐츠(이미지, 동영상, CSS)를 전 세계 여러 서버에 배포하여 빠르게 제공하는 기술 2️⃣ CDN 동작 방식 사용자가 웹사이트 접속 CDN 서버가 가장 가까운 위치에서 콘텐츠 제공 원본 서버 부하 감소 3️⃣ 예제 AWS ClouldFront를 사용하여 정적 콘텐츠 캐싱 ✅7️⃣ 모니터링 & 로깅 시스템 1️⃣ 모니터링 시스템 Prometheus + Grafana : 서버 메트릭 수집 및 시각화 AWS CloudWatch : AWS 리소스 모니터링 2️⃣ 로깅 시스템 ELK Stack (Elasticsearch, Logstash, Kibana) : 로그 수집 및 분석 Fluetd : 경량 로그 수집기 3️⃣ 예제 Spring Boot + ELK logging.file.name=logs/app.log ✅8️⃣ 확장성 (Scalability) 1️⃣ 확장 방법 📌1️⃣ 수직 확장 (Scale-Up) 서버 성능 업그레이드 (CPU, RAM 추가) 한계가 존재함 📌2️⃣ 수평 확장 (Scale-Out) 서버 개수를 늘려 부하 분산 로드 밸런서를 활용 ✅9️⃣ 장애 대응 (Fault Tolerance) 1️⃣ 장애 대응 기법 Failover : 장애 발생 시 자동으로 대체 서버로 전환 Circuit Breaker 패턴 : 서비스가 일정 횟수 이상 실패하면 자동으로 차단 2️⃣ 예제 Spring Cloud + Resilience4j Circuit Breaker @CircuiteBreaker(name = "backendA", fallbackMethod = "fallback") public String callService() { return restTemplate.getForObject("http://example.com/api", String.class); }
Backend Development
· 2025-03-12
📚[Backend Development] 대규모 시스템 서버 인프라
“📚[Backend Development] 대규모 시스템 서버 인프라” ✅1️⃣ 대규모 시스템 서버 인프라란? 대규모 시스템 서버 인프라는 “많은 사용자가 동시에 접속해도 원활하게 동작할 수 있도록 설계된 서버 환경”을 의미합니다. 대표적인 예 클라우드 서비스(AWS, GCP, Azure) 대형 웹사이트(네이버, 토스, 카카오톡) 온라인 게임 서버(베틀그라운드, LOL) etc. 이러한 시스템에서는 트래픽 처리, 확장성(Scalability), 가용성(Availability), 복원력(Resilience)등이 매우 중요합니다. ✅2️⃣ 대규모 시스템 아키텍처의 핵심 요소. 1️⃣ 로드 밸런서 (Load Balancer) 서버에 들어오는 트래픽을 여러 서버로 분산하는 역할을 합니다. 대표적인 로드 밸런서 Nginx HAProxy AWS ELB etc. 예시: 사용자가 많아지면 한 대의 서버만으로 감당하기 어려우므로 여러 대의 서버에 요청을 분배합니다. 2️⃣ 웹 서버 (Web Server)와 애플리케이션 서버 (Application Server) 웹 서버(Nginx, Apache) ➞ 정적 파일(HTML, CSS, JS) 제공. 애플리케이션 서버(Spring Boot, Django, Express) ➞ 비즈니스 로직 수행. 예시: 클라이언트가 로그인하면, 애플리케이션 서버에서 DB를 조회해 사용자 정보를 제공합니다. 3️⃣ 데이터베이스 (Database, DB) 데이터를 저장하고 관리하는 시스템. RDBMS (MySQL, PostgreSQL, Oracle) ➞ 강력한 데이터 무결성 보장. NoSQL (MongoDB, Redis, Cassandra) ➞ 대량의 데이터 처리에 적합. 샤딩(Sharding), 레플리케이션(Replication)을 활용해 성능과 안정성을 높임. 4️⃣ 캐시 시스템 (Cache System) 자주 조회되는 데이터를 빠르게 제공하기 위해 사용됩니다. 대표적인 캐시 기술 Redis Memcached 예시: 인기 게시슬을 캐시에 저장해 DB 부하를 줄입니다. 5️⃣ 메시지 큐(Message Queue, MQ) 비동기 처리를 위해 사용됩니다. 대표적인 메시지 큐 시스템 Kafka RabbitMQ AWS SQS 예시: 유저가 글을 작성하면, MQ에 메시지를 보내고 나중에 비동기로 처리함. 6️⃣ CDN (Content Delivery Network) 정적 콘텐츠 (이미지, 동영상)를 전 세계 여러 서버에 배포하여 빠르게 제공하는 기술. 대표적인 CDN 서비스 Clouldflare AWS CloudFront 예시: 해외 사용자가 한국 서버에서 이미지 다운로드 시, 가까운 CDN 서버에서 제공해 속도를 높입니다. 7️⃣ 모니터링 & 로깅 시스템 서버 상태를 지속적으로 체크하고, 장애 발생 시 빠르게 대응할 수 있도록 합니다. 대표적인 모니터링 도구 Prometheus Grafana AWS CloudWatch 대표적인 로깅 도구 ELK Stack (Elasticsearch, Logstach, Kibana) Fluentd 예시: CPU 사용량이 80%를 초과하면 경고 알림을 발생시켜 장애를 예방합니다. ✅3️⃣ 대규모 시스템 설계의 핵심 개념 ✅ 확장성 (Scalability) 시스템이 사용자 증가에 따라 성능 저하 없이 확장할 수 있는 능력 수평 확장(Scale-Out) : 서버를 여러 대 추가 예: AWS EC2 Auto Scaling 수직 확장(Scale-Up) : 서버의 성능을 높이는 방법 예: CPU, RAM 업그레이드 ✅ 가용성 (Availability) 시스템이 지속적으로 동작할 수 있는 능력 (99.99% Uptime 보장) 예시: 장애 발생 시, 다른 서버가 대신 처리하도록 Failover 설계 ✅ 복원력 (Resilience) 시스템이 장애 발생 후 빠르게 복구하는 능력 예시: AWS RDS Multi-AZ 구성 ➞ 장애 발생 시 자동으로 대체 DB로 전환 ✅ 일관성 (Consistency) vs 가용성 (Availability) vs 분할 내성 (Partition Tolerance) CAP 정리 : 분산 시스템에서는 일관성 (Consistency), 가용성 (Availability), 분할 내성 (Partition Tolerance) 중 두 가지만 선택 가능 예시: 은행 시스템 ➞ 일관성 (Consistency) 우선 예시: SNS 서비스 ➞ 가용성 (Availability) 우선 ✅ 마이크로서비스 아키텍처 (MSA, Microservice Architecture) 하나의 거대한 서비스(모놀리식)를 작은 서비스 여러 개로 분리하는 방식 장점 확장성 증가 독립 배포 기능 장애가 전체 시스템에 영향을 덜 줌 예시 사용자 인증, 결제, 주문, 리뷰 서비스를 각각 독립적인 서비스로 나눔 ✅4️⃣ 대규모 시스템 설계 사례 💎 쇼핑몰 (이커머스) 시스템 웹 서버 (Nginx) 애플리케이션 서버 (Spring Boot) DB (MySQL + Redis 캐시) 메시지 큐 (Kafka) ➞ 주문 처리 CDN ➞ 이미지 및 정적 리소스 제공 💎 게임 서버 (배틀그라운드, LOL) 게임 서버 (C++, Java) 매칭 서버 ➞ 유저 매칭 랭킹 시스템 (Redis) 로그 수집 및 분석 (Elasticsearch) 💎 금융 서비스 (토스, 카카오뱅크) 강력한 보안 및 트랜잭션 무결성 CQRS 패턴 적용 (읽기와 쓰기 분리) 이중화된 DB 및 Failover 시스템 구축 ✅5️⃣ 대규모 시스템 설계 시 고려할 점 📌 트래픽 급증에 대비한 Auto Scaling 설계 📌 DB 부하를 줄이기 위한 캐시 적용 (Redis, Memcached) 📌 장애 발생 대비를 위한 로드 밸런서 및 이중화 (Failover 구성) 📌 비동기 처리를 위한 메시지 큐 도입 (Kafka, RabbitMQ) 📌 빠른 장애 탐지를 위한 모니터링 시스템 구축 💡 결론 대규모 시스템 서버 인프라는 트래픽 분산, 확장성, 장애 대응이 핵심입니다. 백엔드 개발자로서 로드 밸런싱, DB 성능 최적화, 메시지 큐, 캐시 시스템 같은 요소를 깊이 이해하는 게 중요합니다.
Backend Development
· 2025-03-11
📚[Backend Development] increase메서드 실행과정 분석
“📚[Backend Development] increase메서드 실행과정 분석” 📌 CommentPath 클래스. package kobe.board.comment.entity; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @Getter @ToString @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommentPath { private String path; private static final String CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int DEPTH_CHUNK_SIZE = 5; private static final int MAX_DEPTH = 5; // MIN_CHUNK = "00000", MAX_CHUNK = "zzzzz" private static final String MIN_CHUNK = String.valueOf(CHARSET.charAt(0)).repeat(DEPTH_CHUNK_SIZE); private static final String MAX_CHUNK = String.valueOf(CHARSET.charAt(CHARSET.length() - 1)).repeat(DEPTH_CHUNK_SIZE); public static CommentPath create(String path) { if (isDepthOverflowed(path)) { throw new IllegalStateException("depth overflowed"); } CommentPath commentPath = new CommentPath(); commentPath.path = path; return commentPath; } private static boolean isDepthOverflowed(String path) { return calculateDepth(path) > MAX_DEPTH; } private static int calculateDepth(String path) { // 25개의 문자열 / 5 = 5depth return path.length() / DEPTH_CHUNK_SIZE; } // CommentPath 클래스의 path의 depth를 구하는 매서드 public int getDepth() { return calculateDepth(path); } // root인지 확인하는 매서드 public boolean isRoot() { // 현재의 depth가 1인지 확인해주면 됨 return calculateDepth(path) == 1; } // 현재 path의 parentPath를 반환해주는 매서드 public String getParentPath() { // 끝 5자리만 잘라내면 됨 return path.substring(0, path.length() - DEPTH_CHUNK_SIZE); } // 현재 path의 하위 댓글의 path을 만드는 매서드 public CommentPath createChildCommentPath(String descendantsTopPath) { if (descendantsTopPath == null) { return CommentPath.create(path + MIN_CHUNK); } String childrenTopPath = findChildrenTopPath(descendantsTopPath); return CommentPath.create(increase(childrenTopPath)); } // 00a0z0000200000 <- descendantsTopPath private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } private String increase(String path) { // path에서 가장 마지막 5자리를 자른 것 String lastChunk = path.substring(path.length() - DEPTH_CHUNK_SIZE); if (isChunkOverflowed(lastChunk)) { throw new IllegalStateException("chunk overflowed"); } // Character set의 길이 int charsetLength = CHARSET.length(); // lastChunk를 10진수로 먼저 변환하기 위한 값을 저장 int value = 0; for (char character : lastChunk.toCharArray()) { value = value * charsetLength + CHARSET.indexOf(character); System.out.println("value ====> " + value); System.out.println("CHARSET.indexOf ====> " + CHARSET.indexOf(character)); } value = value + 1; String result = ""; for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } return path.substring(0, path.length() - DEPTH_CHUNK_SIZE) + result; } private boolean isChunkOverflowed(String lastChunk) { return MAX_CHUNK.equals(lastChunk); } } ✅1️⃣ 코드 실행 흐름 확인 for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } 📌 코드 핵심 역할 value를 CHARSET(62진수) 기반으로 DEPTH_CHUNK_SIZE(=5) 길이의 문자열로 변환하는 과정 각 반복에서 value의 마지막 자리를 CHARSET에서 찾아 문자열(result)에 추가하고, value를 62로 나눠서 다음 문자를 구함. ✅2️⃣ descendantsTopPath = “00000” 일 때 increas(“00000”)의 실행 과정 📌 increase(“00000”) 실행 과정 1. lastChunk 추출 String lastChunk = path.substring(path.length() - DEPTH_CHUNK_SIZE) “00000” ➞ lastChunk = “00000” 2. lastChunk를 10진수(value)로 변환 int value = 0; for (char character : lastChunk.toCharArray()) { value = value * charsetLength + CHARSET.indexOf(cha) } CHARSET.indexOf(‘0’) = 0 value 값 계산: value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 최종적으로 value = 0 3. value + 1 증가. value = value + 1; value = 0 + 1 = 1 4. value를 다신 62진수 문자열로 변환 for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charserLength) + result; value /= charsetLength; } 반복 과정(DEPTH_CHUNK_SIZE = 5) i = 0: value % 62 = 1 → CHARSET[1] = '1' → result = "1" i = 1: value /= 62 = 0 → CHARSET[0] = '0' → result = "01" i = 2: value = 0 → CHARSET[0] = '0' → result = "001" i = 3: value = 0 → CHARSET[0] = '0' → result = "0001" i = 4: value = 0 → CHARSET[0] = '0' → result = "00001" 최종 result = “00001”
Backend Development
· 2025-03-07
📚[Backend Development] increase메서드 내부 for문 실행 과정 상세 분석
“📚[Backend Development] increase메서드 내부 for문 실행 과정 상세 분석” 📌 CommentPath 클래스. package kobe.board.comment.entity; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @Getter @ToString @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommentPath { private String path; private static final String CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int DEPTH_CHUNK_SIZE = 5; private static final int MAX_DEPTH = 5; // MIN_CHUNK = "00000", MAX_CHUNK = "zzzzz" private static final String MIN_CHUNK = String.valueOf(CHARSET.charAt(0)).repeat(DEPTH_CHUNK_SIZE); private static final String MAX_CHUNK = String.valueOf(CHARSET.charAt(CHARSET.length() - 1)).repeat(DEPTH_CHUNK_SIZE); public static CommentPath create(String path) { if (isDepthOverflowed(path)) { throw new IllegalStateException("depth overflowed"); } CommentPath commentPath = new CommentPath(); commentPath.path = path; return commentPath; } private static boolean isDepthOverflowed(String path) { return calculateDepth(path) > MAX_DEPTH; } private static int calculateDepth(String path) { // 25개의 문자열 / 5 = 5depth return path.length() / DEPTH_CHUNK_SIZE; } // CommentPath 클래스의 path의 depth를 구하는 매서드 public int getDepth() { return calculateDepth(path); } // root인지 확인하는 매서드 public boolean isRoot() { // 현재의 depth가 1인지 확인해주면 됨 return calculateDepth(path) == 1; } // 현재 path의 parentPath를 반환해주는 매서드 public String getParentPath() { // 끝 5자리만 잘라내면 됨 return path.substring(0, path.length() - DEPTH_CHUNK_SIZE); } // 현재 path의 하위 댓글의 path을 만드는 매서드 public CommentPath createChildCommentPath(String descendantsTopPath) { if (descendantsTopPath == null) { return CommentPath.create(path + MIN_CHUNK); } String childrenTopPath = findChildrenTopPath(descendantsTopPath); return CommentPath.create(increase(childrenTopPath)); } // 00a0z0000200000 <- descendantsTopPath private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } private String increase(String path) { // path에서 가장 마지막 5자리를 자른 것 String lastChunk = path.substring(path.length() - DEPTH_CHUNK_SIZE); if (isChunkOverflowed(lastChunk)) { throw new IllegalStateException("chunk overflowed"); } // Character set의 길이 int charsetLength = CHARSET.length(); // lastChunk를 10진수로 먼저 변환하기 위한 값을 저장 int value = 0; for (char character : lastChunk.toCharArray()) { value = value * charsetLength + CHARSET.indexOf(character); System.out.println("value ====> " + value); System.out.println("CHARSET.indexOf ====> " + CHARSET.indexOf(character)); } value = value + 1; String result = ""; for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } return path.substring(0, path.length() - DEPTH_CHUNK_SIZE) + result; } private boolean isChunkOverflowed(String lastChunk) { return MAX_CHUNK.equals(lastChunk); } } 📌 for 문 실행 과정 상세 분석 해당 for ansdms 주어진 value(10진수)를 CHARSET(62진수)로 변환하여 문자열(result)을 생성하는 과정입니다. 1️⃣ for 문 코드 for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } ✅ 주요 개념 value % charsetLength ➞ 62진수에서 가장 낮은 자리수(오른쪽 끝)를 구함 CHARSET.charAt(value % charsetLenht) ➞ CHARSET에서 해당 인덱스의 문자(0~9, A~Z, a~z)를 가져옴 value /= charsetLength ➞ 다음 자리수를 계산하기 위해 value를 62로 나눔 result = CHARSET.charAt(value % charsetLenght) + resultl ➞ 문자열의 앞에 추가하여 변환 결과를 만든다 2️⃣ for 문 실행 과정 단계별 분석 📌 예제 1: value = 1, charsetLenght = 62, DEPTH_CHUNK_SIZE = 5 ✅ 초기 값 value = 1; resutl = ""; ✅ 반복 과정 반복 value value % 62 CHARSET.charAt(value % 62) result value /= 62 i=0 1 1 ‘1’ “1” 0 i=1 0 0 ‘0’ “01” 0 i=2 0 0 ‘0’ “001” 0 i=3 0 0 ‘0’ “0001” 0 i=4 0 0 ‘0’ “00001” 0 ✅ 최종 결과 result = "00001"; 📌 예제 2: value = 62 ✅ 초기 값 value = 62; resutl = ""; ✅ 반복 과정 반복 value value % 62 CHARSET.charAt(value % 62) result value /= 62 i=0 62 0 ‘0’ “0” 1 i=1 1 1 ‘1’ “10” 0 i=2 0 0 ‘0’ “010” 0 i=3 0 0 ‘0’ “0010” 0 i=4 0 0 ‘0’ “00010” 0 ✅ 최종 결과 result = "00010"; 📌 예제 3: value = 3843 ✅ 초기 값 value = 3843; resutl = ""; ✅ 반복 과정 반복 value value % 62 CHARSET.charAt(value % 62) result value /= 62 i=0 3843 61 ‘z’ “z” 61 i=1 61 61 ‘z’ “zz” 0 i=2 0 0 ‘0’ “0zz” 0 i=3 0 0 ‘0’ “00zz” 0 i=4 0 0 ‘0’ “000zz” 0 ✅ 최종 결과 result = "000zz"; 📌 핵심 정리 📝 for 문이 하는 일 value(10진수)를 62진수 문자열로 변환한다. CHARSET.charAt(value % 62)를 사용하여 가장 낮은 자리수부터 변환한다. 3.변환된 문자를 result 앞에 추가(+ result) value /= 62 하여 다음 자리수를 계산. 최종적으로 DEPTH_CHUNK_SIZE(5)만큼 반복하여 5자리 문자열을 만든다. 📌 결론 value의 작은 자리수부터 변환하여 문자열을 구성한다. 62진법 변환원리를 사용하여 댓글의 path를 증가시키는 데 활용한다.
Backend Development
· 2025-03-07
📚[Backend Development] CommentPath 클래스 분석 및 설명
“📚[Backend Development] CommentPath 클래스 분석 및 설명” 📌 CommentPath 클래스 분석 및 설명. CommentPath 클래스는 계층형 댓글 시스템에서 각 댓글의 경로(path)를 관리하는 역할을 합니다. 각 댓글은 path라는 문자열로 표현되며, path는 일정한 규칙을 따라 댓글의 부모-자식 관계를 나타냅니다. 이 클래스는 댓글의 깊이(depth), 부모 댓글(parentPath), 새로운 자식 댓글(createChildCommentPath) 등을 관리하는 기능을 제공합니다. ✅1️⃣ 클래스 전반적인 개요. 📌 핵심 개념 1️⃣ path 필드 path는 댓글의 계층 구조를 표현하는 문자열입니다. 각 댓글은 5자리씩(DEPHT_CHUNK_SIZE = 5)의 문자열을 가지며, 댓글이 깊어질수록 path가 길어집니다. 📝 예시: 루트 댓글: "00000" 첫 번째 자식 댓글: "0000000000" 두 번째 자식 댓글: "000000000000000" 이를 통해 댓글이 어느 계층에 속하는지, 부모가 누구인지, 자식이 어떻게 배치될지를 결정할 수 있습니다. 2️⃣ CHARSET (문자 집합) 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz (총 62개 문자) path의 각 5자리(chunk)는 이 문자 집합을 사용해 표현됩니다. 새로운 댓글이 추가될 때, path는 문자 집합 내에서 증가(increase)하는 방식으로 생성됩니다. ✅2️⃣ 주요 필드 private static final String CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int DEPTH_CHUNK_SIZE = 5; // 각 depth가 5자리로 표현됨 private static final int MAX_DEPTH = 5; // 최대 depth 5까지 허용 private static final String MIN_CHUNK = String.valueOf(CHARSET.charAt(0)).repeat(DEPTH_CHUNK_SIZE); private static final String MAX_CHUNK = String.valueOf(CHARSET.charAt(CHARSET.length() - 1)).repeat(DEPTH_CHUNK_SIZE); MIN_CHUNK = “00000” : 최소 chunk 값 MAX_CHUNK = “zzzzz” : 최대 chunk 값 (즉, 더 이상 증가 불가능한 값) 댓글의 path는 MIN_CHUNK부터 시작해 점진적으로 증가하는 방식입니다. ✅3️⃣ 주요 메서드 분석 각 메서드의 역할, 사용 시기, 동작 방식을 설명하겠습니다. 1️⃣ create(String path) public static CommentPath create(String path) { if (isDepthOverflowed(path)) { throw new IllegalStateException("depth overflowed"); } CommentPath commentPath = new CommentPath(); commentPath.path = path; return commentPath; } ✅ 역할: 주어진 path로 CommentPath 객체를 생성한다. path의 깊이가 MAX_DEPTH를 초과하면 예외 발생. ✅ 사용 시기: 댓글을 DB에 저장할 때, CommentPath를 생성할 때 사용. ✅ 동작 방식: isDepthOverflowed(path)를 호출하여 최대 깊이를 초과하는지 확인한다. 문제가 없으면 CommentPath 객체를 생성하여 반환한다. 2️⃣ calculateDepth(String path) private static int calculateDepth(String path) { return path.length() / DEPTH_CHUNK_SIZE; } ✅ 역할: 현재 path의 깊이를 계산한다. path.length()를 DEPTH_CHUNK_SIZE로 나누면 깊이가 된다. ✅ 사용 시기: 댓글이 몇 번째 깊이인지 확인 할 때. isRoot(), isDepthOverflowed() 등의 메서드에서 사용. ✅ 동작 방식: path.length()를 5로 나눈다. 예: “0000000000” (10글자) ➞ calculateDepth(“0000000000”) ➞ 10 / 5 = 2 3️⃣ getDepth() public int getDepth() { return calculateDepth(path); } ✅ 역할: 현재 댓글의 깊이를 반환한다. ✅ 사용 시기: 댓글의 깊이를 확인할 때. ✅ 동작 방식: calculateDepth(path)를 호출하여 깊이를 구한다. 4️⃣ isRoot() public boolean isRoot() { return calculateDepth(path) == 1; } ✅ 역할: 현재 댓글이 루트 댓글인지 확인한다. ✅ 사용 시기: 부모 댓글이 없는 루트 댓글인지 확인할 때. ✅ 동작 방식: 깊이가 1이면 루트 댓글로 판단. 5️⃣ getParentPath() public String getParentPath() { return path.substring(0, path.length() - DEPTH_CHUNK_SIZE); } ✅ 역할: 부모 댓글의 path를 반환한다. ✅ 사용 시기: 댓글의 부모를 찾을 때. ✅ 동작 방식: 현재 path에서 마지막 5자리를 잘라내어 반환한다. 6️⃣ createChildCommentPath(String descendantsTopPath) public CommentPath createChildCommentPath(String descendantsTopPath) { if (descendantsTopPath == null) { return CommentPath.creat(path + MIN_CHUNK); } String childrenTopPath = findChildrenTopPath(descendantsTopPath); return CommentPath.create(increase(childrenTopPath)); } ✅ 역할: 현재 댓글의 자식 댓글의 path를 생성한다. ✅ 사용 시기: 새로운 대댓글을 추가할 때. ✅ 동작 방식: descendantsTopPath가 null이면 MIN_CHUNK를 붙여서 자식 댓글을 생성. 자식 댓글의 path를 가져온 후 increase()를 호출하여 증가. 7️⃣ findChildrenTopPath(String descendantsTopPath) private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } ✅ 역할: 자손 댓글 중 가장 상위 댓글의 path를 반환한다. ✅ 사용 시기: 새로운 댓글을 추가할 때, 어떤 댓글이 현재 댓글의 가장 최근 자식 댓글인지 찾을 때. ✅ 동작 방식: 현재 깊이보다 한 단계 더 깊은 path 부분을 잘라서 반환. 8️⃣ increase(String path) private String increase(String path) { String lastChunk = path.substring(path.length() - DEPTH_CHUNK_SIZE); if (isChunkOverflowed(lastChunk)) { throw new IllegalStateException("chunk overflowed"); } int charsetLength = CHARSET.length(); int value = 0; for (char character : lastChunk.toCharArray()) { value = value * charsetLength + CHARSET.indexOf(character); } value = value + 1; String result = ""; for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } return path.substring(0, path.length() - DEPTH_CHUNK_SIZE) + result; } ✅ 역할: 댓글 path를 증가시켜 새로운 자식 댓글 생성 ✅ 사용 시기: 새로운 대댓글을 추가할 때 📝 동작 방식: 📌 입력(path) path는 댓글의 계층 구조를 나타내는 문자열. 예를 들어 “0000000000”(2번째 깊이의 댓글)이라는 주어졌다고 가정. 📌 1단계: 마지막 5자리(Chunk) 추출 String lastChunk = path.substring(path.length() - DEPTH_CHUNK_SIZE); path에서 마지막 DEPTH_CHUNK_SIZE(=5)만큼의 문자열을 잘라낸다. 예제: path = "0000000000"; lastChunk = path.substring(5); // "00000" 📌 2단계: lastChunk 값이 최대값인지 확인 if (isChunkOverflowed(lastChunk)) { throw new IllegalStateException("chunk overflowed"); } isChunkOverflowed(lastChunk) 메서드를 호출하여 lastChunk가 “zzzzz”(최대값)인지 확인. 만약 “zzzzz”라면 더 이상 증가할 수 없으므로 예외를 던진다. 📌 3단계: lastChunk 값을 10진수로 변환 int charsetLength = CHARSET.length(); int value = 0; for (char character : lastChunk.toCharArray()) { value = value * charsetLength + CHARSET.indexOf(character); } CHARSET은 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz (총 62개 문자). lastChunk(현재 5자리 문자열)를 62진수에서 10진수로 변환한다. 📝 예제 1: “00000” 변환 CHARSET.index(‘0’) = 0 변환 과정: value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 📝 예제 2: “0000z” 변환 ‘z’의 인덱스는 61 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 0 = 0 value = (0 * 62) + 61 = 61 📌 4단계: 값 증가 (+ 1 연산) value = value + 1; 10진수의 값이 1증가한다. 📝 예제 1: “00000” ➞ “00001” value = 0 + 1 = 1 📝 예제 2: “0000z” ➞ “00010” value = 61 + 1 = 62 📌 5단계: 다시 62진수 문자열로 변환 String result = ""; for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } 증가된 10진수 값을 다시 62진수 문자열로 변환한다. 📝 예제 1: value = 1 value % 62 = 1 ➞ ‘1’ value /= 62 = - 결과: “00001” 📝 예제 2: value = 62 value % 62 = 0 ➞ ‘0’ value /= 62 = 1 value % 62 = 1 ➞ ‘1’ 최종 결과: “00010” 📌 6단계: 기존 path에서 마지막 chunk를 새로운 값으로 교체 return path.substring(0, path.length() - DEPTH_CHUNK_SIZE) + result; 원래 path의 마지막 5자리를 새로운 값으로 변경한 문자열을 반환. 📝 예제 path = "0000000000"; result = "00001"; 최종 반환값: "0000000001" 📌 전체 동작 예제 📌 예제 1 increase("0000000000"); // 기존 댓글의 path “0000000000”에서 마지막 5자리 “00000”을 추출. “00000” → 10진수 변환 ➞ 0 0 + 1 = 1 1 ➞ 62진수 변환 ➞ “00001” “0000000000” ➞ “0000000001”로 변환 ✅ 최종 결과 : “0000000001” 📌 예제 2 increase("000000000z"); // 기존 댓글의 path “000000000z”에서 마지막 5자리 “0000z”을 추출. “0000z” → 10진수 변환 ➞ 61 61 + 1 = 62 62 ➞ 62진수 변환 ➞ “00010” “000000000z” ➞ “0000000010”로 변환 ✅ 최종 결과 : “0000000010”
Backend Development
· 2025-03-05
📚[Backend Development] findChildrenTopPath(String descendantsTopPath) 메서드의 사용 방법, 사용 시기, 동작 방법
“📚[Backend Development] findChildrenTopPath(String descendantsTopPath) 메서드의 사용 방법, 사용 시기, 동작 방법” ✅1️⃣ findChildrenTopPath() 메서드의 역할 findChildrenTopPath() 메서드는 주어진 descendantsTopPath(자손 댓글의 경로)에서 현재 댓글의 직계 자식 댓글의 path를 추출하는 메서드입니다. 즉, descendantsTopPath가 현재 댓글의 여러 자식 댓글 중 하나일 때, 현재 댓글의 자식들 중 최상위 댓글의 path를 가져오는 역할을 합니다. ✅2️⃣ findChildrenTopPath() 메서드의 동작 방식 private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } 이 메서드는 다음과 같은 방식으로 동작합니다. descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE) descendantsTopPath(자손 댓글의 경로)에서 현재 댓글의 바로 다음 깊이(자식 댓글)의 path 부분만 가져옵니다. getDepth()는 현재 댓글의 깊이를 계산하는 메서드로, path.length() / DEPTH_CHUNK_SIZE를 반환합니다. (getDepth() + 1) * DEPTH_CHUNK_SIZE를 통해 현재 댓글보다 한 단계 더 깊은 위치까지의 문자열을 추출합니다. ✅3️⃣ findChildrenTopPath() 메서드의 사용 예시 📝 예제 1: 기본적인 부모-자식 관계에서 사용. CommentPath parent = CommentPath.create("00000"); // 부모 댓글 CommentPath child = CommentPath.creat("0000000000"); // 자식 댓글 CommentPath grandChild = CommentPath.create("000000000000000"); // 손자 댓글 String childrenTopPath = parent.findChildrenTopPath(grandChild.getPath()); System.out.println(childrenTopPath); // 출력: "0000000000" ▶️ 실행 과정. parent.getPath() ➞ “00000” (부모 댓글) grandChild.getPath() ➞ “000000000000000” (손자 댓글) findChildrenTopPath(grandChild.getPath()) 실행: parent.getDepth() = 1 (getDepth() + 1) * DEPTH_CHUNK_SIZE = (1 + 1) * 5 = 10 “000000000000000”.substring(0, 10) ➞ “0000000000” (부모의 직계 자식 댓글 경로 반환) 즉, 손자 댓글 path를 입력받아 현재 댓글의 첫 번째 자식의 path를 반환합니다. ✅4️⃣ findChildrenTopPath() 메서드 사용 시기 1️⃣ 새로운 대댓글을 생성할 때(createChildCommentPath()에서 사용) public CommentPath createChildCommentPath(String descentsTopPath) { if (descentsTopPath == null) { return CommentPath.creat(path + MIN_CHUNK); } String childrenTopPath = findChildrenTopPath(descendantsTopPath); return CommentPath.creat(increase(childrenTopPath)); } descendantsTopPath가 존재하면 findChildrenTopPath()를 사용하여 현재 댓글의 첫 번째 자식 댓글의 path를 가져옴. 이후 increase()를 호출하여 새 댓글의 path를 하나 증가시켜 새로운 자식 댓글을 생성함 2️⃣ 계층형 댓글 조회 시 부모-자식 관계를 파악할 떄 부모 댓글과 자식 댓글의 관계를 파악하여 트리 구조를 구성할 때 사용될 수 있음. 예를 들어, 특정 댓글이 descentdantsTopPath(자손 댓글의 path)를 가질 때, 부모 댓글의 직계 자식이 무엇인지 판단하는 데 활용될 수 있음. ✅5️⃣ findChildrenTopPath() 메서드 실행 예제 CommentPath parent = CommentPath.create("00000"); // 부모 댓글 CommentPath child = CommentPath.create("0000000000"); // 자식 댓글 CommentPath grandChild = CommentPath.create("000000000000000"); // 손자 댓글 String childPath = parent.findChildrenTopPath(grandChild.getPath()); System.out.println("부모 댓글의 첫 번째 자식 path: " + childPath); 📝 출력 부모 댓글의 첫 번째 자식 path: 0000000000 ✅6️⃣ 정리. 역할 : descendantsTopPath(자손 댓글의 경로)에서 현재 댓글의 직계 자식 댓글의 path를 추출. 동작 방식 : descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE)을 사용하여 특정 위치까지의 문자열을 반환. 사용 시기 새로운 대댓글 생성시 (createChildCommentPath() 내부에서 사용됨) 계층형 댓글을 조회할 때 부모-자식 관계 파악 주의할 점 descendantsTopPath가 null이면 substring()에서 NullPointerException이 발생할 수 있음. 댓글이 너무 깊어지면 MAX_DEPTH를 초과할 수 있음. 즉, findChildrenTopPath()는 현재 댓글의 자식 중 첫 번째 댓글의 path를 가져오는 역할을 하며, 새로운 대댓글을 생성할 때 매우 중요한 역할을 합니다.🚀
Backend Development
· 2025-03-03
📚[Backend Development] CommentPath 및 하위 댓글 생성 원리
“📚[Backend Development] CommentPath 및 하위 댓글 생성 원리” 📝 Intro 댓글 시스템에서 각 댓글의 경로(path)를 관리하는 방식은 계층적 구조를 유지하면서도 빠르게 검색할 수 있도록 설계되어야 합니다. 본 글에서는 CommentPath 클래스를 분석하고, 하위 댓글이 생성되는 과정과 관련된 로직을 설명합니다. 📌 CommentPath 클래스. package kobe.board.comment.entity; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @Getter @ToString @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommentPath { private String path; private static final String CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int DEPTH_CHUNK_SIZE = 5; private static final int MAX_DEPTH = 5; // MIN_CHUNK = "00000", MAX_CHUNK = "zzzzz" private static final String MIN_CHUNK = String.valueOf(CHARSET.charAt(0)).repeat(DEPTH_CHUNK_SIZE); private static final String MAX_CHUNK = String.valueOf(CHARSET.charAt(CHARSET.length() - 1)).repeat(DEPTH_CHUNK_SIZE); public static CommentPath create(String path) { if (isDepthOverflowed(path)) { throw new IllegalStateException("depth overflowed"); } CommentPath commentPath = new CommentPath(); commentPath.path = path; return commentPath; } private static boolean isDepthOverflowed(String path) { return calculateDepth(path) > MAX_DEPTH; } private static int calculateDepth(String path) { // 25개의 문자열 / 5 = 5depth return path.length() / DEPTH_CHUNK_SIZE; } // CommentPath 클래스의 path의 depth를 구하는 매서드 public int getDepth() { return calculateDepth(path); } // root인지 확인하는 매서드 public boolean isRoot() { // 현재의 depth가 1인지 확인해주면 됨 return calculateDepth(path) == 1; } // 현재 path의 parentPath를 반환해주는 매서드 public String getParentPath() { // 끝 5자리만 잘라내면 됨 return path.substring(0, path.length() - DEPTH_CHUNK_SIZE); } // 현재 path의 하위 댓글의 path을 만드는 매서드 public CommentPath createChildCommentPath(String descendantsTopPath) { if (descendantsTopPath == null) { return CommentPath.create(path + MIN_CHUNK); } String childrenTopPath = findChildrenTopPath(descendantsTopPath); return CommentPath.create(increase(childrenTopPath)); } // 00a0z0000200000 <- descendantsTopPath private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } private String increase(String path) { // path에서 가장 마지막 5자리를 자른 것 String lastChunk = path.substring(path.length() - DEPTH_CHUNK_SIZE); if (isChunkOverflowed(lastChunk)) { throw new IllegalStateException("chunk overflowed"); } // Character set의 길이 int charsetLength = CHARSET.length(); // lastChunk를 10진수로 먼저 변환하기 위한 값을 저장 int value = 0; for (char character : lastChunk.toCharArray()) { value = value * charsetLength + CHARSET.indexOf(character); } value = value + 1; String result = ""; for (int i = 0; i < DEPTH_CHUNK_SIZE; i++) { result = CHARSET.charAt(value % charsetLength) + result; value /= charsetLength; } return path.substring(0, path.length() - DEPTH_CHUNK_SIZE) + result; } private boolean isChunkOverflowed(String lastChunk) { return MAX_CHUNK.equals(lastChunk); } } ✅1️⃣ CommentPath 클래스 Intro CommentPath 클래스는 댓글의 고유한 경로(path)를 관리하는 역할을 합니다. 경로는 문자열로 표현되며, 각 댓글의 depth를 유지하면서 하위 댓글을 쉽게 찾을 수 있도록 구성됩니다. 📌1️⃣ 주요 상수 및 변수. CHARSET : 0-9, A-Z, a-z로 구성된 총 62개의 문자 셋 DEPTH_CHUNK_SIZE = 5 : 댓글 깊이를 나타내는 단위 크기 MAX_DEPTH = 5 : 댓글의 최대 깊이 MIN_CHUNK = “00000” : 경로에서 사용될 최소 단위 문자열 MAX_CHUNK = “zzzzz” : 경로에서 사용될 최대 단위 문자열 ✅2️⃣ CommentPath.create(String path) 메서드 동작 과정 이 메서드는 새로운 CommentPath 객체를 생성하는 정적 팩토리 메서드입니다. 📌1️⃣ 동작 과정. 1. isDepthOverflowed(path)를 호출하여 path의 깊이가 MAX_DEPTH = 5를 초과하는지 확인합니다. calculateDepth(path) > MAX_DEPTH이면 IllegalStateException을 던집니다. 2. CommentPath 객체를 생성합니다. 3. 객체의 path 필드를 path로 설정합니다. 4. 새로 생성된 CommentPath 객체를 반환합니다. ✅3️⃣ CommentPath.createChildCommentPath(String descendantsTopPath) 메서드 동작 과정 이 메서드는 주어진 descendantsTopPath를 기반으로 새로운 하위 댓글의 path를 생성하는 역할을 합니다. 📌1️⃣ 동작 과정. 1. descedantsTopPath가 null이면 path + MIN_CHUNK(“00000”)를 추가하여 CommentPath를 생성하고 반환합니다. 2. findChildrenTopPath(descendantsTopPath)를 호출하여 childrenTopPath를 찾습니다. descendantsTopPath에서 depth + 1 길이만큼 문자열을 자릅니다. 3. increase(childrenTopPath)를 호출하여 이전 하위 댓글 path 값에서 1을 증가시킵니다. 4. 증가된 childrenTopPath를 기반으로 새로운 CommentPath 객체를 생성하고 반환합니다. ✅4️⃣ “00000”이 생성되려면 어떤 과정을 거쳐야 할까? 1. createChildCommentPath(null)이 호출됩니다. 2. descendantsTopPath가 null이므로 path + MIN_CHUNK가 CommentPath.creat()에 전달됩니다. 3. CommentPath.create() 내부에서 isDepthOverflowed 검사를 통과하면 새로운 CommentPath 객체가 생성 됩니다. 4. path 값은 부모 path + “00000”이 됩니다. ✅5️⃣ “0000000000”이 생성되려면 어떤 과정을 거쳐야 할까? 1. createdChildCommentPath(null)이 두 번 호출되어야 합니다. 2. 첫 번째 호출: descendantsTopPath = null path + “00000”을 CommentPath.create()로 전달하여 “00000”이 생성됨. 3. 두 번째 호출: descendantsTopPath = “00000” “00000”의 하위 댓글을 만들 때 path + “00000”을 추가하여 “0000000000”을 생성. 즉, 2단계 하위 댓글 생성 과정이 필요합니다. ✅6️⃣ 특정 descendantsTopPath가 주어졌을 때 childrenTopPath가 어떻게 변할까요? 📌 예시: descendantsTopPath = “00000”, childrenTopPath = “00001” 1. createChildCommentPath(“00000”)가 호출됨. 2. findChildrenTopPath(“00000”) 호출: descendantsTopPath = “00000” getDepth() + 1 = 2 → depth 2 까지의 5자리(00000)를 가져옴. childrenTopPath = “00000” 3. increase(“00000”) 실행: 62진수 연산으로 “00000” → “00001”로 변환. 4. “00001”이 path로 설정된 CommentPath 객체가 생성됨. 즉, increase(“00000”) 연산을 통해 “00001”이 생성됩니다. ✅7️⃣ 특정 상황에서 새로운 댓글의 path를 생성하는 과정 📌 예시: descendantsTopPath = “0000z”, 하위 댓글이 “abcdz” > “zzzzz” > “zzzzz” 일 때, “abcdz”의 sibling 댓글 생성 1. 현재 descendantsTopPath는 “0000z”이므로 이 댓글이 속한 하위 댓글들이 존재함. 2. createChildCommentPath(“zzzzz”) 실행 → 가장 큰 하위 path를 기준으로 새로운 path를 생성해야 함. 3. findChildCommentTopPaht(“zzzzz”) 실행: “zzzzz”의 depth + 1 길이까지 자름 → “zzzzz” 4. increase(“zzzzz”) 실행: “zzzzz”에서 증가된 값 “aaaaa”가 나옴 하지만 “abcdz” 와 같은 depth의 새로운 댓글을 만들려면 “abcdz”의 sibling 댓글이 되어야 함. 5. increase(“abcdz”) 실행: “abcdz”에서 증가된 값 “abce0”가 생성됨. 📌 결론 : 최종적으로 생성될 “abce0”의 “path”는 부모 “path” + “abce0” 즉, path = “0000zabce0”이 됩니다. ✅8️⃣ 결론 CommentPath를 이용하면 계층적 댓글 시스템을 효과적으로 구현할 수 있습니다. 하위 댓글의 path를 62진수 문자열 증가 방식으로 관리하여, 빠른 정렬 및 검색이 가능합니다. increase 연산을 통해 하위 댓글을 동적으로 생성하며, 경로 오버플로우를 방지할 수 있습니다. 이러한 방식은 트리 구조를 효율적으로 저장하고 조회하는 방법으로, 대규모 댓글 시스템에서도 유용하게 적용될 수 있습니다.
Backend Development
· 2025-03-01
📚[Backend Development] Path 구조의 이해.
“📚[Backend Development] Path 구조의 이해.” ✅1️⃣ “00a0z 00002”의 하위 댓글은 무엇일까요? “00a0z 00002”의 하위 댓글은 “00a0z 00002 00000” 입니다. 즉, “00a0z 00003”이 아니라 “00a0z 00002 00000”이 하위 댓글입니다. ✅2️⃣ 하위 댓글이 “00a0z 00002 00000”인 이유? 📌1️⃣ Path 구조 이해. CommentPath에서 댓글의 path는 부모 댓글의 path + 하위 댓글의 5자리 문자열로 구성됩니다. 각 댓글의 path는 DEPTH_CHUNK_SIZE = 5 기준으로 5자리씩 증가합니다. depth가 깊어질수록 path 문자열 길이가 길어집니다. 📝 예시: 부모 댓글: 00a0z -> 첫 번째 하위 댓글: 00a0z00000 -> 두 번째 하위 댓글: 00a0z0000000000 즉, 하위 댓글의 path는 부모 댓글 path를 포함하며, 5자리씩 추가됩니다. 📌2️⃣ descendantsTopPath vs childrenTopPath 1️⃣ childrenTopPath (현재 댓글의 직접적인 하위) String childrenTopPath = findChildrenTopPath(descendantsTopPath); private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } descendantsTopPath = “00a0z0000200000” childrenTopPath = “00a0z00002” (5자리씩 자름) 2️⃣ descendantsTopPath (현재 댓글의 모든 자손 중 가장 큰 path) descendantsTopPath는 현재 댓글이 포함된 전체 하위 트리에서 가장 마지막 댓글의 path입니다. 즉, 00a0z00002의 모든 자손 댓글 중 가장 마지막 댓글이 00a0z0000200000 입니다. 📌 결론 00a0z00002의 직접적인 하위 댓글은 00a0z0000200000 입니다. 00a0z00003은 00a0z00002와 같은 depth에서 생성될 새로운 sibling 댓글일 뿐, 00a0z00002의 하위 댓글이 아닙니다. 🚀 정리. ✅ 00a0z00002의 하위 댓글은 00a0z0000200000이다. ✅ 댓글 구조에서 부모 댓글의 path + 5자리 문자열이 하위 댓글의 path가 된다. ✅ descendantsTopPath를 활용해 모든 자손 중 가장 마지막 댓글을 찾고, 이를 기반으로 새로운 댓글의 path를 결정한다. ✅ “00a0z00003”은 같은 depth의 sibling(형제 댓글)이지, 하위 댓글이 아니다.
Backend Development
· 2025-02-25
📚[Backend Development] findChildTopPath 메서드의 실행 결과와 동작 방식.
“📚[Backend Development] findChildTopPath 메서드의 실행 결과와 동작 방식.” ✅1️⃣ 예시 코드. package kobe.board.comment.entity; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @Getter @ToString @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommentPath { private String path; private static final String CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int DEPTH_CHUNK_SIZE = 5; private static final int MAX_DEPTH = 5; // MIN_CHUNK = "00000", MAX_CHUNK = "zzzzz" private static final String MIN_CHUNK = String.valueOf(CHARSET.charAt(0)).repeat(DEPTH_CHUNK_SIZE); private static final String MAX_CHUNK = String.valueOf(CHARSET.charAt(CHARSET.length() - 1)).repeat(DEPTH_CHUNK_SIZE); public static CommentPath create(String path) { if (isDepthOverflowed(path)) { throw new IllegalStateException("depth overflowed"); } CommentPath commentPath = new CommentPath(); commentPath.path = path; return commentPath; } private static boolean isDepthOverflowed(String path) { return calculateDepth(path) > MAX_DEPTH; } private static int calculateDepth(String path) { // 25개의 문자열 / 5 = 5depth return path.length() / DEPTH_CHUNK_SIZE; } // CommentPath 클래스의 path의 depth를 구하는 매서드 public int getDepth() { return calculateDepth(path); } // root인지 확인하는 매서드 public boolean isRoot() { // 현재의 depth가 1인지 확인해주면 됨 return calculateDepth(path) == 1; } // 현재 path의 parentPath를 반환해주는 매서드 public String getParentPath() { // 끝 5자리만 잘라내면 됨 return path.substring(0, path.length() - DEPTH_CHUNK_SIZE); } // 현재 path의 하위 댓글의 path을 만드는 매서드 public CommentPath createChildCommentPath(String descendantsTopPath) { if (descendantsTopPath == null) { return CommentPath.create(path + MIN_CHUNK); } String childrenTopPath = findChildrenTopPath(descendantsTopPath); return CommentPath.create(increase(childrenTopPath)); } // 00a0z 00002 00000 <- descendantsTopPath private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } } ✅2️⃣ findChildrenTopPath(String descendantsTopPath) 실행 결과. descendantsTopPath에 “00a0z0000200000” 값이 들어간다고 가정한다. findChildrenTopPath 메서드는 현재 객체의 depth를 기반으로 descendantsTopPath의 특정 길이만큼 잘라낸 값을 반환합니다. 현재 path의 depth는 getDepth() 메서드를 통해 계산됩니다. getDepth()는 현재 path의 길이를 DEPTH_CHUNK_SIZE(5)로 나누어 구합니다. 📌 예제 실행. 예를 들어, CommentPath 객체가 path = “00a0z”라면: getDepth() = 1(문자열 길이 5/5) (getDepth() + 1) * DEPTH_CHUNK_SIZE = (1 + 1) * 5 = 10 descendantsTopPath.substring(0, 10) 📌 결과값: "00a0z00002" ✅3️⃣ findChildrenTopPath 메서드의 동작 방식 private String findChildrenTopPath(String descendantsTopPath) { return descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); } 📌 단계별 동작. 📌1️⃣ 현재 객체의 depth를 구함. getDepth()를 호출하여 현재 path가 몇 단계인지 계산. getDepth()는 path.length() / DEPTH_CHUNK_SIZE로 계산됨. 📌2️⃣ (getDepth() + 1) * DEPTH_CHUNK_SIZE 값 계산. 현재 depth에서 하위 댓글의 depth를 포함한 길이를 계산. 즉, 다음 depth까지 포함한 descendantsTopPath의 일부만 가져오도록 설정, 📌3️⃣ descendantsTopPath의 일부를 잘라 반환. descendantsTopPath.substring(0, (getDepth() + 1) * DEPTH_CHUNK_SIZE); descendantsTopPath에서 앞 부분을 가져와 childrenTopPath를 생성. 🚀 결론 findChildrenTopPath(“00a0z0000200000”)의 실행 결과는 “00a0z00002” 이 메서드는 descendantsTopPath에서 현재 depth 기준으로 한 단계만 더 포함한 경로를 잘라 반환. 결국 현재 객체의 하위 댓글들이 공통적으로 가지는 prefix를 찾아주는 역할을 한다.
Backend Development
· 2025-02-25
📚[Backend Development] 무한 Depth 댓글 조회 - Path Enumeration & 인덱스 최적화
“📚[Backend Development] 무한 Depth 댓글 조회 - Path Enumeration & 인덱스 최적화” ✅1️⃣ Path Enumeration 방식에서 댓글 path 결정 Path Enumeration(경로 열거) 방식은 각 댓글의 path를 계층적으로 저장하여 정렬 및 검색을 빠르게 수행하는 기법입니다. 댓글이 추가될 때 부모 댓글의 path를 상속받고, 하위 댓글 중 가장 큰 path를 기준으로 새로운 path가 결정됩니다. 📌1️⃣ 현재 댓글 트리 구조 현재 댓글 트리의 path는 다음과 같습니다. 00a0z 댓글 아래에 계층적으로 정렬된 하위 댓글들이 있습니다. 댓글의 path는 부모 댓글의 path를 상속받아 00000, 00001, 00002 등으로 추가됩니다. 📌2️⃣ 새로운 댓글 추가 요청 사용자가 00a0z 댓글의 하위에 새로운 댓글을 추가하려고 합니다. 새로운 댓글이 추가될 path를 결정해야 합니다. 기존 댓글 중 가장 큰 path를 찾아 이를 기준으로 +1을 적용하여 새로운 path를 생성합니다. 📌3️⃣ childrenTopPath 찾기 새로운 댓글을 추가할 때 현재 존재하는 하위 댓글 중 가장 큰 path(childrenTopPath)를 찾고 해당 값에 +1을 하여 새로운 댓글의 path를 생성합니다. 현재 00a0z의 하위 댓글 중 가장 큰 path는 00a0z 00002입니다. 따라서, 새로운 댓글의 path는 00a0z 00003이 됩니다. 📌4️⃣ descendantsTopPath를 고려한 최종 path 결정 하지만 자식 댓글이 존재하는 경우, 단순히 childrenTopPath만 고려하면 안 됩니다. 가장 깊은 depth까지 고려한 descendantsTopPath를 찾아야 합니다. descendantsTopPath는 부모 댓글을 포함한 모든 자식 댓글 중 가장 큰 path입니다. childrenTopPath = 00a0z 00002이므로, 새로운 댓글의 path는 00a0z 00003이 됩니다. ✅2️⃣ MySQL에서 descendantsTopPath 찾기. Path Enumeration 방식에서는 빠른 검색을 위해 인덱스를 활용할 수 있습니다. 특히 descendantsTopPath를 찾을 때 Backward Index Scan을 사용하면 성능을 최적화할 수 있습니다. 📌1️⃣ descendantsTopPath를 찾는 SQL SELECT path FROM comment_v2 WHERE article_id = {article_id} AND path > {parentPath} -- 부모 댓글 제외 AND path LIKE {parentPath}% -- 부모 댓글 prefix를 포함하는 모든 자식 댓글 조회 ORDER BY path DESC LIMIT 1; -- 가장 큰 path를 찾기 위해 내림차순 정렬 가장 큰 path(descendantsTopPath)를 찾을 때 내림차순 정렬을 활용합니다. LIMIT 1을 사용하여 불필요한 데이터 조회를 줄이고 성능을 최적화합니다. 📌2️⃣ Backward Index Scan 활용 MySQL에서는 ORDER BY path DESC를 사용할 때 역순으로 인덱스를 탐색하는 Backward Index Scan을 수행합니다. path 필드에 오름차순(ASC) 인덱스가 설정되어 있어도, 내림차순(DESC) 정렬을 통해 가장 큰 path를 빠르게 찾을 수 있습니다. 인덱스 트리(Leaf Node) 간의 양방향 포인터를 활용하여 역순 검색을 수행합니다. ✅3️⃣ MySQL Query Plan 분석(EXPLAIN) MySQL에서 descendantsTopPath를 찾는 쿼리의 실행 계획을 분석해봅니다. EXPLAIN SELECT path FROM comment_v2 WHERE article_id = 1 AND path > '00a0z' AND path LIKE '00a0z%' ORDER BY path DESC LIMIT 1; 📌EXPLAIN 결과 분석 1.idx_article_id_path 인덱스 사용됨 ➞ 인덱스를 활용하여 빠르게 path를 조회할 수 있음 2. Backward Index Scan 적용됨 ➞ ORDER BY path DESC LIMIT 1을 통해 역순 탐색 수행 3. Using Index 적용됨 ➞ 인덱스에서 직접 데이터를 가져오기 때문에 성능 최적화 가능 ✅4️⃣ descendantsTopPath를 활용한 정렬 최적화 Path Enumeration 방식에서는 정렬 상태를 유지한 채 descendantsTopPath를 검색할 수 있습니다. path 정렬 상태를 유지하면서 역순으로 인덱스를 탐색하면, 가장 큰 path(descendantsTopPath)를 빠르게 찾을 수 있습니다. Backward Index Scan을 활용하면 로그 시간(log time) 내에 조회가 가능합니다. ✅5️⃣ 결론 🚀 Path Enumeration 방식에서 댓글을 추가할 때, path를 결정하는 과정. ✅ 가장 큰 childrenTopPath를 찾고, +1을 하여 새로운 path를 생성 ✅ descendantsTopPath를 찾아 계층 구조를 유지하면서 댓글을 정렬 ✅ MySQL의 Backward Index Scan을 활용하여 빠르게 descendantsTopPath를 검색 ✅ 대규모 데이터에서도 인덱스를 활용하여 빠른 성능을 유지 📌 Path Enumeration 방식을 사용할 때는, Backward Index Scan을 활용하여 최적의 성능을 보장하는 것이 중요.
Backend Development
· 2025-02-24
📚[Backend Development] 무한 Depth 댓글 조회 - 문자열 기반 경로 관리, 덧셈 연산, 예외 처리
“📚[Backend Development] 무한 Depth 댓글 조회 - 문자열 기반 경로 관리, 덧셈 연산, 예외 처리” ✅1️⃣ 문자열 기반 댓글 경로(Path) 관리 Path Enumeration 방식에서 숫자가 아닌 문자열을 기반으로 댓글의 경로를 관리해야 합니다. 📌 Path 문자열 연산의 핵심. 댓글 경로를 문자열로 관리하기 때문에, 덧셈 연산을 수행할 때 숫자가 아닌 문자열 기반 연산이 필요합니다. 대소문자 및 숫자 간의 관계(0~9 < A~Z < a~z)를 이해하고, 문자열을 증가시키는 로직이 필요합니다. 0~9 < A~Z < a~z 이러한 정렬 규칙을 이해하면, 댓글의 경로(Path)를 증가시키는 연산을 코드로 구현할 수 있습니다. ✅2️⃣ “00000”부터 “zzzzz”까지 증가하는 문자열 연산 Path 값은 “00000”부터 시작하여 증가하는 방식으로 관리됩니다. 📌 문자열 기반 정렬 방식. “00000” → “00001” → … → “AAAA9” → “AAAAA” → … → “zzzzz”로 증가 문자열의 대소문자 순서를 활용하여 정렬 (0-9, A-Z, a-z) 댓글이 추가될 때마다 이전 댓글의 경로에 1을 더하여 새로운 경로 생성 ✅3️⃣ 문자열 기반 덧셈(증가) 연산. Path 값이 문자열로 관리되므로, 숫자가 아니라 문자열 덧셈을 수행하는 알고리즘이 필요합니다. 📌 문자열 덧셈 방식 오른쪽 문자부터 1씩 증가 carry(올림수)가 있으면 다음 문자도 증가 “zzzzz”에 도달하면 Overflow 발생 📝 예제: a39zz + 1 ------ a3A00 z를 넘어가면 0으로 초기화되고, 앞자리 숫자가 증가함. carry를 반영하여 재귀적으로 처리. ✅4️⃣ 숫자 기반 덧셈 방식 문자열 연산이 복잡할 수 있으므로, 62진수 변환 후 숫자로 연산하는 방법도 고려할 수 있습니다. 📌 숫자로 변환 후 연산하는 방법. 62진수 문자열을 10진수 숫자로 변환 숫자로 덧셈 수행 다시 62진수 문자열로 변환 62진수 ("00000" ~ "zzzzz") → 10진수 변환 → +1 연산 → 다시 62진수 변환 이 방법을 사용하면 문자열 연산보다 효율적인 방식으로 Path를 증가시킬 수 있습니다. ✅5️⃣ 예외 케이스 처리 - 최초 댓글 생성 Path를 관리할 때, 최조 댓글이 생성되는 경우를 처리해야 합니다. 📌 처리 방법. 부모 댓글(00a0z)의 하위 댓글이 없는 경우 첫 번째 하위 댓글을 생성해야 함 Path 값은 부모 Path + “00000”로 설정 📝 예제: 부모 Path: 00a0z 첫 번째 하위 댓글 Path: 00a0z 0000 ✅6️⃣ 댓글 경로가 zzzzz까지 도달한 경우. Path 값이 zzzzz까지 증가하면, 더 이상 새로운 Path를 생성할 수 없는 문제가 발생합니다. 📌 해결 방법. Path를 구성하는 문자 개수를 증가(5 → 6, 7 …) 더 넓은 범위의 Path 값을 허용하도록 개선 62^5 = 16,132,832 (기존) 62^6 = 998,001,488 (확장 가능) Path의 자리 수를 늘리면 더 많은 댓글을 저장하고 정렬 가능합니다. ✅7️⃣ 최종 정리 📌 무한 Depth 댓글 정렬을 위해 고려해야 할 사항 Path 값을 문자열로 관리해야 하므로, 문자열 기반 덧셈 연산이 필요 숫자로 변환하여 62진수 연산을 수행하는 방식도 가능 최초 댓글 생성 시 기본 Path(“00000”)을 추가 Path가 가득 찬 경우, 자리 수를 늘려 더 많은 경로를 저장 가능
Backend Development
· 2025-02-24
📚[Backend Development] 무한 Depth 댓글 정렬 구조의 'Path Enumeration(경로 열거) 방식'이란 무엇일까요?
“📚[Backend Development] 무한 Depth 댓글 정렬 구조의 ‘Path Enumeration(경로 열거) 방식’이란 무엇일까요?” ✅ 무한 Depth 댓글 정렬 구조의 “Path Enumeration(경로 열거) 방식” 설명. Path Enumeration(경로 열거) 방식은 트리 구조의 계층을 문자열 형태로 저장하여 정렬 및 검색을 효율적으로 수행하는 방식입니다. 이 방식은 트리의 부모-자식 관계를 유지하면서 빠르게 정렬 및 조회할 수 있도록 도와줍니다. 🏗️1️⃣ Path Enumeration(경로 열거) 방식이란? Path Enumeration(경로 열거) 방식에서는 각 댓글의 부모-자식 관계를 문자열 경로(path)로 저장합니다. 즉, 각 댓글이 트리 구조에서 어떤 위치에 있는지 경로를 미리 기록하여 정렬 및 검색을 최적화합니다. 🏗️2️⃣ 테이블 구조. Path Enumeration(경로 열거) 방식을 사용하면 다음과 같은 추가적인 path 컬럼이 필요합니다. 필드명 설명 comment_id 댓글의 고유 ID (PK) parent_comment_id 부모 댓글의 ID (최상위 댓글이면 NULL) article_id 해당 댓글이 속한 게시글 ID content 댓글 내용 created_at 댓글 작성 시간 path 댓글의 계층 구조를 나타내는 문자열 (예: “00001.00002.00005” 📌 path 필드는 각 댓글이 트리 구조에서 어디에 속하는지 나타냄 📌 이 값을 활용하면 부모-자식 관계를 정렬 및 조회하는 것이 쉬워짐 🏗️3️⃣ Path 값 저장 방식. path 값은 댓글이 트리에서 어떤 위치에 있는지를 나타냅니다. 각 comment_id를 5자리 문자열(00001, 00002 등)로 변환하여 부모-자식 관계를 저장합니다. 📌 Path 값 예시 comment_id parent_comment_id path 1 NULL 00001 2 1 00001.00002 3 NULL 00003 4 2 00001.00002.00004 5 4 00001.00002.00004.00005 6 NULL 00006 📌 각 댓글은 부모 path를 상속받고, 자신의 ID를 추가하여 path를 생성 📌 부모 댓글이 삭제되더라도 path를 통해 계층 구조를 쉽게 유지 가능 🏗️4️⃣ Path Enumeration(경로 열거)을 활용한 정렬. Path Enumeration(경로 열거)을 활용하면 경로 순서대로 정렬하면 댓글을 계층 구조 그대로 유지할 수 있습니다. SELECT * FROM comment WHERE article_id = ? ORDER BY path ASC; 📌 ORDER BY path ASC를 적용하면 트리 구조를 유지하면서 정렬됨 📌 일반적인 ORDER BY parent_comment_id, comment_id보다 트리 구조 정렬이 정확함 🏗️5️⃣ Path Enumeration(경로 열거) 방식으로 조회 📌 예제 데이터 comment_id parent_comment_id path 1 NULL 00001 2 1 00001.00002 3 NULL 00003 4 2 00001.00002.00004 5 4 000001.00002.00004.00005 6 NULL 00006 📌 정렬된 결과. SELECT * FROM comment WHERE article_id = ? ORDER BY path ASC; ✅ 출력 결과 1. 댓글1 ├── 댓글2 ├── 댓글4 ├── 댓글5 2. 댓글3 3. 댓글6 📌 계층 구조가 정확하게 유지되면서 정렬됨. 🏗️6️⃣ 특정 댓글의 하위 댓글 조회. Path Enumeration(경로 열거) 방식에서는 특정 댓글의 모든 하위 댓글을 손쉽게 조회할 수 있습니다. SELECT * FROM comment WHERE path LIKE '00001.00002%' ORDER BY path ASC; 📌 결과: 00001.00002로 시작하는 모든 하위 댓글을 조회 (댓글2, 댓글4, 댓글5 포함) 🏗️7️⃣ Path Enumeration 방식의 장점과 단점. ✅ 장점. 장점 설명 트리 구조 유지가 쉬움 ORDER BY path ASC만으로 계층 구조 정렬 가능 하위 댓글 조회가 빠름 LIKE ‘경로%’로 손쉽게 하위 댓글 조회 가능 부모 댓글 삭제 시 계층 구조 유지 가능 path를 통해 상위 댓글을 식별 가능 ❌ 단점. 단점 설명 댓글 이동 시 path 업데이트 필요 댓글을 다른 부모로 이동하면 path를 변경해야 함 path 길이 증가 가능성 댓글이 깊어질수록 path 길이가 길어질 수 있음 INSERT 성능 저하 가능성 새로운 댓글 추가 시 path를 계산해야 함 🏗️8️⃣ Path Enumeration(경로 열거) 방식에서 댓글 저장 규칙. 위 그림과 같이, 각 depth(계층)별로 5개의 문자열로 경로 정보를 저장합니다. 1 depth는 5자리 문자열, 2 depth는 10자리, 3 depth는 15자리 N depth는 (N * 5)자리로 표현됩니다. 각 댓글의 경로는 모든 상위 댓글에서 해당 댓글까지의 경로를 포함하도록 저장됩니다. 경로는 부모 경로를 상속하며, 독립적이면서 순차적인 형태를 유지합니다. 📌 이 방식은 댓글의 계층 구조를 명확하게 표현하고, 정렬 및 검색을 효율적으로 수행할 수 있도록 도와줍니다. 🏗️9️⃣ Path Enumeration 방식의 계층형 댓글 구조 예시 좌측 그림의 계층형 댓글 구조는, 우측 그림과 같은 경로 정보를 가질 수 있습니다. 각 경로는 부모 댓글의 경로를 상속받으며, 각 댓글마다 독립적이고 순차적인 경로(문자열 정렬 기준)가 생성됩니다. 📌 이 방식은 댓글을 계층적으로 정렬하고, 빠르게 검색할 수 있도록 도와줍니다. 🏗️1️⃣0️⃣ Path Enumeration 방식에서 경로 표현 범위 확장 방법 각 경로는 depth(계층)마다 5자리의 문자로 표현되므로, 사용할 수 있는 경로의 개수에는 제한이 있습니다. 만약 각 자릿수를 숫자 (0~9)로만 사용한다면, 한 depth당 10⁵ = 100,000개(00000 ~ 99999)의 경로만 표현할 수 있습니다. 하지만 문자열이기 때문에, 반드시 숫자(0~9)만 사용할 필요는 없습니다. 각 자릿수는 0~9(10개), A~Z(26개), a~z(26개) 총 62개의 문자를 사용할 수 있습니다. 문자열의 정렬 순서는 숫자(0~9) ➞ 대문자 알파벳(A~Z) ➞ 소문자 알파벳(a~z) 순서로 지정됩니다. 따라서, 경로는 00000부터 zzzzz까지 순차적으로 생성됩니다. 이 방식에서는 한 depth당 62⁵ = 16,132,832개의 경로를 표현할 수 있습니다. 📌 이러한 방식으로 경로 표현 범위를 확장하면, 더 많은 댓글을 저장할 수 있으며 트리 구조를 더욱 유연하게 유지할 수 있습니다. 🚀 정리. ✅ Path Enumeration(경로 열거) 방식은 댓글의 계층 구조를 문자열(path)로 저장하는 방식 ✅ ORDER BY path ASC를 사용하여 트리 구조를 유지하면서 정렬 가능 ✅ 하위 댓글 조회 시 LIKE ‘경로%’를 활용하여 빠르게 검색 가능 ✅ 부모 댓글이 삭제되더라도 계층 구조를 유지하는 데 유리 ✅ 댓글 이동이 빈번한 경우 path 업데이트가 필요하므로 조심해야 함 ✅ Path Enumeration 방식은 무한 Depth 댓글 정렬 및 조회 성능을 최적화할 수 있는 가장 효과적인 방법 중 하나입니다. ✅ 트리 구조를 유지하면서 ORDER BY path ASC만으로 정렬이 가능하여 성능이 우수합니다. ✅ 경로 길이가 길어지는 단점을 해결하기 위해 Base62와 같은 방식을 고려할 수도 있습니다. 📌 무한 Depth 댓글 정렬 및 조회 성능을 최적화할 수 있는 가장 효과적인 방법 중 하나입니다.
Backend Development
· 2025-02-21
📚[Backend Development] Path Enumeration 방식에서 댓글의 경로(Path) 결정 과정.
“📚[Backend Development] Path Enumeration 방식에서 댓글의 경로(Path) 결정 과정.” 📌1️⃣ 신규 댓글의 경로를 결정하는 과정 Path Enumeration(경로 열거) 방식을 사용할 때, 새로운 댓글이 추가될 경우 해당 댓글의 path를 어떻게 결정할 것인지가 중요합니다. 이 글에서는 이미 존재하는 계층형 댓글 트리에서 새로운 댓글이 추가될 때, path를 어떻게 생성하는지에 대해 설명합니다. 🏗️2️⃣ 기존 댓글 구조 확인 ✅ 기존 댓글 트리 초기 댓글 구조는 위와 같습니다. 최상위 댓글 00a0z 아래에 여러 개의 하위 댓글이 존재합니다. 각 댓글의 path 계층 구조를 따라 부모 댓글의 path를 상속받으며, 새로운 댓글이 추가될 때마다 숫자가 증가하는 방식으로 정렬됩니다. 가장 최근의 하위 댓글은 00a0z 00002이며, 00a0z 00002의 하위 댓글로 00a0z 00002 00000이 존재합니다. 🏗️3️⃣ 신규 댓글 추가 요청. ✅ 새로운 댓글 요청 어떤 사용자가 00a0z 댓글의 하위에 새로운 댓글을 작성하려고 합니다. 하지만, 현재 00a0z의 하위 댓글들은 이미 존재하고 있으므로, 새로운 댓글이 들어갈 올바른 path를 결정해야 합니다. 🏗️4️⃣ 현재 존재하는 하위 댓글 중 가장 큰 path 찾기 ✅ childrenTopPath 찾기 새로운 댓글을 추가할 때는, 현재 존재하는 하위 댓글 중 가장 큰 path(childrenTopPath)를 찾아서 그 값에 +1을 하여 새로운 댓글의 path를 생성합니다. 현재 00a0z의 하위 댓글 중에서 가장 큰 path는 00a0z 00002입니다. 따라서, 새로운 댓글의 path는 00a0z 00003이 됩니다. 🏗️5️⃣ 모든 자식 댓글을 고려한 descendantsTopPath 찾기 하지만, 단순히 childrenTopPath만 고려하면 안됩니다. 자식 댓글이 있는 경우, 가장 깊은 depth에 있는 자식 댓글까지 고려하여 path를 결정해야 합니다. descendantsTopPath는 부모 댓글을 포함한 모든 자식 댓글 중 가장 큰 path를 의미합니다. 즉, 00a0z의 모든 하위 댓글 중 가장 깊은 depth를 가지면서도 가장 큰 path를 찾습니다. 🏗️6️⃣ descendantsTopPath에서 신규 댓글의 depth에 맞는 childrenTopPath 계산 ✅ descendantsTopPath를 기반으로 path 생성 기존 댓글 중 가장 깊은 depth를 가지는 descendantsTopPath를 찾고, 신규 댓글이 들어갈 depth만큼의 path를 남기고 나머지는 잘라냅니다. descendantsTopPath = 00a0z 00002 00000 하지만 신규 댓글이 들어갈 depth는 2이므로, (depth * 5)만큼의 문자만 남깁니다. 결과적으로 childrenTopPath = 00a0z 00002가 됩니다. 🏗️7️⃣ 최종적으로 childrenTopPath를 찾아 신규 댓글의 path 생성 ✅ 최종 path 결정 1. parentPath를 가지는 모든 자식 댓글을 조회 2. 가장 큰 descendantsTopPath를 찾음 3. 신규 댓글이 들어갈 depth만큼 path를 남기고 자름 → childrenTopPath 생성 4. 마지막 숫자에 +1을 하여 최종 path 결정 📌 결과적으로, 새로운 댓글의 path는 00a0z 00003이 됩니다. 🚀8️⃣ 결론. ✅ Path Enumeration 방식을 사용하면, 댓글의 계층 구조를 명확하게 유지하면서도 정렬 및 조회를 빠르게 수행할 수 있습니다. ✅ 신규 댓글이 추가될 때는, 현재 존재하는 하위 댓글 중 가장 큰 path(descendantsTopPath)를 찾아서 새로운 path를 결정합니다. ✅ 이 방식은 무한 Depth 댓글에서도 정렬 순서를 유지하면서 빠르게 댓글을 추가할 수 있도록 도와줍니다.
Backend Development
· 2025-02-21
📚[Backend Development] 무한 depth 댓글 정렬 구조란 무엇일까요?
“📚[Backend Development] 무한 depth 댓글 정렬 구조란 무엇일까요?” ✅ 무한 Depth 댓글 정렬 구조 무한 depth 댓글을 페이징 처리하기 위해서는 트리 구조를 유지하면서 정렬하는 전략이 필요합니다. 보통 parent_comment_id + comment_id 정렬 방식을 사용하는 2-depth 댓글과는 달리, 무한 depth의 경우 댓글의 계층 구조를 유지할 수 있도록 정렬 방식이 개선되어야 합니다. 🚀1️⃣ 트리 구조 기반 정렬 방법 무한 depth의 댓글을 정렬하려면 다음과 같은 방식이 가능합니다. 1️⃣ 정렬 방식 root_comment_id (최상위 부모 ID) 오름차순 path (트리 순서) 오름차순 comment_id (작성 순서) 오름차순 이렇게 정렬하면 트리 구조를 유지하면서 댓글을 시간순으로 정렬할 수 있습니다. 🚀2️⃣ 트리 구조를 유지하는 정렬 필드 필드명 설명 comment_id 댓글의 고유 ID (기본 키) parent_comment_id 부모 댓글의 ID (최상위 댓글이면 NULL) root_comment_id 최상위 부모 댓글의 ID (최상위 댓글이면 자기 자신 comment_id) depth 댓글의 깊이 (0부터 시작) path 트리 구조를 나타내는 정렬용 문자열 🚀3️⃣ 정렬 순서 SQL 예제 📌 무한 Depth 정렬을 위한 ORDER BY SELECT * FROM comment WHERE article_id = ? ORDER BY root_comment_id ASC, path ASC, comment_id ASC LIMIT ?, ?; 📌 정렬 기준. 1. root_comment_id ASC ➞ 최상위 부모 댓글 기준으로 정렬 2. path ASC ➞ 트리 구조를 유지하면서 정렬 3. comment_id ASC ➞ 같은 depth 내에서 작성 순서대로 정렬 🚀4️⃣ path 필드란? 트리 구조를 표현하기 위해 path 필드를 활용할 수 있습니다. path는 부모-자식 관계를 명확하게 하여 정렬을 용이하게 합니다. 예를 들어, path는 다음과 같은 방식으로 저장될 수 있습니다. comment_id parent_comment_id root_comment_id depth path 1 NULL 1 0 00001 2 1 1 1 00001.00002 3 1 1 1 00001.00003 4 2 1 2 00001.00002.00004 5 4 1 3 00001.00002.00004.00005 📌 정렬 시 ORDER BY path ASC를 사용하면 계층 구조를 유지하면서 정렬 가능! 🚀5️⃣ 정리 ✅ 무한 depth 댓글 정렬을 위헤 path 또는 lft/rgt 방식이 필요 ✅ 정렬 순서는 root_comment_id ASC, path ASC, comment_id ASC 방식 사용 ✅ 페이징 처리 시 LIMIT ?,? 적용 가능 📌 기존 2-depth 방식처럼 parent_comment_id 정렬만으로는 무한 depth 정렬이 어려우므로 path를 활용하는 것이 가장 적절합니다.
Backend Development
· 2025-02-20
📚[Backend Development] 최대 2 Depth 댓글 정렬 구조의 '페이지 번호 방식'이란 무엇일까요?
“📚[Backend Development] 최대 2 Depth 댓글 정렬 구조의 ‘페이지 번호 방식’이란 무엇일까요?” ✅ 최대 2 Depth 댓글 정렬 구조의 “페이지 번호 방식” 설명. 최대 2 Depth 댓글을 페이지 번호 기반으로 조회하는 방식은 고정된 개수의 댓글을 불러오는 전통적인 페이징 방식입니다. 이를 통해 오래된 댓글부터 순서대로 불러올 수 있습니다. 🏗️1️⃣ 페이지 번호 방식이란? 페이지 번호 방식은 특정 페이지의 댓글을 불러오기 위해 OFFSET과 LIMIT을 활용하는 방식입니다. SELECT * FROM comment WHERE article_id = ? ORDER BY parent_comment_id ASC, comment_id ASC LIMIT ?, ?; SQL 키워드 설명 ORDER BY parent_comment_by ASC, comment_id ASC 댓글을 부모-자식 관계에 맞게 정렬 LIMIT ?, ? 몇 개의 데이터를 가져올지 지정 OFFSET 특정 페이지의 댓글을 건너뛴 후 가져옴 🏗️2️⃣ 정렬 방식 페이지 번호 방식에서는 댓글을 부모-자식 관계를 유지하면서 정렬해야 합니다. 정렬 기준은 다음과 같습니다. ORDER BY parent_comment_id ASC, comment_id ASC 최상위 댓글을 먼저 정렬 ➞ parent_comment_id IS NULL 순서대로 정렬 대댓글은 같은 부모 아래에서 정렬 ➞ comment_id ASC 순서로 정렬 페이징 처리 ➞ LIMIT ?, ? 사용 🏗️3️⃣ SQL 예제 (페이지 번호 기반 조회) 예를 들어, 한 페이지당 3개 댓글을 가져오도록 설정하고, 2번째 페이지(page = 2)를 조회한다고 가정해보겠습니다. SELECT * FROM comment WHERE article_id = ? ORDER BY parent_comment_id ASC, comment_id ASC LIMIT 3 OFFSET 3; 📌 페이지 번호 공식. OFFSET = (page - 1) * pageSize page = 1 ➞ OFFSET = (1-1) * 3 = 0 (첫 번째 페이지) page = 2 ➞ OFFSET = (2-1) * 3 = 3 (두 번째 페이지) page = 3 ➞ OFFSET = (3-1) * 3 = 6 (세 번째 페이지) 🏗️4️⃣ 데이터 예시 📌 데이터베이스에 저장된 댓글 데이터. comment_id parent_comment_id 내용 1 NULL 댓글1 (최상위 댓글) 2 1 댓글2 (댓글1의 대댓글) 3 NULL 댓글3 (최상위 댓글) 4 1 댓글4 (댓글1의 대댓글) 5 3 댓글5 (댓글3의 대댓글) 6 NULL 댓글6 (최상위 댓글) 📌 페이지 번호 기반 조회 결과 (한 페이지에 3개씩) ✅ 1페이지 조회 (page = 1, pageSize = 3) SELECT * FROM comment WHERE article_id = ? ORDER BY parent_comment_id ASC, comment_id ASC LIMIT 3 OFFSET 0; comment_id parent_comment_id 내용 1 NULL 댓글1 (최상위 댓글) 2 1 댓글2 (댓글1의 대댓글) 4 1 댓글4 (댓글1의 대댓글) ✅ 2페이지 조회 (page = 2, pageSize = 3) SELECT * FROM comment WHERE article_id = ? ORDER BY parent_comment_id ASC, comment_id ASC LIMIT 3 OFFSET 3; comment_id parent_comment_id 내용 3 NULL 댓글3 (최상위 댓글) 5 3 댓글5 (댓글3의 대댓글) 6 NULL 댓글6 (최상위 댓글) 📌 페이지를 넘길 때마다 다음 pageSize 만큼의 데이터를 가져옵니다. 🏗️5️⃣ 장점과 단점. 장점 단점 간단하고 직관적인 페이징 구현 가능 페이지 번호가 커질수록 OFFSET이 증가하여 성능 저하 댓글을 정렬된 순서대로 가져올 수 있음 대량의 데이터에서 OFFSET이 클 경우 속도가 느려질 수 있음 📌 대체 방법 OFFSET이 큰 경우 “Keyset Pagination (무한스크롤 방식)”을 사용하는 것이 더 효율적일 수 있음 ORDER BY parent_comment_id ASC, comment_id ASC 정렬을 유지하면서 WHERE comment_id > ? 방식을 활용하는 방식도 있음 🚀6️⃣ 정리. ✅ 페이지 번호 기반 댓글 조회는 LIMIT ?, OFFSET ?을 사용 ✅ ORDER BY parent_comment_id ASC, comment_id ASC를 사용해 계층 구조 유지 ✅ OFFSET 값이 클 경우 성능 저하 가능 ➞ Keyset Pagination 고려 가능 ✅ 최대 2 Depth 댓글 구조에서는 성능 이슈가 적고 직관적인 방식으로 구현 가능 📌 최대 2 Depth 댓글 구조에서는 페이지 번호 방식이 효율적이며, 오래된 댓글부터 순서대로 불러오기에 적합합니다.
Backend Development
· 2025-02-20
📚[Backend Development] 최대 2 Depth 댓글 정렬 구조의 '무한 스크롤 방식'이란 무엇일까요?
“📚[Backend Development] 최대 2 Depth 댓글 정렬 구조의 ‘무한 스크롤 방식’이란 무엇일까요?” ✅ 최대 2 Depth 댓글 정렬 구조의 “무한 스크롤 방식” 설명. 무한 스크롤 방식은 페이지 번호 방식(LIMIT ?, OFFSET ?)을 사용하지 않고, 마지막으로 불러온 댓글의 ID를 기준으로 다음 댓글을 불러오는 방식(Keyset Pagination)입니다. 이 방식은 페이지 번호 방식보다 성능이 우수하여, 대량의 데이터를 빠르게 로드할 수 있습니다. 🏗️1️⃣ 무한 스크롤 방식이란? 무한 스크롤 방식은 마지막으로 불러온 댓글(lastCommentId)을 기준으로 그 이후 데이터를 가져오는 방식입니다. SELECT * FROM comment WHERE article_id = ? AND comment_id > ? ORDER BY parent_comment_id ASC, comment_id ASC LIMIT ?; SQL 키워드 설명 WHERE comment_id > ? 마지막 댓글 ID 이후 데이터만 가져옴 ORDER BY parent_comment_id ASC, comment_id ASC 부모-자식 관계를 유지하면서 정렬 LIMIT ? 한 번에 가져올 최대 개수 지정 📌 이 방식을 사용하면 OFFSET을 사용하지 않기 때문에 성능이 훨씬 우수합니다. 🏗️2️⃣ SQL 예제 (무한 스크롤 방식) 예를 들어, 한 번에 3개의 댓글을 불러오도록 설정하고, 마지막으로 불러온 댓글의 ID(lastCommentId)가 3이라고 가정합니다. SELECT * FROM comment WHERE article_id = ? AND comment_id > 3 ORDER BY parent_comment_id ASC, comment_id ASC LIMIT 3; 🏗️3️⃣ 정렬 방식 📌 정렬 기준. ORDER BY parent_comment_id ASC, comment_id ASC 최상위 댓글을 먼저 정렬 ➞ parent_comment_id IS NULL 순서대로 정렬 대댓글은 같은 부모 아래에서 정렬 ➞ comment_id ASC 순서로 정렬 페이징 없이 WHERE comment_id > lastCommentId 방식으로 조회 🏗️4️⃣ 데이터 예시. 📌 데이터베이스에 저장된 댓글 데이터 comment_id parent_comment_id 내용 1 NULL 댓글1 (최상위 댓글) 2 1 댓글2 (댓글1의 대댓글) 3 NULL 댓글3 (최상위 댓글) 4 1 댓글4 (댓글1의 대댓글) 5 3 댓글5 (댓글3의 대댓글) 6 NULL 댓글6 (최상위 댓글) 📌 무한 스크롤 방식으로 데이터 조회. ✅ 첫 번째 요청(lastCommentId = 0) SELECT * FROM comment WHERE article_id = ? AND comment_id > 0 ORDER BY parent_comment_id ASC, comment_id ASC LIMIT 3; 📌 결과 comment_id parent_comment_id 내용 1 NULL 댓글1 (최상위 댓글) 2 1 댓글2 (댓글1의 대댓글) 4 1 댓글4 (댓글1의 대댓글) 📌 마지막 댓글 ID = 4 ✅ 두 번째 요청(lastCommentId = 4) SELECT * FROM comment WHERE article_id = ? AND comment_id > 4 ORDER BY parent_comment_id ASC, comment_id ASC LIMIT 3; 📌 결과 comment_id parent_comment_id 내용 3 NULL 댓글3 (최상위 댓글) 5 3 댓글5 (댓글3의 대댓글) 6 NULL 댓글6 (최상위 댓글) 📌 마지막 댓글 ID = 6 🏗️5️⃣ 장점과 단점. 장점 단점 OFFSET 없이 빠른 조회 가능 (성능 최적화) lastCommentId를 클라이언트가 유지해야 함 대량의 댓글이 있는 경우 효율적 댓글이 삭제될 경우 정렬이 흐트러질 가능성이 있음 페이지 번호 방식보다 확장성이 좋음 정렬 순서가 유지되도록 조심해야 함 🚀6️⃣ 정리. ✅ 무한 스크롤 방식은 WHERE comment_id > lastCommentId를 사용하여 데이터 조회 ✅ ORDER BY parent_comment_id ASC, comment_id ASC를 사용해 계층 구조 유지 ✅ 페이지 번호 방식(LIMIT ?, OFFSET ?)보다 성능이 우수하며 대량 데이터 처리에 적합 ✅ 마지막 댓글 ID(lastCommentId)를 유지해야 함 📌 최대 2 Depth 댓글 구조에서는 무한 스크롤 방식이 성능 최적화에 유리하며, 빠르게 댓글을 불러올 수 있습니다.
Backend Development
· 2025-02-20
📚[Backend Development] 최대 2depth 댓글 정렬 구조란 무엇일까요?
“📚[Backend Development] 최대 2depth 댓글 정렬 구조란 무엇일까요?” ✅ 최대 2 Depth 댓글 정렬 구조 설명. 최대 2 Depth(계층이 최대 2단계)까지만 허용하는 댓글 시스템의 정렬 구조는 비교적 단순하면서도 효율적입니다. 🏗️1️⃣ 댓글 테이블 구조. 최대 2 Depth 댓글을 저장하는 테이블 구조는 다음과 같습니다. 필드명 설명 comment_id 댓글의 고유 ID (PK) parent_comment_id 부모 댓글 ID (최상위 댓글이면 NULL) article_id 해당 댓글이 속한 게시글 ID content 댓글 내용 created_at 댓글 작성 시간 📌 특징. 최상위 댓글은 parent_comment_id = NULL (예: 댓글 1, 댓글 3) 자식 댓글은 parent_comment_id = 부모의 comment_id (예: 댓글2, 댓글 4, 댓글 5) 2 Depth까지만 허용 (댓글의 댓글까지만 가능, 대댓글의 대댓글은 불가능) 🏗️2️⃣ 정렬 방식 최대 2 Depth 댓글 정렬은 다음과 같은 순서로 진행됩니다. ORDER BY parent_comment_id ASC, comment_id ASC 📌 정렬 기준. parent_comment_id ASC ➞ 같은 부모 댓글을 기준으로 그룹화. comment_id ASC ➞ 작성된 순서대로 정렬 (오래된 댓글이 먼저 출력됨). 🏗️3️⃣ 정렬 데이터 예시. 위 정렬 방식에 따라 댓글 데이터를 조회하면 다음과 같은 형태가 됩니다. parent_comment_id comment_id 내용 NULL 1 댓글1 (최상위 댓글) 1 2 댓글2 (댓글1의 대댓글) 1 4 댓글4 (댓글 1의 대댓글) NULL 3 댓글3 (최상위 댓글) 3 5 댓글5 (댓글 3의 대댓글) 📌 이 정렬 방식의 장점. parent_comment_id를 기준으로 먼저 정렬하여 최상위 댓글이 먼저 출력됨 같은 parent_comment_id를 가진 댓글(대댓글)은 작성 순서대로 정렬됨 LIMIT ?, ?을 활용하여 페이징 처리 가능 🏗️4️⃣ SQL 정렬 예제 SELECT * FROM comment WHERE article_id = ? ORDER BY parent_comment_id ASC, comment_id ASC LIMIT ?, ?; 🏗️5️⃣ 페이징 처리. 각 페이지에서 N개 댓글을 불러올 수 있도록 LIMIT ?,? 사용 최상위 댓글과 대댓글을 함께 불러오기 위해 parent_comment_id 기준 정렬 유지 최대 depth가 2이므로 성능 최적화에 유리함 ✅6️⃣ 정리. ✅ 최대 2 Depth 구조 ➞ parent_comment_id를 활용해 부모-자식 관계 유지 ✅ 정렬 순서 ➞ ORDER BY parent_comment_id ASC, comment_id ASC ✅ 조회 결과 ➞ 부모 댓글이 먼저, 대댓글이 뒤에 정렬됨 ✅ 페이징 가능 ➞ LIMIT ?, ?를 활용하여 오래된 순으로 페이징 처리
Backend Development
· 2025-02-19
📚[Backend Development] mappedBy란 무엇일까요?
“📚[Backend Development] mappedBy란 무엇일까요?” 🍎 Intro. mappedBy는 양방향 연관관계에서 사용되는 속성으로, 연관 관계의 주인이 아닌(읽기 전용) 쪽에서 사용합니다. 즉, 외래 키(FK)를 관리하지 않는 쪽에서 mappedBy를 사용하여 연관 관계를 매핑합니다. ✅1️⃣ mappedBy의 필요성. 양방향 관계에서는 두 개의 엔티티가 서로를 참조하게 되는데, JPA는 외래 키(FK)를 관리할 “주인”을 하나만 지정해야 합니다. 이때, 연관 관계의 주인이 아닌 쪽에서 mappedBy를 사용하여 주인을 명시합니다. ✅2️⃣ @OneToOne 양방향 관계에서 mappedBy 사용 예제 1️⃣ User 엔티티 (연관 관계의 주인) @Entity public class User { @Id @GeneratedValue(strategy = Generation.IDENTITY) private Long id; private String username; @OneToOne @JoinColumn(name = "profile_id") // FK를 관리하는 주인 (user 테이블에 profile_id FK 생성) private UserProfile profile; // Getter, Setter } 2️⃣ UserProfile 엔티티(mappedBy 사용) @Entity public class UserProfile { @Id @GenerationValue(strategy = Generation.IDENTITY) private Long id; private String bio; private String website; @OneToOne(mappedBy = "profile") // User 엔티티의 profile 필드가 관계의 주인 private User user; // Getter, Setter } ✅3️⃣ mappedBy = “profile”의 의미 “profile”은 User 엔티티의 profile 필드명을 가리킵니다. 즉, 이 관계의 주인은 User.profile이며, UserProfile 엔티티는 읽기 전용입니다. 따라서 UserProfile.user 필드는 외래 키(FK)를 생성하지 않고, 매핑만 수행합니다. ✅4️⃣ 데이터베이스 테이블 구조 위 코드를 실행하면 user 테이블만 profile_id라는 FK 컬럼을 가지며, user_profile 테이블에는 추가 컬럼이 생성되지 않습니다. 📊 user 테이블 id username profile_id (FK) 1 Alice 101 2 Bob 102 📊 user_profile 테이블 id bio website 101 “Gamer” “alice.com” 102 “Developer” “bob.dev” 📌 외래 키는 user.profile_id에만 존재하며, user_profile 테이블에는 FK 컬럼이 없습니다. ✅5️⃣ mappedBy를 사용한 데이터 조회 ✅ User ➞ UserProfile 조회(가능 ✅) User user = entityManager.find(User.class, 1L); UserProfile profile = user.getProfile(); // 정상 작동 ✅ UserProfile ➞ User 조회(가능 ✅) UserProfile profile = entityManager.find(UserProfile.class, 101L); User user = profile.getUser(); // mappedBy를 사용했으므로 가능! 🚀 정리. ✔️ 연관 관계의 주인(Owner)이 아닌 쪽에서 mappedBy를 사용해야 한다. ✔️ “mappedBy = 주인 엔티티 필드명”으로 설정해야 한다. ✔️ 외래 키(FK)는 mappedBy를 사용한 쪽이 아니라 주인이 관리한다. ✔️ mappedBy는 읽기 전용이므로 @JoinColumn을 사용하지 않는다. 📌 mappedBy를 사용하면 불필요한 FK 컬럼 생성 방지 및 데이터베이스 테이블을 깔끔하게 유지할 수 있습니다.
Backend Development
· 2025-02-18
📚[Backend Development] @OneToMany란 무엇일까요?
“📚[Backend Development] @OneToMany란 무엇일까요?” 🍎 Intro. @OneToMany는 일대다(1:N) 관계를 매핑할 때 사용하는 어노테이션입니다. 즉, 하나(One)의 엔티티가 여러 개(Many)의 엔티티를 참조하는 구조입니다. ✅1️⃣ @OneToMany 예제. 게시글(Article)과 댓글(Comment) 관계를 예로 들어보겠습니다. 하나의 게시글(Article)에는 여러 개의 댓글(Comment)이 달릴 수 있습니다. 1️⃣ Article 엔티티(게시글) @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String content; @OneToMany(mappedBy = "article") // Comment 엔티티의 article 필드가 관계의 주인 private List<Comment> comments = new ArrayList<>(); // Getter, Setter } 2️⃣ Comment 엔티티(댓글) @Entity public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String content; @ManyToOne @JoinColumn(name = "article_id") // comment 테이블에 article_id FK 생성 private Article article; // Getter, Setter } ✅2️⃣ @OneToMany(mappedBy = “article”)의 의미 Comment 엔티티의 article 필드를 참조하여 양방향 관계를 설정합니다. 외래 키를 관리하는 주인은 Comment.article 필드이며, Article 엔티티는 mappedBy를 통해 읽기 전용입니다. 즉, Comment 엔티티가 관계의 주인이고, Article 엔티티에서는 직접 FK를 관리하지 않습니다. (➞ @JoinColumn이 Comment 쪽에만 있는 이유) ✅3️⃣ 데이터베이스 테이블 구조. 위 코드를 실행하면 다음과 같은 테이블이 생성됩니다. 📌 article 테이블 (게시글) id title content 1 “Hello JPA” “JPA 배우기” 2 “Spring Boot” “Spring 공부” 📌 comment 테이블 (게시글에 연결된 댓글, article_id FK 포함) id content article_id(FK) 1 “좋은 글이네요!” 1 2 “유익한 정보 감사합니다.” 1 3 “Spring 최고!” 2 📌 article_id 컬럼이 게시글(Article)을 참조하는 외래 키(FK)입니다. 즉, 하나의 Article에는 여러 개의 Comment가 연결될 수 있습니다. ✅4️⃣ 데이터 조회. ✅ 특정 게시글에 속한 댓글 가져오기. 양방향 관계가 설정되어 있으므로, 특정 게시글에 달린 댓글을 쉽게 가져올 수 있습니다. Article article = entityManager.find(Article.class, 1L); List<Comment> comments = article.getComments(); // 해당 게시글의 모든 댓글 가져오기 🚀5️⃣ 단방향 @OneToMany vs 양방향 @OneToMany 1️⃣ 단방향 @OneToMany @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String content; @OneToMany @JoinColumn(name = "article_id") // FK를 직접 관리 (주인 역할) private List<Comment> comments = new ArrayList<>(); // Getter, Setter } ✅ 장점. 단순한 구조. 불필요한 mappedBy 없이 @JoinColumn을 통해 FK 직접 관리 가능 ❌ 단점. 데이터 삽입 시 추가적인 SQL 실행 발생 @OneToMany 단방향 관계에서 @JoinColumn을 사용하면 INSERT 쿼리가 두 번 실행됨 (➞ 댓글 삽입 후, 게시글 ID 업데이트) 2️⃣ 양방향 @OneToMany + @ManyToOne @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String content; @OneToMany(mappedBy = "article") // Comment의 article 필드를 주인으로 설정 private List<Comment> comments = new ArrayList<>(); // Getter, Setter } ✅ 장점. 성능 최적화 가능 (FK는 Comment.article이 관리). INSERT 쿼리 실행이 한 번만 발생. 객체 그래프 탐색이 편리함 (article.getComments() 가능). ❌ 단점 mappedBy로 인해 데이터 저장이 Comment 쪽에서 이루어져야 함. ✅6️⃣ 정리 ✔️ @OneToMany는 하나(One)의 엔티티가 여러 개(Many)의 엔티티를 참조할 때 사용. ✔️ 양방향 관계에서는 @OneToMany(mappedBy = “필드명”) + @ManyToOne 조합 사용 ✔️ 단방향 @OneToMany보다는 양방향을 사용하는 것이 일반적 ✔️ 외래 키(FK)는 @ManyToOne 쪽에서 관리하며, @OneToMany는 읽기 전용 📌 게시글-댓글 관계처럼 1:N 관계가 필요할 때 @OneToMany를 사용하면 됩니다.
Backend Development
· 2025-02-18
📚[Backend Development] @ManyToOne이란 무엇일까요?
“📚[Backend Development] @ManyToOne이란 무엇일까요?” 🍎 Intro. @ManyToOne은 다대일(N:1) 관계를 매핑할 때 사용합니다. 즉, 여러 개(Many)의 엔티티가 하나(One)의 엔티티를 참조하는 구조입니다. ✅1️⃣ @ManyToOne 예제. 게시글(Article)과 댓글(Comment) 관계를 예로 들어보겠습니다. 하나의 게시글(Article)에 여러 개의 댓글(Comment)이 달릴 수 있습니다. 1️⃣ Article 엔티티 (게시글) @Entity public class Article { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String content; // Getter, Setter } 2️⃣ Comment 엔티티 (댓글) @Entity public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String content; @ManyToOne @JoinColumn(name = "article_id") // 외래 키 컬럼명 설정 private Article article; // Getter, Setter } ✅2️⃣ @ManyToOne 설명. @ManyToOne을 사용하여 여러 개의 댓글(Comment)이 하나의 게시글(Article)을 참조하도록 설정합니다. @JoinColumn(name = “article_id”)를 통해 comment 테이블에 article_id 외래 키(FK)를 생성합니다. ✅3️⃣ 데이터베이스 테이블 구조. 위 코드를 실행하면 데이터베이스는 다음과 같은 테이블이 생성됩니다. 📌 article 테이블 id title content 1 “Hello JPA” “JPA 배우기” 2 “Spring Boot” “Spring 공부” 📌 comment 테이블 (article_id FK 포함) id content article_id(FK) 1 “좋은 글이네요!” 1 2 “유익한 정보 감사합니다.” 1 3 “Spring 최고!” 2 📌 article_id 컬럼이 게시글(Article)을 참조하는 외래 키(FK)입니다. 즉, comment 테이블의 여러 행이 article_id를 통해 같은 article을 가리킬 수 있습니다. ✅4️⃣ 데이터 조회 ✅ 특정 게시글에 속한 댓글 가져오기. 게시글 ID(articleId)가 1번인 댓글을 가져오려면: List<Comment> comments = entityManager.createQuery( "SELECT c FROM c WHERE c.article.id = :articleId", Comment.class) .setParameter("articleId", 1L) .getResultList();
Backend Development
· 2025-02-18
📚[Backend Development] 단방향과 @OneToOne이란 무엇일까요?
“📚[Backend Development] 단방향과 @OneToOne이란 무엇일까요?” 🍎 Intro. 단방향 @OneToOne 관계는 엔티티 간의 1:1 관계를 매핑할 때, 한쪽 엔티티에서만 관계를 관리하는 방식입니다. 즉, 한 엔티티에서만 다른 엔티티를 참조하고, 반대쪽에서는 이를 알지 못하는 상태입니다. ✅1️⃣ 예제 코드 예를 들어, User 엔티티와 UserProfile 엔티티가 1:1 관계를 가진다고 가정해봅시다. 📝 User 엔티티에서 UserProfile 엔티티를 단방향으로 참조하는 경우: @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; @OneToOne @JoinColumn(name = "profile_id") // User 테이블의 profile_id 컬럼이 UserProfile의 id를 참조 private UserProfile profile; // Getter, Setter } 📝 UserProfile 엔티티: @Entity public class UserProfile { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String bio; private String website; // Getter, Setter } ✅ 설명: @OneToOne을 사용하여 User 엔티티가 UserProfile 엔티티를 참조합니다. @JoinColumn(name = “profile_id”)를 사용하여 User 테이블에 profile_id 컬럼이 생성됩니다. User 테이블에 profile_id라는 외래 키(FK) 컬럼을 추가하고, 이 컬럼이 UserProfile 테이블의 id(PK)를 참조하도록 만듭니다. 즉, User 테이블의 profile_id가 UserProfile 테이블의 id를 참조하는 FK이다. 하지만 UserProfile 엔티티에는 User와의 관계를 알 수 있는 정보가 없습니다. ➞ 이것이 단방향 관계입니다. ✅ 살제 데이터베이스 테이블 예시: 이 코드를 기반으로 JPA가 생성하는 테이블을 보면 다음과 같이 됩니다. 📊 user 테이블 id username profile_id(FK) 1 Alice 101 2 Bob 102 📊 user_profile 테이블 id bio website 101 “Gamer” “alice.com” 102 “Developer” “bob.dev” 📌 즉, user.profile_id는 user_profile.id를 참조(FK)하는 구조입니다. 따라서 UserProfile 엔티티에는 profile_id가 따로 필요하지 않습니다. 대신 기본 키(id)가 User 엔티티의 외래 키(profile_id)로 사용됩니다. ✅2️⃣ 단방향 관계의 특징. ✅ 장점. 구조가 단순하고 이해하기 쉽다. 한쪽에서만 참조하므로 불필요한 연관관계 로딩을 방지할 수 있다. ❌ 단점. 반대쪽(UserProfile)에서 User를 조회할 방법이 없다. UserProfile이 자신을 참조하는 User가 누구인지 알고 싶다면 별도의 쿼리를 작성해야 한다. ✅3️⃣ 단방향 관계 조회. 사용자가 프로필 정보를 가져오는 코드를 작성하면 다음과 같습니다. User user = entityManager.find(User.class, 1L); UserProfile profile = user.getProfile(); // User -> UserProfile 조회 가능.
Backend Development
· 2025-02-17
📚[Backend Development] 빌더 패턴 사용시 @AllArgsConstructor(access = AccessLevel.PRIVATE)을 활용하는 이유
“📚[Backend Development] 빌더 패턴 사용시 @AllArgsConstructor(access = AccessLevel.PRIVATE)을 활용하는 이유” 🍎 Intro. 빌더 패턴을 사용할 때, @AllArgsConstructor(access = AccessLevel.PRIVATE)를 추가하는 이유를 설명하겠습니다. ✅1️⃣ @AllArgsConstructor가 하는 역할. @AllArgsConstructor는 모든 필드를 포함하는 생성자를 자동으로 생성합니다. 하지만 빌더 패턴을 사용할 경우, 생성자를 직접 호출하지 않고 빌더를 통해 객체를 생성하는 것이 목적입니다. 따라서, 생성자의 접근 제한을 private으로 설정하면, 빌더를 통한 생성만 허용할 수 있습니다. ✅2️⃣ 빌더 패턴 적용 시, @AllArgsConstructor(access = AccessLevel.PRIVATE)가 필요한 이유 ❌ 잘못된 예제 (빌더 패턴 사용했지만, 생성자도 public) @AllArgsConstructor // (기본값이 PUBLIC) @Builder public class Comment { private Long commentId; private String content; private Long articleId; private Long parentCommentId; private Long writerId; private Boolean deleted; private LocalDateTime createdAt; } ✅ 문제점: @AllArgsConstructor의 기본 접근 제어자가 public이므로, 빌더를 사용하지 않고 생성자를 직접 호출하여 객체를 만들 수 있음. 빌더를 사용하는 목적이 객체 생성 시 가독성을 높이고 선택적으로 필드를 초기화 할 수 있도록 하기 위함인데, 생성자가 public이면 빌더 사용을 강제할 수 없음. 🛠️ 해결 방법(@AllArgsConstructor(access = AccessLevel.PRIVATE)) @AllArgsConstructor(access = AccessLevel.PRIVATE) // 생성자를 PRIVATE으로 설정 @Builder public class Comment { private Long commentId; private String content; private Long articleId; private Long parentCommentId; private Long writerId; private Boolean deleted; private LocalDateTime createdAt; } ✅ 이렇게 하면: 객체를 직접 생성하는 것을 막고, 빌더를 통한 생성만 가능하도록 제한 가능. 불필요한 생성자 호출을 막고, 가독정이 좋은 빌더 패턴을 강제할 수 있음. 객체의 필드가 많아질수록, 빌더 패턴이 더 유용하게 동작하게 됨. ✅3️⃣ 정리 - 필요한 이유. 문제점 해결 방법 @AllArgConstructor 기본값이 public이므로, 직접 생성자 호출이 가능함. @AllArgsConstructor(access = AccessLevel.PRIVATE)를 사용하여 생성자 접근 제한. 빌더를 사용해도 생성자를 직접 호출할 수 있어 일관성이 떨어짐. 빌더를 강제하여 가독성 및 유지보수성을 높임. 객체 필드가 많아질 경우, 생성자 호출보다 빌더 패턴이 더 유리함. 빌더를 강제하여 더 가독성이 좋은 코드 유지 가능. ✅ 즉, @AllArgsCOnstructor(access = AccessLevel.PRIVATE)를 사용하면, 빌더를 통한 객체 생성을 강제하여 코드 일관성을 유지할 수 있습니다.
Backend Development
· 2025-02-15
📚[Backend Development] 계층형 댓글 목록 조회 시 페이징 처리 방법
“📚[Backend Development] 계층형 댓글 목록 조회 시 페이징 처리 방법” 💡 가정: 계층별 오래된 순으로 페이징됨. 1페이지 당 2개의 댓글을 보여줌. 👉 이 가정이 맞는지 검토하고, 어떻게 페이징이 이루어지는지 확인해봅시다. ✅1️⃣ 기본적인 계층형 정렬 방식. 📌 계층형 댓글 조회 시 일반적인 정렬 규칙. 1. 최상위 댓글(부모 댓글)이 먼저 정렬됨 (오래된 순). 2. 각 부모 댓글의 하위 댓글(자식 댓글)이 정렬됨 (오래된 순). 3. 같은 계층 내에서도 오래된 순으로 정렬됨. 📌 계층형 정렬된 목록. ✅2️⃣ 1페이지 당 2개의 댓글을 보여줄 경우. 계층형 구조에서는 단순 LIMIT & OFFSET을 사용하면 데이터가 끊어질 수 있음. 트리 구조를 유지하면서 정렬된 순서로 페이징해야 함. 📌 전체 페이징 처리 모습. 📌 1페이지 (첫 2개 댓글) 최상위 댓글 1개(댓글 1) + 그에 대한 하위 댓글(댓글 2)을 포함 하위 댓글이 있는 경우, 다음 댓글을 포함할지 여부는 페이징 로직에 따라 결정됨. 📌 2페이지 (다음 2개 댓글) 첫 번째 페이지에서 댓글 1 ➞ 댓글 2까지 가져왔으므로, 이제 댓글 2의 하위 댓글부터 표시. 즉, 댓글 3과 댓글 5가 다음 페이지에 노출됨. 📌 3페이지 (다음 2개 댓글) 댓글 1 트리가 끝났으므로, 이제 댓글 4를 표시. 댓글 4의 하위 댓글인 댓글 6도 같이 표시됨. ✅3️⃣ 정리 페이지 번호 출력되는 댓글 목록 1 페이지 댓글 1, 댓글 2 2 페이지 댓글 3, 댓글 5 3 페이지 댓글 4, 댓글 6 ✔️ 즉, 최상위 댓글을 기준으로 정렬하되, 계층 구조를 유지하면서 페이징이 이루어짐. ✔️ 각 댓글의 하위 댓글을 보여줄 때, 부모 댓글이 포함된 상태에서 하위 댓글이 순차적으로 정렬됨. ✅4️⃣ 추가적인 고려 사항. ✅ 페이징 성능을 고려한 SQL 쿼리. 계층형 댓글 페이징을 위한 CTE(Common Table Expression) 또는 Recursive Query 활용 가능. ORDER BY parent_id, created_at ASC와 같은 정렬 방식 적용. ✅ UI에서의 표시 방식 첫 번째 페이지에서 댓글 1 ➞ 댓글 2까지만 표시할 수 있고, 첫 번째 페이지에서 댓글 1 ➞ 댓글 2 ➞ 댓글 3 ➞ 댓글 5까지 한 번에 표시할 수도 있음 “더 보기” 버튼을 활용하여 동적 로딩 방식도 고려 가능.
Backend Development
· 2025-02-14
📚[Backend Development] 계층형 대댓글에서 댓글을 삭제하면 어떤 현상이 발생할까요? 3️⃣
“📚[Backend Development] 계층형 대댓글에서 댓글을 삭제하면 어떤 현상이 발생할까요? 3️⃣” ✅ “삭제 표시(Soft Delete)” 방식에서 댓글 5를 삭제하면 어떻게 될까? 💡 가정. Soft Delete(삭제 표시) 방식을 사용 중. 댓글 5는 하위 댓글이 없으므로 완전히 삭제될 것이다. 👉 이 가정이 맞는지 확인해 봅시다. ✅1️⃣ 댓글 5 삭제 전의 구조. 댓글 1과 댓글 2는 삭제 표시(Soft Delete) 상태. 댓글 3과 댓글 5는 남아 있음. 이 상태에서 댓글 5를 삭제하면 어떻게 될까? 🤔 ✅2️⃣ Soft Delete vs Physical Delete 차이점 삭제 방식 설명 댓글 5 삭제 시 결과 Soft Delete (논리 삭제) 데이터베이스에서 삭제하지 않고 is_deleted = TRUE로 표시만 함 댓글 5가 “삭제된 댓글입니다.”로 남음 Physical Delete (물리 삭제) 데이터베이스에서 실제 삭제 댓글 5가 완전히 제거됨 ✔️ Soft Delete라면 “삭제된 댓글입니다.”로 남지만, Physical Delete라면 댓글 5가 실제로 삭제됨. ✔️ 댓글 5는 하위 댓글이 없기 때문에 완전 삭제(Physical Delete)되는 것이 일반적. ✅3️⃣ 댓글 5 삭제 후의 새로운 구조 ✅ Soft Delete 적용 시 (논리 삭제) 댓글 5가 삭제 표시로 남음(Soft Delete) → “삭제된 댓글입니다.”로 보임. 댓글 3이 남아 있으므로 댓글 2의 구조는 유지됨. ✅ Physical Delete 적용 시 (완전 삭제) 댓글 5가 완전히 삭제된(Physical Delete) 댓글 2하위에서 댓글 3만 남음. ✅4️⃣ 결론: 댓글 5는 완전 삭제가 이루어질 가능성이 높음 Soft Delete를 적용하더라도, 댓글 5는 하위 댓글이 없기 때문에 완전히 삭제(Physical Delete) 되는 것이 일반적. 댓글 5를 Soft Delete 처리할 필요가 없으며, 실제로 DB에서 제거되는 것이 최적의 방식.
Backend Development
· 2025-02-13
📚[Backend Development] 계층형 대댓글에서 댓글을 삭제하면 어떤 현상이 발생할까요? 1️⃣
“📚[Backend Development] 계층형 대댓글에서 댓글을 삭제하면 어떤 현상이 발생할까요? 1️⃣” 🍎 Intro. 댓글을 삭제하는 방식에는 여러 가지가 있습니다. 데이터베이스의 외래 키(Foreign Key) 설정 및 삭제 정책에 따라 다른 현상이 발생할 수 있습니다. ✅1️⃣ 댓글 2의 구조 분석. 댓글 2는 댓글 1의 자식 댓글(대댓글). 댓글 3, 댓글 5는 댓글 2의 자식 대댓글. 즉, 댓글 2를 삭제하면 댓글 3과 댓글 5가 고아 상태(부모 없는 상태)가 됨. ✅2️⃣ 댓글 2 삭제 시 발생할 수 있는 시나리오 📌 시나리오 1️⃣: 연쇄 삭제 (Cascading Delete) 댓글 2를 삭제하면 댓글 3과 댓글 5도 함께 삭제됨. ON DELETE CASCADE 옵션이 설정되어 있는 경우 발생. 👉 결과. 삭제 후 남아있는 댓글 목록: 댓글 1 댓글 4 댓글 6 댓글 3과 댓글 5까지 함께 삭제되므로, 해당 스레드 전체가 사라짐. 📌 시나리오 2️⃣: 고아 댓글 처리 (Orphan Handling) 댓글 2를 삭제하면 댓글 3과 댓글 5의 부모(parent_id)를 NULL로 설정. 즉, 댓글 3과 댓글 5가 독립적인 최상위 댓글이 됨. 댓글 4와 댓글 6의 계층 구조는 유지됨 ➞ 댓글 6의 parent_id = 4 👉 결과. 삭제 후 남아있는 댓글 목록: 댓글 1 댓글 3 (기존 댓글 2의 자식 → 최상위 댓글로 이동) 댓글 5 (기존 댓글 2의 자식 → 최상위 댓글로 이동) 댓글 4 └ 댓글 6 📌 시나리오 3️⃣: 부모 댓글 대체 (Reparenting) 댓글 2를 삭제하면 댓글 3과 댓글 4의 부모를 댓글 1로 변경. 즉, 댓글 2의 자식들이 댓글 1의 직접적인 자식 댓글이 됨 👉 결과. 삭제 후 남아있는 댓글 목록: 댓글 1 └ 댓글 3 └ 댓글 5 댓글 4 └ 댓글 6 ✅3️⃣ 댓글 2 삭제를 처리하는 방법 선택. 삭제 방식 설명 장점 단점 연쇄 삭제 (Cascade) 댓글 2를 삭제하면 댓글 3, 5도 삭제 데이터 정합성 유지 유저가 예상치 못한 삭제 발생 가능 고아 댓글 처리 (Orphan) 댓글 3과 5가 최상위 댓글이 됨 삭제 후에도 데이터 보존 UI에서 댓글 관계가 깨질 수 있음 부모 댓글 대체(Reparenting) 댓글 3과 댓글 5가 댓글 1의 자식이 됨 대댓글 구조 유지 데이터 수정이 필요 ✅4️⃣ MySQL에서 삭제 처리 방식 예제. 1️⃣ ON DELETE CASCADE (연쇄 삭제) ALTER TABLE comments ADD CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES comments (comment_id) ON DELETE CASCADE; 댓글 2를 삭제하면 댓글 3과 댓글 5도 자동으로 삭제됨. 2️⃣ 부모 댓글을 NULL로 설정 (고아 댓글 처리) UPDATE comments SET parent_id = NULL WHERE parent_id = 2; DELETE FROM comments WHERE comment_id = 2; 댓글 3과 댓글 5가 부모 없이 최상위 댓글로 변경됨. 3️⃣ 부모 댓글 변경 (Reparenting) UPDATE comments SET parent_id = 1 WHERE parent_id = 2; DELETE FROM comments WHERE comment_id = 2; 댓글 3과 댓글 5가 댓글 1의 자식이 됨. ✅5️⃣ 결론 댓글 2를 삭제하면 댓글 3과 댓글 5의 처리 방법에 따라 다른 결과가 발생. 어떤 방식이 가장 적절한지는 비즈니스 로직과 UX에 따라 결정해야 함. 보통은 부모 댓글을 삭제해도 대댓글이 남도록 처리(고아 댓글 처리 or 부모 댓글 변경)하는 경우가 많음.
Backend Development
· 2025-02-12
📚[Backend Development] 계층형 대댓글에서 댓글을 삭제하면 어떤 현상이 발생할까요? 2️⃣
“📚[Backend Development] 계층형 대댓글에서 댓글을 삭제하면 어떤 현상이 발생할까요? 2️⃣” ✅ 댓글 1을 삭제할 때, 삭제 표시만 될 것인가? 📌 가정 댓글 2는 이미 삭제 상태(“삭제된 댓글입니다.”)로 표시됨. 그렇다면, 댓글 1을 삭제하면 댓글 1도 삭제 표시만 될 것이다. 즉, 댓글 1이 완전히 삭제되지 않고, “삭제된 댓글입니다.”로 유지될 것입니다. 👉 이 가정이 맞는지 한 번 확인해봅시다. ✅1️⃣ 댓글 1 삭제 전의 구조 (댓글 2는 삭제 상태) 댓글 2가 삭제 상태지만, 댓글 3과 댓글 5는 유지됨. 이 상태에서 댓글 1을 삭제하면 어떻게 될까? ✅2️⃣ 댓글 1 삭제 처리 방식에 따른 결과. 댓글이 삭제될 때, 적용할 수 있는 방법은 두 가지 입니다. 📌1️⃣ 연쇄 삭제 (Cascade Delete) 📝 설명. 댓글 1이 삭제되면 그 하위 댓글도 모두 삭제됨. 즉, 댓글 1이 삭제되면 댓글 2, 댓글 3, 댓글 5도 함께 삭제됨. 👉 결과 구조 (Cascade Delete 적용 시) ❌ 이 방식은 가정과 다르게 댓글 1이 삭제되면서 하위 댓글도 모두 삭제됨. 📌2️⃣ 삭제 표시 (Soft Delete) 📝 설명. 댓글 1을 삭제하면 댓글 2처럼 “삭제된 댓글입니다.”로 표시됨. 즉, 댓글 1이 삭제되더라도 댓글 3과 댓글 5가 남아 있기 때문에 완전히 사라지지 않음. 가정과 동일한 방식 👉 결과 구조 (Soft Delete 적용 시) ✅ 이 방식이 사용자의 가정과 일치합니다. ✅ 댓글 1은 “삭제된 댓글입니다.” 상태가 되고, 댓글 3과 댓글 5는 그대로 남음. ✅3️⃣ 가정이 맞는가? ✔ 결론: 사용자의 가정은 “Soft Delete” 방식이 적용될 경우 맞습니다. ✔ 즉, 댓글 1도 삭제 상태로 표시되지만, 하위 댓글이 남아 있기 때문에 완전히 사라지지 않습니다. ✔ 만약 “Cascade Delete”가 적용되었다면, 댓글 1이 삭제되면서 하위 댓글(2, 3, 5)도 모두 삭제되므로 사용자의 가정과 다릅니다. ✅4️⃣ 실제 서비스에서는 어떤 방식을 사용할까? 삭제 방식 설명 적용 서비스 Cascade (완전 삭제) 부모 댓글이 삭제되면 하위 댓글도 삭제됨 일부 게시판, 블로그 Soft Delete(삭제 표시 유지) 부모 댓글이 삭제되더라도 하위 댓글이 있으면 “삭제된 댓글입니다.”로 유지됨 네이버 카페, 인스타그램, 페이스북, 유튜브 댓글 📌 일반적으로 커뮤니티나 SNS 서비스에서는 Soft Delete를 사용하여 부모 댓글을 “삭제된 댓글입니다.”로 유지하는 경우가 많습니다. 📌 즉, 가정이 실제 서비스에서 많이 사용되는 방식과 일치합니다.
Backend Development
· 2025-02-12
📚[Backend Development] 최대 2 Depth의 계층형 대댓글이란?
“📚[Backend Development] 최대 2 Depth의 계층형 대댓글이란?” 🍎 Intro. 댓글(Parent) ➞ 대댓굴(Child)까지만 허용하며, 대댓글의 대댓글(Child of Child)은 허용하지 않는 구조입니다. 즉, 댓글의 깊이가 최대 2단계까지만 유지 되며, 1 Depth (최상위 댓글) 2 Depth (대댓글) 이후에는 더 이상 하위 대댓글을 추가할 수 없는 방식입니다. ✅1️⃣ 최대 2 Depth 계층형 대댓글이 필요한 이유 ❌1️⃣ 일반적인 계층형 댓글 방식의 문제점. 댓글이 무한히 중첩될 경우, 데이터 조회 및 정렬이 복잡해지고 성능 저하 가능성. UI에서 너무 깊은 계층 구조는 사용자 경험(UX)에 좋지 않음. 무한 재귀 호출 방지를 위해 계층을 제한하는 것이 일반적. ✅2️⃣ 최대 2 Depth 계층형 대댓글의 장점. UI/UX 개선 ➞ 대댓글이 많아도 가독성이 유지됨. SQL 성능 최적화 가능 ➞ 복잡한 재귀 쿼리 없이 간단한 JOIN으로 해결 가능. 프론트엔드에서 구현이 쉬움 ➞ 2 Depth까지만 유지하므로 댓글 정렬이 단순함. ✅2️⃣ 최대 2 Depth의 계층형 대댓글 테이블 구조. CREATE TABLE comments ( comment_id BIGINT AUTO_INCREMENT PRIMARY KEY, article_id BIGINT NOT NULL, -- 게시글 ID parent_id BIGINT NULL, -- 부모 댓글 ID (NULL이면 최상위 댓글) depth INT NOT NULL DEFAULT 1, -- 댓글의 깊이 (1: 일반 댓글, 2: 대댓글) author VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parent_id) REFERENCES comments(comment_id) ON DELETE CASCADE ); ✅ 테이블 주요 컬럼 comment_id ➞ 댓글의 고유 ID. article_id ➞ 댓글이 속한 게시글 ID. parent_id ➞ 부모 댓글 ID (NULL이면 최상위 댓글). depth ➞ 댓글의 깊이 (1이면 일반 댓글, 2이면 대댓글). author ➞ 작성자. content ➞ 댓글 내용. created_at ➞ 댓글 작성 시간. ✅3️⃣ 최대 2 Depth 계층형 대댓글의 규칙. Depth 설명 1 Depth 일반 댓글 (Parent) 2 Depth 대댓글 (Child) 3 Depth 이상 ❌ 허용하지 않음 ✅4️⃣ 최대 2 Depth의 계층형 대댓글 목록 조회 API 구현. 🛠️1️⃣ Entity (JPA 기반 계층형 댓글) import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Getter @NoArgsConstructor @Table(name = "comments") public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long commentId; @ManyToOne @JoinColumn(name = "article_id", nullable = false) private Article article; @ManyToOne @JoinColumn(name = "parent_id") private Comment parent; // 부모 댓글 @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) private List<Comment> replies = new ArrayList<>(); // 대댓글 리스트 private Integer depth; // 댓글 깊이 (1: 일반 댓글, 2: 대댓글) private String author; private String content; private LocalDateTime createdAt = LocalDateTime.now(); } ✅ 댓글의 깊이를 depth 필드로 저장하여 최대 2 Depth까지만 허용. 🛠️2️⃣ Repository (2 Depth까지 댓글 조회) import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface CommentRepository extends JpaRepository<Comment, Long> { List<Comment> findByArticle_ArticleIdDepthOrderByCreatedAtAsc(Long articleId, Integer depth); } ✅ 최대 depth = 2까지만 조회하도록 설정. 🛠️3️⃣ Service (비즈니스 로직) import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @Service @RequiredArgsConstructor public class CommentService { private final CommentRepository commentRepository; public List<CommentResponse> getCommentsByArticle(Long articleId) { return commentRepository.findByArticle_ArticleIdAndDepthOrderByCreatedAtAsc(articleId, 1) .stream() .map(CommentResponse::fromEntity) .toList(); } public List<CommentResponse> getRepliesByComment(Long parentId) { return commentRepository.findByArticle_ArticleIdAndDepthOrderByCreatedAtAsc(parentId, 2) .stream() .map(CommentResponse::fromEntity) .toList(); } } ✅ 최대 2 Depth까지만 조회하는 API 로직 구현. 🛠️4️⃣ Controller (API 요청 처리) import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/articles/{articleId}/comments") @RequiredArgsConstructor public class CommentController { private final CommentService commentService; @GetMapping public ResponseEntity<List<CommentResponse>> getComments(@PathVariable Long articleId) { return ResponseEntity.ok(commentService.getCommentsByArticle(articleId)); } @GetMapping("/{commentId}/replies") public RespinseEntity<List<CommentResponse>> getReplies(@PathVariable Long commentId) { return ResponseEntity.ok(commentService.getRepliesByComment(commentId)); } } ✅ RESTful API로 최대 2 Depth까지 댓글 및 대댓글 조회 가능. ✅5️⃣ API 응답 예시. [ { "commentId": 1, "author": "John Doe", "content": "좋은 글이네요!", "createdAt": "2025-02-01T12:00:00Z", "replies": [ { "commentId": 2, "author": "Alice", "content": "저도 그렇게 생각해요!", "createdAt": "2025-02-01T12:05:00Z" } ] }, { "commentId": 3, "author": "Bob", "content": "궁금한 점이 있어요!", "createdAt": "2025-02-01T12:10:00Z", "replies": [] } ] ✅ 최대 2 Depth까지만 유지되며, replies 필드를 이용하여 대댓글을 표현. ✅6️⃣ SQL 쿼리 방식. -- 최상위 댓글(1 Depth) 조회 SELECT * FROM comments WHERE article_id = 123 AND depth = 1 ORDER BY created_at ASC; -- 특정 댓글(2 Depth)의 대댓글 조회 SELECT * FROM comments WHERE parent_id = 1 AND depth = 2 ORDER BY created_at ASC; ✅ SQL을 활용하여 2 Depth까지만 조회 가능하도록 제한. ✅7️⃣ 최대 2 Depth 계층형 대댓글의 활용 사례 서비스 설명 블로그 블로그 게시글의 댓글 + 대댓글(ex: 네이버 블로그) 커뮤니티 게시판 댓글 + 대댓글 (ex: Reddit) SNS SNS 댓글 + 대댓글 (ex: 인스타그램, 트위터) 이커머스 상품 리뷰의 대댓글 (ex: 아마존) ✅8️⃣ 결론. 최대 2 Depth의 계층형 대댓글은 댓글 ➞ 대댓글까지만 허용하고, 더 이상 하위 댓글을 허용하지 않는 방식. 데이터 정렬이 단순해지고 성능 최적화 가능. Spring Boot + JPA 기반으로 쉽게 구현 가능. 대규모 트래픽에서도 적절한 성능을 유지하면서 안정적인 댓글 시스템 제공 가능.
Backend Development
· 2025-02-11
📚[Backend Development] 단방향과 양방향의 개념에 대하여.
“📚[Backend Development] 단방향과 양방향의 개념에 대하여.” 🍎 Intro. JPA에서 엔티티 간의 관계를 설정할 때 단방향과 양방향 관계를 정의할 수 있습니다. 이는 데이터베이스의 외래 키(Foreign Key) 관계를 객체 지향적으로 매핑하는 방식에 따라 달라집니다. ✅1️⃣ 단방향 관계 (Unidirectional) 단방향 관계는 한쪽 엔티티만 다른 엔티티를 참조하는 방식입니다. 📝 예제: 단방향 @OneToOne 관계. @Entity public class Passport { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String passportNumber; } @Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToOne @JoinColumn(name = "passport_id") // 외래 키를 Person 테이블이 가짐 private Passport passport; } 📌 특징 Person 엔티티에서만 Passport를 참조할 수 있습니다. Passport 엔티티에서는 Person을 전혀 모릅니다. 테이블 구조에서는 Person 테이블에 passport_id라는 외개 키가 존재합니다. 데이터 조회 시 Person을 가져올 때 Passport도 함께 조회할 수 있습니다. ✅2️⃣ 양방향 관계 (Bidirectional) 양방향 관계는 두 엔티티가 서로를 참조하는 방식입니다. 📝 예제: 양방향 @OneToOne 관계. @Entity public class Passport { @Id @GeneratedValue(startegy = GenerationType.IDENTITY) private Long id; private String passportNumber; @OneToOne(mappedBy = "passport") // Person 엔티티의 passport 필드와 연결 private Person person; } @Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToOne @JoinColumn(name = "passport_id") // 실제 외래 키를 소유 private Passport passport; } 📌 특징 Person이 Passport를 참조하고, Passport도 Person을 참조합니다. Person이 외래 키를 소유(@JoinColumn(name = “passport_id))” Passport에서 mappedBy = “passport”를 사용하여 반대편에서 매핑을 담당함. 두 엔티티가 서로 참조하기 때문에 양방향 탐색 가능(예: passport.getPerson()) ✅3️⃣ 단방향 VS 양방향 비교. 구분 단방향 관계 양방향 관계 참조 방향 한쪽 엔티티만 다른 엔티티를 참조 두 엔티티가 서로 참조 테이블 구조 한쪽 테이블에만 외래 키 존재 테이블 구조는 동일하나 객체에서 상호 참조 조회 방향 한쪽에서만 조회 가능 양쪽에서 조회 가능 사용 예 단순한 연관 관계 상관 관계가 필요한 경우 ✅4️⃣ 언제 단방향/양방향을 선택해야 할까? 1️⃣ 단방향이 더 적합한 경우. 반대 방향에서 참조할 필요가 없는 경우 예: Order ➞ Payment (주문은 결제를 참조하지만, 결제는 주문을 참조할 필요 없음) 성능을 최적화하고 불필요한 데이터 로딩을 방지하고 싶은 경우 양방향 관계를 만들면 불필요한 연관 객체까지 로딩될 수 있음. FetchType.LAZY를 설정하더라도 관리 부담이 커질 수 있음. 2️⃣ 양방향이 더 적합한 경우. 양쪽에서 참조할 필요가 있는 경우 예: Member ↔ Team (회원이 팀을 참조하고, 팀도 회원 목록을 관리해야 함) 반대 엔티티를 쉽게 조회해야 하는 경우 예를 들어 Passport에서 Person을 조회하는 기능이 자주 필요하다면 양방향이 유리함. OneToMany, ManyToOne 관계에서는 성능 고려 후 양방향 설정을 할 수도 있음. ✅5️⃣ 정리 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 관계는 단방향과 양방향이 모두 가능합니다. 단방향은 한쪽에서만 참조, 양방향은 서로 참조합니다. 양방향 관계에서는 mappedBy를 사용하여 연관 관계의 주인을 지정해야 합니다. 불필요한 양방향 관계를 피하고, 필요한 경우에만 적용하여 성능과 유지보수성을 고려해야 합니다. 👉 일반적으로 단반향을 기본으로 하고, 필요할 때만 양방향을 추가하는 것이 좋습니다.
Backend Development
· 2025-02-11
📚[Backend Development] 계층형 댓글 목록 조회란 무엇일까요?
“📚[Backend Development] 계층형 댓글 목록 조회란 무엇일까요?” 🍎 Intro. 계층형 댓글(Hierachical Commmeents)이란, 댓글과 대댓글(답글)을 계층적으로 표시하는 방식입니다. 이는 일반적인 1차원 목록 형태의 댓글 조회와 달리, 부모-자식 관계를 유지하는 댓글 시스템입니다. ✅1️⃣ 계층형 댓글 목록 조회가 필요한 이유. ❌1️⃣ 일반적인 평면 댓글(flat comments) 방식의 한계. 일반적으로 게시글에 대한 댓글을 단순 목록(List) 형태로 조회. 하지만 답글(대댓글)이 많아질 경우, 계층 구조가 필요. ✅2️⃣ 계층형 댓글의 장점. 댓글에 대한 대댓글(답글)을 구조적으로 표현 가능. 유저가 대화 흐름을 쉽게 파악할 수 있음. 재귀적인 구조를 활용하여 대댓글이 몇 단계까지 달려도 정렬 가능. ✅2️⃣ 계층형 댓글의 데이터 구조. 계층형 댓글을 저장하는 방법은 여러 가지가 있지만, 일반적으로 “부모 댓글(parentId)”를 저장하여 댓글 간 관계를 유지합니다. 📌1️⃣ 테이블 구조 CREATE TABLE comments ( comment_id BIGINT AUTO_INCREMENT PRIMARY KEY, article_id BIGINT NOT NULl, -- 게시글 ID parent_id BIGINT NULL, -- 부모 댓글 ID (NULL이면 최상위 댓글) author VARCHAR(255) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parentt_id) REFERENCES comments(comment_id) ON DELETE CASCADE ); ✅ 주요 컬럼. comment_id ➞ 댓글의 고유 ID. article_id ➞ 해당 댓글이 속한 게시글 ID. parent_id ➞ 부모 댓글 ID (최상위 댓글이면 NULL). author ➞ 댓글 작성자. content ➞ 댓글 내용. created_at ➞ 댓글 작성 시간. ✅3️⃣ 계층형 댓글 목록 조회 API 구현 (Spring Boot + JPA) 🛠️1️⃣ Entity (계층형 구조) import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Getter @NoArgsConstructor @Entity @Table(name = "comments") public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long commentId; @ManyToOne @JoinColumn(name = "article_id", nullable = false) private Article article; @ManyToOne @JoinColumn(name = "parent_id") private Comment parent; // 부모 댓글 @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) private List<Comment> replies = new ArrayList<>(); // 대댓글 리스트 private String author; private String content; private LocalDateTime createdAt = LocalDateTime.now(); } ✅ 부모 댓글(parent)과 대댓글(replies) 관계를 유지하여 계층 구조 형성. 🛠️2️⃣ Repository (계층형 댓글 조회) import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface CommentRepository extends JpaRepository<Comment, Long> { List<Comment> findByArticle_ArticleIdAndParentIsNullOrderByCreatedAtDesc(Long articleId); } ✅ 최상위 댓글(부모가 없는 댓글)만 가져옴 ➞ 이후 replies 필드에서 대댓글을 가져옴. 🛠️3️⃣ Service (계층형 댓글 로직 구현) import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @Service @RequireArgsConstructor public class CommentService { private final CommentService { private final CommentRepository commentRepository; public List<CommentResponse> getCommentsByArticle(Long articleId) { List<Comment> comments = commentRepository.findByArticle_ArticleIdAndParentIsNullOrderByCreatedAtDesc(articleId); return comments.stream().map(CommentResponse::fromEntity).toList(); } } } ✅ 최상위 댓글만 조회하고, 대댓글은 replies 필드를 통해 재귀적으로 가져옴. 🛠️4️⃣ Controller (API 요청 처리) import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/articles/{articleId}/comments") @RequiredArgsConstructor public class CommentController { private final CommentService commentService; @GetMapping public ResponseEntity<List<CommentResponse>> getComments(@PathVariable Long articleId) { return ResponseEntity.ok(commentService.getCommentsByArticle(articleId)); } } ✅ RESTful API 형식으로 GET /api/articles/{articleId}comments 요청을 처리. ✅4️⃣ 계층형 댓글 조회 API의 응답 예시 [ { "commentId": 1, "author": "John Doe", "content": "좋은 글이네요!", "createdAt": "2025-02-01T12:00:00Z", "replies": [ { "commentId": 2, "author": "Alice", "content": "저도 그렇게 생각해요!", "createdAt": "2025-02-01T12:05:00Z", "replies": [] } ] }, { "commentId": 3, "author": "Bob", "content": "궁금한 점이 있어요!", "createdAt": "2025-02-01T12:10:00Z", "replies": [] } ] ✅ replies 필드를 이용해 대댓글을 계층적으로 표현. ✅5️⃣ 계층형 댓글 조회의 SQL 방식. 📌1️⃣ 재귀 쿼리(CTE) WITH RECURSIVE comment_tree AS ( SELECT comment_id, parent_id, content, author, created_at FROM comments WHERE article_id = 123 AND parent_id IS NULL UNION ALL SELECT c.comment_id, c.parent_id, c.content, c.author, c.created_at FROM comments c INNER JOIN comment_tree ct ON c.parent_id = ct.comment_id ) SELECT * FROM comment_tree ORDER BY created_at; ✅ 재귀적으로 부모-자식 관계를 조회하여 계층적 데이터를 정렬. ✅6️⃣ 계층형 댓글 조회 API의 성능 최적화. 1️⃣ JPA의 @BatchSize 또는 JOIN FETCH 활용 ➞ N + 1 문제 해결. @Query("SELECT c FROM Comment c JOIN FETCH c c.replies WHERE c.article.articleId = :articleId") List<Comment> findCommentsWithReplies(@Param("articleId") Long articleId); 2️⃣ Redis 캐싱 적용 ➞ 자주 조회되는 댓글 목록을 캐싱하여 성능 향상 가능. ✅7️⃣ 계층형 댓글 조회 API의 활용 사례. 서비스 설명 블로그 블로그 게시글의 계층형 댓글 (ex: 네이버 블로그, 티스토리) 커뮤니티 게시판 댓글 + 대댓글 (ex: 디시인사이드, Reddit) SNS 소셜 미디어 댓글 (ex: 페이스북, 인스타그램) 이커머스 상품 리뷰 대댓글 (ex: 아마존, 쿠팡) ✅8️⃣ 결론. 계층형 댓글 목록 조회는 댓글과 대댓글(답글)을 계층적으로 표시하는 방식. 부모 댓글(parentId)을 저장하여 관계를 유지. Spring Boot + JPA 기반으로 쉽게 구현 가능. 재귀 쿼리(CTE) 또는 JOIN FETCH를 활용하여 최적화 가능.
Backend Development
· 2025-02-08
<
>
Touch background to close