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;
결과 예시:
| id | nickname | platform_name | account_name |
| 1 | Alice | Instagram | alice_insta |
| 1 | Alice | TikTok | alice_tiktok |
| 2 | Bob | YouTube | bob_channel |
→ Alice 한 명이 두 개의 플랫폼을 가지고 있어서 결과가 2행으로 나와버립니다.
API 응답이나 화면 출력에서는 인플루언서 1명 = 1행 으로 처리하고 싶은데, 이대로는 불편하죠.
2. MySQL 8 이상 – JSON 집계로 깔끔하게 해결
MySQL 8부터는 JSON_ARRAYAGG와 JSON_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_id | nickname | platforms_json | categories_json |
| 1 | Alice | [{"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"}] |
| 2 | Bob | [{"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. 실무 활용 포인트
- API 응답 최적화
JOIN 결과를 그대로 반환하면 중복 데이터가 많아 API 응답 사이즈가 커집니다. - JSON 집계를 사용하면 응답 데이터가 깔끔하고 직관적입니다.
- DB 단에서 JSON 구조로 가공API 서버에서 별도로 가공할 필요가 줄어들어 로직 단순화 효과가 있습니다.
- 특히 MySQL 8 이상 환경이라면 JSON 집계 쿼리를 적극적으로 활용하세요.
- 호환성 고려구버전(MySQL 5.x)이라면
GROUP_CONCAT으로 대체 가능하지만, - 가능하다면 MySQL 8 이상으로 업그레이드하는 것이 장기적으로 유리합니다.
5. 정리
- 단순 JOIN → 인플루언서가 여러 플랫폼/카테고리를 가질 경우 중복 행 발생
- MySQL 8 이상 →
JSON_ARRAYAGG, JSON_OBJECT 활용해 인플루언서당 1행으로 묶기 - MySQL 5.x →
GROUP_CONCAT으로 문자열 합친 뒤 애플리케이션에서 파싱
이 방법을 적용하면 데이터 구조가 단순해지고, API 응답 포맷과도 잘 맞아 실무에서 바로 활용할 수 있습니다.
댓글