Java와 코틀린 혼용

·2021년 12월 28일
0
post-thumbnail

어플리케이션을 대부분 Java로 작성했다면 코틀린 라이브러리를 사용할 수 있다. 그리고 코틀린 소스 파일을 프로젝트에 혼용해서 사용할 수 있다. 코틀린엔 Java에서는 사용 불가능한 특별한 기능들을 가지고 있기 때문에 코틀린 코드를 작성할 때 Java에서 호출해서 사용할 수 있도록 하기 위해서 추가적인 작업을 해야한다.

📌 조인트 컴파일


java와 코틀린을 혼용해서 코드를 사용하는 방법은 두 가지 있다.

  1. Java 또는 코틀린으로 작성된 프로젝트에서 Jar 파일 디펜더시를 통해서 코드를 가져온다.

  2. Java와 코틀린으로 작성된 소스 파일을 프로젝트 내에 나란히 가지고 있는다.

첫 번째 방법이 가장 흔한 방법이다. 현재 Maven이나 JCenter에서 디펜던시를 가져와 Java프로젝트에 Maven이나 Gradle같은 빌드툴을 사용하여 디펜던시를 넣는 것이다.
이와 유사하게 코틀린 프로젝트에 디펜던시를 넣을 수 있다. 코드에 사용되는 JAR 파일은 java 또는 코틀린에서 생성된 것이면 다 가능하다.

두 번째 방법은 레거시 Java 프로젝트에 코틀린을 도입하고 싶거나 어플리케이션의 특정 부분에 코틀린의 힘을 사용하고 싶을 때 사용할 수 있는 방법이다.

두개의 다른 언어로 쓰여딘 코드를 컴파일하기 위해선 각 언어의 컴파일러를 사용해야한다. 하지만 상호의존이 필요한 경우는 문제가 복잡해진다.

🔥 코틀린 코드가 Java 코드를 호출하고, Java 코드가 코틀린 코드를 호출한다고 가정해보자.

✔ Java 소스 파일과 코틀린 소스 파일을 모두 코틀린 컴파일러에 제공해야 한다.
✔ Java 소스 파일을 보면서 코틀린 컴파일러는 주어진 Java 소스 파일에 있는 클래스와 메소드 조각들을 만든다. 그 결과 코틀린 코드에 있는 디펜던시는 충족이 되게 된다.
✔ 코틀린 코드의 바이트코드가 생성되면 Java 컴파일러를 실행시킬 때 Java 코드에 필요한 코틀린 코드 디펜던시를 찾을 수 있게 된다.

📌 코틀린에서 Java 호출하기


java로 작성된 코드를 코틀린 .kt 파일이나 .kts 스크립트 코드에서 호출하는 것은 대부분 자연스럽게 Java와 통합된다. 하지만 때때로 걸림돌이 나타난다.

JavaClass

ublic class JavaClass {
    public int getZero() {
        return 0;
    }

    public List<String> convertToUpper(List<String> names) {
        return names.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());
    }

    public void suspend() {
        System.out.println("suspending...");
    }

    public String when() {
        return "Now!..";
    }
}

JavaClass 클래스는 getter 메소드, convertToUpper() 메소드, suspend() 메소드, when() 메소드를 가지고 있다.

처음 두 메소드는 어떻게 코틀린이 java와 잘 동작하는지를 설명하기 위한 메소드이다. 마지막 두 개의 메소드는 Java 메소드 이름과 코틀린의 키워드가 충돌 될때 코틀린에서 어떻게 처리하는지 설명하기 위해 사용된다.

sample

val javaObject = JavaClass()
println(javaObject.zero) //0

Java 클래스로 클래스의 인스턴스를 만들고 zero 프로퍼티에 접근했다. 코틀린 문법으로 해당 클래스를 자연스럽게 이용했다.

convertToUpper

println(javaObject.convertToUpper(listOf("Jack", "Jill"))) //[JACK, JILL]

convertToUpper() 메소드에 코틀린의 listOf() 메소드를 이용해서 생성한 List<String>의 인스턴스를 전달했다. 코틀린의 콜렉션은 JDK의 콜렉션과 완벽하게 대응하기 때문에 코틀린의 콜렉션 API가 JDK 콜렉션을 사용하는 Java 코드와 상호작용할 때 런타임 오버헤드나 컴파일 시점에 방해가 없다.

suspend

javaObject.suspend() //suspending...

코틀린의 suspend 키워드는 함수를 중단 가능하게 만들기 위해서 사용되지만 컴파일러는 문제없이 실행했다.

❌when()

println(javaObject.when()) //Expecting an expression

when의 익스프레이션이 필요하다며 에러가 발생한다.

Java 코드와 코틀린 사이에서 키워드 충돌이 발생할 때 메소드 또는 프로퍼티 이름에 역따옴표 연산자를 사용해 이스케이프하면 된다.

⭕ when()

println(javaObject.`when`()) //Now!..

📌 Java에서 코틀린 호출하기


코틀린 코드를 Java 코드에서 사용할 목적이라면, 통합을 부드럽게 하기 위해 몇가지 단계를 추가적으로 밟아야 한다.
통합과정이 필요한 프로젝트를 작업 중이라면 일찍 통합하고, 자주 통합해서 코틀린 컴파일러가 바이트코드를 생성하도록 해야한다. 그래야 Java에서 해당 코드들을 사용하는 개발자들에게 도움이 된다.
Java로 작업하는 서드파티 프로그래머들에게 제공할 코틀린 라이브러리를 만드는 중이라면 코틀린 코드로 테스트 코드를 만들고 추가적으로 Java 테스트 코드를 만들어야 한다. 그리고 그 테스트코드를 지속적으로 통합해야 한다. 이런 작업을 하면 코드가 Java에서 사용될 때와 코틀린에서 사용할 때 모두 의도된 대로 동작하는지 검증해준다. 그리고 Java에서 호출된 코드가 잘 통합되고 있는지 역시 검증해준다.

코틀린과 Java의 다른 특징들이있지만 코틀린 언어와 코틀린 스탠다드 라이브러리의 디자이너들은 코틀린과 Java 코드의 통합을 최대한 부드럽게 할 수 있도록 많은 기능을 제공해주었다.

Java에서 오버로드 된 연산자 사용하기

코틀린에서는 간단하게 연산자 오버로딩을 할 수 있다.

Counter

data class Counter(val value: Int) {
    operator fun plus(other: Counter) = Counter(value + other.value)
}

Counter 클래스의 생성자는 value 프로퍼티를 위한 초기값을 Int로 받는다. 클래스는 새로운 Counter 인스턴스를 리턴하는 메소드 하나를 포함하고 있다. 이 메소드는 두 개의 Counter 클래스의 인스턴스를 피연산자로 하여 각 인스턴스의 value를 + 연산자를 이용해서 더한다 그리고 더한 값을 가지는 새로운 Counter 클래스의 인스턴스를 리턴한다.

usecounter.kts

val counter = Counter(1)
println(counter + counter) //Counter(value=2)

+연사자를 이용해서 두 피연산자의 value를 더했다. Java에서는 연산자 오버로딩을 허용하지 않기 때문에 이런 표현은 Java에서는 불가능하다. Java 에서는 + 대신 plus를 이용하면 사용 가능하다.

UseCounter.java

public class UseCounter {
    public static void main(String[] args) {
        Counter counter = new Counter(1);
        System.out.println(counter.plus(counter));//Counter(value=2)
        
        }
   }

main() 메소드 안에서 Counter 클래스를 생성하고 plus() 메소드를 실행시켰다.

코틀린으로. 프로그래밍 할 때 연산자 오버로딩을 사용하기 위해서 Java 코드에 특별한 권한을 얻을 필요가 없다. 코틀린 코드는 연산자를 사용하고, Java 코드는 대응되는 메소드를 사용하게 된다.

static 메소드 생성

코틀린에는 static 메소드가 없다. static 메소드와 가장 가까운 코틀린의 메소드는 우리가 싱글톤과 컴패니언 객체에서 생성한 메소드이다. 싱글톤 객체와 컴패니언 객체를 이용해서 인스턴스 생성 없이 메소드를 호출할 수 있다.

이런 메소드를 Java에서 쉽게 사용하기 위해서는 코틀린 컴파일러에게 이 메소드들을 바이트코드에 static으로 만들라는 지시를 해줘야 한다. JvmStatic 어노테이션을 이용하면 컴파일러에세 지시할 수 있다.

companion

//within the Counter class..

 companion object {
        fun create() = Counter(0)
    }

이 코드를 정확하게 컴파일하기 위해서는 Counter.kt 파일의 package 정의 바로 다음줄에 JvmStatic 클래스를 임포트 해야한다.

@JvmStatic

//within the Counter.kt class..

 companion object {
        @JvmStatic
        fun create() = Counter(0)
    }

UseCounter.java

//within the main method of UseCounter.java class..

 Counter counter0 = Counter.create();
       System.out.println(counter0); //Counter(value=0)

람다 전달하기

java와 코틀린 모두 함수가 객체 뿐만 아니라 다른 함수도 아규먼트로 받을 수 있다. 아규먼트를 받는 쪽은, 람다 표현식을 위한 파라미터가 Java 함수형 인터페이스에 의해서 지원된다. 예를 들면, Runnable, Consumer<T>, Function<T,R>, 하나의 추상 메소드를 사용하는 직접만든 인터페이스가 있다. 코틀린에서 람다 표현식을 받는 함수는 Java와는 다른 문법으로 정의한다. 하지만, 내부적으로 코틀린 역시 람다 표현식을 표현하기 위해서 함수형 인터페이스를 사용한다.

//within the Counter.kt class..

   fun map(mapper: (Counter) -> Counter) = mapper(this)

map 함수의 mapper 파라미터의 타입은 람다 표현식이다. mapper는 Counter 인스턴스를 파라미터로 받고, Counter 인스턴스를 리턴한다. map() 함수는 mapper 파라미터에 의해 참조된 람다 표현식을 실행시키고 현재 객체인 this를 아구먼트로 전달한다. 람다표현식이 리턴하는 인스턴스는 map() 함수에 의해 리턴이 된다.

//within the usecounter.kts class..

println(counter.map { ctr -> ctr + ctr }) //Counter(value=2)

코틀린에서는 { } 를 이용해서 람다 표현식을 함수에 전달할 수 있다. Java 에서는 { } 를 사용하는게 허용되지 않기 때문에 ( ) 안에 작성해야 한다.

//within the main method of UseCounter.java class..

      System.out.println(counter.map(ctr -> ctr.plus(ctr))); //Counter(value=2)

throws 절 추가하기

checked exception과 unchecked exception을 구분하는 Java 컴파일러와 다르게, 코틀린 컴파일러는 exception을 하나로 취급한다. 이에 대해서는 try-catch는 선택사항이다 에서 확인할 수 있다.

이런 코틀린의 유연성은 때때로 Java 코드에서 방해물이 된다. 왜냐하면 Java 컴파일러로 컴파일을 할때 try-catch에서 호출하는 메소드 시그니처에 throws절이 없는 경우 checked exception을 위한 catch 절을 사용할 수 없기 때문이다.

//within the Counter.kt class..
   fun readFIle(path: String) = java.io.File(path).readLines()

readFile() 함수는 java.io.File 클래스를 사용한다. java.io.File 클래스는 주어진 경로가 정확하지 않다면 java.io.FileNotFoundException을 발생시키면서 종료되는 클래스이다.

에러가 발생했을 때를 대비하기 위해서 try-catch로 감싸서 실행해보자.

//within the usecounter.kts class..

try {
  counter.readFIle("blah")
} catch (ex: FileNotFoundException) {
  println("File not found") // File not found

}
//within the main method of UseCounter.java class..

try {
          counter.readFIle("blah");
      } catch (java.io.FileNotFoundException ex) { //exception java.io.FileNotFoundException is never thrown in body of corresponding try statement
          System.out.println("File not found");
      }

Java 코드는 코틀린 코드와 같은 문법을 사용했지만 exception java.io.FileNotFoundException is never thrown in body of corresponding try statement 에러가 발생했다.
readFile() 함수를 Java에서 try 블록안에 넣고 checked exception을 처리하기 위해서 우리는 코틀린 컴파일러에게 해당 적절한 throws절을 생성하라고 말한다. 그렇게 하기 위해서 Throws 어노테이션이 존재한다.

@Throws

//within the Counter.kt class..
@Throws(java.io.FileNotFoundException::class)
   fun readFIle(path: String) = java.io.File(path).readLines()
   
//within the main method of UseCounter.java class..

try {
          counter.readFIle("blah");
      } catch (java.io.FileNotFoundException ex) { 
          System.out.println("File not found");  //File not found
      }

다시 실행해보면 File not found가 출력되는 것을 볼 수 있다.

기본 아규먼트로 함수 사용하기

코틀린의 기본 아규먼트 기능은 우리가 함수 호출을 할 때 몇몇 아규먼트를 생략해도 되게 만들어준다. Java에서 기본 아규먼트로 함수를 호출할 때 이 기능이 어떻게 되는지 알아보자.

Counter.kt

//within the Counter.kt class..
  fun add(n: Int = 1) = Counter(value + n)

usecounter.kts

println(counter.add(3)) //Counter(value=4)
println(counter.add()) //Counter(value=2)

아규먼트 없이 add() 함수를 호출한다면 기본값인 1을 add() 함수로 전달한다.

UseCounter.java

//within the main method of UseCounter.java class..

      System.out.println(counter.add(3));
      System.out.println(counter.add()); //ERROR
//        java: method add in class chapter17.Counter cannot be applied to given types;
//        required: int
//        found:    no arguments
//        reason: actual and formal argument lists differ in length

Java 코드에서는 기본 아규먼트로 실행 할 때 컴파일러가 거부한다.

기본 아규먼트를 사용할 수 있도록 만들고 싶다면, JvmOverloads 어노테이션을 사용하면 코틀린 컴파일러가 필요한 모든 오버로드된 함수를 만든다. 그리고 각 함수는 모든 파라미터를 필요로 하는 함수로 라우팅된다.

@JvmOverloads

//within the Counter.kt class..
   @JvmOverloads
   fun add(n: Int = 1) = Counter(value + n)

변경 후 Java 코드를 컴파일하면 add(3)와 add() 모두 컴파일된다.

println(counter.add(3)) //Counter(value=4)
println(counter.add()) //Counter(value=2)

탑레벨 함수 접근

코틀린에서 함수는 클래스나 싱글톤과 별도로 존재할 수 있다. Java 코드에서 탑레벨 함수응 사용하는 방법을 알아보자.

Counter.kt

//within the Counter.kt class..

package chapter17

//탑 레벨 함수
fun createCounter() = Counter(0)

data class Counter(val value: Int) {
...

탑레벨 함수를 Counter 클래스 바로 위에 만든다.

코틀린에서는 별도의 노력없이 탑레벨 함수를 사용할 수 있다.

usecounter.kts

println(createCounter()) //Counter(value=0)

탑레벨 함수 createCounter()는 Counter.kt 파일 안에 있다. Java에서 탑레벨 함수에 접근하기 위해서는 함수의 이름 앞에 완전한 클래스명이 필요하다.

UseCounter.java

//within the main method of UseCounter.java class..

System.out.println(CounterKt.createCounter());  //Counter(value=0)

생성된 클래스의 이름이 만족스럽지 않다면 JvmName 어노테이션을 이용해서 패키지의 탑레벨 함수를 가지고 있을 클래스의 이름을 바꿀 수 있다.

JvmName

//within the Counter.kt class..
@file:JvmName("CounterTop")

package chapter17

//탑 레벨 함수
fun createCounter() = Counter(0)

pacakge 선언 전에 @fileLJvmName 어노테이션을 사용하여 컴파일러에게 이 파일에 있는 탑레벨 함수는 CounterKt.class 파일이 아닌 CounterTop.class 라는 클래스 파일에 위치해야 한다는 사실을 알려준다.

UseCounter.java

//within the main method of UseCounter.java class..
      System.out.println(CounterTop.createCounter()); //Counter(value=0)

그 외 어노테이션

kotlin.jvm 패키지엔 바이트코드 생성시 자른 특징을 가지게 하는 더 많은 어노테이션이 있다.

Synchronized 어노테이션을 사용하면 코틀린 컴파일러에게 해당 메소드를 동기화 메소드로 만들게 할 수 있다.
JvmDefault 어노테이션을 사용하면 인터페이스의 메소드를 default 메소드로 만들 수 있다.
Volatile 어노테이션을 사용하면 volatile이 되고 Transient 어노테이션을 사용하면 transient으로 만들 수 있다.

이 외에 많은 어노테이션들이 있다. 하지만 해당 어노테이션을 남용하면 안된다. 이 어노테이션들은 정말 필요하고 실제로 사용될 때만 써야한다.





출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글