참고
불변 객체
NOTE
불변 클래스는 인스턴스의 내부 값을 수정할 수 없는 클래스이며 가변 클래스보다 훨씬 설계하기 좋고 안전합니다.
불변 객체는 자바의 공유 참조와 사이드 이펙트의 문제점을 보면 왜 필요한지 알 수 있습니다.
공유참조의 사이드 이펙트 문제
•
자바에서는 기본적으로 하나의 객체를 여러 변수가 공유하지 않도록 막을 수 있는 방법이 없습니다.
•
사실 객체의 공유는 문제가 되지 않습니다. 실제 문제는 이렇게 공유되고 있을 때 객체의 값을 변경하기 때문에 사이드 이펙트가 발생하게 됩니다.
공유참조의 사이드 이펙트를 막기위한 불변객체 사용
•
객체의 변경을 막기 위해서 불변 객체를 사용합니다.
불변객체의 특징
•
불변 객체는 상태를 변경하는 메서드를 제공하지 않습니다.
•
불변 객체는 동일한 값을 가진 객체가 필요할 때마다 기존의 불변 객체를 재사용할 수 있습니다.
•
여러 스레드에서 동시에 불변 객체를 사용해도 안전합니다.
•
불변 객체는 내부 상태가 변경되지 않으므로, 예외가 발생하더라도 객체의 상태가 손상될 위험이 없습니다. (실패 원자성)
불변 객체 생성규칙
NOTE
// final class(상속 불가)
public final class Complex {
// 필드 private final로 선언(수정 불가)
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double realPart() {
return re;
}
// ...
}
Java
복사
불변 클래스 예제
1.
객체 변경 메서드를 제공하지 않으며 모든 필드를 private final로 선언합니다.
2.
final class를 이용하여 상속하지 못하도로 막습니다.
3.
자신 외에 내부 가변 컴포넌트에 접근할 수 없도록 한다.
불변 객체 값 변경
NOTE
불변 클래스를 사용하더라도 값을 변경해야 하는 메서드가 필요한 경우, 새로운 객체를 생성하여 반환하면 됩니다.
public class MutableObj {
private int value;
public MutableObj(int value) {
this.value = value;
}
// 필드의 값을 수정한다.
public void add(int addValue) {
value = value + addValue;
}
}
Java
복사
가변 객체의 값 변경
public class ImmutableObj {
private final int value;
public ImmutableObj(int value) {
this.value = value;
}
// 객체 자체를 새로 만들어서 반환해준다.
public ImmutableObj add(int addValue) {
int result = value + addValue;
return new ImmutableObj(result);
}
Java
복사
불변 객체의 값 변경
withxxx()
•
불변 객체에서 값을 변경하는 경우 일반적으로 withYear()처럼 with로 시작하는 경우가 많습니다.
•
coffee with sugar라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만드는 것을 의미하듯이 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 의미합니다.
모든 클래스를 불변으로 만들면 안된다.
NOTE
불변 클래스는 많은 장점이 있지만, 특정 상황에서는 반대의 결과를 초래할 수 있습니다.
•
불변 객체는 상태를 변경할 때마다 새로운 객체를 생성해야 하므로, 객체 생성과 가비지 컬렉션의 증가로 메모리 사용량이 늘고 성능에 부정적인 영향을 미칩니다.
•
모든 상황에서 불변 객체를 사용하면 사용성이 제한될 수 있습니다. 예를 들어, 대용량 데이터를 처리하는 애플리케이션에서 불변 객체를 사용하면 데이터 수정 작업이 비효율적이 될 수 있습니다.
자바 레코드
NOTE
자바 레코드는 불변 객체를 만드는 작업을 간소화 하기 위해 도입된 클래스 유형입니다.
public record Person(String name, int age) {}
Java
복사
자바 record 코드
// 클래스는 fainl(상속 불가)로 선언된다.
public final class Person {
// 모든 변수는 final(변경 불가)로 선언된다.
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person[name=" + name + ", age=" + age + "]";
}
}
Java
복사
자바 record 코드가 실제로 담고있는 코드
•
멤버 변수는 private final로 선언되며, 각 필드에 대한 getter와 모든 멤버 변수를 포함하는 생성자를 자동으로 생성합니다.
•
기본적으로 equals(), hashCode(), toString() 메서드를 자동으로 생성합니다.
◦
IDE에서 자동으로 생성하는 값을 참고하면 됩니다.
public record Person(String name, int age) {
// 정적 필드 예시
public static final int LEGAL_ADULT_AGE = 18;
// 인스턴스 메서드 예시
public boolean isAdult() {
return age >= LEGAL_ADULT_AGE;
}
// 정적 메서드 예시
public static boolean isAdult(int age) {
return age >= LEGAL_ADULT_AGE;
}
// 인스턴스에 대한 추가 정보를 제공하는 메서드
public String greeting() {
return "Hello, my name is " + name + " and I am " + (isAdult() ? "an adult." : "not an adult.");
}
}
Java
복사
record 추가구현
•
record에서는 추가적인 필드가 기본적으로 허용되지 않지만, static 필드와 메서드의 추가는 가능합니다.
빌더 패턴
NOTE
record에서도 static 메서드로 Builder 패턴을 구현할 수 있습니다.
public record Person(String name, int age) {
public static class Builder {
private String name;
private int age;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(name, age);
}
}
}
Java
복사
커스텀 생성자 - 컴팩트 생성자
NOTE
컴팩트 생성자는 record의 모든 필드를 초기화한 후, 추가적인 검증이나 설정을 수행할 때 주로 사용됩니다.
public record Person(String name, int age) {
public Person {
if (name == null) name = "Unknown";
if (age < 0) age = 0;
}
}
Java
복사
컴팩트 생성자 기법
public final class Person {
private final String name;
private final int age;
// 검증로직
public Person(String name, int age) {
if (name == null) {
this.name = "Unknown";
} else {
this.name = name;
}
if (age < 0) {
this.age = 0;
} else {
this.age = age;
}
}
}
Java
복사
실제로 만들어지는 코드
상속과 record
NOTE
record는 java.lang.Record를 상속받기 때문에, 다른 클래스를 상속받을 수 없습니다. 하지만 인터페이스는 구현할 수 있습니다.
public record AddressRecord(String street,
String city,
String state,
String zipCode)
implements Comparable<AddressRecord> {
@Override
public int compareTo(AddressRecord other) {
return this.zipCode.compareTo(other.zipCode);
}
}
Java
복사
Comparable 구현
Java record의 직렬화 및 역직렬화
NOTE
record가 기본 생성자 없이도 직렬화가 가능한 이유는 자바의 record가 가지는 구조적 특성 덕분입니다. 자바의 record는 불변 객체를 간단히 표현하기 위한 구문으로, 컴파일러가 자동으로 몇 가지 기능을 제공합니다. 이를 통해 직렬화 및 역직렬화가 원활하게 이루어질 수 있습니다.
1.
자동 생성된 생성자:
•
record는 필드를 모두 인자로 받는 주 생성자를 자동으로 생성합니다.
•
이 생성자를 사용하여 역직렬화할 때 필요한 모든 정보를 제공할 수 있습니다.
2.
명확한 필드 정의:
•
record의 필드는 항상 명확히 정의되어 있으며, 불변성으로 인해 변경되지 않습니다.
•
이는 Jackson과 같은 라이브러리가 필드 이름과 타입을 쉽게 파악할 수 있게 합니다.
3.
자동 생성된 메서드:
•
record는 필드를 읽기 위한 getter 메서드를 자동으로 생성합니다.
•
이를 통해 직렬화할 때 객체의 상태를 쉽게 접근할 수 있습니다.
Jackson과 record
Jackson 라이브러리는 자바 객체를 JSON으로 직렬화하거나 JSON을 자바 객체로 역직렬화하는 데 널리 사용됩니다. record는 다음과 같은 이유로 Jackson과 호환됩니다:
1.
주 생성자 활용:
•
Jackson은 기본적으로 클래스의 생성자를 사용하여 객체를 생성합니다.
•
record는 모든 필드를 초기화하는 주 생성자를 제공하므로, Jackson이 이 생성자를 사용하여 객체를 생성할 수 있습니다.
2.
기본 생성자 필요 없음:
•
전통적인 클래스에서는 기본 생성자가 없으면 Jackson이 객체를 생성할 수 없습니다. 그래서 @NoArgsConstructor를 사용하여 기본 생성자를 추가합니다.
•
record는 주 생성자를 통해 모든 필드를 초기화하기 때문에 기본 생성자가 필요 없습니다.
예시 코드
아래는 Jackson을 사용하여 record를 직렬화하고 역직렬화하는 예시입니다:
Record 정의
public record UserDTO(String name, int age) {}
Java
복사
직렬화
ObjectMapper mapper = new ObjectMapper();
UserDTO user = new UserDTO("John", 30);
String json = mapper.writeValueAsString(user);
System.out.println(json); // {"name":"John","age":30}
Java
복사
역직렬화
String json = "{\"name\":\"John\",\"age\":30}";
UserDTO user = mapper.readValue(json, UserDTO.class);
System.out.println(user); // UserDTO[name=John, age=30]
Java
복사
이와 같이, Jackson은 record의 주 생성자를 사용하여 객체를 역직렬화할 수 있습니다. 이는 record가 명확한 구조를 가지고 있어 가능한 것입니다.
결론
•
기본 생성자 없이 직렬화/역직렬화 가능: record는 모든 필드를 초기화하는 주 생성자를 자동으로 제공하므로 기본 생성자가 없어도 직렬화 및 역직렬화가 가능합니다.
•
Jackson 호환성: Jackson은 record의 주 생성자를 활용하여 객체를 생성할 수 있으므로 추가 설정 없이도 잘 동작합니다.