부제목: Query가 뭐에요. 먹는건가?
문제 개요
오류 상황과 영향 범위 (예: 특정 코드 실행 시 발생, 특정 라이브러리 버전 호환 문제 등) 발생 배경 (예: 코드 테스트 중, 배포 중 문제 발견 등)
나는 2차 스프린트에서 신고 기능과 관리자 페이지 3개 정도를 구현해야 됐었다. 2차 스프린트 시작 이후 빠르게 신고 기능을 추가하고 약 3일 만에 끝내버린 관리자 페이지 기능(형식이 비슷해서 한번 구현하면 나머지는 비슷비슷하게 구현하면 됐기에 빨랐다.) 그렇게 맘 놓고 QA를 받았는데
어마어마한 픽스 요청이 들어왔다! 몇개는 다행이도 내가 미쳐 모르고 구현을 빼먹은 것이어서 추가해줬다. 하지만 그 중에서 제일 시간을 많이 잡아먹은 오류를 어떻게 풀어나갔는지 적어두려고 한다.
오류 메시지 및 원인 분석
오류 코드, 오류 메시지 포함 HTTP 상태 코드, 애플리케이션 로그, 서버 로그 등 구체적인 오류 정보 정리 및 로그 메시지를 통한 분석 제공 문제 발생 원인에 대한 심층 분석
우선 문제가 어떤건지 살펴보자.
관리자-신고댓글조회 시 reportCount 정렬 옵션을 사용해도 reportedAt 순으로 나옴
{
"code": "200",
"message": "신고된 댓글 목록이 조회되었습니다.",
"data": {
"list": [
{
"commentId": 3,
"memberId": "ecd69c04-0194-46f1-9368-2ed7baf3d7f3",
"nickname": "admin1",
"postId": 27,
"title": "테스트 제목",
"body": "whgdkdsu",
"reportReason": [
"ETC"
],
"reportedAt": "2025-05-22 13:22",
"reportCount": 1,
"status": "PENDING"
},
{
"commentId": 4,
"memberId": "ecd69c04-0194-46f1-9368-2ed7baf3d7f3",
"nickname": "admin1",
"postId": 27,
"title": "테스트 제목",
"body": "rbntrntr",
"reportReason": [
"ETC"
],
"reportedAt": "2025-05-22 13:22",
"reportCount": 1,
"status": "PENDING"
},
{
"commentId": 1,
"memberId": "ecd69c04-0194-46f1-9368-2ed7baf3d7f3",
"nickname": "admin1",
"postId": 27,
"title": "테스트 제목",
"body": "진정해%%",
"reportReason": [
"BAD_CONTENT",
"ETC"
],
"reportedAt": "2025-05-22 13:21",
"reportCount": 2,
"status": "PENDING"
},
{
"commentId": 2,
"memberId": "ecd69c04-0194-46f1-9368-2ed7baf3d7f3",
"nickname": "admin1",
"postId": 27,
"title": "테스트 제목",
"body": "알겠어",
"reportReason": [
"BAD_CONTENT"
],
"reportedAt": "2025-05-22 13:21",
"reportCount": 1,
"status": "PENDING"
}
],
"meta": {
"page": 0,
"size": 10,
"totalElements": 4,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
}
}
}
나는 예전에 분명히 정렬 기능을 넣었는데, 구현 중간에 쿼리 오류를 수정하던 중 강제로 정렬을 고정시키는 코드를 넣어버렸었다!
ORDER BY latestReportedAt DESC
이걸 쿼리에 넣어서 강제적으로 latestReportedAt(최신 신고일자)를 내림차순으로 출력하게 만든 것이었다!
자 그럼 이걸 빼면 해결된다!
이제 행복한 마음으로 실행해본다!
2025-05-23T19:00:52.776+09:00 ERROR 23192 --- [kkokkio] [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : Unknown column 'cr.latestReportedAt' in 'order clause'
2025-05-23T19:00:52.781+09:00 TRACE 23192 --- [kkokkio] [nio-8080-exec-2] o.s.t.i.TransactionInterceptor : Completing transaction for [site.kkokkio.domain.comment.service.CommentService.getReportedCommentsList] after exception: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL [/* dynamic native SQL query */ SELECT
c.comment_id AS commentId,
BIN_TO_UUID(m.member_id) AS memberId,
m.nickname AS nickname,
(m.deleted_at IS NOT NULL) AS isDeletedMember,
p.post_id AS postId,
p.title AS postTitle,
c.body AS commentBody,
GROUP_CONCAT(cr.reason SEPARATOR ',') AS reportReasons,
DATE_FORMAT(MAX(cr.created_at), '%Y-%m-%d %H:%i') AS latestReportedAt,
COUNT(cr.comment_report_id) AS reportCount,
cr.status AS status
FROM comment_report cr
JOIN comment c ON cr.comment_id = c.comment_id
JOIN post p ON c.post_id = p.post_id
JOIN member m ON c.member_id = m.member_id
WHERE (? IS NULL OR LOWER(m.nickname) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(p.title) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(c.body) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(cr.reason) LIKE LOWER(CONCAT('%', ?, '%')))
AND (c.deleted_at IS NULL)
AND (c.is_hidden = FALSE)
GROUP BY
c.comment_id, m.member_id, m.nickname, m.deleted_at, p.post_id, p.title, c.body, cr.status
order by cr.latestReportedAt desc limit ?] [Unknown column 'cr.latestReportedAt' in 'order clause'] [n/a]; SQL [n/a]
2025-05-23T19:00:52.785+09:00 ERROR 23192 --- [kkokkio] [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL [/* dynamic native SQL query */ SELECT
c.comment_id AS commentId,
BIN_TO_UUID(m.member_id) AS memberId,
m.nickname AS nickname,
(m.deleted_at IS NOT NULL) AS isDeletedMember,
p.post_id AS postId,
p.title AS postTitle,
c.body AS commentBody,
GROUP_CONCAT(cr.reason SEPARATOR ',') AS reportReasons,
DATE_FORMAT(MAX(cr.created_at), '%Y-%m-%d %H:%i') AS latestReportedAt,
COUNT(cr.comment_report_id) AS reportCount,
cr.status AS status
FROM comment_report cr
JOIN comment c ON cr.comment_id = c.comment_id
JOIN post p ON c.post_id = p.post_id
JOIN member m ON c.member_id = m.member_id
WHERE (? IS NULL OR LOWER(m.nickname) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(p.title) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(c.body) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(cr.reason) LIKE LOWER(CONCAT('%', ?, '%')))
AND (c.deleted_at IS NULL)
AND (c.is_hidden = FALSE)
GROUP BY
c.comment_id, m.member_id, m.nickname, m.deleted_at, p.post_id, p.title, c.body, cr.status
order by cr.latestReportedAt desc limit ?] [Unknown column 'cr.latestReportedAt' in 'order clause'] [n/a]; SQL [n/a]] with root cause
java.sql.SQLSyntaxErrorException: Unknown column 'cr.latestReportedAt' in 'order clause'
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:112)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114)
....
해결 및 고민 과정
처음 문제를 접했을 때 고려했던 대안들과 선택하지 않은 이유 실험적으로 적용한 방법이 실패한 사례와 그 이유 여러 해결책 중 최적의 방안을 선택한 과정
우선 천천히 내 코드들을 보았다. 아래는 CommentService에 있는 관리자 전용 신고된 댓글 목록을 볼 수 있는 메서드의 일부이다.
// Repository에 전달할 최종 Pageable 객체 생성
Pageable repositoryPageable;
if (pageable.isPaged()) {
repositoryPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.unsorted()
);
} else {
repositoryPageable = PageRequest.of(0, Integer.MAX_VALUE, Sort.unsorted());
}
아하! Sort.unsorted가 보인다! 이것은 아까 repository 메서드에 있었던 ORDER BY latestReportedAt DESC 때문에 넣은 코드로 기억한다. 이렇게 되면 repository에 "정렬 정보 없음"을 전달하는 것과 같게 되어버린다!
엥? 그럼 정렬 정보를 주는 코드가 두 개나 빠졌는데 어떻게 정렬 정보를 전달하나요?
그래서 repository에 ORDER BY :orderByClause 를 넣고 @Param("orderByClause") String orderByClause 을 추가하여 orderByClause로 정렬 옵션을 받아 서비스 메서드로 처리를 할 셈이다!
if (pageable.getSort().isUnsorted()) {
// 정렬 옵션이 없을 경우 기본 정렬: latestReportedAt (신고된 시간) 내림차순
orderByClause.append("latestReportedAt DESC");
} else {
boolean isFirstOrder = true;
for (Sort.Order order : pageable.getSort()) {
String property = order.getProperty(); // 예: "reportCount"
Sort.Direction direction = order.getDirection(); // 예: "DESC"
int propertyIndex = apiSortProperties.indexOf(property);
if (propertyIndex == -1) {
// 허용되지 않은 정렬 속성이라면 오류 발생
throw new ServiceException("400", "부적절한 정렬 옵션입니다: " + property);
}
String dbColumn = dbColumnMappings.get(propertyIndex); // 예: "reportCount" (SQL에서 AS된 이름)
if (!isFirstOrder) {
orderByClause.append(", "); // 여러 정렬 조건일 경우 쉼표로 구분
}
orderByClause.append(dbColumn).append(" ").append(direction.name());
isFirstOrder = false;
}
boolean hasReportedAtSort = pageable.getSort().stream()
.anyMatch(order -> "reportedAt".equals(order.getProperty()));
if (!hasReportedAtSort) {
orderByClause.append(", latestReportedAt DESC");
}
}
자! 이제 되겠지? 실행을 해보자!
2025-05-23T15:19:25.218+09:00 ERROR 36376 --- [kkokkio] [nio-8080-exec-5] o.h.engine.jdbc.spi.SqlExceptionHelper : Unknown column 'cr.reportedAt' in 'order clause'
2025-05-23T15:19:25.224+09:00 TRACE 36376 --- [kkokkio] [nio-8080-exec-5] o.s.t.i.TransactionInterceptor : Completing transaction for [site.kkokkio.domain.comment.service.CommentService.getReportedCommentsList] after exception: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL [/* dynamic native SQL query */ SELECT
c.comment_id AS commentId,
BIN_TO_UUID(m.member_id) AS memberId,
m.nickname AS nickname,
(m.deleted_at IS NOT NULL) AS isDeletedMember,
p.post_id AS postId,
p.title AS postTitle,
c.body AS commentBody,
GROUP_CONCAT(cr.reason SEPARATOR ',') AS reportReasons,
DATE_FORMAT(MAX(cr.created_at), '%Y-%m-%d %H:%i') AS latestReportedAt,
COUNT(cr.comment_report_id) AS reportCount,
cr.status AS status
FROM comment_report cr
JOIN comment c ON cr.comment_id = c.comment_id
JOIN post p ON c.post_id = p.post_id
JOIN member m ON c.member_id = m.member_id
WHERE (? IS NULL OR LOWER(m.nickname) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(p.title) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(c.body) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(cr.reason) LIKE LOWER(CONCAT('%', ?, '%')))
AND (c.deleted_at IS NULL)
AND (c.is_hidden = FALSE)
GROUP BY
c.comment_id, m.member_id, m.nickname, m.deleted_at, p.post_id, p.title, c.body, cr.status
ORDER BY ?
, cr.reportedAt desc limit ?] [Unknown column 'cr.reportedAt' in 'order clause'] [n/a]; SQL [n/a]
2025-05-23T15:19:25.229+09:00 ERROR 36376 --- [kkokkio] [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL [/* dynamic native SQL query */ SELECT
c.comment_id AS commentId,
BIN_TO_UUID(m.member_id) AS memberId,
m.nickname AS nickname,
(m.deleted_at IS NOT NULL) AS isDeletedMember,
p.post_id AS postId,
p.title AS postTitle,
c.body AS commentBody,
GROUP_CONCAT(cr.reason SEPARATOR ',') AS reportReasons,
DATE_FORMAT(MAX(cr.created_at), '%Y-%m-%d %H:%i') AS latestReportedAt,
COUNT(cr.comment_report_id) AS reportCount,
cr.status AS status
FROM comment_report cr
JOIN comment c ON cr.comment_id = c.comment_id
JOIN post p ON c.post_id = p.post_id
JOIN member m ON c.member_id = m.member_id
WHERE (? IS NULL OR LOWER(m.nickname) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(p.title) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(c.body) LIKE LOWER(CONCAT('%', ?, '%')))
AND (? IS NULL OR LOWER(cr.reason) LIKE LOWER(CONCAT('%', ?, '%')))
AND (c.deleted_at IS NULL)
AND (c.is_hidden = FALSE)
GROUP BY
c.comment_id, m.member_id, m.nickname, m.deleted_at, p.post_id, p.title, c.body, cr.status
ORDER BY ?
, cr.reportedAt desc limit ?] [Unknown column 'cr.reportedAt' in 'order clause'] [n/a]; SQL [n/a]] with root cause
...
최종 해결책 및 구현
문제 해결을 위한 단계별 조치 방법과 코드 예시 (예: 특정 애노테이션 추가, 라이브러리 버전 조정, 설정 변경 등) 참고 자료 (관련 공식 문서, 개발자 커뮤니티 게시물 등)
그렇게 몇 시간이나 이 오류와 싸웠다. Pageable 인자를 없애기도 하고, MySqlConfig를 만들어 다른 쿼리문을 추가해보기도 해보고, native 쿼리 대신 JPQL을 사용해보려 하기도 했다. 이전에도 쿼리 때문에 애를 먹었는데, 역시 이런 것은 배우고 써먹어야 되겠다고 느꼈다. (제미나이의 도움을 계속 받아 문제를 해결했다.)
결론적으로는 이렇게 했다. 레포지터리에서 ORDER BY latestReportedAt DESC 를 없애고 서비스를 다른 방식으로 수정했다. newSort를 만들어 정렬 정보를 넣는 방식으로 채택했다!
// 1. 정렬 옵션 검증 및 Repository 쿼리 별칭에 맞게 매핑
Map<String, String> sortPropertyMapping = new HashMap<>();
sortPropertyMapping.put("reportedAt", "latestReportedAt");
sortPropertyMapping.put("reportCount", "reportCount");
Sort newSort = Sort.unsorted();
// Pageable의 Sort 객체 순회하며 개별 정렬 Order 처리
for (Sort.Order order : pageable.getSort()) {
String property = order.getProperty();
Sort.Direction direction = order.getDirection();
String sqlProperty = sortPropertyMapping.get(property);
// 허용되지 않은 정렬 속성이면 오류 발생
if (sqlProperty == null) {
throw new ServiceException("400", "부적절한 정렬 옵션입니다.");
}
newSort = newSort.and(Sort.by(direction, sqlProperty));
}
// 만약 Pageable에 정렬 정보가 전혀 없었다면 기본 정렬 적용
if (!newSort.isSorted()) {
newSort = Sort.by(Sort.Direction.DESC, "latestReportedAt");
}
// Repository에 전달할 최종 Pageable 객체 생성 시 Unpaged Pageable 처리
Pageable repositoryPageable;
if (pageable.isPaged()) {
repositoryPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
newSort
);
} else {
// 입력 Pageable이 페이징 정보를 가지고 있지 않다면 (Unpaged)
repositoryPageable = PageRequest.of(0, Integer.MAX_VALUE, newSort);
}
결과 및 성능 분석
개선 후 응답 속도, DB 부하 감소 수치 등 구체적인 데이터 포함 문제 해결 전/후 성능 비교 그래프나 로그 분석 내용 포함 이슈 트래킹 및 자동화 방안 추가 (내부 개발자가 반복적인 문제 해결을 효율적으로 관리할 수 있도록 가이드 제공)
이후 실행을 하니까 코드가 완벽하게 동작을 했다! 🥳🥳🥳🥳
reportedAt과 reportCount가 잘 동작한다!
sort: reportedAt,ASC
{
"code": "200",
"message": "신고된 댓글 목록이 조회되었습니다.",
"data": {
"list": [
{
"commentId": 1,
"memberId": "138d9b88-1af2-42ac-9f99-6e653b0ab2ae",
"nickname": "하은",
"postId": 49,
"title": "테스트 제목",
"body": "굿",
"reportReason": [
"BAD_CONTENT"
],
"reportedAt": "2025-05-20 18:55",
"reportCount": 1,
"status": "PENDING"
},
{
"commentId": 2,
"memberId": "138d9b88-1af2-42ac-9f99-6e653b0ab2ae",
"nickname": "하은",
"postId": 49,
"title": "테스트 제목",
"body": "굿굿",
"reportReason": [
"ETC",
"RUDE_LANGUAGE"
],
"reportedAt": "2025-05-23 12:08",
"reportCount": 2,
"status": "PENDING"
}
],
"meta": {
"page": 0,
"size": 10,
"totalElements": 2,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
}
}
}
추가 개선점
유사한 문제를 방지하기 위한 주의할 점 (예: 애노테이션 명시 여부, 프레임워크 업데이트 주기 확인 등) 문제 해결 과정에서 배운 점 및 중요한 교훈 공유 보안 고려 사항 추가 (해결 과정에서 발생할 수 있는 보안 리스크 및 예방 조치 포함)
뭐든지 기초 지식을 쌓고 보자.. 이 문제는 내가 쿼리를 사용하는 방법을 잘 몰라서 생긴 문제였다. 아마 쿼리를 알고 했다면 AI의 도움 없이 문제를 풀거나, 비교적 문제를 풀기 수월했을 것이다.
'kkokkio - 프로젝트 > 트러블슈팅' 카테고리의 다른 글
로그인시 쿠키가 붙지 않는 오류 해결 (1) | 2025.06.16 |
---|---|
모니터링 대시보드 설정 (0) | 2025.06.16 |
JwtAuthenticationFilter에서 요청할 때마다 Member DB 조회하는 문제 (0) | 2025.06.16 |
[Spring 영속성] don't flush the Session after an exception occurs 오류 - 트랜잭션 안에서 JPA 엔티티 재사용 시 문제 (0) | 2025.06.16 |
MySQL에서 중복 데이터를 관리하는 방법 (0) | 2025.06.16 |