23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
조직 내에 Claude Team Plan을 도입했다. (HR에서도 잘 활용할 수 있을까? ㅠㅠ)
난 무엇을 할 수 있을 까? AI 도구를 처음 도입했는데 그래도 좀 성과물을 보여줘야하지 않을까 해피프라이데이 아침에 생각하다
무심코 그냥 내가 운영하고 있는 사이트 성능 개선이나 좀 해볼까하는 아이디어가 떠올라 3시간만에 실행한 결과물입니다.
이 블로그 조차 작업이 끝난 후 기술블로그 Agent 로 생성하였습니다. (이미지는 최근 GPT 5.5가 좋아졌다고 해서 내용을 파악 후 중간중간에 넣어달라고 했고요^^)
한 번 쭈욱 읽어보시고, 앞으로 이런 AI 시대에 개발자/비개발자 모두가 본인의 업무에 AI를 적용한 사례를 블로그로 쉽게 올릴 수 있다는 생각을 했으면 좋겠네요^^
"사이트가 느려요."
개발자라면 이 한마디에 등골이 서늘해진 경험이 있을 겁니다. 특히 그 대상이 10년 넘게 운영되어 온 레거시 시스템이라면요.
JSP, Spring MVC, MyBatis, jQuery... 그 시절의 기술 스택이 켜켜이 쌓여 있는 시스템 앞에서 "느린 부분 찾아서 고쳐주세요"라는 요청은 마치 실타래 뭉치에서 매듭을 찾아달라는 것과 비슷합니다.
불과 몇 년 전까지만 해도, 이런 작업은 전문 컨설팅의 영역이었습니다.
DB 튜닝 전문가가 투입되고, APM 도구를 설치하고, 슬로우 쿼리 로그를 며칠간 수집하고, 실행 계획을 하나하나 분석하고... 수백에서 수천만 원의 비용과 몇 주의 시간이 필요한 프로젝트였죠.
그런데 오늘, 저는 이 작업을 AI와 함께 하루 만에 해냈습니다.
제가 다루는 시스템은 DevOcean의 THub 플랫폼입니다. Spring 4.0 기반의 eGovframework 위에 구축된, 전형적인 엔터프라이즈 웹 애플리케이션이죠.
Java 클래스 380개
MyBatis Mapper XML 49개
JSP 파일 200개
내부 관리 시스템 + 외부 포털 이중 구조
이 정도 규모의 시스템에서 성능 병목을 찾으려면, 우선 코드를 읽어야 합니다.
Controller가 어떤 Service를 호출하고, Service가 어떤 DAO를 거치고, DAO가 어떤 Mapper XML의 어떤 쿼리를 실행하는지.
한 화면을 렌더링하는 데 관여하는 코드가 4~5개 레이어에 걸쳐 있습니다.
[Browser] → Controller (.java)
→ Service (.java)
→ DAO (.java)
→ Mapper XML (.xml)
→ MySQL DBTHub의 레이어 구조. 한 화면의 성능을 분석하려면 이 5개 레이어를 모두 추적해야 한다.
사람이 이 작업을 하면? 한 화면당 반나절입니다.
여기서 Claude Code의 에이전트 팀 기능이 빛을 발했습니다. 저는 3명의 전문가 에이전트를 구성했습니다.
에이전트 | 역할 |
|---|---|
Tracer | Controller에서 Mapper XML까지 호출 경로 추적 |
Query Analyst | SQL 쿼리의 성능 문제 패턴 탐지 |
Optimizer | 두 결과를 종합하여 우선순위별 개선안 제시 |
그리고 메인 페이지와 블로그 페이지를 대상으로, 4개 에이전트를 동시에 실행했습니다.
[Tracer-Main] → 메인 페이지 호출 경로 추적
[Tracer-Blog] → 블로그 페이지 호출 경로 추적
[Query-Main] → Main_SQL.xml 쿼리 분석
[Query-Blog] → Blog_SQL.xml 쿼리 분석사람 4명이 동시에 작업하는 것과 같은 효과입니다. 다만, 이 4명은 380개 클래스를 읽는 데 지치지 않고, 49개 Mapper XML의 모든 쿼리를 빠짐없이 검사합니다.
[리더 (나)]
├── Tracer-Main ──→ 메인 Controller→Service→DAO→XML 추적
├── Tracer-Blog ──→ 블로그 Controller→Service→DAO→XML 추적
├── Query-Main ───→ Main_SQL.xml 전체 쿼리 성능 분석
└── Query-Blog ───→ Blog_SQL.xml 전체 쿼리 성능 분석
↓ (약 5분)
[종합 보고서 생성]Claude Code에서 4개 에이전트가 병렬로 분석하는 구조. 사람 4명이 동시에 코드 리뷰하는 것과 같은 효과다.
약 5분 후, 결과가 도착했습니다.
심각도 | 건수 |
|---|---|
HIGH | 26건 |
MEDIUM | 19건 |
LOW | 11건 |
총 56건. 솔직히 놀랐습니다. 이미 운영 중인 서비스에서 이렇게 많은 개선 포인트가 나올 줄은 몰랐거든요.
그중에서도 특히 눈에 띈 것들을 소개합니다.
성능을 보러 갔다가 보안 구멍을 발견했습니다. MyBatis에서 ${loginId} 형태로 문자열을 직접 치환하는 코드가 7개 파일, 14곳에 존재했습니다.
<!-- 위험: SQL Injection 가능 -->
WHERE USER_ID = '${loginId}'
<!-- 안전: PreparedStatement 바인딩 -->
WHERE USER_ID = #{loginId}${}는 사용자 입력을 SQL에 그대로 삽입합니다. 공격자가 ' OR '1'='1 같은 값을 넣으면? 상상에 맡기겠습니다. 이건 성능과 관계없이 즉시 수정이 필요한 항목이었습니다.
블로그 작성자별 목록 페이지(/blog/writer/index.do)를 추적했더니, 10건의 게시글을 보여주는 데 DB를 46회 호출하고 있었습니다.
원인은 이랬습니다:
// 게시글 10건을 가져온 뒤...
for (int i = 0; i < BlogList.size(); i++) {
// 각 게시글마다 프로필 사진을 3번의 DB 호출로 조회
filePath = externalBlogService.getUserPhoto(writer);
// getUserPhoto 내부: checkTid → checkExtUserInfo → getFilePath
}10건 × 3회 = 30회의 추가 DB 호출. 전형적인 N+1 문제입니다.
수정은 간단했습니다. 목록 쿼리에 LEFT JOIN 3개를 추가해서 프로필 이미지를 한 번에 가져오도록 변경.
개선 전: 6회(기본) + 30회(프로필) + 10회(썸네일) = 46회
개선 후: 6회(기본, JOIN으로 프로필 포함) = 6회블로그 상세 페이지를 렌더링하는 getTechBoadData 쿼리를 열어봤더니, SELECT 절에 상관 서브쿼리가 10개나 들어있었습니다.
댓글 수, 좋아요 수, 북마크 여부, 팔로우 수, 커뮤니티 목록, 닉네임, 프로필 이미지... 각각이 독립된 서브쿼리로 실행되고 있었죠.
Before - 서브쿼리 10개가 각각 독립 실행:
-- 댓글 수: 서브쿼리 1회
,FORMAT((SELECT COUNT(*) FROM TB_EXT_BOARD_REPLY
WHERE BOARD_ID = A.ID AND BOARD_TYPE = 'BLOG'),0) REPLY_CNT
-- 좋아요 수: 서브쿼리 1회
,FORMAT((SELECT COUNT(*) FROM TB_EXT_LIKE
WHERE LIKE_TARGET_ID = A.ID AND LIKE_YN = 'Y'),0) LIKE_CNT
-- 닉네임: 서브쿼리 1회
,(SELECT nickname FROM tb_ext_user_info WHERE tid=c.tid) DEVOCEAN_NICKNAME
-- INX: 같은 테이블 또 조회!
,(SELECT inx FROM tb_ext_user_info WHERE tid=c.tid) DEVOCEAN_INX
-- 프로필 이미지: 같은 테이블 또또 조회 + JOIN까지!
,(SELECT T2.file_path FROM tb_ext_user_info T1
INNER JOIN tb_file T2 ON T1.profile_img_idx = T2.idx
WHERE T1.tid = C.tid) AS extUserFileLocation
-- ... 이런 게 10개After - LEFT JOIN으로 한 방에:
-- 댓글 수: 파생 테이블 1번 JOIN
,FORMAT(IFNULL(RPLY.REPLY_CNT, 0),0) REPLY_CNT
-- 좋아요 수: 파생 테이블 1번 JOIN
,FORMAT(IFNULL(EL.LIKE_CNT, 0),0) LIKE_CNT
-- 닉네임 + INX + 프로필: LEFT JOIN 1번으로 통합!
,EU.NICKNAME DEVOCEAN_NICKNAME
,EU.INX DEVOCEAN_INX
,EUF.FILE_PATH AS extUserFileLocation같은 테이블(tb_ext_user_info)을 3번 따로 조회하던 것을 1번의 JOIN으로 통합한 것이 핵심이었습니다.
성능 최적화가 항상 순탄하지만은 않았습니다.
블로그 목록 쿼리를 최적화하면서 content 필드를 LEFT(A.CONTENT, 500)으로 잘랐는데, SK AI SUMMIT 카테고리의 게시물 미리보기가 공백으로 표시되는 문제가 발생했습니다.
원인을 추적해보니, 해당 게시물들은 슬라이드 임베드 등 HTML 마크업이 매우 많아서 처음 500자가 전부 HTML 태그였습니다.
프론트엔드의 Jsoup.parse().text()가 태그를 제거하면 텍스트가 하나도 안 남는 거죠.
HTML 500자 → Jsoup 태그 제거 → "" (빈 문자열)LEFT(A.CONTENT, 4000)으로 늘려서 해결했지만, 이 경험은 중요한 교훈을 남겼습니다:
성능 최적화는 좋지만, 서비스에 장애를 주면 안 된다.
AI가 제안하는 최적화든 사람이 하는 최적화든, 반드시 다양한 데이터 유형으로 검증해야 합니다.
"평균적인 게시물"에서는 잘 작동하지만, HTML이 많은 프레젠테이션이나 임베드 콘텐츠에서 깨질 수 있으니까요.
정리하면, 오늘 하루 동안 이런 작업을 완료했습니다:
커밋 | 내용 | 효과 |
|---|---|---|
SQL Injection 전체 제거 | 7파일 14곳 | 보안 취약점 제거 |
N+1 getUserPhoto 제거 | JOIN 통합 | DB 호출 30회 → 0회 |
getTechBoadData 서브쿼리→JOIN | 서브쿼리 10개 변환 | 블로그 상세 쿼리 효율화 |
getExternalBoardList 서브쿼리→JOIN | 3벌 쿼리 동시 변환 | 앱 메인 서브쿼리 36회 → 7회 |
Content 잘림 수정 | LEFT(500) → LEFT(4000) | SK AI SUMMIT 표시 정상화 |
그리고 나머지 17건의 개선 항목은 우선순위와 구현 계획이 포함된 결과 보고서로 문서화해두었습니다. 다음에 시간이 날 때 하나씩 진행하면 됩니다.
솔직히 말하면, 오늘 한 작업 중 AI가 "새로운 기술"을 사용한 건 하나도 없습니다.
N+1 문제를 JOIN으로 해결하고, 상관 서브쿼리를 파생 테이블로 변환하고, ${}를 #{}로 바꾸는 건 모두 교과서에 나오는 기본기입니다.
달라진 건 속도와 범위입니다.
과거에 성능 컨설턴트가 했던 일을 떠올려보면:
시스템 구조 파악 → 며칠
슬로우 쿼리 수집 및 분석 → 며칠
개선안 수립 → 며칠
보고서 작성 → 며칠
실제 수정 및 검증 → 며칠~몇 주
AI와 함께하면:
에이전트 팀이 코드 전체를 읽고 분석 → 5분
56건의 이슈를 심각도별로 분류 → 자동
개선안과 코드 수정을 함께 제시 → 즉시
실제 코드 수정 및 검증 → 수 시간
이게 가능한 이유는, AI가 380개 Java 클래스와 49개 Mapper XML을 전부 읽을 수 있기 때문입니다.
사람은 하루에 이 양을 다 읽을 수 없지만, AI는 할 수 있죠. 그리고 읽은 내용을 바탕으로 패턴을 찾아내는 것도 AI가 잘하는 영역입니다.
아닙니다. 오히려 더 중요해질 수도 있습니다.
AI가 "이 쿼리에 상관 서브쿼리가 10개 있으니 JOIN으로 바꾸세요"라고 말해줄 수는 있지만, "이 최적화가 우리 서비스의 트래픽 패턴에서 실제로 의미가 있는가?"는 사람이 판단해야 합니다.
실제로 오늘 작업 중에도 이런 판단이 필요했습니다.
분석에서 getImageBasic의 N+1을 최우선 과제로 올려놓았지만, 실제로는 "모든 게시글에 이미 썸네일이 있어서 해당 분기가 실행되지 않는다"는 도메인 지식이 있었기에 건너뛸 수 있었습니다.
AI는 코드를 읽지만, 비즈니스 맥락은 사람이 알고 있습니다. 둘의 협업이 최고의 결과를 만들어냅니다.
10년 된 레거시 시스템의 성능을 개선하는 일은, 더 이상 몇 주짜리 프로젝트가 아닙니다.
올바른 도구와 함께라면, 커피 몇 잔 마시는 동안 보안 취약점을 잡고, 쿼리를 최적화하고, 결과 보고서까지 만들 수 있습니다.
물론 AI의 제안을 무조건 따르면 안 됩니다.
SK AI SUMMIT 콘텐츠 공백 사건처럼, 최적화에도 사이드이펙트가 있을 수 있으니까요.
하지만 그런 실수조차 빠르게 발견하고 수정할 수 있다는 것, 그것이 AI와 함께 일하는 진짜 장점이 아닐까요.
다음 편에서는 남은 17건의 개선 항목을 하나씩 해치우는 이야기를 들려드리겠습니다. 아마 그것도 하루면 될 겁니다.
이 글에서 다룬 성능 최적화 작업은 Claude Code의 에이전트 팀 기능을 활용하여 수행되었습니다.
03_optimization_report.md)AI 에이전트 팀이 메인 페이지와 블로그 페이지를 분석하여 생성한 종합 보고서입니다.
4개 에이전트(tracer-main, tracer-blog, query-main, query-blog)가 병렬로 분석한 결과를 optimizer가 우선순위별로 정리했습니다.
분석 범위: 메인 페이지(ExternalMainController), 블로그 페이지(ExternalBlogController), 공통 모듈(Common_SQL, Search_SQL)
발견 이슈 요약:
우선순위 | 건수 | 주요 내용 |
|---|---|---|
P0 보안 | 1건 | SQL Injection |
P1 핵심 병목 | 5건 | N+1 쿼리, LIMIT 없는 전체 조회, GROUP_CONCAT+LIKE 패턴, 전체 GROUP BY |
P2 서브쿼리 | 7건 | 상관 서브쿼리(행마다 실행), SELECT *, 페이징 없음, LIKE '%keyword%' |
P3 코드 품질 | 7건 | 중복 서브쿼리, HashMap 과다, @rownum, |
P4 캐싱 | 5건 | 배너/메뉴/태그 등 마스터 데이터 매 요청 조회 |
페이지별 DB 호출 분석:
메인 페이지: 1회 로딩 시 7~21회 DB 호출 (getImageBasic N+1로 최대 12회 추가)
블로그 상세: 25+회 (서브쿼리 10개 + 관련글/추천글 루프)
작성자별 목록: 46회/10건 (getUserPhoto 3회 x N)
04_optimization_result.md)종합 보고서의 25건 항목 대비 실제 수정 현황을 추적한 결과서입니다.
진행 현황: 전체 25건 중 7건 완료, 1건 보류, 17건 미적용
완료 커밋 이력:
커밋 | 내용 | 핵심 효과 |
|---|---|---|
| 메인 페이지 쿼리 최적화 | GROUP_CONCAT+LIKE 제거, 서브쿼리→JOIN |
| 블로그 목록 쿼리 최적화 | 서브쿼리 7개→LEFT JOIN, |
| 미사용 ID 목록 쿼리 제거 | 수천건 전체 조회 제거 |
| Blog content 잘림 수정 | SK AI SUMMIT 콘텐츠 표시 정상화 |
| SQL Injection 전체 제거 | 7파일 14곳 보안 취약점 제거 |
| N+1 getUserPhoto 제거 | DB 호출 30회→0회 |
| 블로그 상세 서브쿼리→JOIN | 서브쿼리 10개→JOIN 9개 |
| 앱 메인 서브쿼리→JOIN | 3벌 쿼리 서브쿼리 36회→JOIN 7회 |
개선 효과 (실측 전 추정):
페이지 | 개선 전 | 개선 후 |
|---|---|---|
메인 (앱) | 7~21회 | 7~9회 |
블로그 목록 | 9+N회 | 9회 |
블로그 상세 | 25+회 | 10~12회 |
작성자별 목록 | 46회 | 6회 |
다음 단계 추천: 페이징/LIMIT 추가(#11~13) → 댓글 쿼리 개선(#9) → 캐싱 적용(P4) → FULLTEXT 검색(#10)
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.