일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- 백준
- Spring
- 파이썬
- AWS
- JIRA
- 재갱신
- 오블완
- CI/CD
- springdataredis
- githubactions
- 티스토리챌린지
- 스프링
- oauth2
- 도커
- java
- 메시지
- 스프링부트
- 프로그래머스
- docker
- 국제화
- 소셜로그인
- 토이프로젝트
- 액세스토큰
- 트랜잭션
- springsecurity
- yaml-resource-bundle
- 스프링시큐리티
- springsecurityoauth2client
- 데이터베이스
- 리프레시토큰
- Today
- Total
땃쥐네
[토이프로젝트] 게시판 시스템(board-system) 7. 로깅 모듈 본문
1. Slf4j
1.1 Slf4j 란?
Slf4j 는 다양한 로깅 프레임워크(예: java.util.logging, logback, Log4j )의 퍼사드 역할을 합니다.
실제 로깅 도구들에 대한 일반 API를 제공하고 로깅을 실제 구현과 독립적으로 만듭니다.
그래서 보통 스프링 개발자분들은 Slf4j 인터페이스를 의존하여 개발하고 구현체로는 logback 을 많이 사용합니다.
실제로 스프링부트의 웬만한 starter 의존성을 추가하면 slf4j 의존성이 추가되고 기본 구현체로는 logback 이 사용됩니다.
1.2 파라미터화 로깅 (Parameterized Logging)/
String variable = "Hello ttasjwi";
logger.debug("Printing variable value: " + variable);
Slf4j 의 logger로 로깅을 할 때 위와 같이 로그를 작성하면 debug 레벨이 활성화되어 있든 아니든 문자열을 연결부터 합니다. 만약 변수에 바인딩 한 객체가 생성이 비싸다면 성능 문제를 발생시킬 수 있습니다.
String variable = "Hello ttasjwi";
if (logger.isDebugEnabled()) {
logger.debug("Printing variable value: " + variable);
}
따라서 이렇게 logger 의 레벨을 체크하는 분기문을 작성하는 방법을 써야합니다.
String variable = "Hello ttasjwi";
logger.debug("Printing variable value: {}", variable);
분기문을 작성하는 방식이 불편하기 때문에 sl4fj 는 파라미터화 로깅(Parameterized Logging)을 제공합니다.
- 이 로깅 방식은 로거가 실제 실행될 때 실제 문자열 결합을 하게 만듭니다.
- 이렇게 로깅 레벨이 debug 일 때만 문자열 결합을 합니다. 따라서 분기처리를 안 해도 됩니다.
1.3 성능적 한계
하지만 아무리 파라미터화 방식을 사용한다 하더라도 이 역시 성능상 이슈가 있습니다.
위의 방식으로 분기처리를 안 해도 되지만, 일단 로깅 활성화 대상이 된다면 다음 작업을 거치게 됩니다
(Refrence: https://realjenius.com/2017/08/31/logging-in-kotlin/)
- 입력 문자열을 구문 분석하고 부분으로 토큰화한다
- 각 위치에 대해 입력 배열의 요소를 교체
- 입력 배열의 경계, 언더플로우/오버플로우 체크
- 인덱스가 0 이상이며 배열의 크기보다 작아야 함을 확인
- 음수 인덱스에 접근하지 않는지 체크
- 배열의 크기를 초과하는 인덱스에 접근하지 않는 지 체크
- 선택한 로그 처리기에 새 문자열을 재구성하여 출력
1.4 코틀린에서
- 코틀린에서는 “xxxx ${…}” 와 같이 문자열 템플릿 기능을 제공합니다.
- 실제 내부적으로 StringBuilder 를 사용해서 성능도 괜찮습니다
- 가독성도 좋습니다.
- 그러나 Slf4j 방식을 사용하면 “… {}” 을 쓰고 뒤에 인자를 나열하는 방식을 써야해서 가독성이 안 좋아집니다.
- 그리고 위에서 언급한대로 "... {}" 의 성능도 그렇게 썩 좋지 못 합니다.
여기서 코틀린 문자열 템플릿의 성능적 이점, 가독성, 로깅 레벨에 따른 선택적 로깅 문자열 결합/생성 실행, 의 장점을 모두 잡을 수 있는 방법이 없을까 의문이 생깁니다.
2. Kotling Logging
위의 문제에 대한 대안으로 kotlin-logging 이라는 라이브러리가 나와있습니다.
logger.debug { "Some $expensive message!" }
- Slf4j 의존성이 필요합니다. Slf4j 를 한 번 더 감싸서 라이브러리가 동작합니다.
- 로깅 레벨이 맞을 때만 실행되므로 분기처리를 안 해도 됩니다.
- 기존 sl4fj 의 “{}” 방식처럼 if 문을 작성하지 않아도 됩니다.
- 문자열을 함수로 전달받습니다. 로깅 레벨이 맞을 때만 함수가 실행되고 그 때 문자열 인스턴스가 생성됩니다.
- 또 코틀린 문자열은 실질적으로 내부적으로 StringBuilder 를 사용하기 때문에 문자열 결합 비용도 저렴합니다.
- 그러면서도 코드의 가독성을 지킬 수 있습니다.
// exception as first parameter with message as lambda
logger.error(exception) { "a $fancy message about the $exception" }
그 외로, 예외를 첫번째 파라미터로 받고 함수를 뒤에 전달하는 것도 가능합니다.
에외를 포함한 로깅은 위와 같이 작성할 수 있습니다.
저는 위의 편의성이 마음에 들어서 Kotlin Logging 을 쓰기로 했습니다.
3. 의존성 문제
import io.github.oshai.kotlinlogging.KotlinLogging
// 생략
private val log = KotlinLogging.logger(clazz.name)
log.info { "hello" }
이제 로깅은 Kotlin logging 을 의존하여 코드를 작성하면 됩니다. 이 내부적으로 Slf4j를 의존하여 동작하게 됩니다.
근데 저렇게 사용하면 그러나 사용하는 측에서는 Slf4j를 의존하던걸 Kotlin Logging 으로 바꿔야합니다.
oshai 의 Kotlin Logging 을 컴파일타임 의존하는 문제가 생깁니다.
이렇게 쓴다 하더라도 향후 이 Kotlin Logging 프로젝트가 망하거나 다른 더 좋은 로거가 나오면 또다시 로거를 변경하는 코드를 작성해야하는 문제가 생길 수 있을 것 같습니다.
그래서 그때 피를 덜 보기 위해(물론 지금 그럴 일은 거의 없을 것 같긴한데...) 저는 로깅 기술을 하나의 로거 모듈에 한 단계 더 추상화 시켜서 격리시키기로 했습니다.
4. 로거 추성화
4.1 로깅 모듈 설정
rootProject.name = "board-system"
include(
// 설정 최종 구성 및 실행
"board-system-container",
// 공용 모듈
"board-system-core",
"board-system-logging",
// api
"board-system-api:api-deploy",
)
settings.gradle.kts 에 로깅모듈을 새로 선언했습니다.
subprojects {
apply { plugin(Plugins.KOTLIN_JVM.id) }
apply { plugin(Plugins.KOTLIN_SPRING.id) }
apply { plugin(Plugins.SPRING_BOOT.id) }
apply { plugin(Plugins.SPRING_DEPENDENCY_MANAGEMENT.id) }
dependencies {
val sharedModuleNames = listOf("board-system-core", "board-system-logging")
if(project.name !in sharedModuleNames) {
implementation(project(":board-system-core"))
implementation(project(":board-system-logging"))
}
implementation(Dependencies.KOTLIN_REFLECT.fullName)
testImplementation(Dependencies.SPRING_BOOT_TEST.fullName)
}
루트 build.gradle.kts 의 dependencies 쪽에서는 코어모듈과 로깅모듈이 아닐 경우 모두 코어모듈, 로깅모듈을 의존하게 설정 했습니다.
intellij 에서 gradle 을 리로드 해보면 모듈 이름으로 폴더를 생성시 모듈로 잘 인식합니다.
글 작성일 기준 kotlin logging 의 버전은 7.0.0 이군요.
enum class Dependencies(
private val groupId: String,
private val artifactId: String,
private val version: String? = null,
private val classifier: String? = null,
) {
// kotlin
KOTLIN_JACKSON(groupId = "com.fasterxml.jackson.module", artifactId = "jackson-module-kotlin"),
KOTLIN_REFLECT(groupId = "org.jetbrains.kotlin", artifactId = "kotlin-reflect"),
KOTLIN_LOGGING(groupId = "io.github.oshai", artifactId = "kotlin-logging", version = "7.0.0"),
buildSrc의 Dependencies 에 의존성 모듈을 모아두고 있는데 이곳에 enum 을 추가로 선언했습니다.
dependencies {
implementation(Dependencies.KOTLIN_LOGGING.fullName)
}
로깅모듈의 build.gradle.kts 의존성으로 아까 선언한 Depenedencies 의 Enum 을 활용합니다.
참고로 kotlin logging 은 slf4j 및 그 구현체가 필요한데
Spring Boot Starater Test 의존성에 spring-boot-starter-logging 이 포함되어 있고 여기에 Slf4j 및 logback 의존성이 있어서 따로 추가할 필요는 없습니다.
그 외의 스프링부트 starter 의존성이 있는 경우도 마찬가지입니다.
4.2 Logger 인터페이스
package com.ttasjwi.board.system.logging
interface Logger {
fun trace(message: () -> Any?)
fun trace(throwable: Throwable, message: () -> Any?)
fun trace(throwable: Throwable)
fun debug(message: () -> Any?)
fun debug(throwable: Throwable, message: () -> Any?)
fun debug(throwable: Throwable)
fun info(message: () -> Any?)
fun info(throwable: Throwable, message: () -> Any?)
fun info(throwable: Throwable)
fun warn(message: () -> Any?)
fun warn(throwable: Throwable, message: () -> Any?)
fun warn(throwable: Throwable)
fun error(message: () -> Any?)
fun error(throwable: Throwable, message: () -> Any?)
fun error(throwable: Throwable)
}
제 서비스의 Logger 인터페이스입니다. 기존 Kotlin logging 과 비슷한 사용법으로 인터페이스를 만들었습니다.
4.3 DelegateLogger
package com.ttasjwi.board.system.logging.impl
import com.ttasjwi.board.system.logging.Logger
import io.github.oshai.kotlinlogging.KotlinLogging
internal class DelegateLogger(clazz: Class<*>) : Logger {
private val target = KotlinLogging.logger(clazz.name)
override fun trace(message: () -> Any?) {
target.trace(message)
}
override fun trace(throwable: Throwable, message: () -> Any?) {
target.trace(throwable, message)
}
override fun trace(throwable: Throwable) {
target.trace(throwable) {}
}
override fun debug(message: () -> Any?) {
target.debug(message)
}
override fun debug(throwable: Throwable, message: () -> Any?) {
target.debug(throwable, message)
}
override fun debug(throwable: Throwable) {
target.debug(throwable) { }
}
override fun info(message: () -> Any?) {
target.info(message)
}
override fun info(throwable: Throwable, message: () -> Any?) {
target.info(throwable, message)
}
override fun info(throwable: Throwable) {
target.info(throwable) { }
}
override fun warn(message: () -> Any?) {
target.warn(message)
}
override fun warn(throwable: Throwable, message: () -> Any?) {
target.warn(throwable, message)
}
override fun warn(throwable: Throwable) {
target.warn(throwable) {}
}
override fun error(message: () -> Any?) {
target.error(message)
}
override fun error(throwable: Throwable, message: () -> Any?) {
target.error(throwable, message)
}
override fun error(throwable: Throwable) {
target.error(throwable) {}
}
}
Logger 구현체입니다. 외부에서 클래스를 전달받아 생성되고 내부적으로 KotlinLogging.logger 를 생성해 사용합니다.
실질적으로는 KotlinLogging 을 의존해서 로거를 대신 위임합니다.
4.5 getLogger
package com.ttasjwi.board.system.logging
import com.ttasjwi.board.system.logging.impl.DelegateLogger
fun getLogger(clazz: Class<*>): Logger = DelegateLogger(clazz)
getLogger 함수입니다. 외부에서는 실제 로거를 얻어올 때 이것을 의존하면 되고, 내부적으로 어떤 식으로 로깅하는 지는 몰라도 됩니다.
4.6 사용례
class TestClass {
companion object {
val log = getLogger(TestClass::class.java)
}
fun hello() {
log.info { "hello" }
val e = IllegalStateException()
log.warn(e) { "something is wrong" }
log.error(e)
}
}
이렇게 클래스에서 getLogger를 호출해서 Logger를 가져오고, 그것을 log 변수에 할당한 뒤
로거의 메서드를 호출하면 됩니다.
log 를 인스턴스에 할당할 것인가, 동반객체에 할당할 것인가 선택의 여지가 있을 수 있는데
스프링 빈으로 등록한 싱글턴 객체들에 대해서는 인스턴스 변수에 할당해도 될 것 같습니다.
다만 여러 인스턴스가 생성되는 경우에는 동반객체에 생성하는게 맞을 것 같고
저는 모든 logger는 동반객체에 선언하도록 하기로 했어요.
실제 테스트코드쪽에서 간단하게 클래스를 만들어서 실행해보니 잘 돌아갑니다.
5. 라이브 템플릿 기능 활용
막 개발 코드를 짜다보면 로거를 작성하는 부분이 귀찮긴합니다.
class TestClass {
companion object {
val log = getLogger(TestClass::class.java)
}
매번 동반객체(companion object)를 선언하고 그 안에 똑같은 선언을 계속 적어줘야하죠.
이것이 귀찮아서 어떻게 할까 찾아보다가 좋은 글을 발견했습니다. 인텔리제이의 라이브 템플릿을 사용하는 방법인데요.
이를 사용해보도록 하겠습니다.
Kotlin 의 live template 를 사용하면 로거 작성을 좀 더 편리하게 할 수 있습니다.
Editor > Live Templates 에 들어가서 Kotlin 쪽을 클릭한채 live template 를 클릭하고
아래쪽의 change 를 눌러 Kotlin 을 사용처로 지정합니다.
private val log = com.ttasjwi.board.system.logging.getLogger($THIS_CLASS$::class.java)
약자는 logger로 지정하고(편한대로) 위처럼 선언하면 로거 가져오기, 변수선언까지 자동완성하는 코드를 만들 수 있어요.
이떄 $THIS_CLASS$ 변수가 선언됐는데 우측 상단의 Edit Variables... 를 클릭하면
변수에 어떤 표현식을 할당할지 설정할 수 있는데요.
여기서 kotlinClassName() 을 지정하면 현재 커서가 위치한 클래스의 이름을 자동완성할 수 있게 합니다.
companion object {
private val log = com.ttasjwi.board.system.logging.getLogger($THIS_CLASS$::class.java)
}
저는 보통 동반객체와 함께 logger 를 만드는걸 선호하는데 자동완성을 두가지 버전으로 만들었습니다.
- logger : 단순히 private log 선언만 (주로 동반객체가 이미 있는 상황)
- clogger : 동반객체 선언과 동시에 logger 선언도 함께
이제 logger 입력시 logger 선언을 더 편하게 할 수 있네요.
clogger 입력시 동반객체 생성과 함께 로거 선언도 할 수 있게 했습니다.
6. 의존성 문제
자, 여기까지 작업한 작업물의 의존성 트리를 보겠습니다.
컨테이너는 핵심이 되는 모듈들을 알게 해서 이 설정들을 취합해 실행을 하는 역할을 하고
그 외 모든 모듈들은 공통적으로 core 모듈과 logging 모듈을 implementation 의존하고 있습니다.
이렇게 하면 getLogger 에 클래스를 전달하여, 그 클래스의 로거를 가져올 수 있게 할 수 있어요.
이러면서도 어떤 기술을 사용하는 지는 컴파일타임에는 모르게 합니다.
근데 문제는 logging 모듈을 보시면 내부적으로 구체적인 Kotlin Logging 모듈을 의존하고 있습니다. 로깅 모듈을 사용하는 입장에서는 어떤 로깅 기술이 쓰이는지는 모르지만 런타임에 Kotlin Logging 모듈을 의존하는 상황이 되는거죠.
이렇게하면 보통 다른 기술에 대한 의존성이 없어야하는 도메인 모듈도 향후 logging 모듈을 의존하고, 실제 Kotlin logging 구현체를 런타임 의존하게 되는 문제가 생깁니다. 도메인의 순수성과 개발의 편의성 사이에서 저울질하다가 저는 로깅의 편의성을 선택하고, 도메인 모듈도 포함해서 모두 로깅모듈을 의존하게 했습니다.
이번 글은 여기서 마무리 짓겠습니다.
글 읽어주셔서 감사합니다.
작성 코드 리포지토리 : https://github.com/ttasjwi/board-system
GitHub - ttasjwi/board-system
Contribute to ttasjwi/board-system development by creating an account on GitHub.
github.com
작성 PR : https://github.com/ttasjwi/board-system/pull/21
Feature: (BRD-35) Logger 제공을 위한 로깅 모듈 정의 by ttasjwi · Pull Request #21 · ttasjwi/board-system
JIRA 티켓 BRD-35 작업 내역
github.com
'Project' 카테고리의 다른 글
[토이프로젝트] 게시판 시스템(board-system) 9. API 예외 메시지 처리 (0) | 2024.10.21 |
---|---|
[토이프로젝트] 게시판 시스템(board-system) 8. 메시지,국제화 / API 응답 규격 (0) | 2024.10.16 |
[토이프로젝트] 게시판 시스템(board-system) 6. 커스텀 예외, core 모듈 (0) | 2024.10.02 |
[토이프로젝트] 게시판 시스템(board-system) 5. 프로젝트 멀티모듈화 (0) | 2024.09.30 |
[토이프로젝트] 게시판 시스템(board-system) 4. 지속적 무중단 배포(AWS EC2, GitHub Actions, Docker, Nginx) (0) | 2024.09.29 |