[Java 11 -> 17] immutable Class와 record class, 그리고 compact constructor
개요
지금 회사에서 kotlin만 사용하던 나에게 다시 Java를 사용할 수 있는 기회가 찾아왔습니다. 뭐든지 역시 실제로 업무에서 사용하는 것 만큼 그 기술에 대해서 공부할 수 있는 좋은 동기부여는 없는것 같다라는 생각이 들었습니다. 소식으로만 접하던 Java의 변화에 대해서 실제 업무에서 쓰면서 그 가치를 제대로 느껴볼 수 있었습니다.
오늘 알아볼 내용은 Java의 record keyword 입니다. record keyword는 무엇이고 어디에 사용할 수 있고 등의 내용을 함께 알아보도록 하겠습니다.
immutable class (불변 클래스) 란 ?
한번 값을 설정하고 나면 수정할 수 없도록 클래스 instance를 생성하는 것을 immutable class 라고 일반적으로 말합니다. 특별한 이유가 없다면 클래스는 immutable 하게 생성하는것이 좋다고 말하는데 그 이유는 아래와 같습니다.
- thread 간에 메모리를 공유해도 동시성 이슈에 대한 걱정이 없습니다. (Thread-Safe)
- 동시성 이슈에 걱정이 없기 때문에 사용에 대한 안정성과 믿음이 있습니다.
- 이러한 특성 때문에 Map, Cache, Set 등의 요소로 사용하기 좋습니다.
물론 이러한 장점의 반대로 수정을 할 수 없기 때문에 수정을 하기 위해서는 값을 copy 하면서 값을 바꿔줘야하는 방식을 사용해야하기에 리소스가 좀 더 소모적일 수 있습니다. 하지만 이러한 단점은 과대 평가되어있다는 Oracle도 있고 GC가 적절하기 정리하기 때문에 안정성에 대한 장점이 오히려 우리가 집중해야할 부분입니다.
data를 immutable하게 하기 위해서는 Java 코드 기준으로 class는 아래를 만족해야합니다.
- class의 각 데이터는 private, final field를 사용합니다.
- 각 field는 getter method를 가집니다.
- public 생성자를 통해서 각 필드에 값을 할당할 수 있습니다.
- 모든 필드의 값이 동일하면 equals method는 true를 반환해야합니다.
- 모든 필드의 값이 동일하면 hashCode method는 true를 반환해야합니다.
- toString method는 class 이름과 각 필드의 이름과 값을 반환해야합니다.
예시 코드는 아래와 같습니다.
public class PersonDTO {
private final String name;
private final String address;
public PersonDTO(String name, String address) {
this.name = name;
this.address = address;
}
@Override
public int hashCode() {
return Objects.hash(name, address);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Person)) {
return false;
} else {
Person other = (Person) obj;
return Objects.equals(name, other.name)
&& Objects.equals(address, other.address);
}
}
@Override
public String toString() {
return "PersonDTO [name=" + name + ", address=" + address + "]";
}
public String name() {
return this.name;
}
public String address() {
return this.address;
}
}
이러한 특정을 가진 Immutable class는 일반적으로 VO 또는 DTO 클래스에 많이 사용됩니다. 반대로 Domain Entity는 mutable 하게 사용하기 때문에 일반 class를 사용하는것이 일반적입니다.
record class 란 ?
immutable class는 사용하기에 상당히 유용한 클래스고 장점도 많습니다. 하지만 이러한 클래스를 서비스가 커져감에 따라서 계속해서 만들어주는것은 반복되는 보일러플레이트 코드를 많이 만들어내게 됩니다. 지치게 되는것이죠. 따라서 Java에서는 이러한 코딩의 단점을 해소하고자 Java 14에서 record라는 keyword를 통해서 자동으로 만들어지도록 했습니다. 사용방법은 아래와 같습니다.
public record PersonDTO(
String name,
String address
){ }
record keyword를 이용하면 위에 만들었던 PersonDTO class를 단 4줄만으로 생성할 수 있게 됩니다.
compact-constructors
record는 그대로 사용해도 굉장히 심플하게 사용할 수 있는것을 알 수 있습니다. 그런데 사실 생성자에서 하는 역할이 필드를 주입하는 것 뿐만 있는 것이 아닙니다. nullable 체크를 할 수 있고 주입받는 값이 적절한지 판단하는 역할도 할 수 있습니다. 코드를 작성하면 아래와 같은 역할을 는 것입니다.
public record PersonDTO(
String name,
String address
){
public Person(String name, String address) {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
this.address = address;
this.name = name;
}
}
record에서는 이러한 역할을 compact constructor라는 개념으로 간편하게 사용할 수 있게 해줍니다. compact constructor는 생성자가 실행되기 전에 실행되는 code block 이며 이는 parameter가 없는 생성자와는 다릅니다. 아래 코드는 위의 생성자 코드와 동일한 역할을 합니다.
public record PersonDTO(
String name,
String address
){
// compact constructor!
PersonDTO {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
}
lombok과의 활용
record를 사용하면 더이상 lombok을 사용안해도 되는 것 아니냐라고 생각하실 수 있습니다. 즉, lombok과 record가 대척점에 있다고 생각하시는 분들이 있을 수 있습니다만 그렇지 않습니다. record도 단점이 없는것이 아니고 이 단점을 lombok이 완해해줄 수 있습니다. 즉 서로 보완할 수 있는 존재라고 인식하는것이 좋습니다.
예를 들어 records는 상속을 지원하지 않습니다. 따라서 이러한 부분이 필요할때는 record를 사용할 수 없으므로 lombok을 활용해야합니다.
추가로 Builder Pattern이 유용할 때가 있습니다. 이럴때는 lombok의 @Builder를 통해서 reocrd에 Builder 패턴을 추가해서 사용할 수 있도록 할 수도 있습니다.
마무리
오늘은 이렇게 Java 14에서 나온 record class와 compact constructor에 대해서 알아보는 시간을 가져보았습니다.
감사합니다.
참조
[1] https://www.baeldung.com/java-record-keyword
[2] https://www.baeldung.com/java-record-vs-lombok
[3] https://mikemybytes.com/2022/02/16/java-records-and-compact-constructors/
[4] https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html