들어가며
팀에서 데이터 드리븐한 의사결정을 할 수 있도록 핵클, 앰플리튜드에 데이터를 적재하는 환경을 구축하고 있습니다. 데이터를 전달하기 위해 비즈니스 로직에 이벤트 전달 코드를 함께 작성했습니다. 그러다 핵클, 앰플리튜드에 전달해야 하는 이벤트 전달 코드가 점진적으로 많아지면서 코드의 가독성을 해치게 되었고 하나의 클래스에서 여러 책임을 갖게 되는 문제가 발생했습니다. 이 글을 통해 제가 겪은 문제를 어떻게 해결했는지 공유하고자 이 글을 씁니다.
이벤트 전달하기
먼저 유저의 회원가입을 처리하는 클래스가 있다고 가정하고 간단한 샘플 코드를 살펴보겠습니다.
만약 유저가 언제 회원가입을 했는지 알기 위해 핵클과 앰플리튜드에 이벤트를 전달하고 싶다면 다음과 같이 코드를 구성할 수 있을 것입니다.
위와 같이 이벤트를 전달하는 로직을 구성하면서 크게 3가지 문제가 발생했습니다. 첫 번째는 비즈니스 로직과 이벤트 전달 코드를 함께 작성하는 과정에서 반복되는 코드를 중복해서 작성해야 했습니다. 두 번째는 코드의 가독성이 떨어지는 문제가 발생했습니다. 그리고 마지막은 Hackle 및 amplitude 라이브러리에 강력하게 의존하는 코드를 작성함으로써 테스트 코드를 작성하는데 어려움이 있었습니다.
세 가지 문제를 해결하기 위해 어떻게 해야 할까 고민하는 과정에서 아래의 글을 통해 마틴 파울러의 DOO라는 개념에 대해 살펴볼 수 있었습니다.
모니터링은 마틴 파울러처럼: Domain-Oriented Observability 도입기
빠르게 변하는 환경에서도 모니터링이 코드를 더럽히지 않게 만들기 위해 마틴 파울러 블로그에 소개된 Domain-Oriented Observability 를 도입한 이야기를 다룹니다.
engineering.ab180.co
위 글에서 DOO를 이렇게 정의 내렸다고 생각했습니다.
도메인 로직에 observability를 추가할 때, 도메인 로직은 이를 가능한 모르게 하고 observability에 대한 구체적인 구현을 책임지는 별도의 객체를 도입하는 것이라 볼 수 있습니다.
그럼 마틴 파울러가 제시한 DOO 코드를 살펴보고, DOO를 활용해서 이벤트를 전달하는 코드를 구성해 보면 어떨까 생각했습니다.
DOO 알아보기
이 글에서 마틴 파울러는 DOO(Domain Oriented Observability)를 설명할 때 아래와 같은 코드를 구성했습니다.
class ShoppingCart…
applyDiscountCode(discountCode){
this.instrumentation.applyingDiscountCode(discountCode);
let discount;
try {
discount = this.discountService.lookupDiscount(discountCode);
} catch (error) {
this.instrumentation.discountCodeLookupFailed(discountCode,error);
return 0;
}
this.instrumentation.discountCodeLookupSucceeded(discountCode);
const amountDiscounted = discount.applyToCart(this);
this.instrumention.discountApplied(discount,amountDiscounted);
return amountDiscounted;
}
ShoppingCart에서는 비즈니스 로직에 집중하고, 로깅을 하거나 메트릭 혹은 이벤트를 전달하는 책임은 DiscountInstrumentation에서 맡게 함으로써 캡슐화했습니다.
class DiscountInstrumentation {
constructor({logger,metrics,analytics}){
this.logger = logger;
this.metrics = metrics;
this.analytics = analytics;
}
applyingDiscountCode(discountCode){
this.logger.log(`attempting to apply discount code: ${discountCode}`);
}
discountCodeLookupFailed(discountCode,error){
this.logger.error('discount lookup failed',error);
this.metrics.increment(
'discount-lookup-failure',
{code:discountCode});
}
discountCodeLookupSucceeded(discountCode){
this.metrics.increment(
'discount-lookup-success',
{code:discountCode});
}
discountApplied(discount,amountDiscounted){
this.logger.log(`Discount applied, of amount: ${amountDiscounted}`);
this.analytics.track('Discount Code Applied',{
code:discount.code,
discount:discount.amount,
amountDiscounted:amountDiscounted
});
}
}
observability에 대한 구체적인 구현을 책임지는 DiscountInstrumentation 클래스를 구성함으로써 결과적으로 비즈니스 로직을 다루는 코드는 도메인 로직에 더 집중할 수 있게 구성할 수 있다는 것이 DOO의 큰 장점이라 판단했습니다. 그럼 어떻게 DOO를 적용해 볼 수 있을까 고민했습니다.
DOO 적용하기
문제 해결을 위해 위 구조와 같이 이벤트를 전달하는 환경을 구성했습니다. 이벤트를 전달하는 책임을 가진 TrackerService 클래스를 구성한 후, TrackerService 클래스를 UserApiService에서 주입받아서 활용한다면 비즈니스 로직과 이벤트 전달 로직을 분리할 수 있게 됨으로 코드의 가독성을 올릴 수 있고, 반복되는 코드도 모두 제거할 수 있다고 판단했습니다. 그리고 TrackerService를 분리함으로써 테스트 코드를 작성하기 좋은 구조를 가져갈 수 있다고 생각했습니다. 테스트 코드를 작성할 때도 이벤트를 전달하는 메서드가 호출되었는지, 메서드를 호출하기 위해 어떤 값이 전달됐는지를 모두 확인할 수 있을 것이라 생각했습니다.
이벤트를 전달하는 책임을 TrackerServiceImpl 클래스가 맡고, UserApiService에서는 주입받은 TrackerService를 활용해서 이벤트를 전달하는 코드를 구성했습니다. TrackerService에 이벤트를 전달할 때, TrackerEventEnum 클래스를 활용해서 이벤트 프로퍼티를 전달하도록 구성했습니다. 그럼 자세하게 코드를 살펴보기 전에 Enum 클래스는 왜 사용했는지, TrackerServiceImpl 클래스를 활용해서 어떻게 관심사를 분리할 수 있었는지 알아보겠습니다.
EnumClass 활용 이벤트 전달 객체 생성
인프런 CTO 이동욱 님의 글을 보면서, EnumClass를 활용한다면 핵클과 앰플리튜드에 전달하는 유저와 이벤트 프로퍼티 객체를 한 곳에서 생성할 수 있게 됨으로써 응집력 있게 이벤트 전달 코드를 관리할 수 있을 것이라 생각했습니다.
기존 코드는 핵클과 앰플리튜드에 이벤트를 전달할 때 properties 데이터 코드를 하드코딩해야 하는 문제가 있었습니다. 특정 객체를 전달하면, properties로 동적으로 전달되게끔 구성하고 싶었습니다.
유저, 이벤트 프로퍼티 객체를 생성하기 위해 위와 같이 TrackerEventEnum 클래스를 생성했습니다.
static 메서드에서 전달받아야 하는 property 정보의 타입을 명시해 줌으로써 핵클과 앰플리튜드에 어떤 데이터를 전달할 것인지를 EnumClass만 보고서도 명확하게 확인할 수 있도록 구성했습니다.
이벤트 전달 구현체 생성
그 후 핵클과 앰플리튜드에 이벤트를 전달하는 로직을 처리하기 위해 TrackerService를 생성합니다.
위와 같이 TrackerService interface를 생성하고, 구현체로 TrackerServiceImpl 클래스를 생성합니다.
주입받은 TrackerService를 활용해서 이벤트를 전달하는 코드를 구성합니다. logMonitor라는 메서드를 구성해서 trackerService의 sendEvent를 호출하는 코드를 작성합니다.
trackerService의 sendEvent 메서드를 호출할 때, EnumClass를 활용해서 이벤트를 전달할 때 필요한 객체를 전달합니다. 이렇게 trackerService와 EnumClass를 활용하여 이벤트를 전달하는 코드를 구성한 결과 비즈니스 로직과 이벤트 전달 로직을 분리할 수 있게 됨으로 코드의 가독성을 올릴 수 있고, 반복되는 코드도 모두 제거할 수 있었습니다. 그리고 UserApiService에서는 핵클과 앰플리튜드 라이브러리에 직접적으로 의존하지 않도록 구성할 수 있었습니다. 이를 통해 만약 다른 라이브러리를 사용해야 한다거나, 다른 이벤트 트래킹 툴을 사용한다고 하더라도 코드를 빠르게 수정할 수 있게 되었습니다.
마지막으로 위와 같이 이벤트 전달 로직을 분리하면서 테스트 코드를 작성할 때도 큰 이점이 있었습니다. 이벤트를 전달하는 코드가 호출되었는지, 코드를 호출하기 위해 어떤 값이 전달됐는지 테스트 코드를 통해 확인할 수 있었습니다. 다음 글에서는 테스트 코드는 어떻게 작성했는지 살펴보도록 하겠습니다.
마치며
글을 읽어주셔서 감사합니다. 저도 완벽하게 혹시나 코드에서 개선이 필요한 부분이 있다면 피드백을 반영해서 수정하겠습니다.
출처
모니터링은 마틴 파울러처럼: Domain-Oriented Observability 도입기
빠르게 변하는 환경에서도 모니터링이 코드를 더럽히지 않게 만들기 위해 마틴 파울러 블로그에 소개된 Domain-Oriented Observability 를 도입한 이야기를 다룹니다.
engineering.ab180.co
Domain-Oriented Observability
Add observability to your code without adding cruft to your domain logic or compromising on testability.
martinfowler.com
ts-jenum 으로 응집력 있는 TS 코드 작성하기 (feat. EnumClass)
TypeScript의 Enum은 딱 열거형으로서만 사용할 수 있습니다. 다른 언어에서 Enum을 Static 객체로 사용해본 경험이 있는 분들이라면 이 지점이 굉장히 답답하다는 것을 느낄 수 있는데요. Enum을 객체로
jojoldu.tistory.com
운영 로그와 디버그 로그 분리하기
최근에 Pete Hodgson가 martinfowler 블로그에 기재한 글을 보면서 로깅도 하나의 기능으로 봐야한다는 생각이 더 강해져서 이 글을 쓰게 되었다. 시스템을 구축하다보면 다음과 같이 크게 두 종류의
jojoldu.tistory.com
'Project > 서버 개발' 카테고리의 다른 글
[Project] 프로젝트 삽질기68 (feat ECR tag 관리) (0) | 2024.08.31 |
---|---|
[Project] 프로젝트 삽질기67 (feat DOO 단위 테스트) (0) | 2024.07.16 |
[Project] 프로젝트 삽질기65 (feat 나이스 인증 3) (0) | 2024.06.24 |
[Project] 프로젝트 삽질기64 (feat 나이스 인증 2) (0) | 2024.06.24 |
[Project] 프로젝트 삽질기63 (feat 나이스 인증 1) (0) | 2024.06.24 |