TokyoAJ

도쿄아재

MEMO 2025.10.02

MySQL에서 JOIN 시 중복 행을 JSON 집계로 해결하기

MySQL에서 JOIN 시 중복 행을 JSON 집계로 해결하기


1. JOIN 시 발생하는 문제

데이터베이스 설계에서 1:N 관계는 아주 흔합니다.

예를 들어 인플루언서(Influencer)는 여러 개의 플랫폼 계정(Instagram, TikTok 등)을 가질 수 있고, 여러 개의 카테고리(음악, 패션 등)에 속할 수 있습니다.

이때 단순히 JOIN을 하면 어떤 문제가 생길까요?

SELECT
i.id, i.nickname, ip.platform_name, ip.account_name
FROM influencers i
LEFT JOIN influencer_platforms ip
ON i.id = ip.influencer_id
WHERE i.is_deleted = 0;

결과 예시:

idnicknameplatform_nameaccount_name
1AliceInstagramalice_insta
1AliceTikTokalice_tiktok
2BobYouTubebob_channel

→ Alice 한 명이 두 개의 플랫폼을 가지고 있어서 결과가 2행으로 나와버립니다.

API 응답이나 화면 출력에서는 인플루언서 1명 = 1행 으로 처리하고 싶은데, 이대로는 불편하죠.


2. MySQL 8 이상 – JSON 집계로 깔끔하게 해결

MySQL 8부터는 JSON_ARRAYAGGJSON_OBJECT라는 강력한 JSON 함수가 추가되었습니다.

이를 활용하면 여러 행을 하나의 JSON 배열로 묶을 수 있습니다.

쿼리 예제

SELECT
i.id AS influencer_id, i.user_id, i.nickname,
i.birth_date, i.gender, i.location,
i.favorite_artist, i.music_genres,
i.postable_menus, i.bank_verify_status,
i.description, i.profile_image_url, i.bank_image_url,
ips.platforms_json,
ics.categories_json
FROM influencers i
LEFT JOIN (
SELECT
influencer_id,
JSON_ARRAYAGG(
JSON_OBJECT(
'platform_id', id,
'platform_name', platform_name,
'account_name', account_name,
'followers_count', followers_count,
'engagement_rate', engagement_rate,
'created_at', created_at,
'updated_at', updated_at
)
) AS platforms_json
FROM influencer_platforms
WHERE is_deleted = 0
GROUP BY influencer_id
) ips ON ips.influencer_id = i.id
LEFT JOIN (
SELECT
influencer_id,
JSON_ARRAYAGG(
JSON_OBJECT(
'category_id', id,
'category', category,
'created_at', created_at
)
) AS categories_json
FROM influencer_categories
WHERE is_deleted = 0
GROUP BY influencer_id
) ics ON ics.influencer_id = i.id
WHERE i.is_deleted = 0
ORDER BY i.id DESC
LIMIT 2 OFFSET 0;

결과 예시

influencer_idnicknameplatforms_jsoncategories_json
1Alice[{"platform_id":10,"platform_name":"Instagram","account_name":"alice_insta"}, {"platform_id":11,"platform_name":"TikTok","account_name":"alice_tiktok"}][{"category_id":5,"category":"Music"}]
2Bob[{"platform_id":12,"platform_name":"YouTube","account_name":"bob_channel"}][{"category_id":6,"category":"Fashion"}]

→ 인플루언서당 1행만 출력되고, 각 플랫폼과 카테고리는 JSON 배열로 정리됩니다.

API로 바로 응답하기도 좋은 형태입니다.


3. MySQL 5.x – GROUP_CONCAT으로 대체하기

MySQL 5.x는 JSON 함수를 지원하지 않기 때문에, GROUP_CONCAT을 사용해서 문자열을 합친 뒤 애플리케이션에서 파싱하는 방법을 씁니다.

SELECT
i.id AS influencer_id,
i.nickname,
CONCAT('[', GROUP_CONCAT(
CONCAT('{',
'"platform_id":', ip.id,
',"platform_name":"', ip.platform_name, '"',
',"account_name":"', ip.account_name, '"',
'}')
), ']') AS platforms_json
FROM influencers i
LEFT JOIN influencer_platforms ip
ON ip.influencer_id = i.id AND ip.is_deleted = 0
WHERE i.is_deleted = 0
GROUP BY i.id;

→ 완벽한 JSON은 아니지만, 애플리케이션에서 문자열을 파싱하면 유사한 효과를 얻을 수 있습니다.


4. 실무 활용 포인트

  1. API 응답 최적화JOIN 결과를 그대로 반환하면 중복 데이터가 많아 API 응답 사이즈가 커집니다.
  2. JSON 집계를 사용하면 응답 데이터가 깔끔하고 직관적입니다.
  3. DB 단에서 JSON 구조로 가공API 서버에서 별도로 가공할 필요가 줄어들어 로직 단순화 효과가 있습니다.
  4. 특히 MySQL 8 이상 환경이라면 JSON 집계 쿼리를 적극적으로 활용하세요.
  5. 호환성 고려구버전(MySQL 5.x)이라면 GROUP_CONCAT으로 대체 가능하지만,
  6. 가능하다면 MySQL 8 이상으로 업그레이드하는 것이 장기적으로 유리합니다.


5. 정리

  1. 단순 JOIN → 인플루언서가 여러 플랫폼/카테고리를 가질 경우 중복 행 발생
  2. MySQL 8 이상 → JSON_ARRAYAGG, JSON_OBJECT 활용해 인플루언서당 1행으로 묶기
  3. MySQL 5.x → GROUP_CONCAT으로 문자열 합친 뒤 애플리케이션에서 파싱

이 방법을 적용하면 데이터 구조가 단순해지고, API 응답 포맷과도 잘 맞아 실무에서 바로 활용할 수 있습니다.

댓글