FeignClient 호출 로그 DB 설계
외부 API 호출 이력(요청·응답)을 DB에 저장하여 장애 추적, 감사, 재처리에 활용한다.
1. 테이블 설계 (T_FEIGN_CALL_LOG)
CREATE TABLE T_FEIGN_CALL_LOG (
FEIGN_CALL_LOG_SN NUMBER NOT NULL, -- 로그 일련번호 (PK)
CLIENT_NM VARCHAR2(100) NOT NULL, -- FeignClient 이름 (예: postClient)
HTTP_METHOD VARCHAR2(10) NOT NULL, -- HTTP 메서드 (GET/POST/PUT/DELETE)
REQUEST_URL VARCHAR2(1000) NOT NULL, -- 호출 URL (전체)
REQUEST_BODY CLOB, -- 요청 바디 (JSON)
RESPONSE_BODY CLOB, -- 응답 바디 (JSON)
HTTP_STATUS NUMBER(3), -- HTTP 상태코드 (200, 400, 500 등)
ELAPSED_MS NUMBER(10), -- 소요 시간 (ms)
SUCCESS_YN CHAR(1) DEFAULT 'Y' NOT NULL, -- 성공 여부 (Y/N)
ERROR_MSG VARCHAR2(2000), -- 오류 메시지 (실패 시)
CALLER_CLASS VARCHAR2(200), -- 호출 클래스명
CALLER_METHOD VARCHAR2(200), -- 호출 메서드명
FRST_REGIST_ID VARCHAR2(100) NOT NULL,
FRST_REGIST_IP VARCHAR2(50) NOT NULL,
FRST_REGIST_DT TIMESTAMP WITH TIME ZONE NOT NULL,
LAST_UPDT_ID VARCHAR2(100) NOT NULL,
LAST_UPDT_IP VARCHAR2(50) NOT NULL,
LAST_UPDT_DT TIMESTAMP WITH TIME ZONE NOT NULL,
CONSTRAINT PK_T_FEIGN_CALL_LOG PRIMARY KEY (FEIGN_CALL_LOG_SN)
);
CREATE SEQUENCE SEQ_T_FEIGN_CALL_LOG
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;
CREATE INDEX IDX_T_FEIGN_CALL_LOG_CLIENT ON T_FEIGN_CALL_LOG (CLIENT_NM, FRST_REGIST_DT);
CREATE INDEX IDX_T_FEIGN_CALL_LOG_STATUS ON T_FEIGN_CALL_LOG (SUCCESS_YN, FRST_REGIST_DT);
2. VO
@Getter @Setter @ToString
public class FeignCallLogVo extends GeneralVO {
private Long feignCallLogSn;
private String clientNm;
private String httpMethod;
private String requestUrl;
private String requestBody;
private String responseBody;
private Integer httpStatus;
private Long elapsedMs;
private String successYn;
private String errorMsg;
private String callerClass;
private String callerMethod;
}
3. Mapper 인터페이스
@Mapper
public interface FeignCallLogMapper {
int reg(FeignCallLogVo vo);
}
4. Mapper XML
<!-- /resources/mybatis/.../mapper/FeignCallLogMapper.xml -->
<mapper namespace="com.example.feign.mapper.FeignCallLogMapper">
<insert id="reg" parameterType="FeignCallLogVo">
INSERT INTO T_FEIGN_CALL_LOG (
FEIGN_CALL_LOG_SN
, CLIENT_NM
, HTTP_METHOD
, REQUEST_URL
, REQUEST_BODY
, RESPONSE_BODY
, HTTP_STATUS
, ELAPSED_MS
, SUCCESS_YN
, ERROR_MSG
, CALLER_CLASS
, CALLER_METHOD
, FRST_REGIST_ID
, FRST_REGIST_IP
, FRST_REGIST_DT
, LAST_UPDT_ID
, LAST_UPDT_IP
, LAST_UPDT_DT
) VALUES (
SEQ_T_FEIGN_CALL_LOG.NEXTVAL
, #{clientNm}
, #{httpMethod}
, #{requestUrl}
, #{requestBody}
, #{responseBody}
, #{httpStatus}
, #{elapsedMs}
, #{successYn}
, #{errorMsg}
, #{callerClass}
, #{callerMethod}
, #{frstRegistId}
, #{frstRegistIp}
, SYSTIMESTAMP
, #{lastUpdtId}
, #{lastUpdtIp}
, SYSTIMESTAMP
)
</insert>
</mapper>
5. Service
@Slf4j
@RequiredArgsConstructor
@Service
public class FeignCallLogService {
private final FeignCallLogMapper feignCallLogMapper;
@Transactional(rollbackFor = Exception.class)
public void reg(FeignCallLogVo vo) {
try {
feignCallLogMapper.reg(vo);
} catch (Exception e) {
// 로그 저장 실패가 본 업무에 영향을 주지 않도록 예외를 삼킴
log.error("FeignClient 호출 로그 저장 실패: {}", e.getMessage(), e);
}
}
}
6. AOP로 자동 기록
FeignClient 인터페이스 메서드 호출 시 자동으로 로그를 DB에 저장한다.
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class FeignCallLogAspect {
private final FeignCallLogService feignCallLogService;
private final SessionManager sessionManager;
// client 패키지 하위의 모든 FeignClient 메서드에 적용
@Around("execution(* com.example..client.*Client.*(..))")
public Object logFeignCall(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String callerClass = pjp.getTarget().getClass().getSimpleName();
String callerMethod = pjp.getSignature().getName();
String clientNm = callerClass;
// 요청 바디 추출 (첫 번째 인자가 RequestBody인 경우)
String requestBody = null;
Object[] args = pjp.getArgs();
if (args != null && args.length > 0 && args[0] != null) {
try {
requestBody = new ObjectMapper().writeValueAsString(args[0]);
} catch (Exception ignored) {}
}
FeignCallLogVo logVo = new FeignCallLogVo();
logVo.setClientNm(clientNm);
logVo.setCallerClass(callerClass);
logVo.setCallerMethod(callerMethod);
logVo.setRequestBody(requestBody);
logVo.setFrstRegistId(getCallerId());
logVo.setFrstRegistIp(getCallerIp());
logVo.setLastUpdtId(getCallerId());
logVo.setLastUpdtIp(getCallerIp());
try {
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
// 성공
logVo.setSuccessYn("Y");
logVo.setElapsedMs(elapsed);
logVo.setHttpStatus(200);
try {
logVo.setResponseBody(new ObjectMapper().writeValueAsString(result));
} catch (Exception ignored) {}
feignCallLogService.reg(logVo);
return result;
} catch (FeignException e) {
long elapsed = System.currentTimeMillis() - start;
// 실패
logVo.setSuccessYn("N");
logVo.setElapsedMs(elapsed);
logVo.setHttpStatus(e.status());
logVo.setErrorMsg(e.getMessage() != null
? e.getMessage().substring(0, Math.min(e.getMessage().length(), 2000))
: null);
feignCallLogService.reg(logVo);
throw e;
}
}
private String getCallerId() {
try {
return sessionManager.getSession().getUserNm();
} catch (Exception e) {
return "SYSTEM";
}
}
private String getCallerIp() {
try {
return sessionManager.getClientIP();
} catch (Exception e) {
return "0.0.0.0";
}
}
}
AOP 적용 범위 커스터마이징
// 특정 클라이언트만 적용
@Around("execution(* com.example.order.client.OrderClient.*(..))" +
"|| execution(* com.example.user.client.UserClient.*(..))")
// 특정 어노테이션이 붙은 메서드만 적용
@Around("@annotation(com.example.common.annotation.FeignLogging)")
7. 조회 쿼리 예시
-- 최근 실패 호출 조회
SELECT
FEIGN_CALL_LOG_SN
, CLIENT_NM
, HTTP_METHOD
, REQUEST_URL
, HTTP_STATUS
, ELAPSED_MS
, ERROR_MSG
, FRST_REGIST_DT
FROM T_FEIGN_CALL_LOG
WHERE SUCCESS_YN = 'N'
ORDER BY FRST_REGIST_DT DESC
OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;
-- 특정 클라이언트 평균 응답시간 조회
SELECT
CLIENT_NM
, COUNT(*) AS CALL_CNT
, AVG(ELAPSED_MS) AS AVG_ELAPSED_MS
, MAX(ELAPSED_MS) AS MAX_ELAPSED_MS
FROM T_FEIGN_CALL_LOG
WHERE FRST_REGIST_DT >= SYSTIMESTAMP - INTERVAL '1' DAY
GROUP BY CLIENT_NM
ORDER BY AVG_ELAPSED_MS DESC;
8. 주의사항
- 로그 저장 실패가 본 업무에 영향을 주면 안 된다 — Service에서 예외를 삼겨서 처리
REQUEST_BODY/RESPONSE_BODY에 비밀번호, 카드번호, 개인정보 포함 금지 — 저장 전 마스킹 처리- CLOB 컬럼 크기 초과 방지를 위해 저장 전 최대 길이 truncate 권장 (예: 50,000자)
- 로그 테이블은 주기적으로 파티셔닝 또는 아카이빙 전략 필요 (호출량에 따라 급증)