불변 객체란 객체가 생성된 이후에 그 상태(필드 값)이 절대로 변하지 않는 객체 입니다.
특징
- 객체 내부의 상태(필드 값)이 변경 되지 않습니다.
- 여러 스레드에서 동시에 읽거나 사용해도 안전하게 동작합니다. 값이 변하지 않아 동기화를 신경 쓰지 않아도 됩니다.
- 상태가 고정되어 있어 외부에서 잘못된 수정이 발생하지 않아 코드의 신뢰성이 높아집니다.
생성 방법
- 필드 값이 수정 되지 않도록 객체 생성(초기화)시에 입력되는 값을 기본으로 변경 되지 않게 코드를 구성해주어야 합니다.
- 이를 만족하기 위해 언어가 제공하는 기능들을 조합하여 환경을 조성합니다.
Java 불변 객체 만들기
- 필드에
final
키워드final
은 필드 값을 한 번 설정한 후에는 변경하지 않겠다는 의미입니다.- 초기화 시점을 강제 해 줍니다. final 변수는 선언 시에 초기화되거나, 클래스 생성자에서 초기화되어야 합니다.
- 해당 키워드를 붙인 필드를 초기화 이후 변경 하는 코드가 있다면 런타임 오류가 발생합니다.
// 오류 발생하는 코드 예시 : 초기화 시점 규칙 지키지 않음 public class FinalTest { static class Car { // final이 선언된 필드를 생성자나 선언 시 동시에 초기화 해 주지 않아 오류가 발생합니다. final String name; final int maxSpeed; void setName(String name) { this.name = name; } void setMaxSpeed(int speed) { this.maxSpeed = speed; } void run() { System.out.println(this.name + "가 달립니다. 최대속도는" + this.maxSpeed + " 입니다."); } } public static void main(String[] args) { Car car = new Car(); car.setName("수정카"); car.setMaxSpeed(200); car.run(); } }
// 오류 발생하는 코드 예시 : 값 초기화 이후 final 필드 값을 수정하려고 함 public class FinalTest { static class Car { final String name = "수정카"; final int maxSpeed = 200; void setName(String name) { this.name = name; } void setMaxSpeed(int speed) { this.maxSpeed = speed; } void run() { System.out.println(this.name + "가 달립니다. 최대속도는" + this.maxSpeed + " 입니다."); } } public static void main(String[] args) { Car car = new Car(); car.setName("수정의끝내주는카"); // 초기화 된 final 필드를 수정하려고 함 car.setMaxSpeed(400); car.run(); } }
// 올바른 사용법 public class FinalTest { static class Car { final String name; // 보통은 필드를 private로 설정하고 get 메서드를 만들어 접근을 막습니다. 해당 주제와 관련이 없어 설정 하지 않았습니다. final int maxSpeed; public Car(String name, int maxSpeed) { this.name = name; this.maxSpeed = maxSpeed; } void run() { System.out.println(this.name + "가 달립니다. 최대속도는" + this.maxSpeed + " 입니다."); } } public static void main(String[] args) { Car car = new Car("수정카", 200); car.run(); } }
- Setter 메서드 제거
- 위에서 살펴봤듯이, final 필드는 setter 메서드로 이후에 수정하려고 하면 컴파일 에러가 발생합니다.
- 따라서 혼동을 주지 않기 위해 애초에 제거 합니다.
- 컬렉션 같은 가변 객체를 필드로 사용할 때에는, 방어적 복사를 통해 변경을 방지
public final class ImmutableList {
private final List<String> items;
public ImmutableList(List<String> items) {
this.items = new ArrayList<>(items); // 생성자를 통해 컬렉션 필드 초기화
}
}
활용 방법 : Java record
record
는 Java16 부터 정식 기능으로 제공되고 있습니다.- 불변 데이터 객체를 쉽게 표현할 수 있도록 타입을 제공합니다.
- 위 처럼 setter를 없애고, final을 선언해주지 않아도 최소한의 코드로 불변 객체를 생성할 수 있습니다.
특징
- 자동으로 불변성을 보장하는 코드 생성
- 모든 필드가 기본적으로
final
로 생성됩니다. - 모든 필드가 읽기 전용으로 생성되어 setter 메서드가 존재하지 않습니다.
- 모든 필드가 기본적으로
- 선언된 필드와 초기화 상태를 기반으로 생성자 메서드를 자동 생성
- 값 기반의 동등성
- 객체의 참조가 아니라 필드 값에 기반하여
equals()
및hashCode()
가 자동 구현됩니다.
- 객체의 참조가 아니라 필드 값에 기반하여
생성 방법
- 아래 코드는 위 java class 예시와 같은 기능을 합니다.
record CarRecord (String name, int maxSpeed) {
void run() {
System.out.println(this.name + "가 달립니다. 최대속도는" + this.maxSpeed + " 입니다.");
}
}
정리
불변 객체를 사용 하면 값의 변경을 방지할 수 있어 동시성 문제를 신경 쓰지 않아도 되는 장점이 있습니다.
동시성에 안전하고 값이 보장되는 것은 코드의 신뢰성을 높여줍니다. 따라서 값의 변경이 자주 일어나지 않는 객체는 되도록 불변 객체로 설계하는 것이 좋은 설계입니다.
java에서 불변 객체를 쉽게 사용하기 위해서 record 타입 사용을 고려해볼 수 있습니다. 짧은 코드로 불변객체를 만들 수 있어 편리합니다.
하지만 record는 class 자체도 final로 선언되기 때문에 클래스 상속 기능을 사용할 수 없습니다.
따라서 실무나 프로젝트를 할 때에는 Entity 계층이 아닌 DTO를 record 타입 불변 객체로 생성하여 사용합니다.
Entity는 상속이나 구성을 이용하여 생성하는게 좋은 설계일 수 있기 때문에 record를 사용하면 확장성이 줄어들 수 있기 때문입니다.
Dto는 보통 요청이나 응답을 전달하기 위한 객체로 사용 됩니다. 확장성이 필요하지 않고 값이 변하지 않게 '전달'하는 것이 주요 목적이기 때문에 사용하기 적합합니다.
오늘도 읽어주셔서 감사합니다 :-)
수정이나 질문사항 있으면 언제든 리뷰 부탁드립니다.
'Architecture' 카테고리의 다른 글
헥사고날 아키텍처를 떠먹어보자 (3) | 2024.12.01 |
---|