본문 바로가기
dev/java

Java 25 새로운 기능으로 더 좋은 코드 작성하기

by igooo 2025. 9. 19.
728x90

개요

JDK 25가 2025/09/16 정식으로 출시(https://openjdk.org/projects/jdk/25/) 되었고 JDK 21부터 JDK 25까지 추가된 특징을 확인하여 더 좋은 코드를 작성할 수 있는 방법에 대하여 알아본다.

(참고 : JDK 24에 추가된 기능에 대해서 이전 글을 https://blog.igooo.org/171 참고한다.)

 

 

Sealed Type에 대한 Switch 표현식

Sealed Type에 대하여 switch 구문을 사용하면 견고함과 확장성을 유지하면서도 코드의 다독성을 크게 향상할 수 있다. 

 

아래 예제에는 서버의 응답을 모델링하는 ServerResponse의 sealed 타입이 있다.

sealed interface ServerResponse {
    record Response(int code, String message) implements ServerResponse {
    }

    record NotFound(int code, String message) implements ServerResponse {
    }

    record ServerError(int code, String message) implements ServerResponse {
    }
}

 

ServerResponse 타입을 처리하는 switch 구문을 작성하면 아래와 같이 작성할 수 있다.

switch 구문을 사용하여 ServerResponse 타입을 처리하면 컴파일러는 switch 구문에서 ServerResponse의 모든 유형을 처리했는지 확인할 수 있다. 아래 코드에서 ServerError를 삭제하게 되면 컴파일러는 모든 ServerResponse 타입의 모든 브랜치를 처리하지 않았다며 컴파일 오류가 발생한다. 이는 구현 중 일부를 추가하거나 제거해야 하는 경우 코드 유지 보수에 매우 효과적이다.

User analyze(ServerResponse serverResponse) {
    return switch (serverResponse) {
        case ServerResponse.Response response -> User.unmarshal(response.message());
        case ServerResponse.NotFound(int code, String message) ->
                /* deal with the error. */
        case ServerResponse.ServerError(int code, String message) ->
                /* deal with the error. */
    };
}

 

위 switch 구문에는 두 가지 다른 종류의 패턴이 사용되어 매칭이 된다.

  • ServerResponse.Response response : Type Pattern Matching으로 serverResponse 변수가 Response 타입인지를 확인하고 맞다면 response 변수를 생성한다. 이때 생성된 response 변수는 패턴 변수 또는 바인딩 변수라 부른다.
  • ServerResponse.NotFound(int code, String message) : Record Pattern Matching으로 serverResponse 변수가 NotFound의 record 타입이라면 NotFound의 구성 요소로 분해한다.

 

이름 없는 변수로(Unnamed Variable) 패턴 단순화 하기

최근에 사용되는 많은 언어에는 이미 추가된 기능이지만 JDK 25에(https://openjdk.org/jeps/456) 추가되었다. 간단하게 설명하면 사용하지 않는 변수에 대해서는 _ 문자를 사용하여 코드상에서 사용하지 않도록 정의하는 것이다.

 

위 예제에서 살펴본 switch 구문을 사용하여 알아보자.

  • NotFound(int _, String message): RuntimeException으로 message만 전달하고 int code는 사용하지 않아 code 변수를 _ 로 처리했다.
  • ServerError(_, String messages): RuntimeException으로 message만 전달하고 int code는 사용하지 않아 code 변수를 _ 로 처리했지만 NotFound 코드와는 다르게 타입도 선언하지 않았다.
User analyze(ServerResponse serverResponse) {
    return switch (serverResponse) {
        case ServerResponse.Response response -> User.unmarshal(response.message());
        case ServerResponse.NotFound(int _, String message) ->
                throw new RuntimeException("Error " + message);
        case ServerResponse.ServerError(_, String message) ->
                throw new RuntimeException("Error " + message);
    };
}

이름 없는 변수(unnamed variable)를 사용하게 되면 코드상에서 해당 변수를 사용하지 않기 때문에 _ 로 처리한 변수를 사용하면서 발생하는 버그를 줄일 수 있다. unnamed variable를 사용하면서 성능상의 이점은 없다.

ServerError에 코드처럼 타입을 선언하지 않으면 추후 ServerError에 code 변수에 타입이 변경되어도 위 코드는 컴파일 오류가 발생하지  않는다. 타입의 선언 여부는 개발자의 선택이다.

 

위 switch 구문은 아래 코드처럼 축약해서 사용할 수도 있다.

User analyze(ServerResponse serverResponse) {
    return switch (serverResponse) {
        case ServerResponse.Response response -> User.unmarshal(response.message());
        case case ServerResponse.NotFound _, ServerResponse.ServerError(int _, _) ->
                throw new RuntimeException("Error ");
    };
}

 

Exception 처리에도 unnamed variable을 사용할 수 있다.

아래 코드는 예외에 대하여 아무것도 처리하지 않겠다는 의미를 가지며, 코드를 줄이고 코드를 더 명확하게 만들 수 있다. (예외의 타입은 정의해야 한다.)

try {
    Integer.parseInt(s);
} catch (NumberFormatException _) {
}

 

lamda 구문에서도 사용 가능하고, 사용하지 않는 변수를 정의하는 방법으로도 사용할 수 있다.

var map = new HashMap<String, List<String>>();
map.computeIfAbsent("test", _ -> new ArrayList()).add("value");

var x = queue.pop();
var y = queue.pop();
var _ = queue.pop();
new Point(x, y);

 

간결한 소스 파일과 인스턴스 Main 메서드

Java도 Kotlin처럼 클래스 선언 없이도 main 메서드를 정의할 수 있다. 

빠르게 사용법을 알아보자.

 

Application.java 파일을 생성하고 아래 내용을 입력한다.

void main() {
	System.out.println("hello world");
}

별도의 컴파일 과정 없이 java 명령어를 사용하여 코드를 실행할 수 있다.

java 명령어를 사용하여 실행하면 매번 처음부터 다시 컴파일되어 실행되고 .class 파일은 생성하지 않는다.

$ java Application.java
hello world

기존의 방법처럼 javac 명령어를 사용하여 실행하는 것도 가능하다.

$ javac Application.java
$ ls
Application.java Application.class
$ java Application
hello world

 

모듈 기능을 사용하여 System.out.println 메서드를 사용하지 않고 별도의 import 구문 없이 메시지를 출력할 수 있다.

void main() {
	// System.out.println("hello world");
	IO.println("hello world");
}

 

 

같은 디렉터리 안에 다른 파일을 정의하고 참조할 수 있다.

class Dependency {
	static String message() {
		return "This is my Dependency";
	}
}

main 메서드를 아래와 같이 수정하면 import 구문 없이 같은 디렉터리에 파일은 자동으로 참조가 가능하다.

void main() {
	IO.println("hello world");
	IO.println(Dependency.message());
}

 

 

외부 라이브러리 사용이 필요한 경우 java 명령어 사용 시 -cp 옵션을 사용하여 jar파일을 추가할 수 있다.

import org.apache.commons.lang3.StringUtils;

void main() {
	IO.println("hello world");
	var result = StringUtils.center("Hello, 11, "-");
	IO.println(result);
}

실행 결과

$ java -cp lib/common-lang303.17.0.jar Application.java
Hello world
---Hello---

 

 

main 메서드를 중복으로 선언하면 기존 시그니처를 가진 main 메서드가 실행 위치로 동작한다.

class Application {
	public static void main(String[] args) {
		// your application starts here
	}
    
	void main() {
		// some other code
	}
}

 

SpringBoot를 사용할 때도 main 메서드를 변경하여 사용할 수 있다.

@SpringBootApplication
class Application {
	void main( ) {
		SpringApplication.run(Application.class);
	}
}

 

유연한 생성자 본문

새로운 기능을 설명을 하기 전 아래 코드에 대한 결과를 한 번식 생각해 보자.

class A {
    A() {
        IO.println(a());
    }

    String a() {
        return "This is a";
    }
}

class B extends A {
    private final String a;

    B(String a) {
        this.a = a;
    }

    public String a() {
        return "This is " + this.a;
    }
}

void main(){
    var a = new A();
    var b = new B("b");
}

 

실행 결과는 아래와 같다.

This is a
This is null

 

B 객체를 생성했을 때 생성자 파라미터로 입력한 "b"가 사용되지 않고 null 값이 출력되었는지는 java 컴파일러가 컴파일 시점에 무슨 일을 하는지 아는 사람은 바로 이해할 수 있다. 

java 컴파일러는 B 객체는 A를 상속하고 있음으로 상속받은 A의 생성자를 먼저 호출할 수 있도록 super()를 추가해 준다 그렇기 때문에 B객체에 멤버변수 a에 값이 할당되기 전에 override 한 B객체의 a() 메서드가 호출되어 null이 출력된 것이다.

class B extends A {
    private final String a;

    B(String a) {
		super();
        this.a = a;
    }

    public String a() {
        return "This is " + this.a;
    }
}

 

JDK 25 이전에는 super() 메서드 이전에 코드를 추가하는 것은 금지되었고, 코드를 추가하면 컴파일 오류가 발생했지만 JDK 25에서는 super() 메서드 호출 전에 코드를 추가할 수 있다. 아래 예제처럼 생성자의 파라미터에 대한 검증이나 부모 생성자 호출 전 초기화가 필요한 코드에 대하여 super() 메서드 호출 전에 추가할 수 있게 되었다.

class B extends A {
    private final String a;

    B(String a) {
        this.a = Obejcts.requireNonNullElse(a, "null");
		super();
    }

    public String a() {
        return "This is " + this.a;
    }
}

 

Markdown을 사용한 주석

JDK 25 이전에는 자바 코드의 주석을 작성할 때 HTML을 사용하여 주석을 작성했다. JDK 25에는 요즘 많이 사용하는 markdown 문법을 사용하여 주석을 작성할 수 있도록 기능이 추가되었다. markdown 주석을 사용하려면 /// 을 사용하여 주석을 달아주면 javadoc은 /// 구문을 markdown 문법으로 인지하고 주석을 생성해 준다.

public class Application {
	/// ## Definition of the Service
	/// This service taskes a [java.lang.String] and resutns on instance of
	/// [org.igooo.jdk25.model.User] corresponding to this index and name
	///
	/// ## How to use the service
	/// ### A first, simple case
	/// ### A more complex case
	///
	/// @param index the index of the user
	/// @param name the name of the user
	/// @return the instace of the corresponding user
	User service(int index, String name) {
		return null;
	}
}

 

 

Primitive Types in Patterns, instanceof, and switch (Thrid Preview)

마지막으로 소개할 기능은 아직 Preview 상태도 정식 기능으로 추가되지 않아 실행 옵션을 추가하여 사용가능하지만 다음 JDK 버전에 추가될 가능성이 커서 같이 알아보도록 하자. 제목에 내용처럼 Primitive 타입을(int, long, ...) switch와 같은 구문에 매칭하여 사용 가능한 기능이다.

 

아직은 swtich 구문에 Primitive 타입을 사용할 수 없지만 위 기능이 추가되면 아래의 주석으로 된 코드로 작성하는 것을 더 선호하게 될 것이다.

String decideWithIf(boolean b) {
	if(b) {
		return "true";
	} else {
		return "false";
	}
    
	// return switch(b) {
	//		case true -> "true";
	//		case false -> "false";
	//	};
}

 

String primitiveTypes(int status) {
	if(status == 200) {
		return "Success";
	} else if(status > 200 && status < 300) {
		return "Success" + status;
	} else if(status >= 300 && status < 400) {
		return "Redirection: " + status;
	} else {	
		return "Other : " + status
	}
    
	// Preview
	// return switch(status) {
	//	case 200 -> "Success";
	//	case int i200
	//			when i200 > 200 && i200 < 300 -> "Moved permanently";
	//	case int i300
	//			when i300 >= 300 && i200 < 400 -> "Redirection: " + i300;
	//	case 401 -> "Unauthorized";
	//	case 404 -> "Not Found";
	//	case int other -> "Other : " + other;
	// }
}

 

참고

https://www.youtube.com/watch?v=X0-TGhktFnE&t=880s

728x90

'dev > java' 카테고리의 다른 글

Mockito - 메서드 체이닝 테스트 코드 작성하기  (0) 2025.08.25
JDK 24 새로운 기능  (0) 2025.03.30
Using JSpecify Annotations  (0) 2025.02.25
JSpecify Nullness User Guide  (0) 2025.02.19
Win10 SDKMAN으로 JAVA 설치하기  (0) 2025.01.14