[Spring/Kotlin] Spring + Kotlin 에서 Slf4j 사용하는 법(feat. KotlinLogging)
resilient
·2023. 8. 30. 18:55
Java + Springboot 조합으로 개발을 한다면 Slf4j어노테이션은 누구나 한 번쯤은 들어봤을 겁니다.
또한 Java + Springboot 조합을 사용한다면 Lombok을 사용해서 각종 어노테이션을 제공받아 객체나 메서드들을 생성해서 사용하곤 하는데요.
Slf4j의 Logger객체도 해당 클래스 위에 @Slf4j 어노테이션을 붙여주는 것만으로 편리하게 사용가능합니다.
하지만 Kotlin에서는 Lombok을 지원하지 않는데요. 이럴 경우 Slf4j를 당연히 사용할 수 없겠죠.
이번 포스팅에서는 Slf4j를 사용, Logger 객체를 직접 생성해서 사용하는 방법에 대해서 정리해보려고 합니다.
0. 일반적인 사용 예시
Slf4j의 Logger 객체를 생성하는 가장 일반적인 방법은 클래스 내부에서 LoggerFactory를 가져와서 직접 객체를 생성해서 사용하는 방법입니다. 아래 예시를 보시죠.
class ExceptionManager {
private val log = LoggerFactory.getLogger(ExceptionManager::class.java)
@ExceptionHandler(ReservationException::class)
@ResponseBody
fun handleReservationLogicException(e: ReservationException): ResponseEntity<ErrorResponse> {
log.error("[handleReservationException]", e)
val errorCode = e.errorCode
val response = ErrorResponse.of(errorCode)
return ResponseEntity(response, HttpStatus.valueOf(errorCode.status))
}
}
getLogger()의 파라미터로는 Logger를 사용하는 클래스의 타입을 넘겨주면 됩니다.
그렇다면 위 방법을 사용했을 때의 단점은 뭐가 있을까요?
로그를 찍기 위해서는 클래스마다 LoggerFactory를 가져와야 하고 이렇게 되면 많은 양의 중복 코드가 발생합니다.
어떻게 해결하면 좋을까요?
1. 팩토리 메서드 패턴 적용
1-1. 팩토리 메서드 패턴이란?
팩토리 메소드 패턴이란 객체 생성을 직접 하지 않고 객체를 생성하는 클래스를 만들어서 다른 곳에서 가져다 쓰는 패턴입니다.
예를 들어보겠습니다.
다양한 타입(예시는 여자, 남자 두 개의 타입만 존재)의 인구객체를 생성할 경우 분기 처리가 필요하고 남자 사람객체는 20살이 넘어야 생성할 수 있다고 가정하면 아래와 같은 코드가 나옵니다.
인구생성(타입)
if (타입 == "여자")
여자 여자1 = new 여자("여자1")
else if (타입 == "남자")
if (나이 > 20)
남자 남자1 = new 남자("남자1")
else
println("남자 생성 불가")
위 코드를 팩토리 메서드 패턴을 적용하면 아래와 같은 코드로 간단하게 바꿔서 사용할 수 있습니다.
아래와 같이 바꾸면 다른 종류의 인구클래스가 추가되거나 로직이 변경돼도 소스코드 변경이 필요 없는 유연한 코드가 되죠.
인구생성(타입)
인구 인구1 = 인구팩토리.인구생성(타입)
1-2. Logger팩토리에 팩토리 메서드 사용
따로 파일을 만들어서 아래와 같이 Logger 객체를 반환하는 메서드를 작성하고 Logger객체를 사용할 클래스에서 해당 메서드를 호출해서 사용할 수 있습니다.
commonLogger.kt 파일을 만들어서 아래와 같이 작성해 줍니다.
// commonLogger.kt
import org.slf4j.LoggerFactory
inline fun <reified T> T.logger() = LoggerFactory.getLogger(T::class.java)!!
Logger객체를 가져다 사용할 클래스에서는 아래와 같이 사용합니다.
@Service
class ReservationServiceImpl(
private val reservationRepository: ReservationRepository,
private val employeeRepository: EmployeeRepository,
private val seatRepository: SeatRepository
) : ReservationService {
val logger = logger() // logger() 메소드 가져와서 사용
private fun getEmployee(employeeId: Long): Employee? {
return employeeRepository.findById(employeeId).orElseThrow {
logger.error("employee does not exist. employeeId : {}", employeeId)
EntityNotFoundException("Employee does not exist.")
}
}
private fun getSeat(seatId: Long): Seat? {
return seatRepository.findById(seatId).orElseThrow {
logger.error("seat does not exist. seatId : {}", seatId)
EntityNotFoundException("Seat does not exist.")
}
}
}
2. 추상 클래스를 상속받는 companion object 사용법
(companion object는 아래 포스팅에 간단하게 정리가 되어있으니까 참고해 주세요.)
이 방법은 Logger 객체를 갖는 추상 클래스를 만들고 Logger 객체를 사용할 클래스에서 이를 상속해서 사용하는 방법입니다.
먼저 아래와 같이 추상클래스를 만들어줍니다.
import org.slf4j.LoggerFactory
abstract class LoggerCreator {
val log = LoggerFactory.getLogger(this.javaClass)!!
}
Logger를 사용할 클래스에서는 추상 클래스를 상속받는 companion object를 선언해서 아래와 같이 사용할 수 있습니다.
@Service
class ReservationServiceImpl(
private val reservationRepository: ReservationRepository,
private val employeeRepository: EmployeeRepository,
private val seatRepository: SeatRepository
) : ReservationService {
companion object : LoggerCreator() {} // companion object사용
private fun getEmployee(employeeId: Long): Employee? {
return employeeRepository.findById(employeeId).orElseThrow {
logger.error("employee does not exist. employeeId : {}", employeeId)
EntityNotFoundException("Employee does not exist.")
}
}
private fun getSeat(seatId: Long): Seat? {
return seatRepository.findById(seatId).orElseThrow {
logger.error("seat does not exist. seatId : {}", seatId)
EntityNotFoundException("Seat does not exist.")
}
}
}
이 방법은 companion obejct를 사용했기 때문에 로그에 찍히는 클래스 타입을 보면 $Companion이 붙습니다.
그럼 왜 companion object를 사용해야 할까요? 그냥 클래스 자체에서 추상 클래스를 상속받아서 사용하면 로그에 찍히는 클래스 타입에 $Companion도 붙지 않을 텐데요.
Logger를 사용할 때 왜 반드시 companion object를 사용해야 하는지에 대해 이해하기 위해서는 LoggerFactory.getLogger() 메서드의 동작 방식과 Kotlin의 클래스와 companion object의 관계에 대해 이해해야 합니다.
- LoggerFactory.getLogger(): LoggerFactory.getLogger() 메서드는 클래스의 이름을 인자로 받아 해당 클래스에 대한 로거를 생성합니다. 이때 클래스 이름은 Java의 Class.getName() 메서드를 사용하여 얻게 됩니다. Kotlin에서 클래스 이름을 얻는 방식은 Java와 다를 수 있습니다.
- Kotlin 클래스와 companion object: Kotlin 클래스의 이름은 Class.getName() 메서드의 결과와 다를 수 있습니다. 이는 Kotlin의 클래스 이름 규칙과 Java의 클래스 이름 규칙의 차이 때문입니다.
이 때문에 Kotlin 클래스의 companion object를 사용하면 클래스 이름을 Java 스타일로 얻을 수 있어서 Java의 로거 생성 규칙과 일치시킬 수 있습니다.
따라서 LoggerFactory.getLogger() 메서드가 Java의 클래스 이름 규칙과 일치하도록 하기 위해, Kotlin에서는 클래스의 companion object를 사용하여 Java 스타일의 클래스 이름을 얻어와 로거를 생성합니다.
결론적으로, companion object를 사용하는 이유는 Java와의 호환성 및 로깅 시 클래스 이름을 일치시키기 위함이라고 할 수 있죠.
3. KotlinLogging 라이브러리 사용
Slf4j를 꼭 써지 않아도 logger을 사용하는 방법은 존재합니다. kotlin에 최적화된 다른 라이브러리를 사용하면 되죠.
kotlinLogging라이브러리 사용을 위해 아래와 같이 build.gradle.kts 에 추가해 줍니다.
dependencies {
//logging
implementation("io.github.microutils:kotlin-logging:1.12.0")
}
그럼 아래와 같이 코틀린 파일에 정의하고 logger를 사용할 수 있게 됩니다.
private val logger = KotlinLogging.logger {} // KotlinLogging 사용
@Service
class ReservationServiceImpl(
private val reservationRepository: ReservationRepository,
private val employeeRepository: EmployeeRepository,
private val seatRepository: SeatRepository
) : ReservationService {
private fun getEmployee(employeeId: Long): Employee? {
return employeeRepository.findById(employeeId).orElseThrow {
logger.error("employee does not exist. employeeId : {}", employeeId)
EntityNotFoundException("Employee does not exist.")
}
}
private fun getSeat(seatId: Long): Seat? {
return seatRepository.findById(seatId).orElseThrow {
logger.error("seat does not exist. seatId : {}", seatId)
EntityNotFoundException("Seat does not exist.")
}
}
}
4. 정리
이번 포스팅에서는 kotlin + Springboot 조합으로 개발을 할 경우 Slf4j를 사용하는 방법과 다른 Logging방법에 대해서 정리해 봤습니다.
위 방법들 말고도 다른 더 좋은 방법들이 있음은 확실합니다. 따라서 어떤 방법이 가장 좋은지는 개발자의 선택이라고 생각합니다.
개인적인 제 생각은 앞으로 개발할 때 Slf4j를 사용해야 한다면 1번 팩토리 메서드를 사용하는 방법을 사용할 것 같습니다.
긴 글 읽어주셔서 감사합니다.
5. Reference
'Back-end > Spring' 카테고리의 다른 글
[Spring/TIL] Autowired와 RequiredArgsConstructor의 비교 (1) | 2024.02.28 |
---|---|
[Spring/TIL] Service 인터페이스와 ServiceImpl 클래스 구조를 나누는 이유와 나의 생각 (0) | 2023.06.21 |
[Spring/TIL] 커스텀 어노테이션(@AuthUser) 구현 (0) | 2023.05.31 |
[Spring/TIL] Github Actions 배포환경에서 Github Submodule로 application.yml 관리하기. (0) | 2023.04.19 |
[Spring/TIL] columnDefinition = "TINYINT(1)" 사용 시 주의할 점 (0) | 2023.04.12 |