Kotlin의 가장 큰 장점 중 하나는 약점일 수도 있습니다. 오늘날 우리가 데이터 클래스에 대해 이야기할 때 Java 레코드와 마찬가지로 우리는 상용구 코드를 완전히 제거하는 데 초점을 맞추는 경향이 있습니다. 적어도 제가 아는 많은 사람들은 그렇습니다. 보일러플레이트 코드는 오랫동안 개발자들을 짜증나게 만들어 왔습니다. Java에만 존재하는 것이 아니라 다른 언어에도 존재합니다. 어떤 방식으로든 데이터 액세스 객체를 생성하려고 한다면 Kotlin이나 Java 또는 다른 언어로 수행하는지 여부는 실제로 중요하지 않습니다.
저장소 생성, 서비스 및 컨트롤러라는 동일한 프로세스를 자주 반복합니다. 우리가 하는 일 중 상당수는 여전히 상용구입니다. 그러나 상용구 코드는 데이터 클래스 또는 Java 레코드를 생성하기로 한 결정의 일부일 수 있으며, 소프트웨어 엔지니어링의 더 중요한 패러다임은 이러한 유형의 데이터 구조를 생성하는 실제 초점이었습니다.
이 모든 것은 불변성 또는 어떤 경우든 불변성의 가능성으로 귀결됩니다. 시간을 되돌려 우리가 다른 것을 지정하는 데이터 구조를 만들기 시작한 1995년으로 돌아가 보겠습니다. 우리는 인수를 메서드, 함수 또는 생성자에 전달할 수 있는 방식으로 필드, getter, setter, 속성, 접근자, 속성 및 매개 변수를 정의합니다. 이 부분에서 우리는 80년대 중반 시리즈의 캐릭터인 The Golden Girls를 사용한 재미있는 프로그래밍입니다. 이렇게 말한 후, 약 15년 동안 수업이 어떤 모습일지 살펴보겠습니다.
public class GoldenGirlsJava { public String goldenGirl1; private final String goldenGirl2; private final String goldenGirl3; private final String goldenGirl4; public GoldenGirlsJava() { this.goldenGirl1 = "Dorothy Zbornak"; this.goldenGirl2 = "Rose Nylund"; this.goldenGirl3 = "Blanche Devereaux"; this.goldenGirl4 = "Sophia Petrillo"; } public GoldenGirlsJava( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { this.goldenGirl1 = goldenGirl1; this.goldenGirl2 = goldenGirl2; this.goldenGirl3 = goldenGirl3; this.goldenGirl4 = goldenGirl4; } public String getGoldenGirl1() { return goldenGirl1; } public void setGoldenGirl1(String goldenGirl1) { this.goldenGirl1 = goldenGirl1; } public String getGoldenGirl2() { return goldenGirl2; } public String getGoldenGirl3() { return goldenGirl3; } public String getGoldenGirl4() { return goldenGirl4; } @Override public String toString() { return "GoldenGirlsJava{" + "goldenGirl1='" + goldenGirl1 + '\'' + ", goldenGirl2='" + goldenGirl2 + '\'' + ", goldenGirl3='" + goldenGirl3 + '\'' + ", goldenGirl4='" + goldenGirl4 + '\'' + '}'; } }
이는 두 개의 생성자를 생성하기 위해 입력하기에는 엄청난 양의 코드였습니다. 해시코드와 등호가 모든 경우에 반드시 필요한 것은 아니지만 거의 항상 보다 일반적인 생성자 옆에 인수 없는 생성자를 구현해야 했습니다. 이 상용구 코드를 모두 만들 때 좋은 점은 바이트코드로 컴파일한 후 모든 것이 어떻게 보일지 매우 명확하게 알 수 있다는 것입니다.
그러나 어떤 경우에도 상용구 코드는 항상 많은 개발자에게 논쟁의 여지가 있는 문제였기 때문에 2009년에 Lombok이 등장하여 데이터 구조를 생성하는 방식에 혁명을 일으켰습니다. 주석 프로세서를 사용하고 클래스에 필요한 품질을 제공하는 특정 주석을 해석하는 개념을 도입했으므로 주석이 달린 롬복 클래스는 다음과 같습니다.
@Getter @Setter @AllArgsConstructor @ToString public class GoldenGirlsLombok { public String goldenGirl1; private final String goldenGirl2; private final String goldenGirl3; private final String goldenGirl4; public GoldenGirlsLombok() { this.goldenGirl1 = "Dorothy Zbornak"; this.goldenGirl2 = "Rose Nylund"; this.goldenGirl3 = "Blanche Devereaux"; this.goldenGirl4 = "Sophia Petrillo"; } }
그리고 한동안 사람들은 롬복에 대해 매우 기뻐했습니다! 마침내 모든 상용구 코드를 작성해야 하는 부담이 사라졌습니다. 그러나 이러한 하락세는 약 7년 동안 지속되었고, 새로운 플레이어가 등장했는데 이번에는 소프트웨어 개발 업계에서는 예상하지 못한 일이었습니다. Kotlin이라고 불리며 2016년에 데이터 클래스가 즉시 도입되면서 데뷔했습니다. Kotlin의 황금 소녀 구현은 이제 다음과 같습니다.
data class GoldenGirls( var goldenGirl1: String = "Dorothy Zbornak", private val goldenGirl2: String = "Rose Nylund", private val goldenGirl3: String = "Blanche Devereaux", private val goldenGirl4: String = "Sophia Petrillo" )
Kotlin이 서서히 팬을 모으기 시작했지만 반면에 Java는 다른 것이 시장에 있다는 것을 깨달았고 해당 측면의 일부 개발은 제작 중이었지만 잠시 보류되었던 프로젝트 Loom과 같이 약간의 추진력을 얻기 시작했습니다. 그렇기 때문에 20202년 Java 14가 출시되면서 Java에 Java 레코드가 도입되었으며 이제 Java의 데이터 구조는 다음과 같습니다.
public record GoldenGirlsRecord( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { public GoldenGirlsRecord() { this( "Dorothy Zbornak", "Rose Nylund", "Blanche Devereaux", "Sophia Petrillo" ); } }
그리고 오늘날까지도 코드 단순화와 코드 축소는 계속될 것으로 보입니다. 그러나 상용구 코드가 줄어들면서 필드, getter, setter, 속성, 접근자, 속성 및 매개 변수의 개념이 훨씬 덜 시각적이고 우리 마음 속에 매핑하기가 더 어려워졌습니다. 우리가 좋아하든 원하지 않든 이러한 개념은 여전히 JVM이 작동하고 바이트 코드로 코드를 구성하는 방식입니다. 그러나 코드의 과도한 단순화는 실제로 코드를 더 쉽게 읽을 수 있도록 만드는 것이며 데이터 클래스 및 Java 레코드의 경우 아이디어는 데이터 구조를 생성하는 것이기도 합니다. 불변이거나 부분적으로 불변입니다.
Java 레코드는 포함된 모든 값이나 참조하는 값을 수정할 수 없다는 점에서 실제로 불변입니다. Kotlin 데이터 클래스도 같은 이유로 실제로 불변일 수 있지만 어쨌든 개발자가 복잡하고 최악의 변경 가능한 코드를 생성할 수 있는 권한을 부여할 필요는 없습니다. 어쨌든 우리가 필요할 때까지 이것은 모두 좋습니다. 주석에 크게 의존하는 Spring Framework와 같은 프레임워크로 작업합니다. 예는 다음과 같습니다.
@Entity @Table(name = "shelf_case") data class Case( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) val id: Long?, var designation: String?, var weight: Long? ) { constructor() : this(0L, null, null) override fun toString(): String { return super.toString() } }
이 예제는 잘 작동합니다. 의심하지 않는 눈에는 여기에 특별한 일이 많이 일어나지 않습니다. 이는 Java에서 작동하는 것과 동일한 방식으로 Kotlin에서 작동합니다. 하지만 이제 비슷한 예를 살펴보겠습니다. 이번에는 Jackson 주석을 사용합니다.
data class AccountNumbersPassiveDto( @NotNull val accountNumberLong: Long?, val accountNumberNullable: Long?, @DecimalMax(value = "10") @DecimalMin(value = "5") val accountNumber: BigDecimal, val accountNumberEven: Int, val accountNumberOdd: Int, @Positive val accountNumberPositive: Int, @Negative val accountNumberNegative: Int, @DecimalMax(value = "5", groups = [LowProfile::class]) @DecimalMax(value = "10", groups = [MiddleProfile::class]) @DecimalMax(value = "15", groups = [HighProfile::class]) @DecimalMax(value = "20") val accountNumberMaxList:Int )
이 예제는 실패하며 그 이유는 Kotlin에서는 주석을 적용할 정확한 위치를 컴파일러에 알릴 방법이 없기 때문입니다. jakarta 지속성 주석인 @Entity
주석이 있는 이전 예에서는 주석이 필드에 올바르게 적용되었습니다. 해당 코드를 디컴파일하면 다음을 찾을 수 있습니다.
public final class Case { @Id @GeneratedValue( strategy = GenerationType.SEQUENCE ) @Nullable private final Long id; @Nullable private String designation; @Nullable private Long weight;
그러나 자카르타 검증 예제인 후자의 경우 다음을 찾을 수 있습니다.
public final class AccountNumbersPassiveDto { @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @NotNull private final BigDecimal accountNumber; private final int accountNumberEven; private final int accountNumberOdd;
이는 AccountNumbersDto
해당 필드의 해당 주석에 의해 영향을 받지 않았음을 의미합니다. 첫 번째 예에서는 운이 좋았습니다. 실제로 실패했을 수도 있습니다. 주석이 어디로 가야 하는지 지정하기 위해 Kotlin은 사용 사이트 대상을 주석의 접두사로 사용할 수 있는 가능성을 제공합니다. 물론 조건이 충족되어야 한다는 조건이 있습니다. 이 경우 이 문제를 해결하는 한 가지 방법은 다음과 같이 모든 주석 앞에 @field
를 붙이는 것입니다.
data class AccountNumbersPassiveDto( @field:NotNull val accountNumberLong: Long?, val accountNumberNullable: Long?, @field:DecimalMax(value = "10") @field:DecimalMin(value = "5") val accountNumber: BigDecimal, val accountNumberEven: Int, val accountNumberOdd: Int, @field:Positive val accountNumberPositive: Int, @field:Negative val accountNumberNegative: Int, @field:DecimalMax(value = "5", groups = [LowProfile::class]) @field:DecimalMax(value = "10", groups = [MiddleProfile::class]) @field:DecimalMax(value = "15", groups = [HighProfile::class]) @field:DecimalMax(value = "20") val accountNumberMaxList:Int )
이제 IntelliJ를 사용하여 결과 바이트코드를 디컴파일하려고 하면 Java에서 다음 코드가 생성되는 것을 알 수 있습니다.
public final class AccountNumbersPassiveDto { @NotNull @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @DecimalMax("10") @DecimalMin("5") @org.jetbrains.annotations.NotNull
이는 주석이 필드에 적용되었으며 코드가 예상대로 작동한다는 것을 의미합니다.
이 시점에서 아마도 오랫동안 Java 작업을 해 온 사람들은 필드, 매개변수, 속성 등이 무엇인지 모두 알고 있기 때문에 Kotlin에 적응하는 데 문제가 없을 것이라고 상상할 수 있습니다. 오늘 우리가 관찰하고 나도 목격한 것은 코드 단순화가 역효과를 낳을 가능성이 있는 것처럼 보인다는 것입니다. 일부 프로젝트나 모듈에서 주석이 작동하지 않는 이유를 파악하기 위해 프로젝트에 소요된 시간을 알게 된 경우가 꽤 많았습니다. 그리고 이 모든 것은 사용 현장 목표를 활용하지 않는 것으로 귀결되는 것 같습니다.
가능성이 가장 높은 것에 대한 나의 이론은 새로운 세대의 개발자들이 아마도 우리 세대가 복잡하다고 생각하지 않았고 본능적으로 배운 것들을 정말 복잡한 자료로 보게 될 것이라는 것입니다. 사용 현장 대상의 경우도 마찬가지입니다. 이것들은 우리가 매우 쉽게 시각화할 수 있었던 것들이었습니다. 그러나 요즘에는 이로 인한 혼란으로 인해 프로젝트가 제때에 개발되지 못하는 경우도 있는 것 같습니다. 물론 일반화할 수는 없지만 Java 레코드와 데이터 클래스에는 좋은 가능성도 있고 나쁜 가능성도 있습니다. 데이터 클래스와 레코드가 우리의 미래를 어떻게 형성할지 말하기는 어렵지만 한 가지 분명한 것은 이러한 문제로 인해 다른 프레임워크에서는 Ktor의 경우처럼 주석이 전혀 없는 방식으로 전환하고 있다는 것입니다.
저는 Scribed: Fields-in-Java-and-Kotlin-and-What-to-Expect 및 슬라이드 공유: fields-in-java-and-kotlin-and-what-to-expect 에서 이에 대한 문서를 만들었습니다.
GitHub에서 제가 제공한 예제를 찾을 수도 있습니다. 골든 걸(Golden Girls) 예제는 jeorg-kotlin-test-drives 에서 찾을 수 있으며, 자카르타 지속성 및 유효성 검사 주석 예제는 https://github.com/jesperancinha/jeorg-spring-master-test-drives 에서 찾을 수 있습니다.
마지막으로 YouTube에서 이에 대한 동영상도 만들었습니다. 바로 여기에서 확인하실 수 있습니다.
여기에도 게시되었습니다.