Java 11 이후 신규 추가된 문법과 수정된 문법에 대해 알아보기

#java

2023-03-06 12:25

대표 이미지

Java 11 이후 16 버전까지 유용한 신규 Feature 또는 수정된 문법에 대해 알아봅시다. 간단한 예제도 같이 제공하니 빠르게 적용해보세요.

개요


Java 11이 2018년 9월 25일 릴리즈된 이후, 크고 작은 업데이트를 거치며 Java 버전은 열심히 오르고 있는 중이다.

그간의 사소하지만 중요한, 알고 있으면 유용할 다음 변경점들을 짚고 넘어가보자.

  • String api
  • Switch문
  • Var type 변수
  • Record 클래스
  • Instance of 변경점
  • Helpful NPE

String api


Java 11~15에 걸쳐 새롭게 업데이트된 String Class

String repeat(int count) - java 11

count 숫자만큼 문자열을 복제한다.

String str = 'hello';
System.out.println(str.repeat(3)); // hellohellohello

String strip() - java 11

문자열 앞, 뒤의 공백을 제거

String str = "\n\t   hello world\u2005";  
System.out.println(str.strip() + "-end"); //hello world-end

String stripLeading() - java 11

문자열 앞의 공백을 제거

String str = "\n\t   hello world\u2005";
System.out.println(str.stripLeading() + "-end"); //hello world -end

String stripTrailing() - java 11

문자열 뒤의 공백을 제거

String str = "\n\t   hello world\u2005";
System.out.println(str.stripTrailing() + "-end"); //hello world-end

String isBlank() - java 11

공백을 모두 제거한 문자열이 비어있으면 true를 반환한다.

isEmpty() 의 경우, 공백 제거를 하지 않고 문자열이 비어 있는지 확인한다.

String str1 = "";
String str2 = " ";

System.out.println(str1.isBlank()); // true
System.out.println(str2.isBlank()); // true

/* isEmpty와 비교 */
System.out.println(str1.isEmpty()); // true
System.out.println(str2.isEmpty()); // false

String lines() - java 11

줄바꿈을 기준으로 분할된 스트림을 반환한다.

String multilineStr = "This is\n a multiline\n string.";

Stream<String> stream = multilineStr.lines();
stream.forEach(System.out::println);
/*
This is
 a multiline
 string.
*/
System.out.println(multilineStr.lines().count()); //3

String indent(int count) - java 12

들여쓰기 수행. 아래 과정으로 처리된다.

  1. 줄바꿈 문자열이 있다면 String.lines() 를 통해 라인을 쪼갠다.
  2. n > 0 이면 라인마다 n 만큼의 공백을 추가한다. n < 0 이면 n 라인마다 만큼의 공백을 제거한다. (단, 공백이 없으면 제거하지 않는다.) n == 0 이면 그냥 둔다.
  3. 라인을 모두 합친다. (줄바꿈 문자열은 유지한다.)
String s = "hello\nworld";
System.out.println(s + " ---length:" + s.length());
/*
hello
world ---length:11
*/

String indentedS = s.indent(3);
System.out.println(indentedS + " ---length:" + indentedS.length());
/*
   hello
   world
 ---length:18
*/

String transform(Function f) - java 12

파라미터로 제공한 함수를 실행한다. (반드시 문자열 반환)

String s = "test code";
String upper = s.transform(String::toUpperCase);
System.out.println(upper); //TEST CODE

----

public class Main {
    public static String foo(String str) {
        return "***" + str + "***";
    }

    public static void main(String[] args) {
        String s = "test code";
        System.out.println(s.transform(Main::foo)); //***test code***
    }
}

String formatted(Object… args) - java 13

문자열이 포함하는 타입에 각 argument를 대입한다. String.format(this, args)와 동일하며, 텍스트 블록의 끝에 연결하여 사용할 수 있다.

System.out.println("hello %s".formatted("test")); //hello test

String output = """
    Name: %s
    Phone: %s
    """.formatted('test', '01012341234');
    
/*
Name: test
Phone: 01012341234
*/

String stripIndent() - java 13

문자열 안의 모든 라인에 strip을 적용해 앞 뒤 공백을 제거

String s = "    foo\n    bar";
System.out.println(s);
System.out.println(s.stripIndent());
/*
    foo
    bar
foo
bar
*/

String translateEscapes() - java 13

연속된 이스케이프 시퀀스를 변환하여 출력한다. (\n => \n)

String output = """
    hello\\nworld
    """;
    
/*
hello
world
*/

Text Blocks - java 14

13, 14 버전에서 프리뷰로 공개되었고 15버전에서 정식으로 추가되었다. 기능 추가보다 Syntactic sugar에 가까워보이나 멀티라인으로 이루어진 String을 다루기 때문에 함께 추가한다. 이스케이프 시퀀스를 피하면서 멀티 라인으로 이루어진 문자열을 쉽게 표현할 수 있다. Text Block은 쌍따옴표 3개(“““)로 열고 닫으며, 블록을 시작한 후에는 반드시 줄바꿈이 필요하다.

  • 블록을 닫기전에 줄바꿈은 없어도된다.
// 기존
String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world</p>\n" +
              "    </body>\n" +
              "</html>\n";
              
// Text Block 사용
String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """; // </html>"""; 로 바로 닫아도 된다.
  • 잘못된 형식
String a = """""";   // 블록을 시작(""")한 후 줄바꿈이 없음
String b = """ """;  // 블록을 시작(""")한 후 줄바꿈이 없음
String c = """
           ";        // 블록을 시작(""")한 후 끝나지 않음
String d = """
           abc \ def
           """;      // 이스케이프 처리되지 않은 백슬래쉬(\)를 포함함
  • 블록 내의 이스케이프 시퀀스 사용은 가능하나 권장하지 않는다. (사실상 필요하지 않다.)
  • 닫는 기호의 위치에 따라 공백을 제거한다. 닫는 기호 앞에 공백이 없는 경우, 모든 행의 공백을 유지한다.
String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
""";

// 결과
|              <html>
|                  <body>
|                      <p>Hello, world</p>
|                  </body>
|              </html>
  • 닫는 기호 앞에 공백이 있는 경우, 해당 기호 앞의 공백만큼 모든 행의 공백을 제거한다.
// 아래 온점은 제거될 공백을 의미
String html = """
........      <html>
........          <body>
........              <p>Hello, world</p>
........          </body>
........      </html>
........""";

// 결과
|      <html>
|          <body>
|              <p>Hello, world</p>
|          </body>
|      </html>
  • 닫는 기호가 공백이 가장 적은 행보다 공백이 많은 경우, 가장 적은 공백을 가진 행에 맞춰 모든 행의 공백을 제거한다.
// 아래 온점은 제거될 공백을 의미
String html = """
..............<html>
..............    <body>
..............        <p>Hello, world</p>
..............    </body>
..............</html>
..............    """;

// 결과
|<html>
|    <body>
|        <p>Hello, world</p>
|    </body>
|</html>
  • 블럭 내에 쌍따옴표는 자유롭게 사용해도 되나, 세 개 연속으로 사용시 이스케이프 처리가 필요하다.
// 아래 온점은 제거될 공백을 의미
String html = """
              hello "world"!
              """;
//hello "world"!

String html = """
              hello \"""world\"""!
              """;
//hello """world"""!
  • 역슬래쉬() 단일 사용을 통해 줄 바꿈이 일어나지 않도록 할 수 있다.
// 아래 온점은 제거될 공백을 의미
String html = """
              hello
              world!
              """;
/*
hello
world!
*/

String html = """
              hello \
              world!
              """;
/*
hello world!
*/

Switch 변경


JDK 12, 13의 Preview로 공개됐으며 14 버전에 정식으로 추가되었다.

기존 switch 문의 문제점

  1. 번거로운 break 사용
  2. 전체 스위치 블록이 하나의 범위로 처리됨
  3. switch 문의 흐름은 (c나 c++의 절차지향과 유사) 낮은 수준의 코드를 작성하는 데는 유용하지만, 실제론 높은 수준의 컨텍스트에서 사용되므로 오류가 발생하기 쉬움
  • 기존의 case ... :를 대체하는 case ... -> 를 사용하여 불필요하게 장황한 코드를 개선할 수 있다.
// 기존
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

// JDK 14
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}
  • case L1, L2 ... -> 에서 n개의 label을 동시에 작성할 수 있다.

화살표 뒤에 단일 명령문만 작성하는 경우 break; 를 제외해도 해당 블럭의 내용만 출력되나, 여러 명령문을 작성하는 경우 break; 가 필요하다.

switch (day) {
    case FRIDAY: System.out.println("is"); System.out.println("friday"); break;
    default: System.out.println("default");
}
/*
is
friday
*/

switch (day) {
    case FRIDAY: System.out.println("is"); System.out.println("friday");
    default: System.out.println("default");
}
/*
is
friday
default
*/

상수값 리턴

기존에 case 블럭 내에서 외부에서 선언된 변수에 값을 대입하는 로직대신, 직접 상수값을 리턴하도록 작성 할 수 있다.

Day day = Day.FRIDAY;
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9; 
};

System.out.println(numLetters); // 6

yield value; 는 Java 12에서 제공한 break value;와 동일한 기능이다. 단, 블럭 {} 내에서만 유효하다.

전체 블럭이 필요하면(case 내부에서 처리되어야하는 로직이 있는 경우) yield 문을 사용하여 리턴 값을 반환할 수 있다.

public static int doubleFunc(int k) {
    return k * 2;
}

public static void main(String[] args) {
    int j = switch (day) {
        case MONDAY  -> 0;
        case TUESDAY -> 1;
        // case SUNDAY -> yield 2; [error] yield 키워드는 블럭 내에서만 유효하다.
        default      -> {
            int k = day.toString().length();
            int result = doubleFunc(k);
            yield result;
        }
    };
    
    System.out.println(j); // 12
}

기존 형식에서도 yield 문을 사용해 다음과 같이 처리 가능하다.

int j = switch (day) {
    case MONDAY:
        yield 0; // case .. : 를 사용하는 경우 이와 같이 모든 case에 대해 yield가 필요하다.
    case TUESDAY:
        yield 1;
    default:
        int k = day.toString().length();
        int result = doubleFunc(k);
        yield result;
};

System.out.println(j);

Var Type / 지역변수 타입 추론


Java 10에서 첫 추가된 var 타입과 지역변수의 타입 추론

지역변수 선언 시 var로 선언하며 타입을 생략 할 수 있게 함으로써 boilerplate code 작성을 줄이고, 가독성을 제고하기 위함. 코드의 빠른 프로토타이핑을 위해 사용하는 것을 추천한다.

// 타입 추론
var list = new ArrayList<String>();   // Compile시 해당 var 변수는 ArrayList<String> 타입임을 추론

var stream = list.stream();           // Compile시 해당 var 변수는 Stream<String> 타입임을 추론

var path = Paths.get(fileName);       // Compile시 해당 var 변수는 Path 타입임을 추론
var bytes = Files.readAllBytes(path); // Compile시 해당 var 변수는 bytes[] 타입임을 추론
bytes.toString();

// boilerplate code 간결화
/*
* var 타입을 사용함으로써 ByteArrayOutputStream 타입 생략
*/
//ByteArrayOutputStream bos = new ByteArrayOutputStream ();
var bos = new ByteArrayOutputStream ();
bos.close();

/*
* forEach 작성시 타입 추론을 이용하여 생산성 향상
*/
for(var project : projectList){
    // do Something
}

사용 시 제약사항

  1. 초기화 필요
    1. var 타입은 Complie시 타입을 추론 가능해야하기 때문에 선언 시 초기화를 통해 compiler에게 타입을 추론할 단서를 주어야 한다.1.png
    2. 어떤 객체든 가질 수 있는 상태인 null로 초기화 할 수 없다. (추론 불가능) 2.png
    3. 여러 var 변수를 동시에 초기화할 수 없다. 3.png
  2. 지역 변수에서만 사용 가능: var 타입은 멤버 변수, 클래스 변수(static)에 사용할 수 없으며 오직 지역 변수 영역에서만 선언 가능하다.
  3. 재사용 불가능: 초기화 시 추론된 타입과 다른 타입을 사용할 수 없다.
  4. 배열 & Lambda expression 사용 불가능: 명확한 Target Type을 요구하는 array 및 lambda에서 사용 시 compile error
    1. java var arr = { 1 , 2 }; // array initializer needs an explicit target-type
  5. 파라미터, 리턴타입 사용 불가: 메소드, 생성자, try-catch 파라미터로 사용 불가, 메소드 return type으로 사용 불가

사용 시 유의사항

var는 자바 keyword가 아니다. 키워드가 아니므로 변수명, 메소드명, 패키지명에 var를 써도 된다. 하지만 클래스나 인터페이스명으로 쓸 수 없다.

var 타입을 사용할 땐 명세적 네이밍을 충분히 고려하여 사용해야 한다.

public void getMyDetail(){
  ...
  var detailValue = getDetailContentString(); // 타입이 String임을 표현
  ...
}

...

public String getDetailContentString(Detail detail){
  ...
  return detail.getDetailContents();
}

Record 선언


Java 14, 15에 preview feature로 존재하다가 자바 16에 정식 스펙으로 추가

데이터 불변성 (final) 이 요구되는 Data Carrier 클래스를 모델링할 때, Record 클래스로 선언함으로써 명시적으로 Data Carrier 클래스임을 표현 생성자, getter와 같은 코드들과 equals(), hashCode(), toString()을 컴파일러가 자동으로 override 생성하여 클래스의 수정에 대한 오버헤드를 최소화

Boilerplate code 작성이 줄어드는 효과는 있으나, 그것이 Record의 주요 설계 목적은 아님을 유의.주 목적은 개발자에게 데이터 모델링에 집중하게끔 하는 것이다.

Record 모델링

다음과 같이 장소에 대한 메인 데이터 클래스 Poi, 장소의 상세 주소에 대한 데이터 클래스 Address, 장소에 대한 리뷰 데이터 클래스 Review가 있다고 가정해보자.

/**
* 장소에 대한 메인 데이터 클래스 Poi
**/
public class Poi {
    private final long id;
    private final String title;
    private final String Description;
    private final Address oldAddress;
    private final Address newAddress;
    private final int ReviewCount;
    private final List<Review> reviews;

    public Poi(long id, String title, String description, Address oldAddress,
               Address newAddress, int reviewCount, List<Review> reviews) {
        this.id = id;
        this.title = title;
        Description = description;
        this.oldAddress = oldAddress;
        this.newAddress = newAddress;
        ReviewCount = reviewCount;
        this.reviews = reviews;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Poi poi = (Poi) o;
        return id == poi.id 
                && ReviewCount == poi.ReviewCount
                && Objects.equals(title, poi.title)
                && Objects.equals(Description, poi.Description)
                && Objects.equals(oldAddress, poi.oldAddress)
                && Objects.equals(newAddress, poi.newAddress)
                && Objects.equals(reviews, poi.reviews);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, title, Description, oldAddress,
                newAddress, ReviewCount, reviews);
    }

    @Override
    public String toString() {
        return "Poi{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", Description='" + Description + '\'' +
                ", oldAddress=" + oldAddress +
                ", newAddress=" + newAddress +
                ", ReviewCount=" + ReviewCount +
                ", reviews=" + reviews +
                '}';
    }
    /*
    getters
    */
}

/**
* 장소의 상세 주소에 대한 데이터 클래스 Address
**/
public class Address {
    private final String city;
    private final String country;
    private final String detail;
    private final String zipcode;

    public Address(String city, String country, String detail,
                   String zipcode) {
        this.city = city;
        this.country = country;
        this.detail = detail;
        this.zipcode = zipcode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city)
                && Objects.equals(country, address.country)
                && Objects.equals(detail, address.detail)
                && Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, country, detail, zipcode);
    }

    @Override
    public String toString() {
        return "Address{" +
                "city='" + city + '\'' +
                ", country='" + country + '\'' +
                ", detail='" + detail + '\'' +
                ", zipcode='" + zipcode + '\'' +
                '}';
    }
    /*
    getters
    */
}

/**
* 장소에 대한 리뷰 데이터 클래스 Review
**/
public class Review {
    private final long id;
    private final long reviewerId;
    private final int ratings;
    private final String comment;

    public Review(long id, long reviewerId, int ratings,
                  String comment) {
        this.id = id;
        this.reviewerId = reviewerId;
        this.ratings = ratings;
        this.comment = comment;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Review review = (Review) o;
        return id == review.id
                && reviewerId == review.reviewerId
                && ratings == review.ratings
                && Objects.equals(comment, review.comment);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, reviewerId, ratings, comment);
    }

    @Override
    public String toString() {
        return "Review{" +
                "id=" + id +
                ", reviewerId=" + reviewerId +
                ", ratings=" + ratings +
                ", comment='" + comment + '\'' +
                '}';
    }
    /*
    getters
    */
}

위와 같은 기존 방식은 세 가지 문제점이 있다.

  1. Boilerplate code
  2. 클래스 멤버 변수 변경에 따른 메소드 수정이 요구됨
  3. Poi, Address, Review 클래스가 단순 Data Carrier 클래스임을 한눈에 알기 어려움

Record 선언을 통해 클래스를 모델하여 선언하면 다음과 같이 완료된다.

/**
* 장소에 대한 메인 데이터 레코드 Poi
**/
public record Poi(long id,
                  String title,
                  String Description,
                  Address oldAddress,
                  Address newAddress,
                  int ReviewCount,
                  List<Review> reviews) {
}

/**
* 장소의 상세 주소에 대한 데이터 레코드 Address
**/
public record Address(String city,
                      String country,
                      String detail,
                      String zipcode) {
}

/**
* 장소에 대한 리뷰 데이터 레코드 Review
**/
public record Review(long id,
                     long reviewerId,
                     int ratings,
                     String comment) {
}

Record 선언으로 인해 세 가지 문제점이 해결되었다.

  1. Boilerplate code → Constructor, Setter, equals, hashCode, toString이 자동 생성
  2. 클래스 멤버 변수 변경에 따른 메소드 수정이 요구됨 → 데이터 멤버가 변경되어도 Record의 Header만 변경하면 완료
  3. Poi, Address, Review 클래스가 단순 Data Carrier 클래스임을 한눈에 알기 어려움 → record로 선언함으로써 명시적으로 데이터 클래스임을 알림

사용 시 유의사항

  1. 상속 불가능: record 클래스는 extends절 사용이 불가함 - No extends clause allowed for record
  2. final class: record 클래스는 암묵적으로 (implicitly) final 클래스이며, abstract 선언이 불가능하다.
  3. 멤버변수 선언 불가능: record 클래스 내부 인스턴스 필드는 명시적으로 선언할 수 없으나, static 변수는 생성 가능하다. record 헤더에 정의된 변수만 record의 상태를 정의하도록 하기 위한 제한
  4. Native Method 사용 불가능: record 클래스 내에서 native method는 사용이 불가능하다.

Instance of 변경


Java 14, 15에 preview feature로 존재하다가 자바 16에 정식 스펙으로 추가

instanceof 사용 시 객체 instance 확인 → 지역 변수 선언하여 객체 Casting 후 사용하는 과정을 단축하여 boilerplate code 작성을 줄이고 잠재적인 error 가능성을 최소화하기 위함

Example

// 기존 instanceof 사용 방식 #1
if (shape instanceof Rectangle) {
    Rectangle r = (Rectangle) shape; // Casting
    return 2 * r.length() + 2 * r.width();
} else if (shape instanceof Circle) {
    Circle c = (Circle) shape;       // Casting
    return 2 * c.radius() * Math.PI;
}

// 기존 instanceof 사용 방식 #2
public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString) &&
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

// 새로운 instanceof 사용 방식 #1
if (shape instanceof Rectangle r) {
    return 2 * r.length() + 2 * r.width();
} else if (shape instanceof Circle c) {
    return 2 * c.radius() * Math.PI;
}

// 새로운 instanceof 사용 방식 #2
public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString cis) &&
        cis.s.equalsIgnoreCase(s);
}

새로운 방식의 instanceof를 사용해 여러 명령문을 단일 표현식으로 결합하여 반복을 제거하고 제어 흐름을 단순화

사용 시 유의사항

  • 변수의 Scope 주의 패턴 변수는 기본적으로 지역 변수이므로 Local variable의 scope를 따르나, 조건문과 결합되어있으므로 instanceof true일 경우에만 도달할 수 있는 범위에서 사용할 수 있음
if (shape instanceof Rectangle r) {
  // r 사용가능
  // c 사용불가능
  return 2 * r.length() + 2 * r.width();
} else if (shape instanceof Circle c) {
  // r 사용불가능
  // c 사용가능
  return 2 * c.radius() * Math.PI;
}

조건문의 조건에 따라 변수를 도입한 문을 넘어 확장될 수 있음

public static boolean shapeTest(Shape shape){
    if (!(shape instanceof Rectangle r)) {
        // 항상 false이므로 r 사용 불가능
        return false;
    }
    // 위와 같은 경우 이 부분에서 r을 사용 가능함
    return r.length() > 10;
}

>>>> 컴파일시 다음과 같이 컴파일된다. (JDK16)

public static boolean shapeTest(Shape shape) {
    if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle)shape;
        return r.length() > 10.0D;
    } else {
        return false;
    }
}

조건문이 and일 경우 선행 조건(instanceof true) 만족하므로 가능하지만, or일경우 compile error 발생

double some = 0D;
if (shape instanceof Rectangle r && r.length() > 10) { // OK
    some = 2 * r.length() + 2 * r.width();
}

double some = 0D;
if (shape instanceof Rectangle r || r.length() > 10) { // cannot find symbol  symbol:   variable r
    some = 2 * r.length() + 2 * r.width();
}

Helpful Null Pointer Exception


Java 14에 정식 스펙으로 추가

JVM이 생성해주는 NPE에 어느 변수가 정확히 Null이었는지에 대한 정보를 추가하여 디버깅에 대한 도움을 주기 위함

JDK 14 이전

다음과 같이 고용인과 고용인의 개인정보를 모델링한 클래스가 있다고 가정한다.

public class Employee { // 고용인 클래스
    ...
    PersonalDetails personalDetails; // 고용인의 개인정보
    ...
    ...    
    public PersonalDetails getPersonalDetails(){ // 개인정보 Getter
        return personalDetails;
    }
    
    ...
}

public class PersonalDetails { // 개인정보 클래스
    ...
    String emailAddress; // 이메일 주소
    ...
    ...
    public String getEmailAddress(){ // 이메일 주소 Getter
        return emailAddress;
    }
    ...
}

고용인 객체를 통해 이메일 정보를 가져오기 위해서 다음과 같은 코드를 작성했을 때, NPE가 발생하였다.

java String emailAddress = employee.getPersonalDetails().getEmailAddress().toLowerCase(); 4.png

NPE가 발생한 것은 알 수 있지만, JVM은 메소드, 파일명, 행 번호만 출력하기 때문에 Root Cause가 무엇인지는 알기가 어렵다.

JDK 14 이후

동일한 코드를 JDK 14에서 실행시켰을 경우 에러 메시지가 달라지지만, 한가지 조건이 있다.

자세한 NPE 메시지는 JDK14에서 기본적으로 OFF 되어 있다. -XX:+ShowCodeDetailsInExceptionMessages 커맨드라인 옵션을 넣어줘야 작동한다.

옵션을 설정하고, 동일한 코드를 실행했을 때 디버깅 메시지는 다음과 같다.

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.manage.PersonalDetails.getEmailAddress()" because the return value of "com.manage.Employee.getPersonalDetails()" is null
at com.manage.Managing.main(Managing.java:7)

getPersonalDetails()가 Null이기 때문에 getEmailAddress()를 invoke하지 못했다고 정확한 Root Cause가 추적된다.

결론


이번 포스팅에서는 그동안 크고 작은 변화가 있었던 Java의 변경점들을 짚어보았다. 자바 11 이후 다음 LTS 버전인 17이 2021년 9월 출시되었으며, 다음 LTS 버전인 자바 21이 2023년 9월 출시가 예정되어 있다. 자바는 수많은 사람들이 고민하고 테스트한 끝에 유용한 기능들이 탑재되고 진화해가고 있으니 자바를 오롯이 잘 써먹는 개발자가 되기 위해 노력해보자.