Spring 5.2 와 Spring boot 2.2 추가된 Test 기능들

오늘 알아볼 내용은 Spring 과 Spring boot의 새로운 Test 기능들을 알아볼 예정이다. 아주 많은 기능을 이야기 할건 아니지만 주로 많이 사용될 만한 것들을 살펴볼 예정이다.

일단 Spring 5.2에 추가된 2가지 내용을 살펴보도록 하자.

생성자 @Autowired

Spring5 부터는 junit5를 적극적으로 지원하기 시작했다. junit5를 사용하기전에는 다음과 같은 코드를 작성해야 했었다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTests {

    @Autowired
    private AccountRepository accountRepository;

    @Test
    public void findAllTest() {

        assertThat(accountRepository.findAll())
                .isEqualTo(Collections.singletonList(new Account(1L, "wonwoo", 22)));
    }

}

위의 코드는 junit4로 작성된 코드이다. junit4에선 디폴트 생성자가 있어야 했다. 그래서 위와 같이 @Autowired을 필드에 주입했어야 했지만 junit5부턴 그럴필요 없다.

junit5에선 생성자를 통해 주입받을 수있게 되었다.

@DataJpaTest
class UserRepositoryTests {

    private final AccountRepository accountRepository;

    @Autowired
    private UserRepositoryTests(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }


    @Test
    void findAllTest() {

        // (대충 테스트한다는 내용)
    }

}

여기까지는 Spring 5.0에서도 가능했던 기능이지만 이제는 생성자에도 @Autowired 애노테이션을 제거할 수도 있게 되었다. 하지만 바로 되는건 아니고 두가지 설정 방법이 존재한다.

첫 번째로는 애노테이션을 사용하는 방법이 존재한다.

@DataJpaTest
@TestConstructor(autowireMode = AutowireMode.ALL)
class UserRepositoryTests {

    private final AccountRepository accountRepository;

    private UserRepositoryTests(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Test
    void findAllTest() {

       // (대충 테스트한다는 내용)

    }

}

AutowireMode.ALL 타입말고 AutowireMode.ANNOTATED 타입이 더 존재하는데 이건 그냥 안쓰는 것과 동일한 기능으로 보인다. 실제 어노테이션을 사용한다는 의미의 타입으로 보여진다.

다음은 두번째 방법인데 이건 전역으로 설정할 수 있다. 아마도 이 기능 때문에 AutowireMode.ANNOTATED 타입이 존재하는듯 싶다. 전역으로 설정한 값보다 개별로한 설정이 나중에 동작해 전역 설정을 덮어 쓰게 되어있다.

전역 설정은 아주 간단하다. spring.properties 파일을 클래스패스에 만들어서 spring.test.constructor.autowire.mode=ALL 이와 같이 작성해주면 전역설정이 된다.

spring.test.constructor.autowire.mode=ALL

설정 후에 아래와 같이 @Autowired 와 @TestConstructor 애노테이션이 없어도 테스트가 가능해진다.

@DataJpaTest
class UserRepositoryTests {

    private final AccountRepository accountRepository;

    private UserRepositoryTests(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Test
    void findAllTest() {

       // (대충 테스트한다는 내용)

    }

}

그럼 위와 같이 작성해도 테스트를 진행할 수 있다. 위에서 말했다시피 전역설정후 개별 테스트에서는 전역 설정을 사용하고 싶지 않다면 아래와 같이 다시 작성해주면 된다.

@DataJpaTest
@TestConstructor(autowireMode = ANNOTATED)
class UserRepositoryTests {

    private final AccountRepository accountRepository;

    private UserRepositoryTests(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Test
    void findAllTest() {

        // (대충 테스트한다는 내용)
    }

}

그럼 위의 코드는 테스트를 진행 할 수 없다. 만약 테스트를 진행하고 싶다면 @Autowired을 작성해줘야 한다.

kotlin dsl web test

spring 5.2 부터는 webmvc에도 dsl을 지원한다. 기존에는 (5.0에는) webflux만 지원했지만 이제는 우리가 흔히 사용하는 webmvc도 지원하게 되었다.
이렇게 webmvc를 지원하면서 webmvctest도 dsl을 지원하게 되었다.

사용법은 아주 간단하다.

위와 같이 MockMvc 에 확장함수들이 추가 되었고 MockHttpServletRequestDsl, ResultActionsDsl, MockMvcResultMatchersDsl, MockMvcResultHandlersDsl 등 몇가지 dsl 클래스들이 새로 추가가 되었다.

POST 테스트 또한 비슷한 맥략이다.

이제는 Spring boot와 관련된 기능들이다. 소소한 것들이니 참고만 하면 되겠다.

junit5

spring boot 2.2부터는 기본적으로 junit5를 디펜더시 받고 있다. 뿐만 아니라 junit vintage engine 도 추가적으로 디펜더시를 받고 있어 만약 junit5가 익숙하지 않는 분들은 그냥 junit4를 이용해도 무방하다.

import org.junit.Test;

public class Junit4Tests {

    @Test
    public void test() {

    }
}

import org.junit.jupiter.api.Test;

class Junit5Tests {

    @Test
    void test() {

    }
}

원하는 버전 아무거나 사용해도 무방하다.

ApplicationArguments

사용할일이 있을 수 있겠지만 아직은 무엇에 어떻게 사용할지 생각이 나지는 않는다. 이런기능도 있으니 만약 사용할일이 있다면 사용해도 좋다.

@SpringBootTest를 사용해 테스트를 한다면 @SpringBootTest의 args 속성을 넣어 아규먼트를 꺼내 사용할 수 있다.

@SpringBootTest(args = "--foo=bar")
class TestArgs {

    private final ApplicationArguments applicationArguments;

    @Autowired
    private TestArgs(ApplicationArguments applicationArguments) {
        this.applicationArguments = applicationArguments;
    }

    @Test
    void test() {
        List<String> optionValues = applicationArguments.getOptionValues("foo");

        //대충 argument를 사용한다는 내용
    }

}

위와 같이 ApplicationArguments 인터페이스를 주입받은 후에 원하는 테스트에 arguments를 꺼내 사용할 수 있다. ApplicationArguments API는 문서를 통해 확인 할 수 있다.

OutputCaptureExtension

마지막으로 OutputCapture와 관련된 내용이다. 예전 junit4에서 사용했던 OutputCaptureRule과 동일한 기능이지만 이것은 junit5 위한 Extension 기능이다.
간단하게 OutputCapture 의 기능을 설명하자면 log나 print를 찍은, 즉 console에 찍히는 내용을 캡쳐해서 그 내용을 검증하는 기능이다.
예전에 junit4의 경우엔 다음과 같이 작성하였다.

public class OutputCaptureRuleTests {

    @Rule
    public final OutputCaptureRule output = new OutputCaptureRule();

    @Test
    public void test() {
        System.out.println("hello world");
        assertThat(output).contains("hello world");
    }

}

콘솔에 hello world라는 내용을 찍고 그것을 검증하는 내용이다. @Rule 이라는 애노테이션은 junit4에만 존재하기에 junit5에선 사용할 수 없다. 그래서 spring boot 2.2부터 OutputCaptureExtension 클래스가 추가 되었다. 사용법은 아주 간단하다. 위의 junit4와 비슷하다.

@ExtendWith(OutputCaptureExtension.class)
class OutputCaptureExtensionTests {

    @Test
    void outputTest(CapturedOutput capturedOutput) {

        System.out.println("hello world");
        assertThat(capturedOutput).contains("hello world");

    }
}

위와 같이 작성해도 되며 다른 곳에도 사용한다면 생성자에 주입받아 사용해도 된다.

@ExtendWith(OutputCaptureExtension.class)
class OutputCaptureExtensionTests {

    private final CapturedOutput capturedOutput;

    OutputCaptureExtensionTests(CapturedOutput capturedOutput) {
        this.capturedOutput = capturedOutput;
    }

    // ...
}

오늘은 이렇게 test와 관련된 내용을 작성해봤다. 몇가지는 필자도 자주 사용할 것 같지만 ApplicationArgumentsOutputCaptureExtension 은 그리 많이 사용할 것 같진 않다. 예전에도 OutputCaptureRule 은 거의 사용하진 않았으니 말이다.

만약 webmvc dsl test나 webmvc dsl 에 관심이 있다면 은 여기에 소스가 있으니 참고하면 되겠다.

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년은 거뜬하지 않을까 싶다. 하지만 시대가 어떻게 바뀔지 모르기에 다른언어 한개정도는 완벽하게 하지는 못해도 알아두면 좋을 것 같다.

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