본문 바로가기
DBMS/PostgreSql

String_agg로 성능 최적화하기

by 천뱅 2025. 4. 11.

 

실무에서 1:N 관계의 테이블 구조를 한 번쯤은 경험해보셨을 겁니다.

게시판과 댓글, 주문과 주문상세, 유저와 활동 로그 등 다양한 도메인에서 자주 등장하죠.

 

이럴 때 고전적인 방법은, 먼저 부모 테이블을 조회한 뒤, 반복문을 돌면서 자식 테이블을 다시 조회하는 방식입니다.

이 구조는 구현이 쉽고 명확하지만, 데이터가 많아지면 성능 이슈나 I/O 비용 증가로 이어질 수 있습니다.

 

PostgreSQL에서는 이 문제를 string_agg 함수로 간결하게 해결할 수 있습니다.

오늘은 게시판의 게시글과 댓글 관계를 예시로, 1:N 테이블 구조에서 string_agg를 어떻게 활용해 성능을 개선할 수 있는지 소개드리겠습니다.

 

 

1. 기존 방식: N+1 문제 발생

기존 방식은 다음과 같은 형태로 동작합니다.

public ResponseEntity<ResponseVO> getPost() {
    List<BoardDetailDto> allPosts = boardDAO.getAllPost();
    for (BoardDetailDto post : allPosts) {
        List<CommentDetailDto> allComments = boardDAO.getCommentByPostId(post.getPostId());
        post.setComments(allComments);
        post.setCommentCount(allComments.size());
    }
    return ofSuccess(Map.of("post", allPosts));
}

 

게시글이 10개면 댓글 조회 쿼리도 10번 발생합니다.

결국 게시글 수(N) + 댓글 쿼리 수(N) = 총 1 + N 쿼리 → N+1 문제 발생.

단순하면서 직관적이지만, 대용량 데이터에선 성능 병목이 심해집니다.

 

3개의 게시글에 4번의 조회 발생

 


2. 개선 방식: string_agg로 한 번에 조회

DB에서는 댓글들을 string_agg로 묶어서 하나의 문자열로 내려주고,

Java에서는 이를 파싱해서 List로 변환하는 방식입니다.

 

PostgreSQL 쿼리 예시

SELECT
    p.post_id,
    p.title,
    u.username AS author,
    p.contents,
    to_char(p.created_at, 'YYYY-MM-DD HH24:MI:SS') AS created_at,
    COUNT(c.comment_id) AS comment_count,
    string_agg(
        c.comment_id || ':' || cu.username || ':' || c.content || ':' || to_char(c.created_at, 'YYYY-MM-DD HH24:MI:SS'),
        '|'
        ORDER BY c.created_at
    ) AS string_agg
FROM post p
JOIN users u ON p.user_id = u.user_id
LEFT JOIN comment c ON p.post_id = c.post_id
LEFT JOIN users cu ON c.user_id = cu.user_id
GROUP BY p.post_id, p.title, author, p.contents, p.created_at
ORDER BY p.created_at DESC;

 

댓글이 중복될 가능성이 있다면 string_agg(DISTINCT ...)처럼 DISTINCT를 쓰고 싶어질 수 있는데,

PostgreSQL에서 DISTINCT는 내부적으로 정렬(Sort)을 수행하고, 중복 제거까지 추가 비용이 발생해 성능이 떨어질 수 있습니다.

 

이럴 때는 GROUP BY를 통해 결과 단위를 명확하게 나눠주는 것이 더 좋습니다.

위 쿼리처럼 GROUP BY post_id 기준으로 묶으면 각 게시글마다 하나의 줄만 나오고, string_agg 대상도 명확해져서 성능상 더 효율적이에요.

 

 

 

 

BoardDetailDto의 stringAgg 파싱 예시

public void setStringAgg(String stringAgg) {
    if (comments == null) {
        comments = new ArrayList<>();
    }
    String[] split = stringAgg.split("\\|");
    for (String entry : split) {
        String[] data = entry.split(":", 4);
        CommentDetailDto build = CommentDetailDto.builder()
            .commentId(Long.parseLong(data[0]))
            .commenter(data[1])
            .comment(data[2])
            .createdAt(data[3])
            .build();
        comments.add(build);
    }
}

 

1번의 조회로 원하는 데이터 추출


비교

비교 기존 방식 String_agg
소요시간 55ms 46ms
쿼리횟수 게시글수 + 1 1
구현 복잡도 낮음 파싱 로직 필요
적합한 상황 소규모 환경 대용량, 댓글 많은 환경

 

 

기존 방식은 쿼리 호출 횟수가 많아질수록 성능 저하가 발생하는 반면

string_agg 방식은 단일 쿼리로 필요한 데이터를 한 번에 조회하므로 I/O를 줄이고, 응답 속도 향상에 유리합니다.

 

 

 


결과 예시 

 {
	"post": [
            {
                "postId": 1,
                "title": "Post about PostgreSQL",
                "author": "alice",
                "contents": " postgreSql 이란 ? PostgreSQL은 오픈 소스 객체-관계형 데이터베이스 시스템(ORDBMS)으로, Enterprise급 DBMS의 기능과 차세대 DBMS에서나 볼 수 있을 법한 기능들을 제공 ",
                "createdAt": "2025-04-11 12:04:06",
                "commentCount": 3,
                "comments": [
                    {
                        "commentId": 1,
                        "commenter": "bob",
                        "comment": "Thanks for the info!",
                        "createdAt": "2025-04-11 12:04:15"
                    },
                    {
                        "commentId": 2,
                        "commenter": "charlie",
                        "comment": "Very helpful.",
                        "createdAt": "2025-04-11 12:04:15"
                    },
                    {
                        "commentId": 3,
                        "commenter": "bob",
                        "comment": "Can you explain more about indexes?",
                        "createdAt": "2025-04-11 12:04:15"
                    }
                ]
            },
            {
                "postId": 2,
                "title": "Understanding JOINs",
                "author": "bob",
                "contents": "A JOIN is a SQL instruction in the FROM clause of your query that is used to identify the tables you are querying and how they should be combined.",
                "createdAt": "2025-04-11 12:04:06",
                "commentCount": 1,
                "comments": [
                    {
                        "commentId": 4,
                        "commenter": "alice",
                        "comment": "Nice summary of joins.",
                        "createdAt": "2025-04-11 12:04:15"
                    }
                ]
            },
            {
                "postId": 3,
                "title": "Tips for Writing Clean Code",
                "author": "alice",
                "contents": " Effectiveness, Efficiency and Simplicity · Format and Syntax · Naming · Conciseness vs Clarity. ",
                "createdAt": "2025-04-11 12:04:06",
                "commentCount": 2,
                "comments": [
                    {
                        "commentId": 5,
                        "commenter": "charlie",
                        "comment": "Great tips!",
                        "createdAt": "2025-04-11 12:04:15"
                    },
                    {
                        "commentId": 6,
                        "commenter": "bob",
                        "comment": "I agree, clean code is essential.",
                        "createdAt": "2025-04-11 12:04:15"
                    }
                ]
            }
        ]
 }

'DBMS > PostgreSql' 카테고리의 다른 글

SQL 쿼리 최적화 - CTE로 복잡한 조건 정리하기  (0) 2025.04.25
PostgreSQL 메타 정보 조회  (0) 2024.10.01