Search
Duplicate
📒

[Java Study] 04-1. 불변 객체, Record

상태
수정중
수업
Java Study
주제
연관 노트
3 more properties
참고

불변 객체

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의 주 생성자를 활용하여 객체를 생성할 수 있으므로 추가 설정 없이도 잘 동작합니다.