
GIF 출처 : https://www.amigoscode.com/courses/java
1 ) 불변성 ( Immutability )
2 ) 레코드 ( Record )
3 ) 빌더 패턴 ( Builder Pattern )
불변성 ( Immutability )
: 객체 지향 프로그래밍에서는 일반적으로 객체에 캡슐화된 가변 프로그램 상태를 다루는 것을 의미하나 함수형 접근 방식에서는 객체의 상태가 변하지 않는 불변 상태의 데이터를 처리하는 방식을 다룬다.
예를 들어 , 자바에서는 setter 메소드를 통해 필드의 상태를 변경하여 객체의 상태를 변화시킨다. 그러나 이러한 변경 작업은 이전 상태의 손실을 유발하며 가변 상태 자체는 복잡성과 불확실성을 유발한다.
복잡성은 공유된 상태에 액세스하는 컴포넌트들의 수명과 관련한 정보에 영향을 끼쳐 많은 문제가 발생하게 된다. 또한 상태 변화로 인한 리소스 낭비 문제가 발생할 가능성이 있다.
ex ) 자바의 String 타입
자바의 스트링 타입의 경우 문자열을 연결하게 되면 그 결과로 새로운 String 객체가 생성된다.
이때 , 새로운 힙 메모리 영역에 새로운 String 인스턴스가 생성되어 메모리를 차지하게 된다.
또한 for 혹은 while 과 같은 루프문으로 인해 인스턴스 생성 및 메모리 할당이 기하급수적으로 증가하게 되어 많은 문제를 발생시킨다.

따라서 이러한 불변성을 유지하기 위해서 함수형 프로그래밍에서는 자료 구조는 생성 후에 변경할 수 없어야 한다 라는 기본 원칙을 준수하도록 제시하고 있다.
- 불변성의 장점
① 예측 가능성
: 자료 구조를 참조하는 한 , 생성된 시점과 동일한 상태임을 알 수 있다.
② 유효성
: 자료 구조는 초기화한 후 완전한 상태가 되며 단 한번의 검증만 필요하며 유효 상태로 유지된다.
③ 스레드 안전성
: 사이드 이펙트가 발생하지 않는 불변 자료 구조는 스레드 경계를 자유롭게 이동할 수 있다.
불변 컬렉션 ( Immutal Collection )
자바의 컬렉션의 경우 총 3가지 방식을 통해서 불변성을 제공하고 있다. 해당 방식 3가지는 모두 필요한 인스턴스를 생성하기 위한 정적 편의 메소드 ( Static Convenience Method ) 를 제공한다.
이러한 정적 편의 메소드는 얕은 불변성만을 가지고 있어 요소를 추가하거나 제거하는 것은 불가능 하다.
① 변경 불가능한 컬렉션
: 변경 불가능한 컬렉션은 java.util.Collections 클래스의 일반 정적 메소드 중 하나를 호출하여 기존 컬렉션에서 생성한다.
ex )
Collection<T> unmodifiableCollection(Collection<? extends T> c)
Set<T> unmodifiableSet(Set<? extends T> s)
List<T> unmodifiableList(List<? extends T> l)
Map<T> unmodifiableMap(Map<? extends k, ? extends V> m)
SortedSet<T> unmodifiableSortedSet(SortedSet<T> s)
NavigableSet<T> unmodifiableNavigableSet(NavigableSet<T> s)
NavigableMap<K,V> unmodifiableNavigableMap(NavigableMap<K,V> m)
② 불변 컬렉션 팩토리 메서드
: 원하는 요소들을 직접 컬렉션 타입의 정적 편의 메서드를 통해 전달하여 불변 컬렉션 객체를 생성한다.
List<E> of( E e1 , ... )
Set<E> of( E e1 , ... )
Map<K,V> of( K k1 , V v1 , ... )
③ 불변 복제
: 단순히 뷰를 제공하는 것이 아닌 새로운 컨테이너를 생성하여 요소들의 참조를 독립적으로 유지한다.
Set<E> copyOf(Collection<? extends E> c)
List<E> copyOf(Collection<? extends E> c)
Map<K,V> copyOf(Map<? extends K,? extends V> map)
자바에서는 이러한 불변성을 위해 14버전 이후 불변 자료 구조인 레코드 ( Record ) 를 도입하였다. 레코드는 상태 선언으로 구성된 얕은 불변성을 가진 데이터 운반체이다.
추가 코드 없이 getter ,toString , hashCode 메소드 사용이 가능하다는 장점이 있다.
< 레코드의 기본 형식 >
public record User ( String username ,
boolean active ,
LocalDateTime lastLogin ) {
// 바디 생략
}
레코드의 장점은 바디 부분을 채우지 않아도 메소드를 사용할 수 있으며 getter 가 아닌 필드 이름을 통해서 데이터에 접근할 수 있다.
ex )
User user = new User(woojuice,true,2025-05-23);
String username = user.username(); // "woojuice" 를 반환
레코드는 일반적인 클래스와 달리 바디를 생략해도 되기때문에 반복적으로 보일러플레이트를 작성할 필요가 없다는 장점이 있다. 또한 표준 생성자 ( Canonical Constructor ) 가 존재하기 때문에 레코드의 각 컴포넌트에 따라 자동으로 생성자가 생성된다.
- 레코드의 특징
① 상속 불가
: 레코드는 상속을 사용할 수 없다. 그러나 인터페이스를 구현할 수 있고 인터페이스를 사용하여 레코드 템플릿을 정의하고 default 메소드를 통해 공통 기능을 공유할 수 있다.
② 표준 생성자
: 앞서 언급한 표준 생성자만을 가지고 있으며 특정 필드값만 설정하는 것이 아닌 모든 요소를 설정하도록 한다.
자바에서 레코드는 불변성 유지를 위해 사용된다. 스프링에서는 DTO처럼 값을 전달하기 위한 목적의 클래스의 경우 값이 변경되어서는 안되기 때문에 final 필드로 처리되는 레코드를 통해 이를 방지한다.
ex )
@Builder
@Getter
@AllArgsConstructor
public class ApiErrorResponse {
private final HttpStatus httpStatus;
private final String code;
private final String message;
public ApiErrorResponse(ErrorCode errorCode){
this.httpStatus = errorCode.getHttpStatus();
this.code = errorCode.name();
this.message = errorCode.getMessage();
}
public static ResponseEntity<ApiErrorResponse> error(CustomException e){
return ResponseEntity
.status(e.getErrorCode().getHttpStatus()) // status 설정
.body(ApiErrorResponse // body 설정
.builder()
.httpStatus(e.getErrorCode().getHttpStatus())
.code(e.getErrorCode().name())
.message(e.getErrorCode().getMessage())
.build());
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<ApiErrorResponse> handleCustomException(CustomException e){
return ApiErrorResponse.error(e);
}
}
또한 getter , setter 다르게 필드의 이름으로 필드 값을 불러오거나 초기화할 수 있기 때문에 보일러플레이트를 제거하여 간결성을 얻을 수 있는 장점이 있다.
그러나 DTO처럼 단순히 값을 전달하기 위한 용도로만 사용해야 하며 서비스 로직을 담은 클래스에서는 값의 변경이 필요하기 때문에 일반클래스를 사용해야 한다.