수 많은 우문은 현답을 만든다

예외처리에 대한 고찰 - 3편 (성능 개선) 본문

개발지식/Springboot

예외처리에 대한 고찰 - 3편 (성능 개선)

aiden.jo 2022. 3. 9. 22:48

안녕하세요, 조영호입니다.

오늘은 개발 이후 PR(Pull Reqeust)를 올린 다음 사수로부터 개선을 요청받은 사항과 어떻게 개선했는지 그 과정을 공유하고 합니다. 기본적인 문제부터 의미있는 문제도 있으니 주니어분들은 눈여겨 보시면 좋을 것 같습니다.

우선 로그 수집 시스템ELK(ElasticSearch, Logstash, Kibana) 를 선택했고 3가지 서비스와 Java Application을 포함한 4가지 서비스를 Docker Container 로 구성했습니다. Java 애플리케이션에서 발생하는 예외나 로그 메세지들은 Logstash 에서 적절히 수집하고, Kibana 대시보드로 로그 모니터링을 할 수 있습니다.

이제 개선 사항을 살펴보겠습니다.

  1. AOP를 통한 MDC처리시 추적번호(Transaction-id) 를 백엔드 내부에서 생성되는 UUID(Universally unique identifier)가 아닌 HTTP Header 정보인 X-Request-Id를 사용하시는게 좋을 듯 합니다. (결국 추적을 위한 것이기 때문에 Client에서 세팅한 UUID를 추적번호로 사용하는게 일반적입니다.)

  2. AOP에 JoinPoint 를 사용해 MDC에 더 다양한 정보를 추가할 수 있습니다.

  3. 서비스 코드 중간중간에에 MDC.put()을 사용하여 id, name 값을 넣어주는 작업을 하고 있는데 행여가 담당자가 바뀌거나 변경이 필요한 경우에 관리가 어려울 수 있습니다. 공통처리가 필요해 보입니다.

 

그럼 각 항목별로 어떻게 개선했는지 공유드리겠습니다.

 


1. X-Request-Id 사용

X-Request-Id 는 Front-end 에서 임의의 id 값을 만들어 헤더정보를 Back-end로 전달하는데 쓰이는 커스컴 헤더값을 뜻합니다.
이런 커스텀 헤더는 RFC 6648는 에서 비표준 필드가 표준이 되었을때 불편함을 유발한다는 이유로 2012년 6월에 폐기되었습니다. 하지만 로깅 시스템에서는 클라이언트의 행동에 따른 추적이 필요하다고 생각해 x-request-id를 사용했습니다.

 

Front-end는 Vue.js 프레임워크를 사용해 개발을 했습니다.
모든 HTTP 요청은 Axios 라이브러리를 사용해 보내고 있으며 모든 요청들의 공통사항은 axios.js 파일에서 관리를 하고 있습니다.

axios.js

 

위 그림처럼 create(options = {}) 에 헤더정보를 추가할 수 있습니다. header name은 'x-request-id'로 지정했고 header value는 UUID를 반환하는 아래 uuidv4() 함수를 작성했습니다.

uuid 반환 함수

 

참고로 AOP에서는 HttpServletRequest 객체를 가져오기 위해 (ServletRequestAttributes)로 캐스팅을 해줘야 헤더 정보를 얻을 수 있습니다. 그리고 혹여라도 프론트에서 x-request-id 값이 빠질 수 있는 경우의 수를 고려해서 아래와같이 빈 값을 넣어줍니다.

MDC.put("trxId", request.getHeader("x-request-id") == null ? 
        UUID.randomUUID().toString() : 
        request.getHeader("x-request-id"));

 

 


2. JoinPoint 사용하기
처음에는 클라이언트의 요청 정보들이 HttpServletRequest 객체에 다 있을 것이라고 생각했습니다. 그러나 JoinPoint 를 사용하면 내가 지정한 Poincut을 기준으로 애플리케이션 횡단 어디서든 AOP 로직이 처리된 시점의 정보를 가져올 수 있습니다. 과연 이 객체가 필요한가 의문이었지만, 컨트롤러에 걸어놓은 Aspect의 결과인 PointCut 에서 중요한 정보들을 얻을 수 있었습니다.

aop.java

  • package : 사용자가 호출한 패키지 경로 (ex. com.example.demo.HomeController)
  • functionName : 사용자가 호출한 함수의 이름(ex. getHomeInfo)
  • args : 사용자가 호출시 전달한 파라미터 정보
               사실 HttpServletRequest 객체에서는 @PathVariable, @RequestBody, String, int 등 어떤 형태로 인수를 받느냐에
               따라 파싱을 달리해야 합니다. JoinPoint를 쓰면 joinPoint.getArgs()로 쉽게 가져올 수 있습니다.

 


3. 공통처리 하기

이 자바 애플리케이션은 같은 역할을 하는 파라미터가 각각 다른 명명 규칙을 따르는 문제가 있었습니다.
예를들면,

  • function(int skillId, skillname)
  • function(int id)
  • function(SkillDto skillDto)
  • function(name)
  • function(req)

위와같이 서로 다른 명명규칙은 공통처리할때 각각 파싱하는 방법이 달라서 복잡해집니다. 처리하기 복잡하다는 이유로 처음에는 각 서비스 별로 파싱 작업을 했는데 아무래도 깨끗한 코드라는 생각이 들지 않았습니다. 그래서 AOP 단에서 공통처리를 하기로 했습니다.

 

aop.java

Switch문에 JoinPoint 의 패키지 경로와 메소드 이름을 활용해 공통처리를 했습니다. Switch를 쓴 이유는 컴파일 시 Look-up Table을 만들어서 if-else 와는 다르게 바로 해당 case로 접근하게 됩니다. 즉 성능적으로 더 뛰어나기 때문입니다.

 

혹시 위  코드의 개선점이 보이시나요?
문자열들을 final 상수로 선언해 놓았는데 보통은 이렇게 하지 않고 유지보수를 위해 클래스를 만들어서 아래와같이 관리합니다.
상수는 변하지 않는 수 이며 static final을 사용합니다. 변수명은 대문자로 하되 구분자는 언더바( _ )를 사용합니다.

Controller path
Method path



그다음 파라미터로 오는 값은 3가지(String, int, 객체) 유형입니다. 어떻게 공통처리를 해야할까요? 
우선 사용자 액션마다 AOP에 의해서 ObjectMapper 를 생성하면 객체 생성 비용이 클 것으로 생각했습니다. 그래서 정규식을 써서 처리했습니다.

aop.java

 

정규식으로 처리한 코드는 테스트를 완벽하게 거쳐야합니다. 그리고 언제나 예외사항이 발생할 수 있습니다.

혹시 더 좋은 방법은 없을까요?

 

정답은 instanceof 를 써서 객체의 타입을 비교하는 방법이 있습니다. 새로 객체를 생성하지 않고도 비교해볼 수 있는 아주 편한 방법입니다. 마지막 주석 'null 반환 금지' 의 의미는 parseSkillInfo() 메소드의 리턴값이 MDC에 저장되는데, null 값을 반환해버리면 Logstash의 filter 조건에 위배되어 filter 설정이 동작하지 않기 때문입니다.

 

자 마지막으로 아래 코드에서 개선할 수 있는 부분은 어떤 것이 있을까요?

List 가 빈값이 올 수도 있기때문에 미리 예외 방어를 해줄 수 있습니다.

 

 

이로써 코드리뷰가 끝났습니다.

즐 코딩 하세요~ 감사합니다.

'개발지식 > Springboot' 카테고리의 다른 글

세마포어(Semaphore)  (0) 2022.07.13
FK 를 쓰지 않는 이유  (0) 2022.03.21
예외처리에 대한 고찰 - 2편 (로깅 기능 설계)  (0) 2022.02.24
예외처리에 대한 고찰 - 1편  (0) 2022.02.21
JPA 와 N+1 문제점  (0) 2022.01.22