로그추적기로 알아보는 디자인 패턴

Single Ko·2023년 5월 21일
0

Spring 강의 정리

목록 보기
29/31

로그는 왜 필요할까?

  • 애플리케이션에서 운영과 모니터링을 잘 하기 위해서는 로그를 남겨야 한다.
  • 실무에서는 실제 이런 로그 운영에 도움을 주는 다양한 툴들이 있지만 여기선 간단하게 로그 추적기를 만들어 보며 애플리케이션 로직과 흐름및 디자인 패턴을 익혀보자.

로그의 목적

  1. 서비스 동작 상태 파악
  2. 장애 파악 & 알람
  • 위의 목적대로 작성된 로그를 통해 서비스 지표의 확인, 트랙잭션, 성능 등 다양한 정보를 확인할 수 있습니다

주의할 점!

  • 로그를 남긴다고 해서 로직의 동작에 영향을 주면 안된다. -> 핵심동작이 로그 코드(부가코드) 때문에 달라진다거나, 코드 자체를 바꿔야되면 안됨.

로그에서 표시할 기능

  • 메서드 호출에 걸린 시간
  • 메서드의 계층 구조 표시
  • HTTP 요청을 구분 (요청 ID를 남겨서 어떤 HTTP 요청에서 시작된 것인지 명확하게 구분 가능 해야함)
  • 정상적인 작동과 예외를 구분 ( 예외 발생시 정보가 남아야 한다)

디자인 패턴 없이 로그를 만들어 보았다.

로그 추적기를 만들어 보니 심각한 문제가 보였다.

//로그 추적기 적용 전
@GetMapping("/v1/request")
public String request(String itemId) {
	orderService.orderItem(itemId); 
	return "ok";
}

   
//로그 추적기 적용 후
@GetMapping("/v1/request")
public String request(String itemId) {
	TraceStatus status = null;
	try {
		status = trace.begin("OrderController.request()");
		orderService.orderItem(itemId);
		trace.end(status);
		return "ok";
	} catch (Exception e) {
		trace.exception(status, e);
		throw e; //예외를 꼭 다시 던져주어야 한다.
    }
}
  1. 기존의 Controller , Service, Repository에 일일이 전부 try-catch로 넣어야 되었다. (Exception 까지 받아서 처리하려면.. 처리하지 않으면 exception이후 흐름이 거기서 끝이난다 즉, log의 end 동작이 실행이 안됨.)
  2. 무엇보다 로그 관련 기능을 고치면 그와 관련된 로그 로직이 있는 부분을 전부 고쳐야 되었다. 서비스가 커진다면? 배보다 배꼽이 커진다고, 비지니스 로직보다 로그 관련 코드들이 더 많아지고 그걸 유지보수 해야되는 상황이 오면 끔찍해질듯.
  • ID를 한 흐름에서 계속 유지해줘야 되는데, 이렇게 하려니 파라미터들을 이용해서 넘겨줘야되었고, log 추적기의 코드를 고칠시 전부 고쳐줘야됨...

인터페이스를 이용한 확장

  • 기존의 단일 클래스로 만들었던 로그 추적기를 확장성까지 생각해서 인터페이스 형태로 바꾸었고, 기능도 추가했다.

  • 직접 빈에 등록했던 Component도 빼서, LogConfigure라는 설정 파일을 따로 만들어 관리.

  private TraceId traceIdHolder; // 클래스의 필드로 줬음.


  // 새롭게 아이디 관리를 위해 만든 메서드들
  private void syncTraceId() {
        if (traceIdHolder == null) {
            traceIdHolder = new TraceId(); // 최초 호출시 새로운 TraceId() 생성
        } else {
            traceIdHolder = traceIdHolder.createNextId(); // level 증가
        }
    }

    private void releaseTraceId() {
        if (traceIdHolder.isFirstLevel()) {
            traceIdHolder = null;  //destroy
        } else {
            traceIdHolder = traceIdHolder.createPreviousId();  // level 감소
        }
    }
  • 기존의 방식과 다르게, sync와 release 메서드를 만들어 필드 동기화를 해주었다. 다만 이 방식을 사용할 시 빈으로 관리되는 스프링에서는 객체의 인스턴스가 싱글톤인데, 동시성 문제가 발생할 수 있다. (traceIdHolder는 전역변수이다.)

  • 트래픽이 적으면 문제가 없어 보일 수 도 있다. 하지만 트래픽이 많아질수록 동시성 문제가 자주 발생하게 될 것이다.

  • 거기다 Java의 Spring을 쓴다면 기본으로 알아야된다.(Why? -> Spring은 기본적으로 Singleton으로 객체를 관리하는 방법을 사용하기 때문이다)

    ThreadLocal

  • 동시성 문제를 해결하기 위한 방법중 하나. ThreadLocal을 사용한다면 Thread 1의 창고가 생기게 되는것이고, Thread 2는 다른 창고를 사용하게 된다.

  • 각 쓰레드가 자기의 창고가 생기는 것이기 때문에 동시성 문제를 해결 가능함.

  • 중요!! 쓰레드 로컬은 쓰고나서 꼭 remove로 threadlocal을 없애야 한다.
    -> why? Threadpool을 사용하는 was. 만들어진 Thread안에 threadlocal이 생성되 삭제되지 않으면 계속 남아 있고, 그 threadlocal의 데이터가 남아 있다.

기존의 문제점

  1. ThreadLocal을 적용하든, 인터페이스를 적용하든 문제점 해결을 하고는 있지만 근본적으로 해결되지 않는 문제가 있다.

    -> 바로 비지니스 로직에 남아있는 부가 코드들이다. 로그 추적기를 고치게 되면 그 코드들을 이용하는 클래스가 몇개가 되었든 전부 고쳐줘야 된다.

  2. 여기서 우리는 로그 추적기 코드를 살펴보면 똑같은 코드들이 많다는 것을 알것이다. 정작 바뀌는 부분은 기존 핵심코드의 비니지스 로직 부분과 간단한 메시지 정도이다.

    	TraceStatus status = null;
    	try {
    		status = trace.begin("메시지");
          
    		// 비지니스 로직 부분
          orderService.orderItem(itemId);
          // 비지니스 로직 끝
          
    		trace.end(status);
          
    		return "ok"; // 응답 메시지
    	} catch (Exception e) {
    		trace.exception(status, e);
    		throw e; 
    	}
      
      공통 부분 추출
      	TraceStatus status = null;
    	try {
    		status = trace.begin("메시지");
          
    		// 비지니스 로직 부분
          
    		trace.end(status);
          
    	} catch (Exception e) {
    		trace.exception(status, e);
    		throw e; 
    	}
  3. 이런 공통적인 부분은 따로 관리하고, 바뀌는 부분들에 대해서만 어떻게 관리할 수 없나?(변하는 것과 변하지 않는 것을 분리하자) -> 이런 부분을 디자인패턴을 적용해 해결 해보자.

템플릿 메서드 패턴과 전략 패턴

템플릿 메서드 패턴

  • 말 그대로 템플릿을 사용하는 패턴이다. 기준이 되는 거대한 틀인 템플릿 부분에 변하지 않는 부분을 몰아 둔다.
  • 일부 변하는 부분을 별도로 호출해서 해결한다.
// 템플릿 클래스, 로그의 공통 부분 추출.
public abstract class AbstractTemplate<T> {
 	
    private final LogTrace trace;
 	
    public AbstractTemplate(LogTrace trace) {
 	this.trace = trace;
	}
    
 	public T execute(String message) {
 	TraceStatus status = null;
 	try {
 		status = trace.begin(message);
        
 		T result = call(); // 로직 호출 부분
        
 		trace.end(status);
 		return result;
 	} catch (Exception e) {
 		trace.exception(status, e);
 		throw e;
 	}
 }
 
 protected abstract T call();
 
}


// 상속으로 구현한 변하는 부분 로직
 @GetMapping("/v4/request")
 public String request(String itemId) {
 
 	AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
 		@Override
 		protected String call() {
 			orderService.orderItem(itemId);
 			return "ok";
 		}
 	};
    
 	return template.execute("OrderController.request()");
 }
  • 익명 클래스를 사용해 구현. 따로 AbstractTemplate을 상속받은 클래스를 구현해서 사용해서 된다.

  • 템플릿 메서드 패턴을 적용해서 소스코드 몇줄을 줄인 것이 전부가 아니다.
    로그를 남기는 부분에 단일 책임 원칙(SRP)을 지킨 것이다. 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만든 것이다

템플릿 메서드 패턴의 문제점.

  • 템플릿 메서드 패턴은 상속을 사용한다. 따라서 상속에서 오는 단점들을 그대로 안고간다.
  • 특히 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다. 이것은 의존관계에 대한 문제이다.(자식 클래스의 코드에 부모 클래스의
    코드가 명확하게 적혀 있다는 뜻)
  • 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는다.
  • 잘못된 의존관계 때문에 부모 클래스를 수정하면, 자식 클래스에도 영향을
    줄 수 있다.

전략 패턴

  • 변하지 않는 부분을 Context 라는 곳에 두고, 변하는 부분을 Strategy 라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다. 상속이 아니라 위임으로 문제를 해결하는 것이다. 전략 패턴에서 Context 는 변하지 않는 템플릿 역할을 하고, Strategy 는 변하는 알고리즘 역할을 한다.

// context
public class ContextV1 {
    private Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        long startTime = System.currentTimeMillis();
        strategy.call(); //위임
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime : {}", resultTime);
    }
}

//Strategy 인터페이스
public interface Strategy {
	void call();
}

// 실제 구현
@Test
@DisplayName("lamda 활용 - 전략패턴")
void strategy() {
	ContextV1 context1 = new ContextV1(() -> log.info("비지니스 로직1 실행"));
    context1.execute();

	ContextV1 context2 = new ContextV1(() -> log.info("비지니스 로직1 실행"));
    context2.execute();
}
  • 자식과 부모의 강력한 의존관계가 없이, 위임을 통해 구현하는 방식.

  • 현재는 Context 내부에 Strategy를 두고 사용중.은 ContextStrategy 를 실행 전에 원하는 모양으로 조립해두고, 그 다음에 Context
    실행하는 선 조립, 후 실행 방식에서 매우 유용하다

  • 하지만 이 방식은 조립한 이후에는 전략 변경이 까다롭다. 다음 방식은 전략 패턴에서 파라미터를 통해 전략을 받는 방식을 사용해 보자.

 //parameter를 통해 전략을 받는 방식
 public class ContextV2 {

    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();
        strategy.call(); //위임
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime : {}", resultTime);
    }
}


// 실제 구현
void strategyV3() {
 	ContextV2 context = new ContextV2();
    
 	context.execute(() -> log.info("비즈니스 로직1 실행"));
 	context.execute(() -> log.info("비즈니스 로직2 실행"));
}
 
  • 이제 실행시점때 마다 전략을 유연하게 변경할 수 있다.
  • 단점 역시 실행할 때 마다 전략을 계속 지정해주어야 한다는 점이다

현재 우리가 원하는 것은 애플리케이션 의존 관계를 설정하는 것처럼 선조립 후실행이 아니다. 단순히 코드 실행시 변하지 않는 템플릿이 존재하고, 그 안에서 원하는 부분(전략)만 바꿔 실행하고 싶음.

따라서 뒤의 방식이 좀 더 적절하고, 이 뒤의 방식을 따로 스프링에서는 템플릿 콜백 패턴이라 한다. (이 템플릿 콜백 패턴은 GOF 패턴은 아니다고 스프링 내부에서 이런 방식을 주로 사용하기 때문에 스프링에서 이렇게 부른다.) 전략 부분에서 템플릿과 콜백 부분이 강조된 패턴.

결론

  • 이렇게 템플릿 메서드 패턴과 전략패턴, 템플릿 콜백 패턴을 이용해서 로그 추적기를 넣어보았다. 하지만 근본적으로 문제가 해결되지 않았다.
    why?
  • 코드의 양이 줄었을 수는 있어도 결과적으로 로그 추적기를 사용하려면 기존 원본 클래스의 코드를 변경해야 하는 것은 변함이 없다. (만약 수백개의 클래스라면? 조금 덜 힘들수는 있어도 많은 양의 코드를 넣어야 되는 것은 변함이 없다.)
  • 이를 해결하기위해서는 또 다른 방법(proxy)이 필요하고 앞으로 이 방법에 대해 공부해보자.

참고 : 본 글은 김영한님의 스프링 강의를 정리한 것이다.

profile
공부 정리 블로그

0개의 댓글