source

왜 우리는 불변의 계급이 필요한가?

factcode 2022. 9. 13. 22:04
반응형

왜 우리는 불변의 계급이 필요한가?

불변의 클래스가 필요한 시나리오가 무엇인지 알 수 없습니다.
당신은 그런 요구를 받아본 적이 있습니까?아니면 이 패턴을 어디에 사용해야 하는지 예를 들어주세요.

다른 대답은 불변성이 왜 좋은지를 설명하는 데 너무 초점을 맞춘 것 같다.매우 좋고, 가능하면 사용하고 있습니다.하지만 그건 당신의 질문이 아닙니다.필요한 답변과 사례를 얻을 수 있도록 질문을 하나씩 받아보겠습니다.

불변의 클래스가 필요한 시나리오가 무엇인지 알 수 없습니다.

여기서 "니즈"는 상대적인 용어입니다.불변의 클래스는 다른 패러다임/패턴/툴과 마찬가지로 소프트웨어 구축을 용이하게 하는 설계 패턴입니다.마찬가지로, OO 패러다임이 나오기 전에는 많은 코드가 작성되었지만, 저는 OO를 필요로 하는 프로그래머에 포함시켜 주세요.OO처럼 불변의 수업이 필요한 것은 아니지만 꼭 필요한 것처럼 행동할 것입니다.

당신은 그런 요구를 받아본 적이 있습니까?

문제 도메인의 개체를 올바른 관점에서 보고 있지 않으면 불변의 개체에 대한 요구 사항이 표시되지 않을 수 있습니다.문제 영역을 유리하게 사용하는 시기를 잘 모르면 문제 도메인에 불변의 클래스가 필요하지 않다고 생각하기 쉽습니다.

문제 영역의 특정 개체를 값 또는 고정 인스턴스로 생각하는 불변의 클래스를 사용하는 경우가 많습니다.이 개념은 때때로 관점 또는 관점에 따라 달라지지만, 이상적으로는 적절한 시점으로 전환하여 적합한 후보 객체를 식별하는 것이 쉽습니다.

다양한 서적/온라인 기사를 읽고 불변의 클래스에 대해 생각하는 방법을 잘 이해함으로써 불변의 사물이 실제로 어디에 유용한지 더 잘 알 수 있습니다(엄밀하게 필요하지 않은 경우).시작하기 위한 좋은 기사 중 하나는 Java 이론과 실천입니다. 돌연변이 시킬까 말까?

원근법에 대한 이해를 돕기 위해 다른 관점(변화 가능과 불변)에서 사물을 보는 방법의 예를 아래에 몇 가지 제시하겠습니다.

이 패턴을 어디에 사용해야 하는지 예를 들어주세요.

실제 사례를 요청하셨으니 몇 가지 예를 들어 보겠습니다만, 먼저 몇 가지 전형적인 예를 들어 보겠습니다.

클래식 값 오브젝트

문자열과 정수는 종종 값으로 생각됩니다.따라서 Java에서는 String 클래스와 Integer 래퍼 클래스(및 다른 래퍼 클래스)가 불변임을 알 수 있습니다.색상은 일반적으로 값으로 간주되며, 따라서 불변의 색상 클래스로 간주됩니다.

반례

대조적으로, 자동차는 보통 가치의 대상으로 생각되지 않는다.자동차를 모델링하는 것은 일반적으로 변화하는 상태(오도미터, 속도, 연료 레벨 등)를 가진 클래스를 만드는 것을 의미합니다.단, 값 객체가 될 수 있는 도메인이 있습니다.예를 들어, 자동차(또는 특히 자동차 모델)는 특정 차량에 적합한 모터 오일을 검색하기 위한 앱에서 가치 개체로 간주될 수 있습니다.

카드놀이

카드놀이 프로그램을 써본 적 있어요?그랬죠, 카드놀이를 변형 가능한 옷과 계급으로 변형 가능한 물건으로 나타낼 수도 있었어요드로우 포커핸드는 5번째 카드를 교환하면 슈트와 랭크 ivar를 변경하여 5번째 카드 인스턴스를 새로운 카드로 변환하는 고정 케이스가 될 수 있습니다.

하지만 저는 카드놀이는 한번 만들어지면 변하지 않는 옷과 등급이 정해져 있는 불변의 물건이라고 생각하는 경향이 있습니다.제 드로 포커핸드는 5개의 인스턴스이며, 카드를 교체하면 그 인스턴스 중 하나를 폐기하고 새로운 랜덤 인스턴스를 손에 추가할 수 있습니다.

지도 투영

마지막 예시는 지도 코드에서 다양한 투영으로 지도를 표시할 수 있는 경우입니다.원래 코드에서는 맵에 고정적이지만 변환 가능한 투영 인스턴스(위의 가변형 플레이 카드 등)가 사용되었습니다.지도 투영 변경은 지도의 투영 인스턴스(instance)의 ivar(투영 유형, 중심점, 줌 등)를 변환하는 것을 의미했습니다.

다만, 투영을 불변의 가치나 고정 인스턴스라고 생각하면 디자인이 심플하다고 느꼈습니다.지도 투영을 변경하는 것은 지도의 고정 투영 인스턴스를 변환하는 것이 아니라 지도에서 다른 투영 인스턴스를 참조하도록 하는 것을 의미합니다.이것에 의해, 다음과 같은 명명된 투영을 간단하게 캡처할 수 있게 되었습니다.MERCATOR_WORLD_VIEW.

불변의 클래스는 일반적으로 올바르게 설계, 구현사용하는 것이 훨씬 간단합니다.예를 들어 String: 의 구현입니다.java.lang.String보다 훨씬 간단하다std::stringC++로 표시됩니다.대부분 불변성 때문입니다.

불변성이 특히 큰 차이를 만드는 한 가지 특별한 영역은 동시성입니다.불변 객체는 여러 스레드 간에 안전하게 공유할 수 있지만, 가변 객체는 신중한 설계와 구현을 통해 스레드 안전성을 확보해야 합니다.일반적으로 이것은 사소한 작업과는 거리가 먼 것입니다.

업데이트: 유효한 Java 2nd Edition에서는 이 문제를 자세히 다룹니다(항목 15: 변경 가능성 최소화).

다음의 관련 투고도 참조해 주세요.

Joshua Bloch의 효과적인 Java는 불변의 클래스를 작성해야 하는 몇 가지 이유를 개략적으로 설명합니다.

  • 심플성 - 각 클래스는 1개의 상태만 있습니다.
  • 스레드 세이프 - 상태를 변경할 수 없으므로 동기화가 필요하지 않습니다.
  • 불변의 문체로 작성하면, 보다 견고한 코드로 연결됩니다.Strings가 불변하지 않는다고 가정해 봅시다.String을 반환한 getter 메서드는 String을 반환하기 전에 구현에서 방어 복사본을 생성해야 합니다.그렇지 않으면 클라이언트가 실수로 또는 악의적으로 오브젝트 상태를 파괴할 수 있습니다.

일반적으로 심각한 성능 문제가 발생하지 않는 한 객체를 불변 상태로 만드는 것이 좋습니다.이러한 상황에서는 가변 빌더 객체를 사용하여 불변의 객체를 구축할 수 있습니다.String Builder

해시맵이 대표적인 예입니다.지도의 열쇠는 반드시 불변의 것이어야 한다.키가 불변하지 않고 hashCode()가 새로운 값이 되도록 키의 값을 변경하면 맵은 파손됩니다(해시 테이블의 잘못된 위치에 키가 있습니다).

자바는 사실상 하나의 레퍼런스입니다.인스턴스가 여러 번 참조되는 경우가 있습니다.이러한 인스턴스를 변경하면 해당 인스턴스의 모든 참조에 반영됩니다.견고성과 스레드 세이프티를 향상시키기 위해 단순히 이것을 가지고 싶지 않을 수도 있습니다.이 경우 변경 불가능한 클래스는 새 인스턴스를 만들고 현재 참조에 재할당하도록 강제할 수 있으므로 유용합니다.이렇게 하면 다른 참조의 원래 인스턴스는 변경되지 않습니다.

자바가 어떤 모습일지 상상해 보세요.String변하기 쉬웠어요

극단적인 경우를 예로 들어봅시다. 정수 상수입니다.만약 내가 "x=x+1"과 같은 문구를 쓴다면, 나는 프로그램의 어느 곳에서든 숫자 "1"이 어떻게든 2가 되지 않을 것이라고 100% 확신하고 싶다.

정수 상수는 클래스는 아니지만 개념은 동일합니다.예를 들어 다음과 같습니다.

String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);

간단해 보이는데요.그러나 Strings가 불변의 것이 아니라면 getCustomerName이 customerId를 변경하여 getCustomerBalance를 호출할 때 다른 고객에 대한 잔금을 받을 수 있도록 할 필요가 있습니다.이제 "getCustomerName 함수를 쓰는 사람이 왜 ID를 변경하려고 합니까?그건 말이 안 돼요.하지만 바로 거기서 문제가 생길 수 있어위의 코드를 쓰는 사람은 함수가 파라미터를 변경하지 않는 것이 당연하다고 생각할 수 있습니다.그 후, 고객이 같은 이름의 계정을 여러 개 가지고 있는 경우에 대처하기 위해서, 그 기능의 또 다른 용도를 변경할 필요가 있는 사람이 등장합니다.그리고 그는 "아, 여기 이미 이름을 검색 중인 편리한 get Customer name 기능이 있습니다.아이디를 같은 이름의 다음 계정으로 자동 변경해서 루프에 넣으면..."그러면 이상하게 프로그램이 작동하지 않게 됩니다.코딩 방식이 안 좋은가요?아마 그럴 거예요.하지만 그것은 부작용이 명백하지 않은 경우에 정확히 문제가 된다.

불변성은 단순히 물체의 특정 클래스가 상수라는 것을 의미하며, 우리는 그것들을 상수로 취급할 수 있습니다.

(물론 사용자는 변수에 다른 "상수 개체"를 할당할 수 있습니다.누군가는 String s="hello"라고 쓰고 나중에 s="hello"라고 쓸 수 있다. 변수를 최종화하지 않는 한, 나는 그것이 내 코드 블록 내에서 변경되지 않았는지 확신할 수 없다.정수 상수가 "1"은 항상 같은 숫자이지만 "x=2"를 써서 "x=1"을 절대 바꾸지 않는다는 것을 보증하는 것처럼 말이다.그러나 내가 불변의 객체에 대한 핸들을 가지고 있다면, 내가 전달한 어떤 함수도 나에게 그것을 바꿀 수 없다거나, 복사본을 두 개 만들었을 때, 한 복사본을 보관하고 있는 변수에 대한 변경은 다른 복사본을 변경하지 않을 것이라고 확신할 수 있습니다.기타.

불변의 클래스 자체는 필요 없습니다만, 몇개의 프로그래밍 태스크(특히 여러 스레드가 관련되어 있는 경우)를 간단하게 할 수 있습니다.불변의 객체에 액세스하기 위해 잠금을 수행할 필요가 없으며, 이러한 객체에 대해 이미 설정한 사실은 앞으로도 계속 적용됩니다.

불변의 원인은 다양합니다.

  • 스레드 안전성:불변의 객체는 변경할 수도 없고 내부 상태도 변경할 수도 없으므로 동기화할 필요가 없습니다.
  • 또, (네트워크 경유로) 송신하는 모든 것이, 이전에 송신한 것과 같은 상태가 되는 것을 보증합니다.즉, 누구도 (eavesdropper) 와서 불변의 집합에 랜덤 데이터를 추가할 수 없습니다.
  • 개발도 간단합니다.오브젝트가 불변인 경우 서브클래스가 존재하지 않음을 보증합니다.예: aString학급.

따라서 네트워크 서비스를 통해 데이터를 전송하고 전송한 결과와 동일한 결과를 얻을 수 있다는 보장을 원할 경우 데이터를 불변으로 설정합니다.

다음 방문자를 위한 2센트:


불변 객체가 적절한 선택지는 다음 2가지 시나리오입니다.

멀티스레딩의 경우

멀티 스레드 환경에서의 동시성 문제는 동기화로 충분히 해결할 수 있지만 동기화는 비용이 많이 들기 때문에(여기에서는 "왜"에 대해서는 자세히 설명하지 않습니다), 불변의 오브젝트를 사용하고 있는 경우 동시성 문제를 해결할 수 없습니다.불변의 오브젝트 상태는 변경할 수 없고, 상태를 변경할 수 없는 경우 모두 변경할 수 없습니다.스레드는 오브젝트에 심리스하게 액세스 할 수 있습니다.따라서 다중 스레드 환경에서 공유 개체는 불변의 개체를 선택하는 것이 좋습니다.


해시 기반 컬렉션의 키로 사용

해시 기반 컬렉션을 사용할 때 가장 주의해야 할 사항 중 하나는 이 키가 다음과 같이 되어 있어야 한다는 것입니다.hashCode()는 오브젝트의 라이프 타임 동안 항상 같은 값을 반환해야 합니다.이 값을 변경하면 해당 오브젝트를 사용하여 해시 기반 컬렉션에 작성된 오래된 엔트리는 취득할 수 없기 때문에 메모리 누수가 발생합니다.불변 객체의 상태는 변경할 수 없으므로 해시 기반 컬렉션에서 키로 선택하는 것이 좋습니다.따라서 해시 기반 수집의 키로 불변의 객체를 사용하는 경우 메모리 누수가 발생하지 않음을 확인할 수 있습니다(키로서 사용되는 개체가 다른 곳에서 참조되지 않은 경우에도 메모리 누수가 발생할 수 있지만, 여기서 중요한 것은 아닙니다).

I'm going to attack this from a different perspective. I find immutable objects make life easier for me when reading code.

If I have a mutable object I am never sure what its value is if it's ever used outside of my immediate scope. Let's say I create MyMutableObject in a method's local variables, fill it out with values, then pass it to five other methods. ANY ONE of those methods can change my object's state, so one of two things has to occur:

  1. I have to keep track of the bodies of five additional methods while thinking about my code's logic.
  2. I have to make five wasteful defensive copies of my object to ensure that the right values get passed to each method.

The first makes reasoning about my code difficult. The second makes my code suck in performance -- I'm basically mimicking an immutable object with copy-on-write semantics anyway, but doing it all the time whether or not the called methods actually modify my object's state.

If I instead use MyImmutableObject, I can be assured that what I set is what the values will be for the life of my method. There's no "spooky action at a distance" that will change it out from under me and there's no need for me to make defensive copies of my object before invoking the five other methods. If the other methods want to change things for their purposes they have to make the copy – but they only do this if they really have to make a copy (as opposed to my doing it before each and every external method call). I spare myself the mental resources of keeping track of methods which may not even be in my current source file, and I spare the system the overhead of endlessly making unnecessary defensive copies just in case.

(If I go outside of the Java world and into, say, the C++ world, among others, I can get even trickier. I can make the objects appear as if they're mutable, but behind the scenes make them transparently clone on any kind of state change—that's copy-on-write—with nobody being the wiser.)

Immutable objects are instances whose states do not change once initiated. The use of such objects is requirement specific.

불변 클래스는 캐싱 목적에 적합하며 스레드 세이프입니다.

불변성을 통해 기초가 되는 불변 객체의 동작/상태가 변경되지 않도록 할 수 있으며 추가 작업을 수행할 수 있는 이점을 얻을 수 있습니다.

  • 복수의 코어/처리(동시/병렬 처리)를 간단하게 사용할 수 있습니다(동시/병렬 처리).

  • 비용이 많이 드는 작업을 위해 캐싱을 수행할 수 있습니다(확실히 마찬가지임).
    결과)를 참조해 주세요.

  • 디버깅을 쉽게 실행할 수 있습니다(실행 이력은 문제가 되지 않으므로).
    더 이상)

final 키워드를 사용한다고 해서 반드시 불변하는 것은 아닙니다.

public class Scratchpad {
    public static void main(String[] args) throws Exception {
        SomeData sd = new SomeData("foo");
        System.out.println(sd.data); //prints "foo"
        voodoo(sd, "data", "bar");
        System.out.println(sd.data); //prints "bar"
    }

    private static void voodoo(Object obj, String fieldName, Object value) throws Exception {
        Field f = SomeData.class.getDeclaredField("data");
        f.setAccessible(true);
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL);
        f.set(obj, "bar");
    }
}

class SomeData {
    final String data;
    SomeData(String data) {
        this.data = data;
    }
}

"final" 키워드는 프로그래머의 오류를 방지하기 위해 존재하며 그 이상은 아님을 보여주는 예에 불과합니다.최종 키워드가 없는 값은 실수로 재할당하기 쉽지만 값을 변경하기 위해 이 길이로 이동하는 것은 의도적으로 수행해야 합니다.이것은 문서화와 프로그래머의 오류를 방지하기 위한 것입니다.

불변의 데이터 구조는 재귀 알고리즘을 코딩할 때도 도움이 됩니다.예를 들어, 3SAT 문제를 해결하려고 한다고 가정해 보십시오.한 가지 방법은 다음과 같습니다.

  • 할당되지 않은 변수를 선택합니다.
  • TRUE 값을 지정합니다. 현재 만족하는 절을 제거하여 인스턴스를 단순화하고 반복하면 더 단순한 인스턴스를 해결할 수 있습니다.
  • TRUE 케이스의 재귀가 실패했을 경우 대신 해당 변수를 FALSE로 할당합니다.이 새로운 인스턴스를 단순화하고 반복하여 해결합니다.

문제를 나타내는 가변 구조를 가진 경우 TRUE 분기에서 인스턴스를 단순화할 때 다음 중 하나를 수행해야 합니다.

  • 변경한 내용을 모두 기록하고 문제를 해결할 수 없다는 것을 알게 되면 모두 실행 취소하십시오.재귀가 상당히 깊어질 수 있고 코드화도 까다롭기 때문에 오버헤드가 큽니다.
  • 인스턴스의 복사본을 만든 다음 복사본을 수정합니다.재귀의 깊이가 수십 레벨인 경우 인스턴스의 복사본을 많이 만들어야 하므로 이 작업은 느립니다.

그러나 현명한 방법으로 코드를 작성하면 불변의 구조를 가질 수 있습니다.어떤 조작이든 문제의 최신 버전(와 유사)을 반환합니다.String.replace - it doesn't replace the string, just gives you a new one). The naive way to implement this is to have the "immutable" structure just copy and make a new one on any modification, reducing it to the 2nd solution when having a mutable one, with all that overhead, but you can do it in a more efficient way.

One of the reasons for the "need" for immutable classes is the combination of passing everything by reference and having no support for read-only views of an object (i.e. C++'s const).

Consider the simple case of a class having support for the observer pattern:

class Person {
    public string getName() { ... }
    public void registerForNameChange(NameChangedObserver o) { ... }
}

If string were not immutable, it would be impossible for the Person class to implement registerForNameChange() correctly, because someone could write the following, effectively modifying the person's name without triggering any notification.

void foo(Person p) {
    p.getName().prepend("Mr. ");
}

In C++, getName() returning a const std::string& has the effect of returning by reference and preventing access to mutators, meaning immutable classes are not necessary in that context.

They also give us a guarantee. The guarantee of immutability means that we can expand on them and create new patters for efficiency that are otherwise not possible.

http://en.wikipedia.org/wiki/Singleton_pattern

One feature of immutable classes which hasn't yet been called out: storing a reference to a deeply-immutable class object is an efficient means of storing all of the state contained therein. Suppose I have a mutable object which uses a deeply-immutable object to hold 50K worth of state information. Suppose, further, that I wish to on 25 occasions make a "copy" of my original (mutable) object (e.g. for an "undo" buffer); the state could change between copy operations, but usually doesn't. Making a "copy" of the mutable object would simply require copying a reference to its immutable state, so 20 copies would simply amount to 20 references. By contrast, if the state were held in 50K worth of mutable objects, each of the 25 copy operations would have to produce its own copy of 50K worth of data; holding all 25 copies would require holding over a meg worth of mostly-duplicated data. Even though the first copy operation would produce a copy of the data that will never change, and the other 24 operations could in theory simply refer back to that, in most implementations there would be no way for the second object asking for a copy of the information to know that an immutable copy already exists(*).

(*) One pattern that can sometimes be useful is for mutable objects to have two fields to hold their state--one in mutable form and one in immutable form. Objects can be copied as mutable or immutable, and would begin life with one or the other reference set. As soon as the object wants to change its state, it copies the immutable reference to the mutable one (if it hasn't been done already) and invalidates the immutable one. When the object is copied as immutable, if its immutable reference isn't set, an immutable copy will be created and the immutable reference pointed to that. This approach will require a few more copy operations than would a "full-fledged copy on write" (e.g. asking to copy an object which has been mutated since the last copy would require a copy operation, even if the original object is never again mutated) but it avoids the threading complexities that FFCOW would entail.

Why Immutable class?

Once an object is instantiated it state cannot be changed in lifetime. Which also makes it thread safe.

Examples :

Obviously String, Integer and BigDecimal etc. Once these values are created cannot be changed in lifetime.

Use-case : Once Database connection object is created with its configuration values you might not need to change its state where you can use an immutable class

from Effective Java; An immutable class is simply a class whose instances cannot be modified. All of the information contained in each instance is provided when it is created and is fixed for the lifetime of the object. The Java platform libraries contain many immutable classes, including String, the boxed primitive classes, and BigInte- ger and BigDecimal. There are many good reasons for this: Immutable classes are easier to design, implement and use than mutable classes. They are less prone to error and are more secure.

ReferenceURL : https://stackoverflow.com/questions/3769607/why-do-we-need-immutable-class

반응형