본문 바로가기

함수형 자바

[JAVA] 불변성 - Chapter 04

함수형 프로그래밍 4장에서는 불변성을 다뤘다.

JDK에서 제공하는 불변 타입과, 자료 구조를 불변하게 만드는 방법에 대해 소개하였다.

 

객체 지향 프로그래밍의 가변성

객체는 일반적으로 setter 메서드를 사용하여 객체의 상태를 변화시킬 수 있다.

JDK의 컬렉션 프레임워크들도 대부분 가변상태와 변경 가능한 방식 기반으로 설계 되었다. (ex List.add, List.remove)

 

가변 상태는 복잡성과 불확실성을 유발한다.

이러한 가변 상태의 공유는 동시성 프로그래밍에서 문제가 발생 할 수 있다.

 

함수형 프로그래밍의 불변성

불변성은 생성 후 변경할 수 없는 상태이다.

자료 구조를 변경하려면 새로운 복사본을 생성해야 한다.

 

아래에서 몇가지 불변성의 특징에 대해 알아보자.

 

1) 예측 가능성 (Predictability)

자료 구조를 참조하는 한, 생성된 시점과 동일한 상태이다.

 

2) 유효성 (Validity)

초기화한 후, 자료 구조는 완전한 상태가 된다. (단 한번의 검증만 필요)

 

3) 숨겨진 사이드 이펙트 없음 (No hidden side effect)

불변 구조는 값을 변경하지 않아 의도하지 않은 사이드 이펙트를 발생시키지 않는다.

 

4) 스레드 안정성 (Thread safety)

어떤 스레드도 불변인 자료 구조를 변경할 수 없다.

 

5) 캐시 가능성 및 최적화 (Cacheability and optimization)

불변 자료 구조를 신뢰하고 캐싱 할 수 있다.

메모이제이션(Memoization, 주어진 입력에 대해 계산된 결과를 저장해 두었다가, 같은 입력이 들어오면 저장된 결과를 다시 사용하는 기법)과 같은 최적화 기술은 불변 자료에만 사용이 가능하다.

 

6) 변경 추적 (Change tracking)

모든 변경이 새로운 자료 구조를 생성한다면, 이전 참조를 저장함으로써 이전 상태를 추적 할 수 있다.

 

 

자바 불변성

자바의 초기에는 다양한 불변 자료 구조를 제공하지 않았다.

14버전 부터는 불변 자료 구조인 레코드 (Record)를 도입하였다.

 

아래에서 JDK에서 사용 가능한 불변 타입들을 알아보자.

 

1) String

문자열을 + 로 연결 가능하다. 이 때 새로운 객체가 생성된다.

따라서 iter로 연결 작업 지양하는 것이 좋다.

가변인 String Builder로 사용하고, 변경이 끝났을 때 불변 객체로 바꾸거나  invokedynamic을 활용하는 것이 좋다.

 

문자열 리터럴 (Literal) 같은 경우에는 동일한 리터럴은 한 번만 저장되고 재사용 되어 메모리 절약에 도움이 된다.

 

 

문자열 리터럴의 경우에는 같은 참조값을 가지므로 == 비교가 가능하지만, 객체로 생성되는 String의 경우, 값은 같더라도 참조 값이 달라 equals()를 사용하여 동등성 비교를 해야한다.

 

다만 String은 완전히 불변한 타입이 아닌데,

이는 String 객체 내부의 hash 필드의 value가 지연해서 계산되기 때문이다.

String에서 처음 hashCode를 호출 할 때, 계산된 값으로 해당 필드 값이 변경되기 때문에 완전한 불변 객체라 볼 수는 없다.

 

hash 필드는 value와 다르게 final로 선언되어 있지 않다.

 

2) 불변 컬렉션

Set, List, Map 같은 Collections이 이에 해당된다.

new 키워드를 사용하여 직접 인스턴스화 할 수 있는 public 타입은 아니나 정적 편의 메서드를 제공한다.

 

얕은 불변성만 갖고 있어, 요소 자체의 불변성을 보장하지는 않는다.

즉, 참조는 변경되지 않지만, 참조된 자료 구조 요소들은 변경 될 수있다. (List의 add메서드를 통해 요소에 값을 변경이 가능하다.)

 

완전한 불변성을 지닌 자료구조를 구현하기 위해서는 요소 또한 불변해야 하는데,

이를 위해 Collections의 클래스의 정적 메서드들이 유용한 도구들을 제공한다.

 

변경 불가능한 컬렉션

 

Collections의 unmodifiable + 컬렉션 타입 형태 (ex unmodifiableList)의 정적 메서드들이 존재하는데,

이들은 인수로 받은 타입과 같은 타입을 반환하지만, 반환된 인스턴스를 수정할 때 UnsupportedOperationException을 발생시켜 요소들이 불변하도록 한다.

 

다만, 인수로 받은 Collections의 원본의 요소들의 변경은 막지 못한다.

 

List<String> original = new ArrayList◊(); 

original.add("blue");
original.add("red");

List<String>unmodifiable = Collections.unmodifiablelist(original);

original.add("green");

System.out.println(unmodifiable.size()); // 2일거라 기대했지만 3이 출력됨

 

 

 

 

불변 컬렉션 팩토리 메서드

 

자바 9에서 도입되었으며, of 형태의 해당 컬렉션 타입의 정적 편의 메서드를 통해 전달한다.

// 불변 리스트 생성
List<String> immutableList = List.of("apple", "banana", "cherry");


// 요소 변경 시도 (UnsupportedOperationException 발생)
try {
	immutableList.add("date");
} catch (UnsupportedOperationException e) {
	System.out.println("Cannot add element to immutable list");
}

 

 

불변 복제

 

자바 10+ 에서 사용 가능하고, copyOf를 호출하여 더 깊은 불변성을 제공한다.

// 원본 리스트 설정
List<String> original = newArrayList<>();
original.add("blue");
original.add("red");

//복사본 생성
List<String> copiedlist = List.copyOf(original);

//원본 리스트에 새 항목 추가
original.add("green");

//내용 확인
System.out.println(original); //[blue, red, green]
System.out.println(copiedli t); //[blue, red]

 

원본 리스트 요소는 변경 가능하지만, 변경 불가능한 컬렉션과는 달리 요소들의 참조를 독립적으로 유지한다.

 

 

3) 원시 타입과 원시 래퍼

원시 타입 (byte, char, short, int, long ...)등은 객체와 다르게 동작한다.

이들은 리터럴 또는 표현식을 통해 초기화 되는 단순한 값이고, 사실상 불변의 특성을 갖는다.

 

원시 타입 외에 객체 래퍼 타입 (Byte, Integer, Long ..)등은 원시타입을 객체 타입을 캡슐화 한 것이다.

제네릭 처럼 원시 타입이 허용 되지 않은 경우에 사용 가능하게 해준다.

 

4) 불변 수학

BigInteger, BigDecimal같은 불변 클래스가 제공된다.

이 때, 연산 시, 새로운 객체를 반환한다는 사실을 잊지말고, 계산된 값을 객체에 할당을 해줘야한다.

 

5) 자바 시간 API

java.time 패키지를 통해 다양한 정밀도를 가진 여러 불변의 날짜, 시간 타입을 사용할 수 있다. (LocalDateTime, Instant, ...)

 

6) enum

상수로 구성된 불변성을 가진 타입. 필드 또한 추가 할 수 있다.

필드에 가변 객체 타입이나 setter를 사용 가능하나 권장하지 않는다.

 

7) final

해당 키워드를 통해 특정 상황에서의 불변성을 보장한다. (모든 상황에서 불변하게 만들지는 않음)

참조를 재할당 할 수는 없지만, 참조한 자료 구조의 요소들은 여전히 변경 가능하다.

 

final 키워드의 특징 몇가지를 알아보면,

 

final 클래스는 하위 클래스화 될 수 없다.

final 매서드는 오버라이딩 될 수 없다.

final 필드는 생성자 내부에 선언될 때 한 번 할당되며, 재할당 불가능하다.

final 변수 참조 또한 필드와 같이 재할당이 불가능하다. (참조된 변수의 내용은 변경 가능함) 

 

8) record

자바 14에서 도입되었다.

얕은 불변성을 가진 데이터 운반체이다. 추후 5장에서 자세히 다룬다.

 

불변성 만들기

프로그램 상태를 어떻게 불변하게 유지할 수 있을까.

타입은 setter가 없는 경우, final 필드를 가지게 하여 재할당이 불가능하게 할 . 수있다.

 

다만 공유 자료 구조는 한 번에 전부 생성되지 않으므로,

시간의 흐름에 따라 변경 가능한 대신, 최종적으로 불변 자료 구조를 완성하도록 한다.

 

1) 기본적인 불변성

데이터 전송 객체, 값 또는 어떠한 종류의 상태와 같은 새로운 자료 구조는 불변하도록 설계 해야 한다.

 

2) 항상 불변성 고려하기

특별히 명시되지 않았거나, 직접 생성하지 않은 경우에는 불변이라 가정한다.

변경해야 한다면, 새로운 것을 만드는 것이 안전하다.

 

3) 기존 타입 수정하기

기존 타입이 불변하지 않더라도, 새롭게 추가되는 사항들은 불변하는 것이 좋다.

 

4) 필요한 경우 불변성 깨기

불변성이 코드와 맞지 않는다면 강제로 불변성으로 바꾸려고 하지 않는다.

 

5) 외부 자료 구조 불변 처리하기

자신의 스코프가 아닌 모든 자료 구조는 불변이라고 가정한다.

이는 메서드를 순수하게 유지하고 의도치 않은 변경을 방지한다.