이 내용은 "Java 언어로 배우는 디자인 패턴 입문 - 멀티쓰레드편" 책의 Chapter 02 부분을 정리한 것이다.
Immutable 패턴
- immutable: 불변의, 변하는 것이 없는.
- immutable 클래스: 인스턴스 생성 후에는 인스턴스의 상태가 절대 변하지 않는 클래스.
public final class Person { // final 클래스이므로 상속 불가 // name, address는 final 필드이므로 setter 메소드 미존재, getter 메소드만 존재 // private 필드이므로 외부에서 접근 불가 private final String name; private final String address; // final 필드들의 값을 생성자에서 초기화 public Person(String name, String address) { this.name = name; this.address = address; } public String getName() { return name; } public String getAddress() { return address; } public String toString() { return "[ Person: name = " + name + ", address = " + address + " ]"; } }
1. final 필드
- 이 Person 클래스의 모든 필드는 final 이다. 따라서 생성자에서만 초기값을 설정하며, 이후 값을 변경할 수 없다.
- 즉, Person 클래스의 인스턴스는 일단 생성된 이후에는 필드 값이 절대 바뀌지 않는다. 필드 값을 변경하는 메소드가 존재하지 않는다.
- 따라서 인스턴스에 여러개의 쓰레드가 동시에 접근하더라도 안전하다. 변경되지 않는 값을 read 하는 메소드만 존재하기 때문에 synchronized 선언이 필요 없다.
- 필드에 final을 선언하는 것이 Immutable 패턴에서의 필수 조건은 아니지만, 프로그래머의 의도를 명확히 하고 실수로 코드를 잘못 작성하더라도 컴파일 에러를 일으키도록 할 수 있다.
2. final 클래스
- 클래스 자체에도 final이 선언되어 있어서 서브 클래스를 만들 수 없다.
- 필드와 마찬가지로 final 클래스 선언이 Immutable 패턴의 필수 조건은 아니지만, 서브 클래스로 인해 필드 값이 변경되는 것을 막기 위한 예방조치이다. (어차피 필드 값이 private라서 서브 클래스도 접근 못하긴 한다.)
3. 결론
- Person 클래스와 같은 Immutable 클래스의 인스턴스는 여러 쓰레드가 동시에 접근하더라도 절대 문제가 일어나지 않는다. 즉 synchronized를 굳이 사용할 필요가 없음.
- 하지만 Immutable 클래스임을 보장하는 것은 생각보다 어렵기 때문에 주의가 필요함.
메모
- Thread.currentThread() : 현재의 쓰레드에 대응하는 java.lang.Thread 클래스의 인스턴스를 반환하는 메소드
- Thread.currentThread().getName() : 현재의 쓰레드의 이름을 반환하는 메소드
- System.out.println(인스턴스명) : 해당 인스턴스의 toString() 메소드가 호출되어 출력됨.
- 클래스 다이어그램에서 필드명 옆에 { frozen } 을 명기하는 것은 인스턴스가 생성된 이후에는 필드의 값이 변경되지 않음을 의미함. (자바에서의 final 필드)
- 클래스 다이어그램에서 메소드명 옆에 { concurrent } 를 명기하는 것은 여러개의 쓰레드에서 동시에 실행해도 괜찮다는 것을 의미함. (자바에서 synchronized를 붙이지 않아도 바르게 동작함)
어떤 경우에 사용하는가
1) 인스턴스 생성 후에는 인스턴스 상태가 변하지 않을 때
- 필드에 final이 선언되어 있고, setter 메소드가 존재하지 않아야 함.
- 하지만 필드에 final이 선언되어 있고, setter 메소드가 존재하지 않더라도 immutable이 아닐 수 있음. 필드값 자체는 변하지 않더라도 필드가 참조하고 있는 곳의 인스턴스가 변할 수 있기 때문임. 주의 필요. (예: private final StringBuffer sb; - sb 필드의 값 자체는 어떤 StringBuffer 인스턴스의 "레퍼런스"이다. sb 필드의 값 자체는 변하지 않지만, StringBuffer 인스턴스의 상태는 변할 수 있다.)
2) 인스턴스가 공유되어 빈번하게 액세스될 때
- Immutable 패턴의 이점은 'synchronized로 guard하지 않아도 된다'는 것임. 이는 안전성과 생존성을 유지하면서도 성능을 높일 수 있음을 의미함.
- 인스턴스를 여러개의 쓰레드에서 공유하고, 빈번하게 액세스할 가능성이 있는 경우에는 Immutable 패턴이 큰 효력을 발휘할 수 있음.
mutable & immutable
자바 클래스 라이브러리에는 mutable 클래스와 immutable 클래스가 짝을 이루고 있는 경우가 있음. 대표적으로 java.lang.StringBuffer 클래스는 mutable 하고, java.lang.String 클래스는 immutable 하다.
- StringBuffer 클래스의 문자열은 자유롭게 값 변경이 가능하다. 값 변경시에는 synchronized가 적절히 사용된다.
- String 클래스는 값 변경이 불가능하지만 메소드에 synchronized가 사용되지 않기 때문에 바른 참조가 가능하다.
- StringBuffer 클래스에는 String을 인자로 가지는 생성자가 있고, 반대로 String 클래스에는 StringBuffer를 인자로 갖는 생성자가 있다. 즉 이 두 클래스의 인스턴스는 상호 변환이 가능하다.
- 문자열의 내용을 자주 변경한다면 StringBuffer를 사용하고. 내용을 변경하지 않고 참조만 한다면 String을 사용한다.
자바 클래스 라이브러리에서 사용되는 Immutable 패턴
- java.lang.String : 문자열
- java.math.BigInteger : 큰 정수
- java.math.Decimal : 큰 수
- java.util.regex.Pattern : 정규 표현 패턴
- java.lang 패키지의 기본형 랩퍼 클래스 (Boolean, Byte, Char, Double, Float, Int, Long, Short, Void)
참고사항) java.lang.Void 클래스는 다른 랩퍼 클래스들과는 달리 인스턴스를 만들 수 없음.
관련 패턴
Single Threaded Execution 패턴
- 하나의 쓰레드가 인스턴스의 상태를 바꾸는 중에는 다른 쓰레드가 인스턴스에 접근하지 못하게 하는 패턴. 이 때 벌어지는 일은 다음 둘 중 하나임.
1) write-write conflict : 하나의 쓰레드가 인스턴스의 필드에 write 하는 도중에 다른 쓰레드도 write 하려는 충돌
2) read-write conflict : 하나의 쓰레드가 인스턴스의 필드에 read 하는 도중에 다른 쓰레드가 write 하려는 충돌
- Immutable 패턴에서는 상태의 변화(필드에 write)가 없기 때문에 아무 conflict도 일어나지 않음. read-read 상황만 발생.
Read-Write Lock 패턴
- Immutable 패턴에서는 read-read 상황만 발생함.
- Read-Write Lock 패턴에서는 read를 수행하는 쓰레드와 write를 수행하는 쓰레드를 나누어서 생각함. write-write / read-write와 같이 conflict가 일어나는 경우에는 쓰레드의 mutual exclusion을 수행하고, read-read의 경우에는 mutual exclusion을 수행하지 않음으로써 성능을 높임.
Flyweight 패턴
- Immutable 패턴에서는 인스턴스의 상태가 변하지 않기 때문에 여러개의 쓰레드가 한 개의 인스턴스를 공유함.
- Flyweight 패턴에서는 메모리의 이용 효율성을 높이기 위해 인스턴스를 공유함.
- 따라서 Immutable 패턴은 Flyweight 패턴과 동시에 이용될 수 있음.
Java 언어의 Final
1) final 클래스
- final 클래스는 서브클래스를 만들 수 없음. 즉 final 클래스의 메소드들은 오버라이드 될 수 없음
2) final 메소드
- final 메소드는 서브클래스에서 오버라이드 할 수 없음.
3) final 필드
- final 필드에는 값을 딱 한번만 대입할 수 있음. 이후에는 값 대입(변경) 불가능.
방법 1) 필드 선언시 초기 값을 지정. ( final int value=123; )
방법 2) 필드 선언시에는 blank final로 냅두고, 생성자에서 값을 지정. ( 선언시에는 final int value; 생성자에서 this.value=123; )
4) static final 필드
- 마찬가지로 final 필드이기 때문에 값을 딱 한번만 대입할 수 있음. 이후에는 값 대입(변경) 불가능.
방법 1) 필드 선언시 초기 값을 지정. ( static final int value=123; )
방법 2 ) 필드 선언시에는 blank final로 냅두고, static 블록 안에서 값을 지정.
static final int value;
static {
value=123;
}
4) final 지역 변수
- 지역 변수 또한 final 선언 가능. 값을 딱 한번만 대입할 수 있음
5) final 인자/매개변수 (parameter)
- 메소드의 매개변수를 final 선언 가능.
- 메소드가 호출 되었을 때 이미 값이 대입되었기 때문에, 메소드 내에서는 값을 한번도 대입할 수 없음.
댓글