LLM 호출 로그 DB 설계
AI 가이드 검색 요청마다 질문·응답·토큰 사용량·접속 정보를 MySQL에 저장한다.
로컬 개발 시에는 DB 없이도 동작하며, EC2 배포 시 SPRING_PROFILES_ACTIVE=db 활성화로 자동 연결된다.
1. 테이블 DDL (MySQL)
CREATE TABLE T_LLM_CALL_LOG (
LLM_CALL_LOG_SN BIGINT NOT NULL AUTO_INCREMENT COMMENT '로그 일련번호 (PK)',
QUESTION TEXT COMMENT '사용자 질문',
ANSWER LONGTEXT COMMENT 'AI 응답 본문',
MODEL VARCHAR(100) COMMENT '사용 모델명 (예: claude-sonnet-4-20250514)',
INPUT_TOKENS INT COMMENT '입력 토큰 수',
OUTPUT_TOKENS INT COMMENT '출력 토큰 수',
ELAPSED_MS BIGINT COMMENT '응답 소요 시간 (ms)',
SUCCESS_YN CHAR(1) DEFAULT 'Y' COMMENT '성공 여부 (Y/N)',
ERROR_MSG VARCHAR(1000) COMMENT '오류 메시지 (실패 시)',
CLIENT_IP VARCHAR(50) COMMENT '요청자 IP',
USER_AGENT VARCHAR(500) COMMENT '브라우저 User-Agent',
REGIST_DT DATETIME(3) DEFAULT NOW(3) COMMENT '등록 일시',
PRIMARY KEY (LLM_CALL_LOG_SN)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LLM 호출 로그';
CREATE INDEX IDX_T_LLM_CALL_LOG_DT ON T_LLM_CALL_LOG (REGIST_DT);
CREATE INDEX IDX_T_LLM_CALL_LOG_SUCCESS ON T_LLM_CALL_LOG (SUCCESS_YN, REGIST_DT);
2. 프로파일 기반 DB 활성화
로컬 기본 프로파일에는 DataSource 설정이 없다.
EC2 배포 시 환경변수로 db 프로파일을 활성화하면 MySQL이 연결된다.
# EC2 실행 시
export SPRING_PROFILES_ACTIVE=db
export DB_HOST=your-mysql-host
export DB_NAME=guide
export DB_USER=guide
export DB_PASSWORD=your-password
java -jar proj.jar
application-db.yaml:
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:guide}?characterEncoding=UTF-8&serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true
username: ${DB_USER:guide}
password: ${DB_PASSWORD:}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 5
connection-timeout: 5000
3. VO
public class LlmCallLogVo {
private String question;
private String answer;
private String model;
private Integer inputTokens;
private Integer outputTokens;
private Long elapsedMs;
private String successYn;
private String errorMsg;
private String clientIp;
private String userAgent;
// getter / setter
}
4. Service — @ConditionalOnBean
DB 프로파일이 꺼져 있으면 DataSource 빈이 없어 이 서비스도 로드되지 않는다.
Controller에서 @Autowired(required = false)로 주입하면 null-safe하게 처리된다.
@Service
@ConditionalOnBean(DataSource.class)
public class LlmCallLogService {
private final JdbcTemplate jdbcTemplate;
public LlmCallLogService(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void save(LlmCallLogVo vo) {
try {
jdbcTemplate.update(
"INSERT INTO T_LLM_CALL_LOG " +
"(QUESTION, ANSWER, MODEL, INPUT_TOKENS, OUTPUT_TOKENS, ELAPSED_MS, " +
" SUCCESS_YN, ERROR_MSG, CLIENT_IP, USER_AGENT, REGIST_DT) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(3))",
vo.getQuestion(), vo.getAnswer(), vo.getModel(),
vo.getInputTokens(), vo.getOutputTokens(), vo.getElapsedMs(),
vo.getSuccessYn(), vo.getErrorMsg(),
vo.getClientIp(), vo.getUserAgent()
);
} catch (Exception e) {
log.error("LLM 호출 로그 저장 실패: {}", e.getMessage(), e);
}
}
}
5. Controller 연동 요점
@Autowired(required = false)
private LlmCallLogService llmCallLogService;
// 검색 후 로그 저장
if (llmCallLogService != null) {
llmCallLogService.save(vo);
}
AnthropicService.ask()는AnthropicResult를 반환 (answer + inputTokens + outputTokens + successYn)- 클라이언트 IP는
X-Forwarded-For헤더 우선, 없으면request.getRemoteAddr()
6. 조회 쿼리 예시
-- 최근 실패 호출
SELECT LLM_CALL_LOG_SN, QUESTION, MODEL, INPUT_TOKENS, OUTPUT_TOKENS,
ELAPSED_MS, ERROR_MSG, CLIENT_IP, REGIST_DT
FROM T_LLM_CALL_LOG
WHERE SUCCESS_YN = 'N'
ORDER BY REGIST_DT DESC
LIMIT 20;
-- 일별 호출 통계
SELECT DATE(REGIST_DT) AS DT,
COUNT(*) AS CALL_CNT,
SUM(INPUT_TOKENS) AS TOTAL_INPUT,
SUM(OUTPUT_TOKENS) AS TOTAL_OUTPUT,
AVG(ELAPSED_MS) AS AVG_MS
FROM T_LLM_CALL_LOG
GROUP BY DATE(REGIST_DT)
ORDER BY DT DESC;
7. 주의사항
- QUESTION / ANSWER에 개인정보가 포함될 수 있다 — 운영 환경에서 접근 권한 제한 필요
- 로그 저장 실패가 본 검색 기능에 영향을 주면 안 된다 — Service에서 예외를 삼킴
- 장기 운영 시 파티셔닝 또는 주기적 아카이빙 전략 필요 (REGIST_DT 기준)