Spring boot Actuator 사용해보자 (2)

오늘역시 저번시간에 이어 Spring boot Actuator 를 좀 더 살펴보기로 하겠다. 그 전에 좋은 소식이 하나 있다. java9의 포함예정이였던 jigsaw 프로젝트 jsr376 (JPMS) 가 드디어 한달간의 리뷰를 마치고 통과하였다. 이번 리뷰에는 저번보다 두 회사가 늘어 25개 회사 중 24개 회사가 찬성표를 던졌고 한 회사(Red Hat)이 기권을 하였다. 이제 java9에 jigsaw 프로젝트가 포함되니 슬슬 공부좀 해야 겠다. 조만간 기회가 된다면 한번 포스팅을 해보도록 하자.

CounterService

metrics에 우리가 원하는 커스텀한 정보를 넣을 수 없을까? 만약 타 API와 호출하는 부분을 카운팅을 하고 싶다고 가정해보자. 얼마나 호출하는지 알고 싶다면 우리는 Spring boot에서 제공해주는 CounterService 인터페이스를 사용하면 된다. 참고로 언어는 코틀린으로 만들었다. 그리 어려운 문법을 사용한건 아니고 대충 봐도 무슨 내용인지 다 알 듯 싶다.

@RestController
class HelloController(val counterService: CounterService) {

  @GetMapping("/increment")
  fun increment(): String {
    counterService.increment("hello.foo.bar")
    return "hello";
  }

  @GetMapping("/decrement")
  fun decrement(): String {
    counterService.decrement("hello.foo.bar")
    return "hello";
  }

  @GetMapping("/reset")
  fun reset(): String {
    counterService.reset("hello.foo.bar")
    return "hello";
  }
}

우리는 위와 같이 increment() 메서드를 사용해서 카운트를 증가 시킬 수 있다. 만약 감소 시키고 싶다면 decrement() 메서드를 사용하면 되고 리셋을 하고 싶다면 reset() 메서드를 사용하면 된다.

한번 호출 해보자.

http localhost:8080/increment

http localhost:9090/application/metrics
{
    "classes": 6290,
    "classes.loaded": 6290,
    "classes.unloaded": 0,
    "counter.hello.foo.bar": 1
    ...
}

위와 같이 increment 엔드포인트를 호출한 후에 다시 metrics 을 호출해보면 counter.hello.foo.bar의 값이 증가했다. 이렇게 우리는 아주 손쉽게 metrics에 정보를 추가할 수 있다.

GaugeService

CounterService 인터페이스 경우에는 카운트만 증가 시키는 용도로 사용한다. 카운트만 증가 하는 것 말고 어떠한 게이지를 기록하고 싶다면 GaugeService 인터페이스를 사용하면 된다.

@RestController
class FooController(val gaugeService: GaugeService) {

  @GetMapping("/submit")
  fun submit(): String {
    val startTime = System.currentTimeMillis();
    Thread.sleep(100)
    gaugeService.submit("hello.foo.bar", (System.currentTimeMillis() - startTime).toDouble())
    return "submit"
  }
}

submit() 메서드를 사용해서 게이지의 값을 넣어주면 된다.

http localhost:8080/submit

http localhost:9090/application/metrics
{
    "classes": 6255,
    "classes.loaded": 6255,
    "classes.unloaded": 0,
    "counter.status.200.submit": 1,
    "gauge.hello.foo.bar": 105.0,
    "gauge.response.submit": 114.0
    ...
}

이렇게 특정한 게이지의 값 예를 들어 타 API 호출시 걸린 속도를 넣어 줘도 된다.

PublicMetrics

위의 CounterService 와 GaugeService으로 나타내기 어려운 것들도 존재 할지 모른다. 예를 들어 시스템의 사양이라던지 메모리라던지 기타 정보들을 나타내고 싶다면 PublicMetrics 인터페이스를 이용하면 된다.

@Component
class ControllerMetrics : PublicMetrics {
  override fun metrics(): List<Metric<*>> {
    return listOf(Metric("max.mem", Runtime.getRuntime().maxMemory()));
  };
}

만약 자상머신의 최대 메모리의 양을 구하고 메트릭으로 표현하고 싶다면 위와 같이 해주면된다. 간단하다.

http localhost:9090/application/metrics
{
    ...
    "threads.peak": 43,
    "threads.totalStarted": 46,
    "max.mem": 1908932608,
    "uptime": 30381
}

우리는 아주 쉽게 메트릭에 표현할 수 있다.

HealthIndicator

만약 특정한 곳에 상태 체크를 하고 싶다면 HealthIndicator 인터페이스를 이용하면 된다. 예를들어 외부와 연결된 서버라던지 DB라던지 혹은 외부 파일 기타 외부의 접근요소들의 상태 체크를 할 수 있다. Spring boot에서는 기존의 다양한 상태체크를 지원한다. DB, redis, Mail, Jms, 기타 다양한 Nosql 서버등이 자동설정에 포함되어 있다. 하지만 만약에 다른 타 외부 API의 상태를 체크 하고 싶다면 커스텀하게 구현해주면 된다.

@Component
class GithubHealth : HealthIndicator {
  override fun health(): Health {
    val restTemplate = RestTemplate()
    try {
      restTemplate.getForEntity("https://api.github.com", String::class.java);
      return Health.up().build()
    } catch (e: Exception) {
      return Health.down().build()
    }
  }
}

위와 같이 간단하게 HealthIndicator 인터페이스를 구현해주면 된다. 정상적일 경우에는 Health.up() 을 해주면 되고 비정상일 경우에는 Health.down() 메서드를 사용하면 된다. 한번 호출해보도록 하자.

http localhost:9090/application/health
{
    "diskSpace": {
        "free": 71309070336,
        "status": "UP",
        "threshold": 10485760,
        "total": 120137318400
    },
    "githubHealth": {
        "status": "UP"
    },
    "status": "UP"
}

githubHealth 라는 키에 해당 상태가 포함되어 있다. 만약 커스텀하게 더 많은 정보를 넣고 싶다면 아래와 같이 하면 된다.

Health.up().withDetail("statusCode", response.statusCode.value()).build()

그럼 다음과 같이 표현 된다.

{
    "diskSpace": {
        "free": 71307206656,
        "status": "UP",
        "threshold": 10485760,
        "total": 120137318400
    },
    "githubHealth": {
        "status": "UP",
        "statusCode": 200
    },
    "status": "UP"
}

HealthIndicator 인터페이스를 사용해도 되지만 좀 더 추상화된 AbstractHealthIndicator 클래스를 이용해도 된다.

@Component
class GithubHealth2 : AbstractHealthIndicator() {
  override fun doHealthCheck(builder: Health.Builder?) {
    val restTemplate = RestTemplate()
    val response = restTemplate.getForEntity("https://api.github.com", String::class.java);
    builder?.up()?.withDetail("statusCode", response.statusCode.value())
  }
}

해당 클래스를 사용하면 좀 더 깔끔해진다. try catch 문도 사라질 뿐더러 return 값도 필요 없다. 파라미터로 전달 된 Health.Builder를 이용해서 값을 넣으면 된다.

이들의 정보는 Spring shell을 이용해서 확인도 가능하다. 위의 정보보다는 없지만.. 하지만 spring shell은 spring boot 2.0 이후에는 지원하지 않으니 2.0 이전의 버전들만 사용가능하다. Spring shell 프로젝트도 나름 괜찮다고 생각했는데 없어지니 아쉽다.

오늘도 이렇게 좀 더 유용한 spring Actuator 를 살펴 봤다. 다음 시간에는 전 시간에 말한 trace를 영구적으로 저장해서 사용할 수 있도록 모색하며 개발을 해보자. 어제보다는 조금 유용한 정보이길 바라며 오늘은 이만.

kotlin (코틀린) 시작해보기 (7)

오늘은 코틀린을 마지막으로 배워보자. 오늘 이 시간에는 뭘 딱 정해서 배우는 것이 아니라 이것저것 빠진 것이나 기타 여러가지들을 배워보자.

Operator Overloading

말 그대로 연산자 오버라이딩이다. 우리가 흔히 아는 연산자가 맞다. 그 연산자를 다시 재정의 할 수 있다. 우리는 코드로 보는게 더 좋지 않는가? 코드를 보자.

class Account(val age: Int) {
    operator fun plus(account: Account): Account {
        return Account(age + account.age)
    }

    operator fun minus(account: Account): Account {
        return Account(age - account.age)
    }

    //...
    override fun toString(): String {
        return "$age years old"
    }
}

위와 같은 코틀린 코드가 있다고 가정하자. 단지 예제일 뿐이니. operator라는 키워드를 함수에 적용하고 plus(), minus()를 정의하였다. 그럼 사용법은 다음과 같다.

println(Account(10) + Account(20))
println(Account(20) - Account(10))

object 그대로 연산을 할 수 있다. 함수를 호출 하는 것이 아니라 연산자를 사용하여 숫자, 문자 마냥 object을 연산 할 수 있다. 물론 아무 함수나 되는 것은 아니다. 정해져 있는 함수만 가능하다. 이외에도 *(곱), /(나누기), %(나머지), ++(증감), --(감소) 등 여러가지가 더 있다. 꽤 많은 연산자들을 재정의 할 수 있다.
물론 plus 함수를 직접 사용해도 상관은 없다.

println(Account(10).plus(Account(20)))

c++ 에도 연산자 오버라이딩이 있던걸로 기억하는데 쓰읍.. 노기억. 아무튼 연산자를 object에 사용할 수 있다.

invoke

코틀린에는 invoke 라는 특이한 함수가 존재 한다. 이것은 스칼라에도 있다. 스칼라와 잠시 비교해보자.
일단 스칼라코드를 보자.

object Foo {
  def apply() = println("hello world")
  def apply(str: String) = println(s"hello $str")
}

스칼라도 object라는 키워드가 존재 한다. 우리는 코틀린에서 object를 잠시 배웠었다. 코틀린과 비슷하게 이것 또한 싱글톤이다.
보통 함수를 호출할 때는 함수의 이름을 사용해서 호출해야 된다. 예를들어 다음과 같다.

object Foo {
  def hello() {
    println("hello")
  }
}

위의 코드에서 hello() 함수를 사용하려면 우리는 다음과 같이 작성해서 사용해야 된다.

Foo.hello()

hello() 함수를 사용하려면 hello() 메서드를 명시적으로 호출해 줘야 hello() 메서드에 접근할 수 있다. 하지만 스칼라의 apply()란 메서드를 사용할 때에는 그럴 필요 없다.

Foo()
Foo("world")

위의 코드는 명시적으로 apply() 라는 함수를 호출 하지 않아도 자동으로 호출 되어진다. 특이한 함수이다. 스칼라와 마찬가지로 코틀린에도 apply() 와 같은 메서드가 존재한다. 위의 제목에서 보다시피 invoke() 라는 함수이다. 한번 만들어 보자.

object Foo {
    operator fun invoke() = println("hello world")
    operator fun invoke(message: String) = println("hello $message")
}

스칼라에서는 그냥 apply() 메서드를 일반 메서드 처럼 만들어도 되지만 코틀린에서는 operator 키워드를 사용해야 한다. 왜 그냥 되도록 만들지 않았지? 흠..
위의 코드도 동일하게 함수명을 명시적으로 호출하지 않아도 된다.

Foo()
Foo("world")

스칼라와 동일하다. 물론 스칼라와 코트린 모두 함수호출을 명시적으로 해줘도 상관은 없다.

//코틀린
Foo.invoke()

//스칼라 
Foo.apply()

잠시 스칼라와 몇가지만 비교해보자.

Data class vs Case class

코틀린에 data class라는 것이 존재한다. 우리는 data class를 아주 잠깐 배웠었다. 스칼라에도 data class와 비슷한 case class라는게 존재한다.
아래는 코틀린의 data class 이다

data class Person(val id: Long, val name: String)

다음은 스칼라 코드이다.

case class Person(id: Long, name: String)

문법 역시 비슷하다. 하지만 스칼라에서는 valvar 키워드를 명시적으로 써주지 않아도 되며 기본값으로 val로 되어 있다. 한마디로 아무 선언을 하지 않는다면 setter는 생성되지 않는다. 스칼라 역시 hashCode, equals, copy, getter, setter, toString 등등 자동으로 생성해 준다.

아래는 코틀린으로 data class를 호출할 때이다.

val person = Person(1, "wonwoo")
println(person)

//Person(id=1, name=wonwoo)

다음으로는 스칼라의 case class를 호출할 때이다.

val person = Person(1, "wonwoo")
println(person)

//Person(1,wonwoo)

문법역시 거의 비슷하다. 이건 똑같다. 필자가 스칼라 코드로 만들고 그냥 복붙해서 코틀린에 코드에 넣었는데 아무 이상없이 잘 된다.

When vs Pattern Matching

코틀린의 when도 우리는 살짝 배웠었다. 물론 When절도 괜찮다. 자바의 switch case문은 저리가라 할만큼 괜찮다. 하지만 스칼라의 Pattern Matching이 좀 더 강력한 것 같다.
아래의 코드는 코틀린의 when절이다. person의 id가 1이면 id와 name을 출력해주고 그렇지 않다면 nothing을 출력한다.

when {
    person.id == 1L -> println("id: 1, name: ${person.name}")
    else -> println("nothing")
}

위의 코드도 나쁘지 않다. 이런걸 할 수 있는 자체가 나쁘지 않다.
하지만 스칼라의 case class를 사용하면 바로 비교도 가능하고 그 안의 원소를 간단하게 비교도 할 수 있다.

person match {
  case Person(1, name) => println(s"id: 1, age : $name")
  case _ => println("nothing")
}

위의 코드는 person object에서 id가 1이면 id와 name을 출력한다. name은 상관없다. name은 아무거나 와도 된다. 하지만 id는 무조건 1이 와야 해당 문자를 출력한다. 원소를 굳이 person에서 꺼내지 않고 바로 사용해도 되서 더욱 간편한듯 싶다.
스칼라도 좋지만 코틀린도 그렇게 나쁘지 않다. 자바에 비교하면 훨씬 좋아 보인다.

Lambda Parameter

며칠 전에 배웠던 Lambda Parameter도 비교해보자. 코틀린에서 단일 파라미터를 좀 더 간편하게 쓸 수 있는 it이라는게 존재 하였다. 예를들어 1~5까지의 자연수중에 2보다 큰수를 2배하는 코드를 코틀린 코드로 작성한다고 가정하자. 그럼 아래와 같이 작성 할 수 있다.

val list = mutableListOf(1, 2, 3, 4, 5)
println(list.filter { i -> i > 2 }.map { i -> i * 2 })

원래는 이런 코드가 들어가겠지만 우리는 저번에 배웠던 it라는 키워드?를 사용해서 좀 더 깔끔하게 만들 수 있었다.

val list = mutableListOf(1, 2, 3, 4, 5)
println(list.filter { it > 2 }.map { it * 2 })

코틀린의 it 같이 스칼라에도 it가 동일한 키워드?가 존재 한다. 아래는 스칼라 코드이다.

val list = List(1,2,3,4,5)
println(list.filter(i => i > 2).map(i => i * 2))

원래는 위의 코드처럼 사용해도 되겠지만 코틀린의 it과 비슷한 개념으로 _가 존재 한다. 위의 코드를 _ 형태로 바꿔보자.

val list = List(1,2,3,4,5)
println(list.filter(_ > 2).map(_ * 2))

코틀린과 문법자체는 비슷하다. 키워드만 다를뿐이지 사용법은 동일하다.

우리는 이렇게 코틀린에 대해서 살펴봤다. 아직 익숙하지 않기에 좀 더 공부를 헤야 할 것 같다. 하지만 좀 더 공부를 할 지 하지 않을지는 미지수다. 그래도 잠깐이나마 자바를 벗어나 다른언어도 살펴보니 좋은 기능이 많은 것 같다. 예전에도 잠깐 스칼라를 공부했었는데 조금 공부하다가 그만둬서 기억이 가물가물하고 그때는 함수형 언어가 익숙치도 않아 많이 어려웠다. 이제는 코틀린을 공부해봤으니 그때보다는 조금 쉬워졌겠지? 코틀린을 사용하지는 않더라도 함수형 언어에 한발자국 다가간 느낌이다. 요즘 또 GO언어가 급부상하면서 언어 사용률도 몇십계단이나 올라갔던 뉴스?를 본게 기억난다. 다음에는 GO를 잠깐 살펴볼까?ㅎㅎㅎㅎ 흠흠 그러기엔 아직 역량이 부족하다. 아주아주 나중에 기회가 된다면 살펴봐도 나쁘지 않을 것같다. 아직 한국에서 자바로 먹고 살만큼 자바 사용률이 높다. (물론 세계적으로도 높긴 하다.)향후 10년은 거뜬하지 않을까 싶다. 하지만 시대가 어떻게 바뀔지 모르기에 다른언어 한개정도는 완벽하게 하지는 못해도 알아두면 좋을 것 같다.

이상으로 코틀린에 대해서 간단하게나마 알아봤다. 끝!

kotlin (코틀린) 시작해보기 (6)

오늘은 코틀린의 함수에 대해서 살펴보도록하자.

함수

함수 선언 및 사용

함수 선언은 자바와는 달리 fun을 사용해서 함수를 선언한다.

fun function(x: Int): Int {
    return x * 2
}

위와 같은 함수가 있을 경우 사용법은 아래와 같다.

val result = function(2)
println(result)

쓰는 법은 일반 다른 언어와 비슷한 문법을 가지고 있다.
또한 위의 함수는 expression 으로도 나타낼 수 있다.

fun function(x: Int) = x * 2

expression으로 쓸 경우에는 return 타입을 명시해주지 않아도 되며 return 키워드는 사용하지 않아야 한다.

Infix notation

infix 키워드를 사용해서 중위(Infix) 표현으로 사용할 수 도 있다.

fun Int.multiply(x: Int): Int {
    return this * x
}

위의 코드는 Int를 확장한 확장 함수이다. 간단하게 생각하면 Int 클래스의 함수를 더 추가 한다고 생각하면 되겠다.

val multiply = 3.multiply(10)

그럼 위와 같이 확장함수를 이용해서 간단하게 만든 multiply 를 사용할 수 있다. 중위 표현으로 변경하면 다음과 같다.

infix fun Int.multiply(x: Int): Int {
    return this * x
}

위와 같이 infix 키워드를 사용하면 중위 표현을 사용할 수 있다.

val multiply = 3 multiply 10

Default Arguments

함수의 파라미터(인자)에 기본값을 정의해 줄 수도 있다.

fun defaultArgumentsFun(x: Int = 100, y: Int = 200) {
    println(x + y)
}

위의 함수의 파라미터에는 x = 100 y = 200 으로 기본값으로 정의해 두었다.

defaultArgumentsFun()

위와 같이 사용할 수 있는데 파라미터가 없이 함수를 사용할 경우에는 기본값인 x에는 100이 y에는 200이 들어간다.

defaultArgumentsFun(100, 200)

위의 두개의 함수 사용은 같다고 볼 수 있다.

Named Parameters

함수의 파라미터는 함수를 호출 할 때 이름을 지정해 줄수 있다. 파라미터의 개수가 많거나 기본 값을 가진 경우에 사용하면 매우 유용하다.

fun namedParametersFun(x: Int = 100, y: Int = 200, z: Int) {
    println(x + y + z)
}

위와 같은 함수가 정의 되어 있을 경우에도 (z는 기본값이 없다.) Named Parameters를 사용할 수 있다. 사용 법은 아래와 같다.

namedParametersFun(x = 200, z = 100)

함수를 호출할 때 해당하는 이름을 지정해주면 된다. 당연히 z의 경우에는 기본값이 없기에 무조건 넣어줘야한다.

namedParametersFun(x = 200)

위와 같이 호출 할 경우에는 컴파일 에러가 발생한다.

Unit-returning functions

Unit은 어떠한 값도 리턴 하지 않는 것을 의미한다. 자바의 void와 비슷한 의미를 갖고 있다.
Unit을 리턴할 경우에는 리턴 타입을 명시해주지 않아도 된다.

fun unitReturnFun(x: Int) : Unit{
    println(x)
}

위의 코드는 Unit을 리턴하는 (리턴 값이 없는) 코드이다. 위의 코드는 Unit을 생략해도 된다.

fun unitReturnFun(x: Int) {
    println(x)
}

위와 같이 사용해도 컴파일 에러가 나지 않는다.

Variable number of arguments (Varargs)

코틀린에서도 자바와 같이 가변인자를 지원한다.

public static <T> void varargsParameters(T... ts){
  for(T t: ts){
    System.out.println(t);
  }
}

자바의 경우에는 타입뒤에 ...을 사용해서 가변인자로 만들 수 있다. 코틀린의 경우에는 vararg 키워드를 사용해서 만들수 있다. 위 자바 코드를 코틀린으로 바꾼다면 아래와 같다.

fun <T> varargsParametersFun(vararg ts: T) {
    for (t in ts){
        println(t)
    }
}

Higher-Order Functions and Lambdas

자바와 비슷하다. 코틀린은 고차함수도 사용가능하며 람다 표현식도 가능하다. 자바8을 사용하는 개발자라면 쉽게 다가갈 수 있다.
Higher-Order Functions은 파라미터로 함수를 받거나 함수를 리턴하는 함수이다.

잠깐 언급만 하고 가자. 일급언어의 조건은 다음과 같다.
1. 함수를 변수나 데이터 구조안에 담을 수 있다.
2. 함수를 파라미터로 전달 할 수 있다.
3. 함수를 반환(리턴) 값으로 사용할 수 있다.

위의 3가지를 만족하면 일급언어가 될 수 있다. 자바8도 3가지를 만족해서 일급 언어라고 할 수 있다. 메서드 레퍼런스가 영 맘에 안들어도 일단 만족은 하니…

아래 코드를 보자.

val list = mutableListOf(1, 2, 3, 4, 5)
val doubled = list.map { i -> i * 2 }

위의 코드는 고차 함수의 간단한 예이다. map은 함수를 파라미터로 받고 있다. 실제 코드는 아래와 같다.

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

위에서 봤듯이 람다의 문법은 자바와 비슷하다. 다른 점이 있다면 () 대신 {} 감싸주는 부분이 다르다. 자바와는 조금 다르게 단일 파라미터에 it 이라는 명을 사용해서 조금더 간단하게 만들 수 있다. 위에 코드를 it을 사용해 바꾸어 보면 다음과 같다.

val list = mutableListOf(1, 2, 3, 4, 5)
val doubled = list.map { it * 2 }

-> 도 생략하고 조금더 간단해 졌다.

코틀린의 중요한 문법은 이정도로 된 듯 싶다. 아직 많긴 하지만 나머지는 공식 문서를 보면서 한번씩 해보는 것이 좋겠다.
다음시간에는 마지막으로 기타 여러 사항을 간단간단하게만 살펴보고 끝내자. 그래도 어느정도 공부는 된 듯 싶다.

필자가 한달전 쯤에 spring boot를 코틀린으로 만든 간단한 웹페이지가 있다. spring boot를 쓰는 개발자들은 한번씩 간단하게 살펴봐도 괜찮을 거 같다. 근데 워낙 자바로 만든거나 코틀린으로 만든거나 비슷한거 같다.

spring-boot-kotlin-example

위의 github에서 소스를 확인가능하다.