일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 자바의 정석 기초편 ch12
- 스프링 mvc2 - 로그인 처리
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch2
- 자바 중급2편 - 컬렉션 프레임워크
- 자바 고급2편 - io
- 스프링 mvc2 - 검증
- 스프링 mvc2 - 타임리프
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch1
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch7
- 스프링 입문(무료)
- 데이터 접근 기술
- 자바 중급1편 - 날짜와 시간
- @Aspect
- 자바의 정석 기초편 ch13
- 스프링 트랜잭션
- 자바 기초
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch11
- 자바로 계산기 만들기
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch4
- 자바로 키오스크 만들기
- 자바의 정석 기초편 ch9
- 자바 고급2편 - 네트워크 프로그램
- 람다
- 2024 정보처리기사 수제비 실기
- Today
- Total
개발공부기록
불변 객체는 무엇이며 Java에서 어떻게 구현하는가, String 클래스가 불변인 이유 본문
불변 객체란?
Immutable Object(불변 객체)는 말 그대로 객체 생성 후 그 상태를 바꿀 수 없는 객체를 말한다
이와 반대 개념으로 객체 생성 후 상태를 변경할 수 있는 Mutable Object(가변 객체)도 존재한다.
불변 객체는 객체의 상태를 바꿀 수 없는 단순한 제약을 사용하여 사이드 이펙트라는 큰 문제를 막을 수 있어 코드의 안정성을 높여준다.
객체의 상태를 바꿀 수 없기 때문에 객체 내부의 값을 변경하는 메서드가 존재해도 실제 내부의 값을 변경하는 것이 아니라 새로운 객체를 생성해서 새로운 참조값을 반환하여 완전히 새로운 객체를 반환한다.
객체의 값을 변경할 수 없기 때문에 멀티스레드 환경에서도 안전하게 동작하는데, 불변 객체 내부에 참조하는 객체들도 모두 불변이여야 멀티스레드 환경에서 완전한 안정성을 보장할 수 있다 -> 이부분은 멀티스레드의 동기화로 넘어간다.
자바에서는 대표적으로 String, Integer, Double 등의 래퍼 클래스들이 불변 클래스로 설계 되어 있다
공유 참조의 값 변경으로 인한 사이드 이펙트
자바의 데이터 타입은 크게 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있는데 기본형은 하나의 값을 여러 변수에서 절대로 공유하지 않지만 참조형은 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
이렇게 서로 다른 변수가 객체의 참조값을 공유하여 값이 변경되면 예상하지 못하는 사이드 이펙트(Side Effect; 부수 효과)가 발생하여 다른 부분에 영향을 미쳐 알지 못하는 버그가 발생하거나 버그를 찾기위한 디버깅도 어려워진다.
공유 참조 자체는 자바 문법상 막을 수도 없고 사실상 문제는 공유 참조된 값이 변경되었기 때문에 문제가 발생하는 것이므로 불변 객체로 공유 참조가 발생하더라도 공유 참조하는 인스턴스의 값을 변경하지 못하게 함으로써 문제를 원천 차단시킬 수 있다.
MutableClass - 가변 클래스
이 클래스는 멤버 변수가 final이 아닌 가변클래스이다.
이 가변 클래스를 생성하고, 또다른 MutableClass 타입 참조형 변수 user2에 user1의 참조값을 대입하면 참조값이 공유되는 공유 참조가 일어난다.
여기서 user2를 통해 인스턴스의 필드의 값을 변경하면, user2만 변경했음에도 user1과 user2의 이름이 모두 박이름으로 변경되는 것을 확인할 수 있다.
user2의 이름만 변경하고자 했지만 엉뚱하게 user1의 이름이 갑자기 변경되어 MyPage에 들어갔는데 나의 이름이 아닌 다른 사람의 이름이 갑자기 보일 수 있게 되는 것이다.
ImmutableClass - 불변 클래스
이런 사이드 이펙트 문제를 클래스의 필드에 간단히 final 키워드를 적용하여 불변객체를 만드는 것만으로 해결할 수 있다
final 키워드가 붙은 멤버 변수는 값을 변경할 수 없기 때문에 setName(String name)메서드에서 내부의 값을 변경하는 것이 아니라 new ImmutableClass(name)으로 새로운 ImmutableClass 객체를 생성해서 반환하는 것을 확인할 수 있다
이런 식으로 자바의 불변 객체들이 제공하는 메서드로 값이 변경된다면 모두 내부에서 객체를 새로 생성해서 값을 반환하기 때문에 모두 다른 인스턴스이다.
불변 객체를 사용한 덕분에 실수로 공유 참조가 발생하더라도 user2의 이름을 변경하기위해 setName("오자바")를 호출하면 user1의 이름은 그대로 있고 user2의 이름만 변경되어 사이드 이펙트가 방지되는 것을 확인할 수 있다.
String 클래스가 불변인 이유
문자열 리터럴로 초기화
String str1 = "문자열이다";
String str2 = "문자열이다";
Java에서 String 클래스는 매우 자주 사용되기 때문에 내부 구조에서 다양한 최적화를 많이 진행하여 제공한다.
그래서 String 클래스는 매우 특별하게 클래스이지만 new 연산자가 아니라 대입 연산자와 문자열 리터럴을 통해 값을 초기화할 수 있는데 이렇게 생성된 문자열은 문자열 풀(힙메모리 영역)에서 별도에 관리된다.
참고로 이 문자열 풀은 원래는 static 영역(메서드 영역) 관리되었지만 자바7부터 힙 영역에서 관리되고 있음
동일한 문자열 풀에서 관리 되기 때문에 같은 문자열 리터럴을 담고있는 str1과 str2는 서로 다른 메모리 주소값을 참조하는 것이 아니라 문자열 풀에서 관리되고있는 "문자열이다"라는 같은 문자열 리터럴을 가리키고 있기 때문에 같은 인스턴스를 가리린다
즉, "문자열이다"라는 문자열이 2개 생성되는 것이 아니라 내부의 최적화 로직(캐싱 기능)을 통해 같은 문자열 리터럴은 하나만 생성되고 각 변수들은 이를 공유해서 참조한다
String은 내부적으로 toString()을 오버라이딩 하고 있기 때문에 메모리 주소값을 강제로 Hex로 변환하도록 하여 출력해보면 str1과 str2이 동일한 값을 출력하는 것을 확인할 수 있다.
new String()으로 초기화
String도 원래 클래스를 생성하는 것처럼 new String()을 통해 생성할 수 있는데, 이렇게 생성한 두 값을 동일하게 메모리 주소값을 출력해보면 각자의 힙 메모리 영역에서 관리되어 주소값이 다른 것을 확인할 수 있다.
그래서 흔히 알고 있듯이 대입 연산자로 초기화한 String 타입의 두 변수를 비교할 때 참조값을 비교하는 == 비교를 해도 같은 문자열 리터럴이기 때문에 문자열 풀의 같은 인스턴스를 가리키고 있어 true가 나오지만,
new String()으로 생성한 String 타입의 두 변수를 == 비교하면 false가 나오는 이유가 문자열 풀에서 관리되지 않고 별도의 힙 메모리에서 생성되어 참조하는 인스턴스가 다르기 때문이다.
참고로 문자열 리터럴로 초기화된 문자열 변수를 비교할 때 == 비교가 true로 나온다고하여 문자열 비교시 == 비교를 하면 안되고 항상 equals()를 사용해야 하는데, 어디서 문자열이 new String()으로 생성되었는지 확인할 수도 없고 보장할 수 없기 때문이다.
문자열 풀의 공유참조를 막는 불변 객체 String
여기에서 String이 불변으로 설계된 이유가 나오는데, 문자열 풀에서 관리되고있는 str1과 str2가 만약 가변이라면 위에서 공유 참조의 사이드 이펙트에서 설명했듯이 str2의 값을 변경할때 str1의 값이 변경되는 사이드 이펙드가 발생할 것이다.
즉 문자열을 최적화하여 사용하기 위한 다양한 장치(문자열 풀, 캐싱을 통한 메모리 절약 등)를 정상적으로 동작하기 위해서는 String 객체를 불변으로 설계해야 할 수밖에 없다.
그래서 불변 객체인 String 클래스를 조작하게 되면 항상 새로운 문자열을 생성하기 때문에 성능이 떨어져 문자열을 자주 조작해야 한다면 가변 String 객체인 StringBuilder(동기화 되어있지 않음) StringBuffer(동기화 되어있음, synchronized 적용)를 사용하여 문자열을 조작한 뒤에 String으로 변환하면 문자열 조작시 성능을 높일 수 있다.