
이번 문제는 창고 수정 기능 자체의 오류가 아니라, 수정 과정에서 함께 저장되던 Audit Log 때문에 전체 요청이 실패한 케이스였다. 처음에는 Warehouse 수정 API 자체가 잘못된 줄 알았지만, 실제 원인은 감사 로그의 detail_json 저장 과정에 있었다.
창고 수정 요청을 보내면 프론트에서는 아래와 같이 500 Internal Server Error가 발생했다.
처음 보이는 현상만 보면 Warehouse 수정 API 문제처럼 보였지만, 백엔드 로그를 확인해보니 실제 예외는 audit_logs 테이블 insert 시점에 발생하고 있었다.
PUT /api/warehouses/{id} -> 500 Internal Server Error
서버 로그 핵심 메시지는 아래와 같았다.
<p>서버 로그 핵심 메시지는 아래와 같았다.</p>
즉, 창고 수정 자체가 실패한 것이 아니라, 수정 이력을 Audit Log로 남기는 과정에서 detail_json 컬럼 길이를 초과해 insert가 실패했고, 그 결과 전체 트랜잭션이 같이 롤백된 것이다.
처음 AuditLog 엔티티를 보면 detailJson 필드는 이미 LOB로 선언되어 있었다. 그래서 처음에는 엔티티 문제보다는 DB 컬럼이 오래된 스키마로 남아 있다고 판단했다.
@Lob
@Column(name = "detail_json")
private String detailJson;
이 상태만 보면 충분해 보이지만, 실제 실행 로그를 보면 Hibernate가 애플리케이션 시작 시 아래 DDL을 자동 실행하고 있었다.
Hibernate:
alter table if exists audit_logs
modify column detail_json tinytext
이 로그가 핵심이었다. 내가 MariaDB에서 직접 detail_json 컬럼을 LONGTEXT로 바꿔도, 서버를 재시작하면 Hibernate가 다시 tinytext로 내려버리고 있었다. 그래서 DB를 수동으로 수정해도 계속 같은 에러가 발생했던 것이다.
즉 최종 원인은 다음과 같았다.
해결은 두 단계로 진행했다.
단순히 @Lob만 두면 Hibernate가 MariaDB에서 기대와 다르게 tinytext로 판단할 수 있으므로, columnDefinition을 사용해서 LONGTEXT로 직접 고정했다.
@Lob
@Column(name = "detail_json", columnDefinition = "LONGTEXT")
private String detailJson;
이렇게 수정하면 Hibernate가 더 이상 부팅 시 detail_json을 tinytext로 바꾸지 않는다.
엔티티만 바꾸는 것으로 끝나는 것이 아니라, 이미 잘못 잡혀 있던 DB 컬럼도 실제로 LONGTEXT로 다시 변경해야 했다.
ALTER TABLE audit_logs
MODIFY COLUMN detail_json LONGTEXT;
그리고 실제 반영 여부는 아래 쿼리로 확인했다.
SHOW COLUMNS FROM audit_logs LIKE 'detail_json';
정상적으로 반영되면 Type이 longtext로 표시된다.
수정 후 AuditLog 엔티티는 아래와 같이 정리했다.
package com.coreerp.audit.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(
name = "audit_logs",
indexes = {
@Index(name = "idx_audit_logs_created_at", columnList = "created_at"),
@Index(name = "idx_audit_logs_user_id", columnList = "user_id"),
@Index(name = "idx_audit_logs_action", columnList = "action"),
@Index(name = "idx_audit_logs_entity", columnList = "entity_type, entity_id")
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "username", length = 100)
private String username;
@Enumerated(EnumType.STRING)
@Column(name = "action", nullable = false, length = 30)
private AuditAction action;
@Enumerated(EnumType.STRING)
@Column(name = "entity_type", nullable = false, length = 50)
private AuditEntityType entityType;
@Column(name = "entity_id")
private Long entityId;
@Column(name = "description", nullable = false, length = 500)
private String description;
@Lob
@Column(name = "detail_json", columnDefinition = "LONGTEXT")
private String detailJson;
@Column(name = "ip_address", length = 45)
private String ipAddress;
@Column(name = "user_agent", length = 255)
private String userAgent;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Builder
public AuditLog(
Long userId,
String username,
AuditAction action,
AuditEntityType entityType,
Long entityId,
String description,
String detailJson,
String ipAddress,
String userAgent,
LocalDateTime createdAt
) {
this.userId = userId;
this.username = username;
this.action = action;
this.entityType = entityType;
this.entityId = entityId;
this.description = description;
this.detailJson = detailJson;
this.ipAddress = ipAddress;
this.userAgent = userAgent;
this.createdAt = createdAt;
}
}
수정 후에는 서버 시작 로그에서 더 이상 아래 문장이 나오지 않아야 한다.
alter table if exists audit_logs
modify column detail_json tinytext
이 문장이 계속 보이면 엔티티 수정이 반영되지 않았거나, 다른 매핑이 여전히 detail_json을 작은 타입으로 판단하고 있다는 뜻이다.
반대로 이 문장이 사라지고, 창고 수정 요청이 200 또는 정상 응답으로 돌아오면 문제는 해결된 것이다.
이번 이슈는 단순히 DB 컬럼 길이 문제처럼 보였지만, 실제로는 JPA 엔티티와 Hibernate ddl-auto update 동작 방식까지 같이 봐야 풀 수 있는 문제였다. 특히 개발 중에는 엔티티만 수정하면 끝난다고 생각하기 쉬운데, 실제 DB 스키마와 Hibernate startup 로그를 함께 확인해야 정확한 원인을 찾을 수 있다는 점을 다시 느꼈다.
또 하나 중요한 점은, 기능 로직이 아니라 부가 기능인 Audit Log 때문에 핵심 기능 전체가 실패할 수 있다는 것이다. 즉 재고, 창고, 입출고처럼 ERP의 본 기능을 안정적으로 운영하려면 로그 기능도 데이터 구조와 저장 용량을 충분히 고려해서 설계해야 한다.
이번 문제는 단순 CRUD 오류가 아니라, Backend persistence layer와 DB schema synchronization 문제까지 같이 확인해야 해결할 수 있었던 케이스였다.