FeignClient 호출 로그 DB 설계

외부 API 호출 요청/응답을 DB에 기록하는 테이블 설계 및 사용 가이드

마지막 수정: 2026-05

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자)
  • 로그 테이블은 주기적으로 파티셔닝 또는 아카이빙 전략 필요 (호출량에 따라 급증)