최근 프로젝트에서 REST API의 DTO를 개발하며 불변객체를 사용하는게 적합하다고 생각했고, 그 이유에 대해서 공유하고 싶어서 글을 쓰게 되었다.
불변객체(Immutable Object)가 뭘까?
객체가 생성된 이후 내부 상태가 일정하게 유지되는 객체
즉, 객체가 생성되고 파괴될 때까지 동일한 동작을 할 것을 보장하는 객체라는 뜻이다. 불변객체는 내부 상태를 조회하는 메서드는 가질 수 있지만 setter같은 내부 상태를 변화하는 메서드는 가질 수 없다. 일정한 상태를 유지하기 때문에 멀티 스레드 환경에서 안정적이고, side-effect가 적다.
Java에서의 불변객체는?
자바에서는 field의 값의 변경을 막지 않기 때문에, final 키워드를 사용하여 내부 field의 값을 변경하는 것을 막을 수 있다. 이때 주의해야할 점이 있는 데, field가 Reference 타입일 경우에 final 키워드는 field에 저장된 참조값의 변경만 막기 때문에 실제 메모리에 저장된 타입의 상태 변경을 막을 수 없다. 또한 Reflection API를 사용한 객체 상태 변경은 불변객체의 원칙에 위반되므로 지양해야한다.
자바에서 불변객체를 선언하는 방법은 다음과 같다.
1. 데이터의 모든 필드는 private과 final로 선언되어야 한다.
2. 각 필드에 대한 getter 메서드
3. 각 필드에 해당하는 인수가 있는 public 생성자
4. 모든 필드가 일치할 때 동일한 클래스의 객체에 대해 true를 반환하는 equals
5. 메서드모든 필드가 일치할 때 동일한 값을 반환하는 hashCode 메서드
6. 클래스 이름과 각 필드 이름 및 해당 값을 포함하는 toString 메서드
// 조회하는 메서드만 가질 수 있다.
@Getter
@toString
@EqualsAndHashCode
public class Person {
private final int age;
public Person(int age) {
// 상태 변경은 딱 한번, 생성자에서만 일어난다.
this.age = age;
}
}
그렇다면 불변객체를 사용하기 위해서 매번 규칙에 맞게 객체를 선언해야할까? 객체 간 불변한 데이터를 전하기 위해 코드가 반복되고 의미에 맞지 않게 사용하는 문제가 있지 않을까?
record 키워드에 대해서 알아보자
이에 대한 해결책으로 Java 14부터 record를 지원하기 시작했다. record를 적용하면 위의 예제 코드가 다음과 같이 변경된다.
public record Person (int age) {}
record 키워드는 별도의 코드 작성 없이 public 생성자, getter(<record명>.<field명>()), equals, hashCode, toString 메서드를 제공하여 코드를 깔끔하게 유지할 수 있다.
Lombok을 쓰면 될텐데 record를 사용하는 이유는?
결론부터 말하자면 둘 중 어떤게 옳다고 할 수 없다. 상황에 맞게 선택해아한다.
작은 객체일 때는 record를 사용하자
Lombok @Value로 간단하게 불변객체를 선언할 수 있다. Lombok은 Java에서 기본적으로 제공하는 라이브러리가 아니기 때문에, Java 14 이후 버전이라면 record를 쓰는 편이 합리적이다.
public record ColorRecord(int red, int green, int blue) {
public String getHexString() {
return String.format("#%02X%02X%02X", red, green, blue);
}
}
@Value
public class ColorValueObject {
int red;
int green;
int blue;
public String getHexString() {
return String.format("#%02X%02X%02X", red, green, blue);
}
}
Transparent Data Carriers인 record
record는 Transparent Data Carriers이기 때문에 무조건 전체 field가 getter메서드로 노출된다. 특정 field를 노출되지 않게 하고 싶다면 Lombok을 사용하여 선택적으로 노출할 수 있고, 아니라면 record를 사용하는 편이 좋다.
field가 많은 객체의 경우에는 Lombok을 사용하자
record의 코드와 Lombok의 @Builder 어노테이션을 사용한 코드를 비교해보자.
record를 사용한 코드
public record StudentRecord(
String firstName,
String lastName,
Long studentId,
String email,
String phoneNumber,
String address,
String country,
int age) {
}
StudentRecord john = new StudentRecord(
"John", "Doe", null, "john@doe.com", null, null, "England", 20);
Lombok을 사용한 코드
@Getter
@Builder
public class StudentBuilder {
private String firstName;
private String lastName;
private Long studentId;
private String email;
private String phoneNumber;
private String address;
private String country;
private int age;
}
StudentBuilder john = StudentBuilder.builder()
.firstName("John")
.lastName("Doe")
.email("john@doe.com")
.country("England")
.age(20)
.build();
Lombok의 @Builder을 사용하는 편이 코드의 가독성이 좋기 때문에 Lombok 사용을 추천한다.
상속이 필요할 때는 Lombok을 사용하자
record는 상속이 불가능하다. 하지만 Lombok의 @Value는 상속이 가능하면서 final이다.
@Value
public class MonochromeColor extends ColorData {
public MonochromeColor(int grayScale) {
super(grayScale, grayScale, grayScale);
}
}
글을 마치며
불변객체와 record에 대해서 알아봤다. 앞으로 단순히 데이터를 옮기는 작업을 하는 객체에 적용해보도록 하자. 코드 중복이 줄어들고 가독성 좋은 코드를 작성할 수 있을 것이다. 물론 java 개발환경에서 Reference 타입에는 조금 더 주의를 기울여야 하지만, 멀티 스레드 환경에도 안정적으로 상태를 유지할 수 있다면 안 쓸 이유가 없는 것 같다.
References
https://www.baeldung.com/java-immutable-object
https://www.baeldung.com/java-record-keyword
https://www.baeldung.com/java-record-vs-lombok
'Java' 카테고리의 다른 글
만두의 Exception? (0) | 2024.08.05 |
---|---|
븟츠의 JWT 소개 및 정리 (0) | 2024.08.03 |
븟츠의 try-finally 보다는 try-with-resources를 사용하자! (0) | 2024.07.28 |
[디자인 패턴] 븟츠의 객체 생성시 생성자 vs 빌더 어떤 것을 사용할까? (0) | 2024.07.20 |
[디자인 패턴] 만두의 팩토리 메서드 패턴(Factory Method) (0) | 2024.07.14 |