프로그래밍에서 가변성의 문제
가변성은 상태를 가지는 경우를 얘기한다.
상태를 변경하는 행위는 메모리의 저장된 값을 변경하는 행위
이렇게 메모리에 저장된 하나의 값을 누구든지 변경할 수 있다는 것은 무분별한 상태가 변경이 된다는 것을 의미한다.
가변성의 문제
- 멀티스레드에서 값을 보장하지 못함.
- 값의 예측이 어렵고 변경에 있어서 위험하다.
- 테스트와 디버깅이 어렵다.
- 상태 변경 발생 시 처리를 해주어야한다.
불변 객체 (Immutable Object)란
- 객체지향 프로그래밍에 있어, 생성 후 그 상태를 바꿀 수 없는 객체를 말한다.
- 경우에 따라서 내부에서 사용하는 속성이 변화해도 외부에서 그 객체의 상태가 변하지 않은 것처럼 보인다면 불변 객체로 보기도 한다.
- 불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고, 성능 개선에도 도움을 준다.
- but 객체가 변경 가능한 데이터를 많이 가지고 있는 경우, 불변이 오히려 부적절한 경우가 있다
즉. 불변객체는 재할당은 가능하지만, 한번 할당하면 내부 데이터를 변경할 수 없는 객체
보통, String을 선언할 때 아래처럼 선언한다.
String str = "ab";
만약 문자열을 더 추가하게 된다면?
String str = "ab";
str = str + "cd";
str
이 변경 됐다 생각할 수 있지만, 실제로 "ab"라는 String 객체는 그대로 남아있고,"abcd"라는 새로운 객체가 str
변수에 할당된다.
Java에서 String은 특이하게도 Heap영역에 String Constant Pool이 존재하는데, String을 Literal(ex: String a="apple")로 생성하면 이 영역에 저장되어 재사용 된다.
하지만 new 생성자로 생성하면 Constant Pool을 사용하지 않고, 일반적인 Heap영역에 생성하여 재사용하지 않게 된다.
불변객체의 장단점
- 장점
- 객체에 대한 신뢰도가 높아진다. 객체가 한번 생성되고 변하지 않는다면 트랜잭션내에서 우리가 믿고 쓸 수 있기 때문이다.
- 생성자,접근메서드에 대한 방어 복사가 필요 없다.
- 멀티스레드 환경에서 동기화 처리없이 객체를 공유할 수 있다.(공유 자원이 불변이기 때문에 항상 동일한 값을 반환하기 때문) =
Thread-safe
하다 - 가비지 컬렉션(GC) 성능을 높일 수 있다 = 가비지 컬렉터가 스캔하는 객체의 수가 줄기 때문에 GC 수행 시 지연시간도 줄어든다.
- 단점
- 생성할 때 초기값이 아닌, 새로운 값을 입력하려면 새 객체를 만들어야 한다. -> 그 만큼 자원의 소모가 많아지며,코드 재사용성이 떨어진다.
불변객체와 final
- final의 사전상 의미는 '마지막','최종'의 의미가 있다.
- final 키워드는 크게 변수,메서드,클래스에서 사용이 가능하다.
- 변수에서의 final:
- 더이상 재할당할 수 없다.
- 초기화 방법: 1. 클래스의 필드에 선언 2. 생성자에서 초기화
- 메서드에서의 final:
- 해당 메서드는 오버라이딩(메서드 재정의) 될 수 없다.
- 클래스에서의 final:
- 해당 클래스는 상속할 수 없다. 즉 부모 클래스가 될수 없다.
- Java에서는 필드가 원시 타입(primitive type)인 경우 final 키워드를 사용해 불변 객체를 만들 수 있고, 참조 타입(reference type)일 경우 *
추가적인 작업
이 필요하다.- 참조 타입은 대표적으로 1.객체를 참조할 수도 있고, 2.배열이나 3.List 등을 참조할 수 있다.
- 1.참조 변수가 일반 객체인 경우 객체를 사용하는 필드의 참조 변수도 불변 객체로 변경해야 한다.
- 2.배열일 경우 배열을 받아 copy해서 저장하고, getter를 clone으로 반환하도록 하면 된다. (배열을 그대로 참조하거나, 반환할 경우 외부에서 내부 값을 변경할 수 있음. 때문에 clone을 반환해 외부에서 값 변경하지 못하게 함)
- 3.리스트인 경우에도 배열과 마찬가지로 생성시 새로운 List를 만들어 값을 복사하도록 해야 한다.
- 참조 타입은 대표적으로 1.객체를 참조할 수도 있고, 2.배열이나 3.List 등을 참조할 수 있다.
배열과 리스트는 내부를 복사하여 전달하는데, 이를 방어적 복사(defensive-copy)라고 한다.
그렇다면 final이 불변을 의미할까?
- 정답은 완벽한 불변성을 의미하지는 않는다. final은 해당 변수의 재할당만 막아줄 뿐, 참조하고 있는 객체 내부의 상태가 변하지 않았음을 보장해주지는 않는다 .
불변객체를 만드는 방법
- 객체의 상태를 변경할 수 있는 메서드를 제공하지 않는다. (ex. setter)
- 모든 필드를 final과 private를 사용해서 선언한다.
- 클래스를 final로 선언하여 하위클래스에서의 오버라이딩을 막아라.
- 객체를 생성하기 위한 생성자 or 정적 팩토리 메서드를 추가해라.
- 참조에 의해 값의 변경이 있는 경우, 방어적 복사를 이용하여 전달해라.
코틀린에서의 불변
Kotlin에서는 가변성을 어떻게 제한하고 있을까?
코틀린에서는 크게 3가지로 가변성을 제한하고 있다.
- 읽기 전용 프로퍼티 val
- Mutable 컬렉션과 read-only 컬렉션 구분
- data class의 copy()코틀린에서의 final?코틀린은 왜 default로 final을 채택했을까?기본적으로 코틀린은 함수형 프로그래밍에서 아이디어를 얻어왔다. 그래서 가변을 사용했을 때, 발생하는 문제점들을 줄이기 위해서 불변을 사용한다. 그래서 모든 클래스들이 기본값으로 final로 선언되어 있는 이유이기도 하다.
타입을 추론하면 final이 기본으로 붙고,
상속도 기본으로final
이 붙어서 안되고,
오버라이딩도final
이 기본으로 붙는다......
코틀린은 왜 default로 final을 채택 했을까?
기본적으로 코틀린은 함수형 프로그래밍에서 아이디어를 얻어왔다. 그래서 가변을 사용했을 때, 발생하는 문제점들을 줄이기 위해서 불변을 사용한다. 그래서 모든 클래스들이 기본값으로 final로 선언되어 있는 이유이기도 하다.
더 나아가
상속
By default, Kotlin classes are final – they can't be inherited. To make a class inheritable, mark it with the open keyword:
-> 코틀린 클래스는 기본적으로 final이며, 이는 상속이 불가능하게 한다. 만약, 상속이 가능하게 하려면 open 키워드를 써야 한다.
- 개발자들이 상속을 올바르게 사용하지 못하거나 실수를 해서 부작용을 일으킬 가능성이 있다. 이로 인해 객체지향의 의도나 목적과는 다르게 설계할 수 있다.
- kotlin에서는 이러한 문제를 방지하기 위해 final을 default로 가지게 함으로써 상속과 오버라이딩을 허용하지 않는다.
- 이를 통해 코드의 예측 가능성을 높이고 안정성을 챙길 수 있다. (상속을 통한 동작의 변경이나 오버라이딩에 따른 부작용을 방지하여 코드의 의도를 명확하게 보여줌)
- 컴파일 시점에 결정되는
정적 바인딩(static binding)
을 사용하므로 런타임에 발생할 수 있는동적 바인딩(dynamic binding)
에 따른 오류 가능성을 줄인다. 이는 런타임에서의 예기치 않은 동작을 방지하여 안정성을 향상시킨다.- 이는 더 효율적인 컴파일러 최적화를 가능하게 한다.
- 컴파일러는 final 클래스의 인스턴스 생성과 호출된 final 멤버 함수의 동작을 미리 알고 있기 때문에 불필요한 가상 호출 메커니즘을 건너뛰고 직접 호출할 수 있다. 이로 인해 성능 향상을 기대할 수 있다.
'JAVA' 카테고리의 다른 글
Java - 컬렉션 (Collection) 프레임 워크 (0) | 2023.02.05 |
---|---|
Exception! - 체크 예외와 언체크 예외 (0) | 2023.02.05 |
자바의 다형성 (짧음 주의) (0) | 2023.02.05 |
생성자와 초기화 (feat .this) (0) | 2023.02.05 |
Java - 선언 위치에 따른 변수의 종류 (0) | 2023.02.05 |