Java 17 주요 기능과 실무 적용
이 글은 Connectrip 개발 당시 Java 17을 사용하면서 실제로 활용한 주요 기능들을 정리한다. Java 11부터 17까지의 변경사항 중에서 실무에서 자주 사용하게 된 기능들을 중심으로 작성했다. 현재는 Kotlin을 주로 사용하고 있어서, Java 17 기능들을 Kotlin과 비교한 관점도 함께 다룬다.
Pattern Matching for instanceof
Pattern Matching for instanceof는 기존 instanceof
후 명시적 캐스팅 패턴에서 발생하기 쉬운 ClassCastException
을 방지하고 코드 안전성을 향상시킨다. 실제 적용 결과 타입 체크와 캐스팅 과정의 실수가 현저히 감소했다.
기존 방식
public class PatternMatchingExample {
public static void main(String[] args) {
Object obj = "Hello, World!";
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str.toUpperCase());
}
}
}
개선된 방식
public class PatternMatchingExample {
public static void main(String[] args) {
Object obj = "Hello, World!";
if (obj instanceof String str) {
// 타입 변환 없이 바로 사용
System.out.println(str.toUpperCase());
}
}
}
API 응답 처리 시 Object 타입 데이터의 타입별 분기 처리에서 특히 효과적이었다. 외부 API에서 받은 다양한 형태의 데이터를 처리하거나, 인터페이스나 추상 클래스를 구현한 여러 타입의 객체를 다룰 때, 그리고 입력 데이터의 타입 확인 후 즉시 해당 타입의 메서드를 호출해야 하는 유효성 검증 과정에서 유용했다. 기존 방식 대비 캐스팅 관련 실수가 감소했고, 코드 가독성도 향상되었다.
Kotlin과의 비교
Kotlin의 smart cast와 비슷한 개념이다. Kotlin에서는 이미 이런 방식이 기본이었다.
fun processData(obj: Any) {
if (obj is String) {
// obj는 자동으로 String 타입으로 캐스팅됨
println(obj.uppercase())
}
}
Java 17에서 이 기능이 추가되면서 Kotlin과 Java 사이의 문법적 격차가 많이 줄어들었다. 다만 Kotlin의 when
표현식과 결합했을 때의 강력함에는 아직 못 미친다. Kotlin은 패턴 매칭과 when을 조합해서 타입 체크, 값 범위 검사, 조건식을 모두 한 번에 처리할 수 있지만, Java는 아직 instanceof pattern matching이 switch와 완전히 통합되지 않았기 때문이다.
Record - 불변 데이터 클래스의 혁신
Record는 DTO 클래스 작성 시 반복되는 boilerplate 코드 문제를 근본적으로 해결한다. 기존에는 getter, setter, equals, hashCode, toString 등을 수동으로 구현해야 했지만, Record는 이 모든 기능을 자동 생성하면서도 불변성을 보장한다.
public record PersonRecord(String name, int age) {
}
위 한 줄로 다음 기능들이 자동 생성된다.
public class Main {
public static void main(String[] args) {
// 1. 생성자 - 모든 필드를 초기화하는 생성자 자동 생성
PersonRecord person = new PersonRecord("Taehyeon", 20);
// 2. 접근자 메서드 - 필드명과 동일한 메서드명 생성
System.out.println("name: " + person.name());
System.out.println("age: " + person.age());
// 3. equals() - 모든 필드 값 비교
System.out.println("equals(): " +
person.equals(new PersonRecord("Taehyeon", 20)));
// 4. hashCode() - 모든 필드 기반 해시 생성
System.out.println("hashcode(): " +
(person.hashCode() == new PersonRecord("Taehyeon", 20).hashCode()));
// 5. toString() - 클래스명[필드명=값] 형식 출력
System.out.println(person); // PersonRecord[name=Taehyeon, age=20]
}
}
Record 사용 시 고려사항
Record는 강력하지만 몇 가지 제약사항이 존재한다.
- 상속 불가: Record는 암시적으로 final이며 extends 사용 불가
- 불변성: 인스턴스 생성 후 필드 값 변경 불가, 새 인스턴스 생성 필요
- 인터페이스 구현: 인터페이스 구현은 가능하므로 추상화 설계에 활용
- 제네릭 지원:
public record PersonRecord<T>(T data) {}
형태로 사용 가능
API 응답 객체나 설정 데이터처럼 불변성이 중요한 도메인에서 Record를 활용하면 제약사항이 오히려 설계상 이점으로 작용한다. 코드 가독성과 유지보수성이 동시에 향상되는 효과를 얻었다.
Kotlin data class와의 비교
Kotlin의 data class와 매우 유사하다.
data class PersonData(val name: String, val age: Int)
둘 다 자동으로 equals, hashCode, toString을 생성한다. 하지만 몇 가지 차이점이 있다.
Java: Record
- 완전한 불변성 (모든 필드가 final)
- 상속 불가
- 생성자 파라미터만 필드로 사용 가능
Kotlin: data class
- var 사용 시 가변성 허용
- open 클래스 상속 가능
- copy() 메서드 자동 생성 (불변 객체의 부분 수정 편의성)
- 컴포넌트 함수 (구조 분해) 지원
JVM 바이트코드 레벨에서는 둘 다 비슷하게 컴파일되지만, Kotlin data class가 더 유연하다. 다만 Java Record의 완전한 불변성은 함수형 프로그래밍 관점에서는 장점이 될 수 있다.
Switch Expression과 Text Block
Switch Expression
Switch Expression이 도입되면서 기존 switch문의 주요 문제점들이 해결되었다. 특히 break
누락으로 인한 fall-through 버그를 원천적으로 방지하며, 표현식으로 값을 직접 반환할 수 있어 코드 안전성과 가독성이 향상되었다.
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> {
System.out.println("Processing Wednesday");
yield "Wednesday";
}
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
case 7 -> "Sunday";
default -> throw new IllegalArgumentException("Invalid day: " + day);
};
주요 개선점
- fall-through 방지로 런타임 에러 감소
yield
키워드를 통한 복합 로직 처리- 표현식으로 직접 사용 가능하여 변수 할당 및 반환값 처리 간소화
Kotlin when과의 비교
Kotlin의 when
은 더 강력하다.
val dayName = when (day) {
1 -> "Monday"
2 -> "Tuesday"
in 3..5 -> "Midweek"
else -> "Weekend"
}
when
은 범위 검사, 타입 검사, 조건식 등이 모두 가능하지만, Java switch expression은 아직 값 비교에 제한된다. 하지만 Java도 pattern matching이 계속 발전하고 있어서 앞으로는 더 비슷해질 것 같다.
Text Block
Text Block 기능으로 멀티라인 문자열 처리가 크게 개선되었다. JSON, SQL 쿼리, HTML 등의 텍스트 블록을 다룰 때 기존의 문자열 연결이나 StringBuilder
사용 방식 대비 가독성과 유지보수성이 향상되었다.
public class TextBlockExample {
public static void main(String[] args) {
String textBlock = """
This is a text block.
It can span multiple lines.
Indentation is handled automatically.
""";
System.out.println(textBlock);
}
}
테스트 코드에서 JSON 데이터 작성 시 특히 효과적이었다. 자동 인덴테이션 처리와 이스케이프 문자 불필요로 개발 효율성이 크게 향상되었으며, SQL 쿼리 작성에서도 동일한 이점을 확인할 수 있었다.
Kotlin: raw string과의 비교
val textBlock = """
This is a raw string.
It can span multiple lines.
Indentation needs manual handling.
""".trimIndent()
둘 다 멀티라인 문자열을 지원하지만 차이가 있다.
- Java Text Block: 인덴테이션 자동 처리
- Kotlin raw string: trimIndent() 같은 함수로 수동 처리
Kotlin이 먼저 이 기능을 도입했고, Java가 나중에 더 편리한 형태로 구현했다는 느낌이다.
정리하며
Java 17 사용 과정에서 느낀 점은 단순한 문법 개선을 넘어서 개발 패러다임의 변화를 경험할 수 있었다는 것이다. Record와 Pattern Matching은 도메인 모델링을 더 명확하게 만들어주었고, Switch Expression은 조건 처리 로직의 가독성을 크게 향상시켰다.
Kotlin 사용자 관점에서의 Java 17
현재 Kotlin을 주로 사용하는 입장에서 보면, Java 17은 Kotlin의 여러 장점들을 따라잡으려는 노력이 보인다. Pattern matching, Record, Switch expression 등이 모두 Kotlin에서 이미 제공하던 기능들과 유사하다.
하지만 완전히 같지는 않다.
- Kotlin의 우위: 더 간결한 문법, null safety, 확장 함수, 코루틴 등
- Java의 장점: 더 큰 생태계, 엔터프라이즈 환경에서의 안정성, 점진적 마이그레이션 가능
JVM에서 두 언어가 완벽하게 호환되므로, 상황에 따라 적절한 언어를 선택할 수 있다는 점이 장점이다. Java 17의 발전으로 두 언어 간 격차가 줄어들어서 팀 내에서 혼용하기도 더 수월해졌다.
성과 측정
Java 17 도입의 구체적 효과는 다음과 같았다.
- Record: DTO 클래스 작성 시간 약 60% 단축, boilerplate 코드 제거로 코드 리뷰 효율성 향상 (기존 평균 50-80줄에서 30줄 미만 감소)
- Pattern Matching: 타입 캐스팅 관련 런타임 에러 발생률 감소
- Text Block: 테스트 코드 내 JSON/SQL 문자열 작성 시간 단축 및 가독성 개선
결론
Java 17은 개발 생산성과 코드 품질 측면에서 의미 있는 개선을 제공한다. 특히 Kotlin과의 기능적 유사성 증가로 JVM 생태계 내에서의 언어 간 호환성이 강화되어, 팀 내 기술 스택 선택의 유연성이 향상되었다. 엔터프라이즈 환경에서 안정성을 유지하면서도 모던 언어의 장점을 점진적으로 도입할 수 있다는 점에서 전략적 가치가 크다.