본문 바로가기
카테고리 없음

JAVA 24 Flexible Constructor Bodies

by igooo 2025. 3. 26.
728x90

개요

2025/03/18 JDK 24 GA 버전이(https://jdk.java.net/24/) 출시되었다. JDK 24 추가된 기능들에 대해서는 다음 포스팅에서 알아보고, 이번 포스트에서는 여러 추가된 기능 중 Flexible Constructor Bodies에 대하여 알아본다.

Flexible Constructor Bodies 기능은 Java 프로그래밍 언어에서 생성자(constructor) 내에서 명시적 생성자 호출(super(...) 또는 this(...)) 전에 코드를 작성할 수 있는 기능을 제공한다.(이전 버전에서는 생성자 호출 이전에 작성한 코드에 대해서는 컴파일 오류가 발생한다.) super(...), this(...)는 생성 중인 인스턴스를 참조할 수는 없지만, 인스턴스의 필드를 초기화할 수는 있다. 다른 생성자를 호출하기 전에 필드를 초기화하면, 메서드가 재정의(overriding) 될 때 클래스의 신뢰성은 높아지게 된다. 이는 미리 보기(Preview) language feature이다.

 

Goals

  • 객체 초기화 과정에서 생성자의 역할을 재구성하여 개발자가 현재 고려해야 하는 논리를 보조 정적 메서드, 보조 중간 생성자 또는 생성자 인수에 보다 자연스럽게 배치할 수 있도록 한다.
  • 생성자 본문에서 두 가지 뚜렷한 단계를 소개한다. 프롤로그는 superclass(부모 클래스) 생성자가 호출되기 전에 실행되는 코드를 포함하고, 에필로그는 superclass 생성자가 호출된 후에 실행된다.
  • 하위 클래스 생성자의 코드가 superclass 인스턴스화를 방해할 수 없다는 기존 보장은 유지한다.

 

Motivation

클래스의 생성자는 해당 클래스의 유효한 인스턴스를 만드는 일을 담당한다. 예를 들어 Percon 클래스의 age 필드는 절대 음수가 될 수 없다고 가정하면 생성자에서는 age의 값을 검증하고 유효한 값을 보장하거나 예외를 생성해야 한다. 

또한, 클래스의 생성자는 하위 클래스가 존재할 경우 유효성을 보장하는 역할을 한다. 예를 들어, Employee 클래스가 Person 클래스의 하위 클래스라고 가정해 보자. 모든 Employee 생성자는 암시적이든 명시적이든 Person 클래스의 생성자를 호출한다. 두 생성자는 함께 작동하여 유효한 인스턴스 생성을 보장해야 한다. 즉 Employee, Person 각각의 생성자는 각 클래스에서 선언된 필드에 대한 책임을 가지게 된다.

Java 프로그래밍 언어는 간단한 해결책을 통해 위 내용을 보장한다. 즉, 생성자는 상위 클래스에서 하위 클래스로 내려오는 순서대로 실행되어야 한다. 하위 클래스의 생성자가 실행되기 전에 반드시 상위 클래스의 생성자가 먼저 실행되어, 상위 클래스에서 선언된 필드의 유효성을 보장해야 한다.

생성자가 상위 클래스에서 하위 클래스로 실행되도록 보장하기 위해, Java 언어에서는 생성자 본문에 첫 번째 문장이 반드시 다른 생성자를 명시적으로 호출해야 한다. 즉 super(...) 또는 this(...)가 와야 한다. 만약 생성자 본문에 명시적은 생성자 호출이 없다면, 컴파일러가 자동으로 super()를 생성사 본문의 첫 번째 문장으로 삽입한다.

 

Example: Validating superclass constructor arguments 

superclass(부모 클래스) 생성자에 전달할 값을 검증해야 한다. superclass 생성자를 호출한 후에 값을 검증할 수 있지만, 그것은 잠재적으로 불필요한 작업 일 수 있다.

public class Employee extends Person {
	public Employee(int age) {
		super(age);  // Potentially unnecessary work
		if(age <= 0) throw new IllegalArgmentException(...);
	}
}

superclass 생성자를 호출하기 전에 값을 검증하여 빠르게 실패하는 생성자를 구현하는 것이 더 좋은 방법이다. JDK 24 이전 버전에서는 보조 메서드를 인라인으로 호출하여 이를 수행할 수 있었다.

public class Employee extends Person {
    private static int verifyAge(int age){
        if(age < 8) throw new IllegalArgumentException("");
        return age;
    }
    public Employee(int age) {
        super(verifyAge(age));
    }
}

JDK 24에서는 검증 로직을 생성자 본문에 넣어 코드를 더 읽기 쉽게 만들 수 있다.

public class Employee extends Person {
    public Employee(int age) {
        if(age < 8) throw new IllegalArgumentException("");
        super(age);
    }
}

 

Example: Perparing superclass constrictor arguments

superclass 생성자에 대한 인수를 준비하기 위해 특정 로직을 처리해야 하는 경우가 있다.

예를 들어 생성자가 Certificate를 인자로 받지만 superclass 생성자를 호출하기 위해 byte 배열로 변환해야하는 경우 아래와 같이 코드를 작성할 수 있다.

public class Sub extends Super {

    private static byte[] prepareByteArray(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) throw new IllegalArgumentException(..);
        return switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            default -> ...
        };
    }

    public Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

}

하지만 생성자 본문에서 직접 인수를 준비할 수 있다면 코드를 더 읽기 쉽게 만들 수 있다.

public Sub(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) throw ...
        byte[] certBytes = switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            default -> ...
        };
        super(certBytes );
    }

 

Example: Sharing superclass constructor arguments

superclass 생성자에 같은 값을 여러 번, 다른 인수로 전달해야 할 때가 있다. 이를 수행하는 유일한 방법은 보조 생성자를 사용하는 것이다.

public class Super {
    public Super(C x, C y) { ... }
}

public class Sub extends Super {
    private Sub(C x)   { super(x, x); }     // Pass the argument twice to Super's constructor
    public  Sub(int i) { this(new C(i)); }  // Prepare the argument for Super's constructor
}

코드의 유지보수성을 높이려면, 보조 생성자를 사용하지 않고 생성자 본문에서 직접 코드 공유를 할 수 있도록 구성하는 것이 더 바람직하다.

public class Sub extends Super {
    public Sub(int i) {
        var x = new C(i);
        super(x, x);
    }
}

 

Summary

위에서 살펴본 예제들은 생성자 본문에서 명시적 생서자 호출 전에 실행하고 싶은 여러 로직을 포함하고 있다. 살펴본 로직은 생성중인 인스턴스를 사용하지 않는 코드로 구성되어, 명시적 생성자 호출 전에 실행해도 안전한 코드들이다. 그러나 이러한 생성자 본문이 안전한 객체 초기화를 보장하더라도, 현재(JDK 24 이전) Java 언어에서는 이러한 방식의 작성이 금지되어 있다.

만약 Java 언어가 보다 유연한 규칙을 통해 안전한 객체 초기화를 보장할 수 있다면, 생성자 본문을 더 쉽게 작성하고 유지보수할 구 있을 것이다. 생성자 본문에서 보자 메서나드 보조 생성자를 번거롭게 호출하지 않고도 자연스럽게 인수 검증, 인수 준비, 인수 공유를 수행할 수 있게 되는 것이다.

 

 

 

728x90