자바의 Immutable Object(불변 객체) 에 대해서 다뤄보려고 한다.

불변 객체가 무엇이고 왜 필요한 것일까? 그리고 어떻게 사용되고 있는 것일까? 

1. 불변 객체란 무엇일까?

"객체 지향 프로그래밍에 있어서 불변객체는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 반대 개념으로는 가변 객체로 생성 후에도 상태를 변경할 수 있다"라고 정의되어 있다.  명료하지만 예시를 바로 떠올릴 수 없어서 다소 추상적이기도 하다. 

 

먼저 예시를 들면 자바에서 제공하는 기본 Wrapper Class ( String, Integer , Boolean ... ) 가 불변 객체에 해당한다. 

 

2.불변객체에 대한 메커니즘 이해 

 

우리가 생성하는 객체와 자바의 String,Integer 와 같은 기본객체들은 어떤 차이가 있을까?  

개발자들은 글을 보는 것보다 코드를 통한 이해가 더 빠르기 때문에 바로 예시를 들어 이야기 해보자.

 

우리가 정의한 A라는 클래스의 인스턴스를 생성하고, 해당 인스턴스를 myObject1,myObject2라는 변수가 참조할 때, 같은 곳을 참조하게 되고, A 오브젝트의 name 필드값이 변경되면 같은곳을 참조하고 있기 때문에 둘다 Bye라는 값을 반환한다. 

 

 

 

우리가 정의한 클래스 말고 자바에 정의된 기본객체들은 아래와 같은 간단한 코드가 있을 때 어떻게 동작할까 ?

우리가 정의한 객체와 같은 메커니즘으로 동작한다면 Hello,hi 가 아닌 hi,hi 가 나와야 한다. 

하지만 불변객체로 설계된 기본객체들은 참조값의 값을 변경하는 것이 아니라 새로운 공간에 새로운 값을 할당한다. 

즉, example1과 exampl2 가 같은 주소값(1번지라고 예시를 들겠음) 을 바라보고 있고 example2를 통해 새로운 문자열 "hi" 로 값을 변경한다해도 example1은 기존과 동일하게 1번지를 바라보게 되고 example2는 hi가 위치하고 있는 새로운 번지수를 바라보게 된다. 

 

3. 왜 불변객체를 사용하는가?

- 스레드 안전성: 여러 스레드가 동시에 같은 불변 객체를 참조하더라도, 어느 한 스레드가 객체의 상태를 변경할 수 없기 때문에 동기화 문제가 발생하지 않는다. 이는 복잡한 동기화 로직 없이도 데이터 일관성을 유지할 수 있게 해준다.

 

이해를 돕기 위한 시나리오를 추가하자면 

  • A가 "Hello"라는 데이터를 가진 String 객체를 읽고 있을 때, 이 데이터는 메모리에 저장된 후 변경될 수 없다. 즉, 객체 내부의 데이터는 생성 시점의 상태로 고정된다.
  • B의 변경 시도: B가 같은 "Hello" 값을 변경하고 싶다면, String 객체는 변경할 수 없기 때문에 새로운 String 객체를 생성해야 한다. 예를 들어, B가 "Hello"에 "Bye"를 추가하고 싶다면 new String("HelloBye")와 같은 새로운 객체를 만들 수 있다. 하지만 이는 원래 A가 참조하고 있는 "Hello" 객체와는 전혀 다른, 새로운 메모리 주소에 저장된 객체이다.
  • 결과: 따라서 A가 읽고 있는 "Hello" String 객체는 그대로 유지되며, B의 작업은 원래 객체에 영향을 주지 않는다. A는 여전히 원본 "Hello" 데이터를 안전하게 읽을 수 있다.

- 데이터 일관성: 불변 객체를 사용하면 데이터의 신뢰성이 보장된다. 데이터가 예측 가능하고, 오류 가능성이 줄어들어 버그 발생 확률이 낮아진다.

 

- 메모리 효율성: 같은 값의 불변 객체는 메모리에서 한 번만 생성되어 여러 참조에서 공유될 수 있다. 이는 메모리 사용을 최적화하고 성능을 개선하는 데 도움이 된다.

 

4.불변객체를 만드는 방법 

 

1. setter를 사용하지 않는다. (객체의 상태를 변경하는 메소드 미제공)
2. private으로 선언한다.

3. final을 선언한다.

 

 

ImmutableCar객체를 보면 필드값을 수정할 수 없게 설계 되었다. 따라서 한번 지정한 값에 대해 불변이다.

 

하지만 불변객체 내에 가변객체를 참조받고 있다면 이야기가 달라진다. 

 

 

 

ImmutableCar 객체를 불변객체로 설계하기 위해 다음과 같이 코드를 작성했다. 하지만 Sonata객체 자체가 가변객체이기 때문에 Sonata 객체의 version에 대한 수정은 열려있다. 그렇기 때문에 불변객체가 되려면 참조객체 역시 불변객체여야 한다는 것을 알 수 있다.

 

5. 자바의 불변 List

자바는 List를 불변으로 만드는 방법을 제공한다. Collections의 unmodifiableList와 List.of 를 통해 불변 리스트를 얻을 수 있다. 

아래 예시와 같이 해당 리스트에 값을 추가하려고 하면 아래와 같은 친철한 Intellij의 설명과 , 오류를 만나게 된다.

 

 

하지만 위 Collections.unmodifiableList() 로 얻는 객체또한 완전한 불변이라고는 할 수 없는데, 만약 unmodifiableList의 element가 가변객체라면 위 Sonata의 예시처럼 가변객체 자체가 변경될 수 있다는점에 유의해야 한다. 

 

 

'Java > Base' 카테고리의 다른 글

[Java] Wrapper 클래스와 Boxing , Unboxing  (0) 2023.09.14

잡설 

 문득 코딩테스트 공부를 하며 코드를 작성하다가 현타가 왔다. 원래 중간중간 나에게 질문을 하는 버릇이 있는데, 평소에 익숙하게 사용하던 코드를 보다가 단순한 의문이 들었다. HashSet을 사용하다가 타입 파라미터.. ->  제네릭 ..-> Reference Type , Primitive Type 이렇게 의식의 흐름대로 생각을 따라가며 정리를 하다보니 미흡한 부분이 있었고 이 참에 블로그 첫 게시글을 시작으로 부족했던 개념들을 복습할겸 정리해보기로 했다. 

 

Primitive Type(원시 타입) vs Reference Type(참조 타입)

 먼저, 래퍼클래스를 이해하기 위해서는 primitive type 변수와, reference type 변수의 차이를 이해해야 한다. 자바에서는 아래 총 8가지의 primitive type을 정의하고 제공한다.

  타입 할당되는 메모리크기 기본값 데이터의 표현범위
논리형 boolean 1 byte false true,false
정수형 byte 1 byte 0 -128~127
short 2 byte 0  -32,768 ~ 32,767
int (기본형) 4 byte 0 -2,147,483,648 ~ 2,147,483,647
long 8 byte 0L -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
실수형 float 4 byte 0.0F (3.4 X 10-38) ~ (3.4 X 1038) 의 근사값
double (기본형) 8 byte 0.0  (1.7 X 10-308) ~ (1.7 X 10308) 의 근사값
문자형 char 2 byte (유니코드) '\u000' 0 ~ 65,535

primitive type의 특징으로는 다음과 같다.

  • 모두 소문자로 시작된다.
  • 비객체 타입이므로 null 값을 가질 수 없다. 
  • 제너릭에서 타입파라미터로 사용될 수 없다.
  • stack에 바로 사용할 수 있는 값(실제 값)을 바이트 단위로 저장한다.

 reference type의 특징으로는 다음과 같다.

  • 기본형 과는 달리 실제 값이 저장되지 않고, 자료가 저장된 공간의 주소를 저장한다.
  • 즉, 실제 값은 다른 곳에 있으며 값이 있는 주소를 가지고 있어서 나중에 그 주소를 참조해서 값을 가져온다.
  • 메모리의 힙(heap)에 실제 값을 저장하고, 그 참조값(주소값)을 갖는 변수는 스택에 저장
  • 참조형 변수는 null로 초기화 시킬 수 있다
더보기

쉽게 얘기하면 reference type 은 위 8가지의 primitive type 자료형을 제외한  모든 자료형을 reference type 이라고 볼 수 있다. 

ex) 기본적으로 제공하는 클래스, 프로그래머가 스스로 만든 클래스, 배열, 열거 타입 등...

 

Wrapper Class(래퍼 클래스)

코딩을 하다보면 primitive type의 변수를 reference type 변수로 변경해야할 때가 생기곤 한다. 예를들어 메서드 파라미터의 타입이 reference type 이라면 primitive type의 변수를 그대로 사용할 수 없다. 이럴 때는 기본타입의 변수를 객체타입의 변수로 변환해 주는 작업이 필요하다. 이렇게 8개의 기본 타입에 해당하는 데이터를 객체로 포장해 주는 클래스를 래퍼 클래스(Wrapper class)라고 한다.  

primitive type(기본타입) wrapper class(래퍼클래스)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

 

Boxing & Unboxing

 래퍼 클래스(Wrapper class)는 산술 연산을 위해 정의된 클래스가 아니므로, 인스턴스에 저장된 값을 변경할 수 없다. 단지, 값을 참조하기 위해 새로운 인스턴스를 생성하고, 생성된 인스턴스의 값만을 참조할 수 있다.  그렇기 때문에 값을 변경할 때에는 래퍼 클래스를 언박싱 한 뒤에 값을 변경하고 다시 박싱을 해야하는 중간단계를 거쳐야 한다.

 

  • Boxing : 기본 타입의 데이터 → 래퍼 클래스의 인스턴스로 변환
  • UnBoxing : 래퍼 클래스의 인스턴스에 저장된 값 → 기본 타입의 데이터로 변환
Integer num1 = new Integer(1); // boxing
Integer num1 = 1; // 이런식으로도 표현 가능

Double num2 = new Double(1.11); // boxing
Double num2 = 1.11; // 이런식으로도 표현 가능



int n1 = num1.intValue(); // unboxing

n1 = n1 + 100; // 101 (값변경)
num1 = new Integer(n1); //boxing (재포장)


double n2 = num2.doubleValue(); // unboxing

n2 = n2 + 100.0; // 101.11 (값변경)
num2 = new Double(n2); //boxing (재포장)

 각 Wrapper Class 의 언박싱 메서드들은 다음과 같다.

메서드 반환값 설명
byteValue() byte 객체의 값을 byte 값으로 변환하여 반환
shortValue() short 객체의 값을 short 값으로 변환하여 반환
intValue() int 객체의 값을 int 값으로 변환하여 반환
longValue() long 객체의 값을 long 값으로 변환하여 반환
floatValue() float 객체의 값을 float 값으로 변환하여 반환
doubleValue() double 객체의 값을 double 값으로 변환하여 반환
charValue() char 객체의 값을 char 값으로 변환하여 반환
booleanValue() boolean 객체의 값을 boolean 값으로 변환하여 반환 

 기능적 편의성을 위하여 오토 박싱 / 언박싱을 제공하지만, 다른 타입간의 형 변환은 어플리케이션의 성능에 영향을 미치게 된다. 비록 사소한 차이 일지라도 어플리케이션의 성능 측면에서 봤을때 반드시 필요한 상황인지 한번 생각해 볼 필요가 있을 것 같다.

오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)

 JDK 1.5부터는 박싱과 언박싱이 필요한 상황에서 자바 컴파일러가 이를 자동으로 처리해 준다고 한다. 이렇게 자동화된 박싱과 언박싱을 오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)이라고 부른다.

/* 기존 박싱 & 언박싱 */
Integer num = new Integer(17); // 박싱
int n = num.intValue();        // 언박싱

/* 오토 박싱 & 언박싱 */
Integer num = 17; // new Integer() 생략
int n = num; // intValue() 생략

참조

'Java > Base' 카테고리의 다른 글

[Java] Immutable Object(불변객체)  (0) 2024.04.21

+ Recent posts