|
final
로 만든다.
필드를 Java에서 final
로 정의할 때, 선언 시점 또는 생성자에서 이를 초기화해야 한다. IDE가 독자가 선언 사이트에서 이를 초기화하지 않았다고 표시해도 당황할 필요는 없다. 독자가 생성자에서 적절한 코드를 쓸 때 제정신으로 돌아왔음을 인식할 것이다.
final
로 만든다. 그러므로 이는 오버라이드될 수 없다.
클래스가 오버라이드될 수 있으면, 메소드의 작동이 오버라이드될 수 있으며, 그러므로 가장 안전한 방안은 서브 클래싱을 허용하지 않는 것이다. 이는 Java의 String
클래스가 사용한 전략임을 참고한다.
불변 오브젝트가 있으면, 생성자에 포함할 상태가 무엇이든지 설정해야 한다. 설정된 상태가 없으면, 왜 오브젝트를 보유하는가? Stateless 클래스에서 정적 메소드도 작동할 것이다. 그러므로 불변 클래스에 대해 인수 없는 생성자는 절대 보유해서는 안 된다. 어떠한 이유로 이를 요구하는 프레임워크를 사용하는 중인 경우, private 인수 없는 생성자(이는 리플렉션을 통해 시각화됨)를 제공하여 충족될 수 있는지 확인한다.
인수 없는 생성자의 부재는 기본 생성자를 강력히 주장하는 JavaBeans 표준을 위반하는 것을 참고한다. 하지만 JavaBeans는 setXXX
메소드가 작동하는 방법으로 인해 어쨌든 불변이 될 수 없다.
인수 없는 생성자를 제공하지 않았다면 이는 오브젝트로 일부 상태를 추가하는 마지막 기회이다!
일반적인 JavaBeans에 영향을 받은 setXXX
메소드를 방지해야 할 뿐만 아니라 불변 오브젝트 참조를 리턴하지 않도록 주의해야 한다. 오브젝트 참조가 final
이라는 사실은 가리키는 것을 변경할 수 없음을 의미하지 않는다. 그러므로 getXXX
메소드에서 리턴한 모든 오브젝트 참조를 방어용으로 복사하도록 한다.
이전 요구사항에 부합하는 불변 클래스는 다음 목록 1에 나타난다.
public final class Address { private final String name; private final List<String> streets; private final String city; private final String state; private final String zip; public Address(String name, List<String> streets, String city, String state, String zip) { this.name = name; this.streets = streets; this.city = city; this.state = state; this.zip = zip; } public String getName() { return name; } public List<String> getStreets() { return Collections.unmodifiableList(streets); } public String getCity() { return city; } public String getState() { return state; } public String getZip() { return zip; } } |
거리 목록의 방어용 사본을 만들기 위해 목록 1에서 Collections.unmodifiableList()
메소드의 사용을 참고하자. 배열 대신에 불변 목록을 작성하기 위해 항상 콜렉션을 사용해야 한다. 비록 배열을 방어용으로 복사할 수 있지만, 이로 인해 원하지 않는 일부 부작용이 나타난다. 다음 목록 2의 코드를 살펴보자.
목록 2. 콜렉션 대신에 배열을 사용하는 Customer
클래스
public class Customer { public final String name; private final Address[] address; public Customer(String name, Address[] address) { this.name = name; this.address = address; } public Address[] getAddress() { return address.clone(); } } |
목록 2의 코드 관련 문제점은 다음 목록 3과 같이 호출에서 getAddress()
메소드로 돌아오는 복제된 배열과 관련하여 어느 것이나 시도할 때 명시한다.
목록 3. 정확하지만 직관적이지 않은 결과를 보여주는 테스트
public static List<String> streets(String... streets) { return asList(streets); } public static Address address(List<String> streets, String city, String state, String zip) { return new Address(streets, city, state, zip); } @Test public void immutability_of_array_references_issue() { Address [] addresses = new Address[] { address(streets("201 E Washington Ave", "Ste 600"), "Chicago", "IL", "60601")}; Customer c = new Customer("ACME", addresses); assertEquals(c.getAddress()[0].city, addresses[0].city); Address newAddress = new Address( streets("HackerzRulz Ln"), "Hackerville", "LA", "00000"); // doesn't work, but fails invisibly c.getAddress()[0] = newAddress; // illustration that the above unable to change to Customer's address assertNotSame(c.getAddress()[0].city, newAddress.city); assertSame(c.getAddress()[0].city, addresses[0].city); assertEquals(c.getAddress()[0].city, addresses[0].city); } |
복제된 배열을 리턴할 때, 기본 배열을 보호한다 — 하지만 일상적인 배열과 같아 보이는 배열을 돌려주는 중이며, 이는 배열의 컨텐츠를 변경할 수 있음을 의미한다. (배열을 보유하는 변수가 final
인 경우에도, 이는 배열의 컨텐츠가 아니라 배열 참조 그 자체에만 적용된다.) Collections.unmodifiableList()
(및 다른 유형의 Collections
에서 메소드의 제품군)를 사용하면 사용 가능한 변형 메소드가 없는 오브젝트 참조를 수신한다.
독자는 불변 필드를 private로 만들어야 한다는 것도 자주 접하게 된다. 필자는 다르지만 명확한 시각을 가지고 있는 사람이 뿌리깊은 가정을 명확히 설명하는 것을 들었으므로 이러한 감상에 동의하지 않는다. Michael Fogus의 Clojure 창시자 Rich Hickey와의 인터뷰에서(참고자료 참조) Hickey는 Clojure의 많은 핵심적인 부분의 데이터 숨기기 요약의 부재에 대해 논한다. Clojure의 이러한 면은 필자가 상태 기반 사고에 매우 열중하고 있기 때문에 항상 필자를 괴롭힌다. 하지만 필자는 노출된 필드가 불변인 경우 이에 대해 독자가 걱정할 필요가 없다는 것을 인식했다. 요약을 위해 우리가 사용하는 많은 보호 장치들은 정말 변형을 방지하는 것에 불과하다. 이러한 두 가지 개념을 분리하여 잘라내면, 더 깔끔한 Java 구현 방식이 나타난다.
다음 목록 4의 Address
클래스의 버전을 고려하자.
목록 4. public 불변 필드가 있는 Address
클래스
public final class Address { private final List<String> streets; public final String city; public final String state; public final String zip; public Address(List<String> streets, String city, String state, String zip) { this.streets = streets; this.city = city; this.state = state; this.zip = zip; } public final List<String> getStreets() { return Collections.unmodifiableList(streets); } } |
불변 필드에 대해 공용 getXXX()
메소드를 선언하면 기본적인 표현을 숨기려고 하는 경우에만 혜택을 누린다. 하지만, 이는 이러한 변경을 사소하게 찾을 수 있는 리팩토링 IDE의 시기에 모호한 혜택이다. 필드를 public과 불변으로 만들어 우발적으로 이를 변경하는 것에 대해 걱정하지 않고 코드에서 이에 직접 액세스할 수 있다.
콜렉션을 내부적으로 변형할 필요가 전혀 없는 경우, 생성자에서 임베드된 목록을 unmodifiableList
로 캐스트할 수 있으며, 이를 통해 streets
필드를 public으로 만들고 getStreets()
메소드에 대한 필요를 없앨 수 있다. 필자가 다음 예제에서 설명하는 대로, Groovy를 사용하면 getStreets()
와 같은 보호 액세스 메소드를 작성하여 필드로 나타나도록 할 수 있다.
불변 public 필드를 사용하는 것은 화난 원숭이에 귀를 기울인다면 처음에는 부자연스럽게 보이겠지만 이러한 차이점이야말로 혜택이다. 즉, 독자는 Java에서 불변 유형을 처리하는 데 익숙하지 않고 이는 다음 목록 5에 시연한 대로 새 유형과 같이 보인다.
@Test (expected = UnsupportedOperationException.class) public void address_access_to_fields_but_enforces_immutability() { Address a = new Address( streets("201 E Randolph St", "Ste 25"), "Chicago", "IL", "60601"); assertEquals("Chicago", a.city); assertEquals("IL", a.state); assertEquals("60601", a.zip); assertEquals("201 E Randolph St", a.getStreets().get(0)); assertEquals("Ste 25", a.getStreets().get(1)); // compiler disallows //a.city = "New York"; a.getStreets().clear(); } |
public 불변 필드에 접근하는 것은 getXXX()
호출의 시리즈의 시각적 오버헤드를 방지한다. 컴파일러는 원시 언어 중 하나를 지정하도록 허용하지 않을 것이라는 점도 주목하자. 그리고 street
콜렉션에서 변형 메소드를 호출하려고 노력하는 경우, UnsupportedOperationException
이 나타날 것이다(테스트의 맨 위에 포착됨). 코드의 이 스타일을 사용하면 이는 불변 클래스임을 알려주는 강력한 시각적 표시기이다.
더 깔끔한 구문에서 발생 가능한 불이익은 이러한 새로운 관용어를 배우는 면에서 나오는 노력이다. 하지만 필자는 이를 가치있다고 생각한다. 명백한 스타일적 차이점으로 인해 클래스를 작성할 때 불변성에 대해 생각하도록 권장하고, 불필요한 중복된 코드를 줄인다. 하지만 Java에서 이 코딩 스타일에 일부 부작용이 있다(이는 공평하게 불변성을 직접 수용하도록 절대 제작되지 않았음).
Address
클래스의 getStreets()
메소드는 다른 필드와 균등하지 않다. 이 문제는 정말로 Java에서 해결될 수 없다. 이는 불변성을 사용하는 방식으로 다른 일부 JVM 언어로 해결된다. Groovy로 된 Address
클래스의 public 불변 필드 버전을 빌드하면 다음 목록 6과 같이 훌륭하고 깔끔한 구현 방식이 나타난다.
목록 6. Groovy로 된 불변 Address
클래스
class Address { def public final List<String> streets; def public final city; def public final state; def public final zip; def Address(streets, city, state, zip) { this.streets = streets; this.city = city; this.state = state; this.zip = zip; } def getStreets() { Collections.unmodifiableList(streets); } } |
평소대로 Groovy는 Java보다 중복된 코드를 적게 요청한다 — 그리고 다른 혜택도 있다. Groovy를 통해 익숙한 get
/set
구문을 사용하여 특성을 작성하도록 허용하기 때문에, 오브젝트 참조에 진정으로 보호된 특성을 작성할 수 있다. 다음 목록 7의 유닛 테스트를 고려하자.
목록 7. Groovy로 균등한 액세스를 보여주는 유닛 테스트
class AddressTest { @Test (expected = ReadOnlyPropertyException.class) void address_primitives_immutability() { Address a = new Address( ["201 E Randolph St", "25th Floor"], "Chicago", "IL", "60601") assertEquals "Chicago", a.city a.city = "New York" } @Test (expected=UnsupportedOperationException.class) void address_list_references() { Address a = new Address( ["201 E Randolph St", "25th Floor"], "Chicago", "IL", "60601") assertEquals "201 E Randolph St", a.streets[0] assertEquals "25th Floor", a.streets[1] a.streets[0] = "404 W Randoph St" } } |
두 경우 모두 불변성 계약 위반으로 인해 예외가 발생할 때 테스트가 종료된다는 것을 참고한다. 하지만, 목록 7에서 streets
특성은 원시 언어와 같이 보이지만 getStreets()
메소드를 통해 실제로 보호된다.
이 시리즈의 기본적인 주장 중 하나는 함수형 언어가 낮은 레벨 세부사항을 더 많이 처리해야 한다는 것이다. 훌륭한 시연은 Groovy 버전 1.7에 추가된 @Immutable
어노테이션이며, 이는 목록 6의 모든 코딩을 미해결로 만든다. 다음 목록 8은 이 어노테이션을 사용하는 Client
클래스를 보여준다.
@Immutable class Client { String name, city, state, zip String[] streets } |
@Immutable
어노테이션의 사용으로 인해 이 클래스에 다음 특성이 있다.
ReadOnlyPropertyException
이 나타난다. equals
, hashcode
및 toString
메소드는 자동으로 생성된다. 이 어노테이션은 투자에 부합하는 많은 가치를 제공한다! 이는 또한 다음 목록 9와 같이 예상한 대로 작동한다.
목록 9. 예상된 케이스를 제대로 처리하는 @Immutable
어노테이션
@Test (expected = ReadOnlyPropertyException) void client_object_references_protected() { def c = new Client([streets: ["201 E Randolph St", "Ste 25"]]) c.streets = new ArrayList(); } @Test (expected = UnsupportedOperationException) void client_reference_contents_protected() { def c = new Client ([streets: ["201 E Randolph St", "Ste 25"]]) c.streets[0] = "525 Broadway St" } @Test void equality() { def d = new Client( [name: "ACME", city:"Chicago", state:"IL", zip:"60601", streets: ["201 E Randolph St", "Ste 25"]]) def c = new Client( [name: "ACME", city:"Chicago", state:"IL", zip:"60601", streets: ["201 E Randolph St", "Ste 25"]]) assertEquals(c, d) assertEquals(c.hashCode(), d.hashCode()) assertFalse(c.is(d)) } |
오브젝트 참조를 대체하기 위해 시도하면 ReadOnlyPropertyException
을 야기한다. 그리고 요약된 오브젝트 참조 중 하나가 가리키는 것을 변경하려고 시도하면 UnsupportedOperationException
을 생성한다. 이는 또한 마지막 테스트와 같이 적절한 equals
및 hashcode
메소드를 작성한다 — 오브젝트 컨텐츠는 동일하지만 동일한 참조를 가리키지는 않는다.
물론, Scala와 Clojure 둘 다 불변성을 지원하고 권장하며 이를 위한 정리 구문이 있다. 이에 대한 시사점은 향후 기사에서 나타날 것이다.
불변성을 받아들이는 것은 함수형 프로그래머와 같이 생각하는 방법에서 중요한 사항이다. 비록 Java에서 불변 오브젝트를 빌드하면 선행 복잡도를 약간 더 요청하지만, 이 추상으로 강제된 다운스트림 간소화로 노력이 간편하게 상쇄된다.
불변 클래스는 Java에서 일반적인 걱정거리의 주체를 사라지게 만든다. 함수형 사고방식으로 전환하는 이점 중 하나는 테스트가 코드에서 변경이 발생하는지 확인하기 위해 존재하는 것에 대한 깨달음이다. 다시 말해서, 테스팅의 진짜 용도는 변형을 유효성 검증하는 것이다 — 그리고 변형이 많으면 많을수록 올바르게 되기 위해 더 많은 테스팅이 필요하다. 심각하게 변형을 제한하여 변경이 발생하는 장소를 격리하면, 발생하는 오류에 훨씬 더 적은 공간을 제작하고 테스트할 장소를 더 적게 보유한다. 변경이 구성에서만 발생하기 때문에, 불변 클래스는 유닛 테스트를 쓰기에 더 단순하게 만든다. 복사 생성자가 필요하지 않으며, clone()
메소드를 구현하는 것의 처절한 세부사항에 힘들게 노력할 필요가 없다. 불변 오브젝트는 Map
또는 Set
중 하나에서 키로 사용하기에 훌륭한 후보를 만든다. 이는 Java로 된 사전 콜렉션에서 키는 키로 사용되는 동안 값을 변경할 수 없으므로 불변 오브젝트는 훌륭한 키를 만든다.
불변 오브젝트는 자동으로 스레드로부터 안전하고 동기화 문제가 없다. 이는 또한 예외이기 때문에 알려지지 않거나 원하지 않는 상태에서 절대 존재할 수 없다. 모든 초기화가 구성 시점에 발생하기 때문에, 이는 Java에서 원자적이다. 오브젝트 인스턴스를 보유하기 전에 어느 예외나 발생한다. Joshua Bloch는 이 실패 원자성을 환기시킨다. 즉, 이는 한 번 오브젝트가 생성되면 불변성에 따라 성공 또는 실패가 영원히 해결된다(참고자료 참조).
마지막으로 불변 클래스의 최고의 특성 중 하나는 불변 클래스가 컴포지션 추상에 얼마나 잘 맞는지이다. 다음 기사에서 필자는 컴포지션과 함수형 사고 영역에서 이 점이 왜 중요한지 조사하기 시작할 것이다.