공통 로직과 서비스 로직 분리 해보기. Feat Template Method Pattern, Strategy Pattern

BaekGwa·2024년 9월 3일
0

Spring

목록 보기
3/9

공통 로직과 서비스 로직

  • 개발을 진행 하다보면, 공통으로 많은 class, Method 단위에서 사용되게 되는 로직이 생성되게 됩니다.
    • 예시) 해당 메서드의 Running Time 측정 Logger
    • 요청 Client의 유효성 검사 (로그인 검증, 벤 회원 검증 등)
    • 요청 데이터 캐싱 확인
  • 인증인가등의 공통 로직은 Filter나, Servlet (Spring-DispatcherServlet)을 사용하여 관심사를 분리 할 수 있습니다.
  • 또한, Spring의 AOP 기능을 사용하여 Class, Method Level 에서의 관심사를 분리 할 수도 있습니다.
  • 위의 패턴 말고도, 디자인 패턴을 적용하여 관심사를 분리하는 방법또한 있습니다.

예시 도입

요구사항

  • 개발자A는, Running Time을 체크하는 로거를 만들고자 합니다.
  • 당연히 사용성은 좋아야하며, 로그 추적또한 가능하도록, 작업 Thread의 고유한 ID또한 같이 표기가 되어야 합니다.
  • 이 사람은, AOP를 사용할 줄 모릅니다.

해당 내용을 참조해서 간단하게 api 방식의 프로젝트를 하나 작성하겠습니다.

디자인 패턴 없이 적용해보기

Github 의 web-v0 패키지 참조.

  • 간단한 예제 코드로, name Param과 함께, Get 요청이 들어오면, 저장하고 return 하는 예제 입니다.
  • 진행되는 순서는 (Controller) -> (Service) -> (Repository) 순서로 진행됩니다.
//출력결과
2024-09-03T23:08:52.819+09:00  INFO 248552 --- [designpattern] [nio-8080-exec-1] b.designpattern.web.v0.TestRepository    : [a7e2849c][save] : Running Time : 1013
2024-09-03T23:08:52.820+09:00  INFO 248552 --- [designpattern] [nio-8080-exec-1] b.designpattern.web.v0.TestService       : [4682f049][saveName] : Running Time : 1014
2024-09-03T23:08:52.820+09:00  INFO 248552 --- [designpattern] [nio-8080-exec-1] b.designpattern.web.v0.TestController    : [56017a09][saveName] : Running Time : 1015
  • 이 로그를 남기기 위해서 다음과 같이 작성을 진행하였습니다.
  • 실제 해당 메서드의 기능에 해당하는 로직은 testService.saveName(name); 입니다.
  • 하지만, 위의 로그를 남기기 위해서 쓸모없는 로직이 추가되고 말았죠. 또한, Repository, Service에도 완전 동일한 로직이 추가되어 버렸습니다.
  • 또한, 맨앞의 스레드 식별자가 달라 같은 트랜잭션 범위에서 동작하였는지 조차 확인이 불가능 합니다.
  • 객체 설계의 원칙 중, 단일 책임 원칙을 가볍게 무시하는 모습입니다. 이렇게 개발하면... 같이 일하시는 분들이 참 많이 힘들 것 입니다.
  • 이점을, 조금더 개선 해 보겠습니다.

단일책임 지키고, thread id 식별할 수 있도록 변경...

Github 의 web-v1 패키지 참조.

  • Logging 관련된 기능을 공통 메서드로 분리하여 start(), end() 두개의 메서드로 변경하였습니다.
  • 모든 class에서 사용 가능하여야 하기 때문에, util class로 만들었습니다.
  • 동시성 문제가 발생할 수 있어, ThreadLocal을 도입하였습니다.

//결과
2024-09-03T23:32:42.975+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-2] b.designpattern.global.ThreadLogger      : [06878be1][save] : Running Time : 1009
2024-09-03T23:32:42.975+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-2] b.designpattern.global.ThreadLogger      : [06878be1][saveName] : Running Time : 1009
2024-09-03T23:32:42.975+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-2] b.designpattern.global.ThreadLogger      : [06878be1][saveName] : Running Time : 1009
2024-09-03T23:33:02.210+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-4] b.designpattern.global.ThreadLogger      : [ac7b56da][save] : Running Time : 1005
2024-09-03T23:33:02.210+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-4] b.designpattern.global.ThreadLogger      : [ac7b56da][saveName] : Running Time : 1005
2024-09-03T23:33:02.210+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-4] b.designpattern.global.ThreadLogger      : [ac7b56da][saveName] : Running Time : 1005
2024-09-03T23:33:02.375+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-5] b.designpattern.global.ThreadLogger      : [7681b74f][save] : Running Time : 1009
2024-09-03T23:33:02.375+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-5] b.designpattern.global.ThreadLogger      : [7681b74f][saveName] : Running Time : 1009
2024-09-03T23:33:02.376+09:00  INFO 230764 --- [designpattern] [nio-8080-exec-5] b.designpattern.global.ThreadLogger      : [7681b74f][saveName] : Running Time : 1010
  • Thread 별로 구분또한 되고, Logging에 변경이 생길경우, 공통 공통 Util class인 ThreadLogger만 수정하면 모든 곳에 변경이 적용됩니다.
  • 박수!!! 치고 끝내면 안되겠죠? 여전히 문제가 존재합니다.
  • 아직도 기능 Class의 Method들은, 엄밀히 따지면, 단일 책임 원칙을 지키고 있지 않고, 공통된 uuid를 사용하기 위해 threadLogger 인스턴스를 파라미터로 전달하고 있습니다.
    • 로깅을 위해서는 logger의 start() 메서드를, 끝날땐 end() 메서드를 실행해야 합니다.
  • 해당 문제를 해결하기 위해 템플릿 메서드 패턴을 적용 해 보겠습니다.

템플릿 메서드 패턴

Github 의 web.v2 패키지, global.templatemethodpattern 패키지 참조

템플릿 메서드의 구조

  • 전체적인 구조는 다음과 같습니다.
  • Abstract class를 선언하고, execute에 공통 로직을 작성합니다.
    • 현재 예제는 로그 출력 부분이 공통 로직이 됩니다!
  • Abstract method call()을 선언합니다.
    • 해당 부분을 사용하는 곳에서 @Override 하여 동작할 서비스 로직을 구현 합니다.

작성해보기

상세 코드는 github 코드 참조 부탁드립니다.

  • 다음과 같이 추상화 객체를 선언하고, Logger 를 Container로부터 의존성 주입을 받습니다. Logger 객체는, 기존의 ThreadLogger와 유사합니다.
  • 해당 class의 execute 메서드를 보면, call() 메서드 전/후로 Logger의 메서드를 실행하게 됩니다.
    • 해당 메서드는, Logging을 위한 작업을 진행합니다.

  • 실제 사용시, AbstractThreadLogger를 받고, Thread에서 공통적으로 사용할 Logger를 의존성 주입받고 생성자를 통해 넣어줍니다.
  • 다음으로 call() 메서드를 정의하여 실제 동작할 내용을 정의 해줍니다.
  • execute() 메서드를 실행하여 위에서 정의한 순서대로 진행되도록 합니다.
  • 다음과 같은 사진으로 진행되는것을 확인할 수 있습니다.

결과 확인

2024-09-04T06:04:39.541+09:00  INFO 264360 --- [designpattern] [nio-8080-exec-4] b.d.g.t.ThreadLocalLogger                : [6cbfb17e][save] : Running Time : 1013
2024-09-04T06:04:39.541+09:00  INFO 264360 --- [designpattern] [nio-8080-exec-4] b.d.g.t.ThreadLocalLogger                : [6cbfb17e][saveName] : Running Time : 1013
2024-09-04T06:04:39.541+09:00  INFO 264360 --- [designpattern] [nio-8080-exec-4] b.d.g.t.ThreadLocalLogger                : [6cbfb17e][saveName] : Running Time : 1013
2024-09-04T06:04:39.766+09:00  INFO 264360 --- [designpattern] [nio-8080-exec-5] b.d.g.t.ThreadLocalLogger                : [1c59ddb2][save] : Running Time : 1001
2024-09-04T06:04:39.766+09:00  INFO 264360 --- [designpattern] [nio-8080-exec-5] b.d.g.t.ThreadLocalLogger                : [1c59ddb2][saveName] : Running Time : 1001
2024-09-04T06:04:39.766+09:00  INFO 264360 --- [designpattern] [nio-8080-exec-5] b.d.g.t.ThreadLocalLogger                : [1c59ddb2][saveName] : Running Time : 1001

문제점

  • 상속과 오버라이딩을 통해 문제를 해결한 만큼, 상속에서 나타나는 문제가 그대로 드러납니다.
  • 자식 클래스와, 부모 클래스가 컴파일 시점에 강하게 결합 된다.
    • 현재 코드는, 익명함수를 통해 구현하고 있지만, 별도의 class를 생성할 경우, extends AbstractThreadLogger 를 통해 상속관계를 맺게 될 것이다.
    • 이는, 자식 클래스의 입장에서는 부모 클래스의 기능을 사용하지 안흔ㄴ데, 부모 클래스를 알아야 되는 문제가 발생한다.
    • 만약, 부모 클래스를 수정하면, 자식 클래스에도 영향을 줄 수 있다.
  • 상속 구조를 사용하기 때문에, 별도의 클래스나 익명 내부 클래스를 만들어야 한다는 점 또한, 단점으로 뽑을 수 있다.
    • 여전히 비지니스 로직 > 공통 로직 이다.

따라서, Strategy Pattern을 사용해서 이러한 문제를 일부 회피해보도록 하겠습니다.

Strategy Pattern (전략패턴)

Github 의 web.v3 패키지, global.strategypattern 패키지 참조

  • 전략 패턴은 간단하게, 사용할 전략을 주입 받는 것입니다.
  • Spring의 의존성 주입과 매우 흡사 합니다.

작성해보기

  • 다음과 같이, 전략을 주입받도록 설정하고, 주입된 전략을 실행하도록 합니다.
  • 주입받을 전략은, interface 객체로, 람다식을 활용하여 필요한 전략(로직)을 구현 합니다.

결과

2024-09-04T07:01:36.638+09:00  INFO 61684 --- [designpattern] [nio-8080-exec-5] b.d.g.t.ThreadLocalLogger                : [35ca09f3][save] : Running Time : 1004
2024-09-04T07:01:36.638+09:00  INFO 61684 --- [designpattern] [nio-8080-exec-5] b.d.g.t.ThreadLocalLogger                : [35ca09f3][saveName] : Running Time : 1004
2024-09-04T07:01:36.638+09:00  INFO 61684 --- [designpattern] [nio-8080-exec-5] b.d.g.t.ThreadLocalLogger                : [35ca09f3][saveName] : Running Time : 1004
2024-09-04T07:01:36.814+09:00  INFO 61684 --- [designpattern] [nio-8080-exec-6] b.d.g.t.ThreadLocalLogger                : [029c10eb][save] : Running Time : 1013
2024-09-04T07:01:36.814+09:00  INFO 61684 --- [designpattern] [nio-8080-exec-6] b.d.g.t.ThreadLocalLogger                : [029c10eb][saveName] : Running Time : 1013
2024-09-04T07:01:36.814+09:00  INFO 61684 --- [designpattern] [nio-8080-exec-6] b.d.g.t.ThreadLocalLogger                : [029c10eb][saveName] : Running Time : 1013
  • 탬플릿 메서드의 단점인, 강한 결합과 코드 작성의 어려움은 어느정도 해소되었습니다.
  • 하지만, StrategyLogger 생성 시점에 전략을 주입해야 된다는 부분이 조금 범용성이 낮게 느껴집니다.
    • 도중에 전략을 변경하기에 힘들어 보입니다.
  • 따라서 조금더 변경을 진행해 보겠습니다.

리팩토링 해보기

Github 의 web.v4 패키지, global.strategypatternrefactoring 패키지 참조

  • 공통적인 로직은 하나로 묶되, 생성자를 통해 전략을 주입받지 않고, 메서드를 실행할 때, 전략을 주입받도록 하겠습니다.



결과

2024-09-04T07:18:28.786+09:00  INFO 171476 --- [designpattern] [nio-8080-exec-2] b.d.g.t.ThreadLocalLogger                : [9704c8dd][save] : Running Time : 1009
2024-09-04T07:18:28.786+09:00  INFO 171476 --- [designpattern] [nio-8080-exec-2] b.d.g.t.ThreadLocalLogger                : [9704c8dd][saveName] : Running Time : 1009
2024-09-04T07:18:28.786+09:00  INFO 171476 --- [designpattern] [nio-8080-exec-2] b.d.g.t.ThreadLocalLogger                : [9704c8dd][saveName] : Running Time : 1009
2024-09-04T07:18:28.996+09:00  INFO 171476 --- [designpattern] [nio-8080-exec-3] b.d.g.t.ThreadLocalLogger                : [bc72c712][save] : Running Time : 1010
2024-09-04T07:18:28.996+09:00  INFO 171476 --- [designpattern] [nio-8080-exec-3] b.d.g.t.ThreadLocalLogger                : [bc72c712][saveName] : Running Time : 1010
2024-09-04T07:18:28.996+09:00  INFO 171476 --- [designpattern] [nio-8080-exec-3] b.d.g.t.ThreadLocalLogger                : [bc72c712][saveName] : Running Time : 1011
  • 결과적으로는 처음 v0 코드에 비해서 관심사가 어느정도 분리가 된 것으로 보입니다.
  • 또한, Logging 코드를 수정하기 위해서 모든 로깅 관련된 method 들을 검색하고 수정할 필요가 없어 졌습니다.
  • 위에서 알아본 다양한 디자인 패턴을 사용하면, 관심사의 분리를 AOP 기능을 적용하지 않고 진행할 수 있을 듯 합니다.
profile
현재 블로그 이전 중입니다. https://blog.baekgwa.site/

0개의 댓글