Java에서는 최근에 데이터 중심 프로그래밍(Data-Oriented Programming, DOP)을 지원하는 기능들이 도입됐다. 특히 Java 17 이후 이러한 기능들이 더욱 강화되면서 데이터를 보다 직관적이고 효율적으로 다룰 수 있게 되었다.
Java에서 지원하는 4가지 주요 DOP 기능(Sealed Types, Records, Pattern Matching, Smart Switch Expressions)을 소개하고, 각각의 기능이 어떻게 활용될 수 있는지 예시와 함께 알아보려고 한다.
1. Sealed Types (봉인된 타입)
Sealed Types는 클래스 계층 구조를 제어할 수 있게 해주는 기능이다. 어떤 클래스가 특정 클래스나 인터페이스를 확장할 수 있는지를 명시적으로 제한한다. 이를 통해 상속 구조를 명확히 정의하고, 의도치 않은 클래스 확장을 방지할 수 있다.
예시
// 부모 클래스 Animal을 sealed로 선언하여 확장할 수 있는 서브클래스를 제한
public sealed class Animal permits Dog, Cat { }
final class Dog extends Animal { }
final class Cat extends Animal { }
// 추가 확장을 시도하면 컴파일 에러 발생
// class Bird extends Animal { } // 컴파일 에러
위 예시에서는 'Animal' 클래스는 'Dog'와 'Cat'만이 확장할 수 있고, 다른 클래스는 이를 확장하지 못한다. 이를 통해 타입 계층을 안전하게 관리할 수 있다.
2. Records
Records는 불변 데이터 객체를 간편하게 정의할 수 있는 기능이다. 일반적으로 DTO 클래스를 만들 때 반복적으로 작성해야 했던 getter, 생성자, equals, hashCode, toString 등을 자동으로 생성해준다. 이를 통해 코드의 간결함과 유지보수성을 높일 수 있다.
예시
// Record 선언, 생성자 및 모든 메서드 자동 생성
public record Person(String name, int age) { }
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
System.out.println(person.name()); // Alice
System.out.println(person.age()); // 30
}
}
Person 레코드는 불변 객체로, 생성과 동시에 값을 고정할 수 있고, 그 외의 복잡한 메서드나 로직을 정의하지 않아도 된다.
Java의 Record는 getter는 자동으로 생성해주지만, setter는 제공하지 않는다. Record는 불변 객체를 목표로 하기 때문에 필드를 변경할 수 있는 setter는 기본적으로 포함되지 않는다. 각 필드는 생성자에서 한 번 설정된 후 변경할 수 없으며, 대신 필드에 접근할 수 있는 getter 메서드는 자동으로 생성된다.
Record에서 생성되는 getter는 getFieldName() 형식이 아니라 필드명과 동일한 이름을 가진 메서드로 제공된다. (ex. person.name(), person.age()
3. Pattern Matching (패턴 매칭)
패턴 매칭은 객체의 타입을 확인하고 동시에 변수를 바인딩할 수 있는 기능이다. 기존의 'instanceof'와 캐스팅을 줄여주며, 간결하고 읽기 쉬운 코드를 작성하는 데 유용하다.
예시
public class Main {
public static void main(String[] args) {
Object obj = "Hello, world!";
// 기존 방식
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str.length());
}
// 패턴 매칭 방식
if (obj instanceof String str) {
System.out.println(str.length());
}
}
}
패턴 매칭을 사용하면 `instanceof`를 통해 타입을 확인하면서 동시에 변수를 선언할 수 있어 코드가 훨씬 간결해진다.
4. Smart Switch Expressions (스마트 스위치 표현식)
Smart Switch Expressions는 기존의 'switch' 문을 더 유연하고 강력하게 사용할 수 있도록 개선된 기능이다. switch가 표현식으로 사용될 수 있으며, 여러 값을 처리하거나 패턴 매칭과 결합할 수 있다.
예시
public class Main {
public static void main(String[] args) {
String day = "MONDAY";
// 기존 switch 문
switch (day) {
case "MONDAY", "FRIDAY", "SUNDAY":
System.out.println("Start of the week or weekend");
break;
case "TUESDAY":
System.out.println("Midweek");
break;
default:
System.out.println("Other day");
}
// Switch 표현식 (Java 14 이상)
String result = switch (day) {
case "MONDAY", "FRIDAY", "SUNDAY" -> "Start of the week or weekend";
case "TUESDAY" -> "Midweek";
default -> "Other day";
};
System.out.println(result);
}
}
스마트 스위치 표현식은 더 직관적이고 표현력이 높아 기존의 복잡한 switch 문을 간결하게 바꿔준다. 또한 yield 키워드를 통해 값을 반환할 수도 있다.
public class Main {
public static void main(String[] args) {
String day = "MONDAY";
// 복잡한 switch 표현식에서 yield 사용
String result = switch (day) {
case "MONDAY", "FRIDAY", "SUNDAY" -> "Start of the week or weekend";
case "TUESDAY" -> "Midweek";
case "WEDNESDAY" -> {
// 여러 줄의 로직이 필요한 경우
System.out.println("It's the middle of the week");
yield "Wednesday is tough";
}
default -> "Other day";
};
System.out.println(result);
}
}
Java에서 yield 키워드는 switch 표현식 내에서 값을 반환할 때 사용된다. 기존의 switch 문에서는 break를 사용하여 분기 처리를 종료했지만, switch가 표현식으로 사용될 때는 yield를 사용해 값을 반환할 수 있다. 이를 통해 복잡한 분기 처리를 간결하게 하고, 특정 조건에 따라 값을 반환할 수 있다.
yield는 복잡한 연산이나 로직이 필요한 경우, case 블록 안에서 값을 반환할 때 유용하다. 특히 여러 줄로 이루어진 로직이 필요한 경우, yield 키워드를 통해 명시적으로 값을 반환할 수 있다.
Java에서 데이터 중심 프로그래밍을 지원하는 기능인 Sealed Types, Records, Pattern Matching, Smart Switch Expressions는 데이터 처리의 복잡성을 줄이고, 코드의 가독성을 높여주는 도구들이다.
참고로 코틀린에도 위 4개 기능과 대응되는 기능들이 모두 존재한다.
- Sealed Types : Kotlin의 sealed class
- Records : Kotlin의 data class
- Pattern Matching : Kotlin의 when 식 (패턴 매칭 가능)
- Smart Switch Expressions : Kotlin의 when 식 (표현식으로 사용 가능)
이러한 기능들을 충분히 활용하면 훨씬 간결하고 안정성 있는 프로그래밍을 할 수 있을 것이다.