개발자 노트

Logging AOP 구현 - 1 본문

이것저것

Logging AOP 구현 - 1

jurogrammer 2021. 9. 19. 18:30

서론

logging은 비즈니스 로직을 처리를 위한 코드라기 보다는 모니터링을 위한 코드라고 생각됩니다. 그래서 비즈니스 로직 중간에 나타나는 logging이 매우 보기 불편했죠.

Spring이 AOP를 제공해준다고 하지만... Spring에서 구현한 답지를 보지 않고 직접 구현해보고 싶었습니다. 따라서 이번 시간에는 어떻게 해야 기가막힌 logging aop를 할까... 그 고찰을 적어보겠습니다.

상황

person class의 sayHello 메서드를 작성하고, main에서 sayHello method를 호출합니다. 이때, framework를 통해서 person 객체를 전달 받는다고 하겠습니다.

Person

public class Person {

    private String firstName;
    private String lastName;
    private int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public void sayHello(String statement) {
        System.out.println(String.format("%s, my name is %s, and age is %d", statement, firstName + lastName, age));
    }

}

여러 스타일의 프레임워크를 만들 것이므로 framework interface를 정의합니다.

Framework

public interface Framework {
    Person getPerson(String firstName, String lastName, int age);
}

다음으로 실행을 위한 클라이언트 코드를 작성합니다. 특정 framework를 파라미터로 받구요, framework에게 arguments를 전달하여 person 객체를 받습니다. 그리고 hello method를 실행하는 것이죠.

Runner

public class Runner {
    public void frameworkRun(Framework framework) {
        Person person = framework.getPerson("juro", "grammer", 100);
        person.sayHello("nice to meet you~");
    }
}

당장 생각나는 방법들

흐음... 이거 전에 reflection을 배웠는데 마법은 아니더라구요? 런타임 중 method 호출을 감지하는 기능도 있을 것 같았는데 reflection 설명에는 없었습니다.

그래서 hibernate를 배울 때를 떠올리면 proxy pattern으로 인스턴스를 넘겨주는 형태를 떠올릴 수 있습니다. proxy pattern을 적용한다면 method를 호출하기 전 후 동작을 구현할 수 있을 뿐더러, 클라이언트는 proxy 객체인지 모른 채 사용하게 할 수 있으니까요.

따라서 proxy pattern이라는 키워드를 바탕으로 방법들을 떠올려보겠습니다.

[방법 1] 정통한 proxy pattern (interface 이용)

Person class가 구현할 인터페이스를 정의합니다.

그리고 Person class, ProxyPerson class가 해당 인터페이스를 구현합니다. 마지막으로 클라이언트가 person 클래스를 요청시 ProxyPerson class를 전달하는 겁니다.

문제점

개발자가 굳이굳이~~ 인터페이스를 구현해야 합니다. 단순히 Person class 만이 필요한데 logging을 해야 한다고 인터페이스도 만들으라고 강요하는 것이죠.

냄새가 나서 코드로 구현하지도 않았습니다.

[방법 2] 상속을 통한 proxy pattern

Person class를 상속받은 ProxyPerson class를 구현합니다.

liskov substitution principle에 따라 sub class를 전달 해주어도 잘 작동되니까 상속을 이용하는 겁니다. 코드는 아래와 같습니다.

PersonProxy

public class PersonProxy extends Person {
    public PersonProxy(String firstName, String lastName, int age) {
        super(firstName, lastName, age);
    }

    @Override
    public void sayHello(String statement) {
        System.out.printf("current Time: %s%n", LocalDateTime.now());
        System.out.printf("current statement: %s%n", statement);
        super.sayHello(statement);
    }
}

그리고 sayHello method를 호출하기 전 로깅을 한 뒤에 메서드를 실행해줍니다.

프레임워크에서는 Perosn class의 객체 요청시 이 PersonProxy 객체를 전달해주어야겠죠?

PersonFramework

public class ProxyFramework implements Framework {

    @Override
    public Person getPerson(String firstName, String lastName, int age) {
        return new PersonProxy(firstName, lastName, age);
    }
}

run

@Test
@DisplayName("proxy기반으로 aop를 적용한다.")
void apply_proxy_framework() {
    Framework proxyFramework = new ProxyFramework();
    runner.frameworkRun(proxyFramework);
}

출력

current Time: 2021-09-19T17:41:21.082118
current statement: nice to meet you~
nice to meet you~, my name is jurogrammer, and age is 100

문제점

ProxyPerson class를 작성해야 하는데요.. 누가 작성하죠? 결국 프레임워크를 사용하는 개발자가 작성해주어야 합니다. 첫번째와 동일한 문제가 발생했네요.

결국 클래스를 작성해주지 않고 뭔가 조작할 방법을 떠올려야 하네요...

[방법 2] Anonymous class

그렇다면, Proxy 클래스를 작성하지 맙시다! 그리고 anonymous class를 이용하는 것이죠!

anonymous class에서 super를 통해서 Person 클래스의 메서드에 접근할 수 있습니다. 그러니 앞에 logging을 한 뒤에 메서드를 호출해줍시다.

AnonymousFramework

public class AnonymousFramework implements Framework {

  @Override
  public Person getPerson(String firstName, String lastName, int age) {
      return new Person(firstName, lastName, age) {
          @Override
          public void sayHello(String statement) {
              System.out.printf("current Time: %s%n", LocalDateTime.now());
              System.out.printf("current statement: %s%n", statement);
              super.sayHello(statement);
          }
      };
  }
}

run

@Test
@DisplayName("anonymous기반으로 aop를 적용한다.")
void apply_anonymous_framework() {
    Framework anonymousFramework = new AnonymousFramework();
    runner.frameworkRun(anonymousFramework);
}

출력

current Time: 2021-09-19T17:58:07.775068
current statement: nice to meet you~
nice to meet you~, my name is jurogrammer, and age is 100

Process finished with exit code 0

잘 출력되네요~ 사용자가 proxy class파일을 작성하지 않고도 잘 작동했습니다.

위 방법들의 문제점 정리

하지만... 잘 생각해보면 여러 문제점이 존재합니다.

임의의 클래스에 대해 logging을 적용할 수 있는가?

framework 자체가 메서드 명에서 알아볼 수 있 듯이 Person class를 알고 있습니다.

framework를 개발할 때는 Person class를 존재조차 모르며 다양한 클래스에서도 동일하게 logging을 할 수 있어야 합니다.

어떻게 하면 일반화시킬 수 있을까요?

전 흠... 약간의 힌트를 찾아보았고, 다음과 같은 자료를 찾았습니다. dynamic proxy라는 것이죠.

https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html

다음 글을 이에 대해 다뤄보도록 하겠습니다.

만약에 메서드 호출 전 logging 말고 다른 작업도 수행할거면?

지금은 super.sayHello 이전에 system.out.println으로 로깅을 했지만, 다른 작업도 수행해야 한다면요? 다음과 같은 문제를 떠올릴 수 있겠습니다.

  1. 변경에 대한 책임이 커집니다.
  2. 추가 작업에 대한 code를 어떻게 추가하지...?

제 생각엔... 위 두가지를 처리하기 위해 observer pattern 방식으로 구현하는게 좋을 것 같습니다.

super.sayHello를 실행하기 전에 다음과 같은 event를 event store 자료구조에 dispatch 합니다.

{
    "type": "preInvokeMethod",
    "targetClass": "Person",
    "info": {
                    "parameterNames": [],
    ...
}

event store 자료 구조에서는 class, event type별 subscribers 들을 저장해둡니다. 그리고 dispatch method가 실행됬을 때 subscriber에게 위 argument를 전달하여 특정 메서드를 호출하면 될 것 같습니다.

for(Subscriber subscriber : subscribers) {
    subscriber.handle(event)
}

이처럼 실행하면 되지 않을까요?

(여기서 subscriber는 logging 객체 따위들을 의미합니다.)

끝으로,

이번 시간에는 아주 쉽게 떠올릴 수 있는 방법들을 구현해보았고 그 문제점들을 알아보았습니다. 다음 시간에는 Perosn class에 국한되지 않고 로깅할 수 있는 framework를 구현해보겠습니다. 점진적으로 발전시켜보겠습니다~~

반응형
Comments