11장 그 밖의 코틀린 기능

코틀린 쿡북 11장을 요약한 내용 입니다.

코틀린 버전 알아내기

KotlinVersion 클래스 동반 객체의 CURRENT 속성을 사용해 현재 버전을 알수 있다.

fun main(args: Array<String>) {
	println("The current Kotlin version is $(KotlinVersion.CURRENT}")
}

반복적으로 람다 실행하기

특정 구문을 임의의 횟수만큼 반복하고 싶을 경우에는 repeat 함수를 사용하면 된다.

repeat 함수는 코틀린 표준 라이브러리에 들어 있다. repeat 함수는 반복할 횟수를 나타내는 정수와 실행할 (Int) → Unit 형식의 함수, 이 두가지를 인자로 받는 inline 함수다.

@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
	contract { callsInPlcase(action) }
	
	for (index in 0 until times) {
		action(index)
	}
}
fun main(args: Array<String>) {
	repeat(10) {
		println("Counting: $it")
	}
}

완벽한 when 강제하기

코틀린 컴파일러가 when 문에서 가능한 모든 절을 가지도록 강제 하도록 체크하고 싶다면 제네릭 타입에 exhaustive라는 간단한 확장 속성을 추가하고 when block에 이를 연결하면 된다.

fun printMethod(n: Int) {
	when (n % 3) {
		0 -> println("$n % 3 == 0")
		1 -> println("$n % 3 == 1")
		2 -> println("$n % 3 == 2")
	}
}

fun printMethod(n: Int) : Int {
	return when (n % 3) {
		0 -> 0
		1 -> 1
		2 -> 2
	}
}

when 표현식이 값을 리턴하지 않는다면 코틀린은 when 식이 완벽하길 요구하지 않는다. 따라서 위와 같은 경우에도 정상적으로 컴파일 될 것이다.

그러나 모든 케이스를 고려한 when 절을 만들려면 어떻게 해야 할까?

  • 등호를 사용하면 된다.

    등호는 할당이 있다는 의미로, 코틀린은 완벽한 조건식을 요구하게 된다.

    fun printMethod(n: Int) = when ( n % 3) {
    		0 -> println("$n % 3 == 0")
    		1 -> println("$n % 3 == 1")
    		2 -> println("$n % 3 == 2")
    	}
  • exhaustive라는 확장 속성을 사용하면 된다.

    fun printMethod(n: Int) {
    	when ( n % 3) {
    		0 -> println("$n % 3 == 0")
    		1 -> println("$n % 3 == 1")
    		2 -> println("$n % 3 == 2")
    		else -> println("not found")
    	}.exhaustive
    }

정규표현식과 함께 replace 함수 사용하기

replace 함수는 일치하는 모든 문자열 또는 정규표현식에 일치하는 모든 문자여을 제공한 값으로 대체한다. String에 정의된 replace 함수는 대소문자 구분 여부를 선택 인자로 받고 이 인자의 기본값은 대소문자를 무시한다.

아래 테스트는 replace 함수의 차이를 보여준다.

@Test
fun `test`() {
	assertAll(
		{ assertEquals("one*two*", "one.two.".replace(".", "*")) }, // true
		{ assertEquals("********", "one.two.".replace(".".toRegex(), "*")) } // true
	)
}

첫 번째 테스트 케이스는 마침표를 별표로 교체하는 반면, 두 번째 예제는 마침표를 모든 단일 글자를 의미하는 정규표현시그로 취급한다.

자바 개발자가 빠지기 쉬운 함정

  • replace 함수는 첫 번째 항목이 아니라 발생하는 모든 항목을 교체한다.

  • 첫 번째 인자로 문자열은 정규표현식으로 해석하지 않는다.

실행 가능한 클래스 만들기

클래스에서 단일 함수를 간단하게 호출하고 싶을 경우 invoke 연산자 함수를 재정의하라

{
	"people": [
		{ "name" : "jung incheol", "craft": "ISS" },
		{ "name" : "park bokum", "craft": "ISS" },
		{ "name" : "song joonggi", "craft": "ISS" }
	],
	"number": 3,
	"message": "success"
}

JSON 응답을 보면 3명의 우주 비행사는 현재 국제 우주 정거장에 탑승해 있다.

class AstroRequst {
	companion object {
		private const val ASTRO_URL =
			"<http://api.open-notify.org/astros.json>"
	}

//	fun execute(): AstroResult {
	operator fun invoke(): AstroResult {
		val responseString = URL(ASTRO_URL).readText()
		return Gson().fromJson(responseString,
			AstroResult::class.java)
	}
	
	operator fun invoke(int num): AstroResult {
		val responseString = URL(ASTRO_URL).readText()
		return Gson().fromJson(responseString,
			AstroResult::class.java)
	}
	
	operator fun invoke(String str): AstroResult {
		val responseString = URL(ASTRO_URL).readText()
		return Gson().fromJson(responseString,
			AstroResult::class.java)
	}
}
val request = AstroRequest()
val result = request.execute()
println(result.message)

이 접근 방식에는 아무 문제가 없지만 AstroRequest 클래스의 목적은 오직 하나이므로 해당 함수의 이름을 invoke로 변경하고 operator 키워드를 추가해 다음과 같이 실행시킬 수 있다.

internal class AstroRequestTest {
	val request = AstroRequest()

	@Test
	internal fun `get people in space`() {
		val result = request(2)
		asssertThat(result.message, `is`("success"))
	}
}

이처럼 invoke 연자라 함수를 제공하고 클래스 레퍼런스에 괄호를 추가하면 클래스 인스턴스를 바로 실행할 수 있다. 원한다면 필요한 인자를 추가한 invoke 함수 중복도 추가할 수 있다.

경과 시간 측정하기

코드 블록이 실행되는 데 걸린 시간을 알고 싶을 경우 measureTimeMillis 또는 mearueNanoTime 함수를 사용한다.

fun doubleIt(x: Int): Int {
	Thread.sleep(100L)
	println("doubling $x with on thread %{Thread.currentThread().name}")
	return x * 2
}

fun main() {
	println("${Runtime.getRuntime().availableProcessors()} processors")
	var time = measureTimeMillis {
		IntStream.rangeClosed(1, 6)
			.map { doubleIt(it) }
			.sum()
	}
}

자바에서는 경과 시간을 구할 때 시작 시간과 종료시간의 차를 별도로 구해주어야 했다.

@Around("execution(* *.*.*(..))")
	public Object callMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = System.currentTimeMillis();
		Object retVal = joinPoint.proceed();
		long elapsed = System.currentTimeMillis() - start;

		Signature sig = joinPoint.getSignature();

		logger.info("info : "+sig.getDeclaringTypeName() + "#" + sig.getName() + "\\t" + elapsed + " ms");
		
		return retVal;
	}

스레드 시작하기

코드 블록을 동시적 스레드에서 실행시키고 싶을 경우에는 kotlin.concurrent 패키지의 thread 함수를 사용해라

thread 확장함수의 시그니처는 다음과 같다.

fun thread(
	start: Boolean = true,
	isDaemon: Boolean = false,
	contextClassLoader: ClassLoader? = null, 
	name: String? = null,
	priority: Int = -1,
	block: () -> Unit
): Thread

start 인자의 기본값이 true이므로 다음 예제처럼 다수의 스레드를 쉽게 생성하고 시작할 수 있다.

(0..5).forEach { n ->
	val sleepTime = Random.nextLong(range = 0..1000L)
	thread {
		Thread.sleep(sleepTime)
		println("${Thread.currentThread().name} for $n after ${sleepTime}ms")
	}
}

이 코드는 6개의 스레드를 시작하고, 각 스레드는 0부터 1000 사이에서 무작위로 결정된 밀리초 동안 sleep한 다음 해당 스레드의 이름을 출력한다.

thread 함수의 start 파라미터 기본값이 true 이므로 각 스레드마다 start를 호출할 필요가 없다.

TODO로 완성 강제하기

개발자가 특정 함수나 테스트를 강제로 구현할 수 있도록 TODO 함수를 사용해라

개발자는 종종 어떤 시점에 구현을 완료할 준비가 되지 않은 함수를 완성하기 위해 그들 스스로 메모를 남겨 놓는다. 대부분의 언어에서는 다음 예제처럼 TODO 문을 주석에 추가한다.

fun myCleverFunction() {
	// TODO: 멋진 구현을 찾는 중
}

코틀린 표준 라이브러리에는 TODO라는 함수가 있는데 이 함수는 다음과 같이 구현되어 있다.

public inline fun TODO(reason: String): Nothing = 
	throw NotImplementedError("An operation is not implemented: $reason")

효율성을 이유로 소스는 인라인되어 있고 함수가 호출될 때 NotImplementedError를 던진다.

fun main() {
	TODO(reason = "none, really")
}

함수 이름에 특수 문자 사용하기

함수 이름을 읽기 쉽게 작성하고 싶을 경우엔 함수 이름을 백틱으로 감싸 읽기 쉽게 만들 수 있다. 하지만 이 기법은 테스트에서만 사용하자

fun `인철 짱`() {
	println("인철 짱")
}

fun main() {
	`인철 짱`()
}

위 처럼 사용하게 되면 함수 이름에 띄어쓰기도 사용할 수 있고 테스트 메소드의 경우에는 어떤 테스트의 메소드를 실행하였는지 더 명확하게 전달되어서 가독성을 높여준다.

자바에게 예외 알리기

코틀린 함수가 자바에서 체크 예외로 예외를 던지는 경우에는 @Throws 애노테이션을 추가하면 알릴 수 있다.

코틀린의 모든 예외는 언체크 예외다. 즉 컴파일러는 개발자에게 해당 예외를 처리할것을 요구하지 않는다. 예외를 붙잡기 위해 코틀린 함수에 try/catch/finally 블록을 추가하는 방법은 아주 쉽지만 강제사항은 아니다.

fun housonWeHaveAProblem() {
	throw IOException("File or resource not found")
}

public static void doNothing() {
	housonWeHaveAProblem()
}

그래서 자바 사용자는 아래와 같이 문제를 해결하려고 시도할 것이다.

// solution 1 : try-catch로 잡기
public static void useTryCatchBlock() {
	try {
		housonWeHaveAProblem();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

// solution 2 : throws 하여 예외처리 위임하기
public static void useThrowsCluause() throws IOException {
	housonWeHaveAProblem();
}

그러나 둘 중 어느 방식도 원하는 대로 동작하지 않는다. 명시적으로 try/catch 블록 추가를 시도하면 자바는 catch 블록 안에 명시된 IOException이 연관된 try 블록 내에서 해당 예외를 절대 던지지 않는다고 생각하기 때문에 코드가 컴파일되지 않는다. 두 번째 throws 절을 추가하는 경우에는 코드가 컴파일되지만 IDE는 '불필요한' 코드가 있다고 경고할 것이다.

두 가지 해결방법을 동작하게 만드는 방법은 다음과 같이 @Throws 애노테이션을 코틀린 코드에 추가하는 것이다.

@Throws(IOException::class)
fun housonWeHaveAProblem() {
	throw IOException("File or resource not found")
} 

이제 자바 컴파일러는 IOEception을 대비해야 한다는 것을 안다. @Throws 애노테이션은 그저 자바/코틀린 통합을 위해서 존재한다.

Last updated