자바에 코틀린 도입

짱구·2023년 3월 26일
0

kotlin

목록 보기
7/8

자바에서 코틀린 Getter, Setter 호출

자바에서 코틀린 프로퍼티를 호출할때 Getter, Setter를 사용합니다.

  • Kotlin Student Class
class Student {
    var name: String? = null
    var birthDate: LocalDate? = null
}
  • Java에서 Kotlin Class의 Getter, Setter 사용
public class JavaGetterSetterExample {
    public static void main(String[] args) {
        Student student = new Student();
        student.setName("홍길동");
        student.setBirthDate(LocalDate.of(1918, 7, 4));
        
        System.out.println(student.getName()); //홍길동
        System.out.println(student.getBirthDate()); //1918-07-04
    }
}

val로 선언한 프로퍼티는 불변이기 때문에 Setter가 존재할 수 없고 Getter만 존재할 수 있습니다.

class Student {
    var name: String? = null
    var birthDate: LocalDate? = null
    val age: Int = 10
}

public static void main(String[] args) {
    student.setAge(10); // 컴파일 에러
    System.out.println(student.getAge());
}

Setter를 private 가시성으로 변경하면 var로 선언해도 Setter를 없앨 수 있습니다.

class Student {
  var name: String? = null
  var birthDate: LocalDate? = null
  val age: Int = 10
  
  var grade: String? = null
  	  private set
}

private set으로 세터를 제거하면 내부에서만 변경하거나 외부 접근 함수를 별도로 만들어야합니다.

class Student {
    var grade: String? = null
    private set
    
    fun changeGrade(grade:String) {
        this.grade = grade
    }
}

@JvmField

코틀린의 프로퍼티는 자바로 변환시 필드는 private이고 Getter, Setter를 통해서만 접근이 가능합니다.
@JvmField를 사용하면 Getter, Setter를 쓰지 않고 자바 프로퍼티로 사용할 수 있습니다.

class Student {
    @JvmField
    var name: String? = null
}

public static void main(String[] args) {
    Student student = new Student();
    student.setName("홍길동"); // 컴파일 에러
    System.out.println(student.getName()); // 컴파일 에러
    
    student.name = "홍길동"; // 정상 실행
    System.out.println(student.name); // 정상 실행, '홍길동' 출력
}

자바에서 확장함수 호출

  • Kotlin의 확장 함수
fun String.first(): Char {
    return this[0]
}

fun String.addFirst(char: Char): String {
    return char + this.substring(0)
}

fun main() {
    println("ABCD".first()) // 출력 : A
    println("ABCD".addFirst('Z')) // 출력 : ZABCD
}
  • 자바에서 코틀린과 같이 사용하면 컴파일 에러가 발생
public class ExtensionExample {
    public static void main(String[] args) {
        "ABCD".first(); // 컴파일 에러
    }
}
  • 왜 안되는지 이유를 알기위해 자바로 컴파일된 코드를 보면 아래와 같이 변환되어 있습니다.
public final class MyExtensionsKt {
    public static final char first(@NotNull String $this$first) {
        Intrinsics.checkNotNullParameter($this$first, "$this$first");
        return $this$first.charAt(0);
    }

    @NotNull
    public static final String addFirst(@NotNull String $this$addFirst, char var1) {
        Intrinsics.checkNotNullParameter($this$addFirst, "$this$addFirst");
        byte var4 = 0;
        String var5 = $this$addFirst.substring(var4);
        Intrinsics.checkNotNullExpressionValue(var5, "this as java.lang.String).substring(startIndex)");
        return var1 + var5;
    }
}

즉 자바에서 사용할땐 파일명(클래스명).메서드명 형태로 사용해야합니다.
ex) MyExtensionsKt.first(”ABCD”);

public static void main(String[] args) {
    //"ABCD".first();와 동일!!
    MyExtensionsKt.first("ABCD");
    
    //"ABCD".addFirst();와 동일!!
    MyExtensionsKt.addFirst("ABCD", 'Z');
}

코틀린과 롬복

코틀린에서 롬복 사용시 발생하는 문제

롬복은 자바 진영에서 가장 많이 사용되는 라이브러리 중 하나로 애너테이션을 통해 생성자, 게터, 세터, 빌더 등을 자동으로 생성해줍니다.

@EqualsAndHashCode
@ToString
public class Hero {
    @Getter
    @Setter
    private String name;
    
    private int age = 53;
    
    private String address;
    
    public String getAddress() {
        return address;
    }
    
    public void setAddress(String address) {
        this.address = address;
    }
    
    public static void main(String[] args) {
        Hero hero = new Hero();
        hero.setName("아이언맨");
        hero.setAddress("스타크타워");
    // 자바에선이와 같은 방식으로 사용!
    }
}

그러나 코틀린과 자바를 상호 운용하는 경우 롬복을 사용하면 정상 동작하지 않습니다.

fun main() {
    val hero = Hero()
    hero.name = "아이언맨"
    println(hero.name)
}
// 실행시 컴파일 에러 발생
  • 직접 만든 getter, setter를 사용한 address 프로퍼티는 정상 동작합니다.
fun main() {
    val hero = Hero()
    hero.address = "스타크타워"
    println(hero.address)
    // 스타크타워
}

코틀린과 자바 통합 프로젝트에서 컴파일되는 과정

    1. 가장 먼저 코틀린 컴파일러가 코틀린 코드와 코틀린 코드에서 참조하는 자바 코드를 컴파일해서 바이트코드를 생성합니다.
    1. 그 다음 자바 컴파일러가 자바 코드를 컴파일하는데 이때 애노테이션 프로세싱 단계가 동작합니다.
    1. 애노테이션 프로세서(Annotation Processor) 는 컴파일 타임에 애노테이션을 읽어서 동적으로 코드를 생성하거나 변경하는 등의 기능을 말합니다.
    1. 이러한 컴파일 순서 이슈로 인해 기본적으론 롬복을 사용하지 못합니다.

일반적인 해결 방법

1. IDE의 자동 생성 기능 사용

롬복을 제거하고 IDE의 생성 기능을 이용합니다.

public class Hero {
    private String name;
    private int age;
    private String address;
    public String getAddress() {
        Chapter 05.자바 프로젝트에 코틀린 도입해보기 - 05.코틀린과 롬복 4
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Hero hero = (Hero) o;
        return age == hero.age && Objects.equals(name, hero.name) && Objects.equals(address, hero.address);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age, address);
    }
    @Override
    public String toString() {
        return "Hero{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

2. 데이터 클래스로 마이그레이션

롬복이 적용된 클래스는 대부분 데이터 관련 클래스인 경우가 많습니다.
코틀린의 데이터 클래스로 마이그레이션하는 것을 추천합니다.
자바 프로젝트를 점진적으로 코틀린으로 전환하기 위해 이 방법이 가장 좋다고 생각합니다.
이 외에도 코틀린에서 제공하는 롬복 플러그인을 사용할 수 있습니다.

data class HeroKt(
        val name: String,
        val age: Int,
        val address: String,
        )
    
fun main() {
        val heroKt = HeroKt(name = "아이언맨", age = 50, address = "스타크타워")
        println(heroKt)
}

Spring Plugin

  • 코틀린의 클래스는 기본적으로 상속이 불가능한 final 클래스입니다.
  • 상속을 열어뒀을때 발생하는 부작용으로 인해 코틀린은 상속이 꼭 필요한 경우에만 적용하
    도록 open 키워드를 사용해 상속을 허용하도록 지원합니다.
  • 문제는 스프링은 기본적으로 CGLIB 프록시를 사용해 애노테이션이 붙은 클래스에 대한 프록
    시 객체를 생성하는데 CGLIB 프록시는 대상 객체를 상속하여 프록시 객체를 생성합니다.
  • 코틀린의 클래스는 기본적으로 final이기 때문에 상속이 불가능해 프록시 객체를 생성할 수
    없습니다.

아래 코드는 컴파일 에러가 발생하므로 open 키워드를 붙여야합니다.

@SpringBootApplication
// class KotlinJavaSpringApplication // 컴파일 에러
open class KotlinJavaSpringApplication

fun main(args: Array<String>) {
	runApplication<KotlinJavaSpringApplication>()
}

매번 open 키워드를 붙이는 건 불편하므로 코틀린은 All-open 컴파일러 플러그인을 제공합니다.

  • build.gradle.kts에 all-open plugin 추가
plugins {
    id ("org.springframework.boot") version "2.7.0"
    id ("io.spring.dependency-management") version "1.0.11.RELEASE"
    id ("org.jetbrains.kotlin.jvm") version "1.6.21"
    id ("org.jetbrains.kotlin.plugin.allopen") version "1.6.21" // 추가
}

// 추가
allOpen {
	annotations("org.springframework.boot.autoconfigure.SpringBootApplication")
}

@Transactional

@Transactional도 open을 붙이지 않으면 컴파일 에러가 발생하며 플러그인에 추가해야합니다.

allOpen {
    annotations(
        "org.springframework.boot.autoconfigure.SpringBootApplication",
        "org.springframework.transaction.annotation.Transactional"
    )
}

이처럼 매번 문제가 생길때마다 allopen에 추가하기 번거롭고 어려우므로 allopen 플러그인을 래핑한 kotlin-spring 플러그인을 사용하면 매우 간편해집니다.

plugins {
    id("org.springframework.boot") version "2.7.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    id("org.jetbrains.kotlin.jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
}

kotlin-spring 플러그인은 스프링에서 CGLIB 프록시를 사용하는 모든 애노테이션에 대해 자동으로 open 처리를 해줍니다.

  • @Component, @Transactional, @Configuration, @Controller, @Service, @Repository 등 open 처리

JPA Plugin

  • JPA에서 엔티티 클래스를 생성하려면 매개 변수가 없는 기본 생성자가 필요합니다.
    • ex) NoArgumentConstructor

실제로 예시와 같이 엔티티를 작성하면 컴파일 에러가 발생합니다.

@Entity
@Table
class Person(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long?,
    
    @Column
    var name: String,
    
    @Column
    var age: Int,
)

코틀린은 매개 변수가 없는 기본 생성자를 자동으로 만들어주는 no-arg 컴파일러 플러그인을 제공합니다.

plugins {
    id("org.springframework.boot") version "2.7.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    id("org.jetbrains.kotlin.jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.noarg") version "1.6.21"
}

noArg {
    annotation("javax.persistence.Entity")
}

또한 JPA를 쓸 경우 Spring Plugin과 마찬가지로 kotlin-jpa 플러그인을 제공합니다.
JPA 플러그인을 쓰면 @Entity, @Embeddable, @MappedSuperclass에 대한 기본 생성자를 자동으로 생성해줍니다.

plugins {
    id("org.springframework.boot") version "2.7.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    id("org.jetbrains.kotlin.jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.jpa") version "1.6.21"
}

출처 : fastcampus

profile
코드를 거의 아트의 경지로 끌어올려서 내가 코드고 코드가 나인 물아일체의 경지

0개의 댓글