Thread safe, Mutable, Immutable, final, String

Dayeon myeong·2022년 3월 4일
1

면접

목록 보기
8/35

Thread safe 란 뭔가요?

여러 스레드가 어떤 객체에 접근할 때, 해당 스레드들이 어떻게 스케쥴링을 하든, 동시에 객체를 다루든, 순차적으로 객체를 다루든, 어떤 순서로 객체를 다루둔 항상 정확하게 동작하면 해당 클래스는 thread safe하다고 한다.

항상 정확하게 동작한다는 것은 개발자가 생각하는 클래스 명세에 항상 부합한다는 뜻이다.
클래스 명세에는

  • 불변 조건 invariants : 객체의 상태를 제약하는 조건
  • 후조건 postcondition : 연산 수행 후 효과를 기술하는 후조건

두가지가 존재한다. 이 두가지 명세에 따라 항상 특정 코드가 작동하고, 어떤 효과를 가질것이라고 개발자가 신뢰할 수 있다면 그 코드는 정확하게 동작한다고 볼 수 있다. 이 클래스 명세를 통해 개발자는 코드 신뢰도code confidence 얻을 수 있다.

개인적인 의견으로는 이 thread safe는 race condition과는 반대되는 개념이라고 생각한다.
race condition은 여러 스레드나 프로세스가 접근하여 조작할 때, 어떤 순서로 접근하냐에 따라 실행 결과가 달라지는 상황을 말한다.

어떻게 구현해야 Thread safe 한 코드를 만들 수 있나요?

Atomicity 를 확보해야 한다.
여러 스레드들이 동시에 접근할 수 있는 공유 영역을 임계영역이라고 하는데, 이 임계영역에 대해서 다른 스레드가 접근하지 못하도록 해야 한다. 해당 임계영역이 실행될 동안에는 다른 스레드가 접근하지 못해야한다. 그러기 위해선 이 임계영역 작업은 항상 원자적으로 실행되어 단일 연산처럼 수행되어야 한다. 그러면 다른 스레드에서 끼어들지 못하고 context switch가 일어나지 않는다. 더이상 interrupt 걸수 없는 연산이라고 보면 된다.

예를 들어 운영체제에서 대표적인 race condition 문제로 프로듀서 컨슈머 문제가 있다. 프로듀서는 아이템을 생성해 버퍼에 담고, 버퍼에서 아이템을 꺼내 컨슈머가 처리를 한다. 이 버퍼가 가득찼는지 비어있는지 확인하기 위해 버퍼에 담긴 아이템의 개수를 세는 count라는 공유 변수가 있다. 프로듀서는 아이템을 생성시 count++을 수행하고, 컨슈머는 아이템 소비시 count--를 수행한다.
프로듀서와 컨슈머가 동시에 수행할 경우 이 count++과 count--가 동시에 수행되면 race condition이 일어난다.

이유는 실제로 count ++같은 명령어는 실제로는 기계어로 보면 3개의 명령어로 이루어져있기 때문이다. 그 사이사이 명령어에 context switch가 일어날 수 있다. 그래서 순차적으로 명령어가 실행되지 않고 기계어 차원에서 count++과 count—명령어들이 임의 순서로 뒤섞어져 실행된다.

Atomicity를 확보하기 위해선

  1. Atomic Variable 사용하기
    자바는 AtomicLong, AtomicInteger 등의 클래스를 제공하여, 이 Atomic Variable을 통해 단일 연산을 제공한다.

Atomic Variable 연산은 내부적으로 CAS를 기반으로 하여, 락이 아닌 cpu busy wait 방식으로 동작한다. 따라서 CPU를 계속쓰는 문제가 생길 수 있다. 하지만 여러 스레드가 context switch 하는 비용이 클 경우 context switch 비용보다는 CPU를 계속쓰는 비용이 더 적을 수 있으니 상황에 따라 사용해야 한다.

또한, 클래스에 상태 변수가 여러개인 경우에는 Atomic Variable을 사용해도 문제가 생길 수 있다.

  1. 락을 사용하기 - synchronized
    클래스에 상태 변수가 여러개인 경우에는 Atomic Variable을 사용해도 문제가 생길 수 있다. 어떤 메서드에서의 연산시 여러 Atomic Variable을 사용한다면 각 Atomic Variable은 스레드에 안전하지만 그 메서드 자체는 틀린 결과를 낼 수 있는 race condition을 가지고 있다. 해당 메서드 자체에 대해 단일 연산처럼 만들어야 한다.

예를 들어 자바에는 Sychronized가 있다.
Synchronized는 모니터락을 사용하여 락을 가진 객체를 지정할 수 있다. Synchronized 메소드나 Synchronized블록을 통해 락을 확보하며, 해당 블록을 벗어날 때 락이 해제된다.
하지만 synchornized는 아예 락을 걸어버리는 것이기 때문에 성능에 영향을 줄 수 있다.
어떤 메서드에 synchronized를 걸어버리면 해당 객체는 한번에 한 스레드만 실행하기 때문에 동시 처리가 불가하고,서블릿 프레임워크같이 요청별로 스레드를 생성해서 동시에 여러 요청을 처리해야할 경우 속도가 느려진다. 다른 클라이언트 요청은 현재 요청이 완료할 때까지 마냥 기다려야 한다.

따라서 synchronized 블록의 크기를 적정하게 유지하여야 한다.

Mutable, Immutable 이란 뭔가요? 각각은 어떤 특징이 있을까요?

Immutable이란 맨 처음 객체가 생성되는 시점을 제외하고는 객체 내부 상태가 전혀 바뀌지 않는 것을 말한다. 별다른 동기화를 하지 않아도 항상 thread safe하기 때문에 어느 스레드에서건 마음껏 안전하게 사용할 수 있다. 대표적인 불변 객체로는 String이 있다.

클래스를 불변으로 만들려면 다음 규칙을 따르면 된다.

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다.
  2. 모든 클래스 변수를 private와 final로 선언하라 : final 키워드는 변수의 재할당을 막고, private은 외부에서 변수를 직접 접근하도록 한다.
  3. 자신 외에는 내부의 가변 객체에 접근하지 못하도록 한다 : 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 외부에서는 그 객체의 참조를 얻을 수 없도록 해야한다. 참조에 의해 변경가능성이 있는 경우 방어적 복사를 이용하여 전달하라.
//Game.class
public class Game {
	private final List<String> list = new ArrayList<>();
	...
	public List<String> getList() { 
    	return Collections.unmodifiableList(list); 
    }
  1. 클래스를 확장하지 못하도록 한다. : 상속을 막는 방법으로는 클래스를 final로 선언하는 방법도 있지만 정적 팩토리를 제공하는 방법도 있다. (정적 팩토리는 생성자를 private 으로 두고, public 정적 팩토리 메서드를 제공한다. public이나 protected 생성자가 없으니 다른 패키지에서 이 클래스를 확장하는 게 불가능하기 때문이다. )
public class Complex {
	private final double re;
	private final double im;

	private Complex(double re, double im) {

		this.re = re;this.im = im;
	}

	public static Complex valueOf(doulble re, double im) {
		return new Complex(re, im);
	}
}

장점은

  • thread safe하기 때문에 동기화 필요 없이 멀티 스레드 환경에서 안전하게 사용가능하다.
  • 한번 만든 인스턴스는 재활용이 가능하다. 즉, 객체 생성 개수를 줄일 수 있다.
public static final Complex ZERO = new Complex(0,0);

단점은

  • 값이 다르면 반드시 독립된 객체로 만들어야 한다.값의 가짓수가 많으면 이를 모두 만드는데 큰 비용이 필요하다. 값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 한다. 예컨데 백만 비트짜리 BigInteger에서 비트 하나를 바꿔야 한다고 해보자. flibBIt 메서드는 새로운 BigInteger인스턴스를 생성할 때, 워본과 단지 한비트만 다름에도 백만 비트짜리 인스턴스를 또 생성하는 것이다. 이 연산은 BigInteger의 크기에 비례해 시간과 공간을 잡아먹는다. BItSet도 BigInteger처럼 임의 길의의 비트 순열을 표현하지만, BigInteger와는 달리 ‘가변'이다. BigSet 클래스는 원하는 비트 하나만 상수 시간 안에 바꿔주는 메서드를 제공한다.

이펙티브 자바 아이템 17 변경 가능성을 최소화하라 글 참고

Java 의 Collections.unmodifiableList 같은 API 를 이용해 List 같은 collection 을 변경 불가능하게 만들 수 있습니다. 그렇다면 이 API 를 사용하면 immutability 를 달성할 수 있을까요?

Collections.unmodifiableList()같은 메서드들은 Read-Only 용도로만 사용할 수 있으며 add(), addAll()과 같은 수정 메서드를 호출 시에 exception을 발생시킨다.
보통 클래스의 내부에 컬렉션인 가변 객체가 있을 때 참조에 의해 변경 가능성이 있기 때문에 그대로 가변객체를 주지 않고, unmodifiableList로 방어적 복사를 해서 전달한다. 복사된 리스트로는 수정이 되지 않는다. 하지만 원본 자체에 대한 수정을 막을 수는 없다.


public class Test {

	public static void main(String []args) {

		List<String> list = new ArrayList<String>();
		list.add("a");
        list.add("b");
		list.add("c");
		
        //수정을 못하도록 방어적 복사를 함.
		List<String> unmodifiableList = Collections.unmodifiableList(list);

		try {
			unmodifiableList.add("d"); //수정을 못하기 때문에 exception 터짐
			System.out.println("cannot be reached here");
		} catch (UnsupportedOperationException e) {
			System.out.println("Cannot modify unmodifiable list");//exception 터짐
		}

		list.add("d"); //원본자체에 대한 수정은 못막음!!!

		System.out.println(unmodifiableList.get(3));//d로 수정된 값 출력
	}
}


//출력 결과
Cannot modify unmodifiable list
d

Immutable이란 맨 처음 객체가 생성되는 시점을 제외하고는 객체 내부 상태가 전혀 바뀌지 않는 것을 말한다. unmodifiable을 써도 방어적 복사된 리스트에 대해서는 수정을 못하지만, 원본 자체에 대해서는 수정이 가능하기 때문에 불변성을 달성할 수 없다.

final 키워드를 변수, 메소드, 클래스에 선언하는 것은 어떤 의미가 있습니까?

  • final 클래스 : 상속을 할 수 없다. 즉, 다른 클래스의 부모가 될 수 없다.
  • final 메서드 : 오버라이딩이 되지 않는다.
  • final 변수 : 값의 재할당을 막는다.

final 필드는 무조건 불변인가?

final은 값의 재할당을 하지 못한다. final 이란 영어 뜻 최종적, 마지막이라는 의미 그대로 초기에 한 번 할당을 하게 되면 최종적인 값이 된다. 즉, 재할당을 막는다는 것이지 불변이라는 것은 아니다.

final int num = 3; //할당 완료

num = 4;//불가

final 필드에는 primitive type, reference type이 있을 수 있다.
primitive type의 경우에는 객체가 아니라 리터럴 값이라서 불변하다.
하지만 reference type의 final 필드는 불변하지 않을 수 있다.

// reference type 필드
//Game.class
public class Game {
	private final Car car = new Car();
	...
}

//Main.class
public void makeCar() {
	car.setPosition(3);//상태 변경 가능
}
//Game.class
public class Game {
	private final List<String> playerNames = new ArrayList<>();
	...
}

...

//Main.class
player.getPlayerNames().add("one");// 상태 변경 가능
player.getPlayerNames().add("two");// 상태 변경 가능

위 코드에서는 reference type인 car, playerNames 모두 내부 상태를 변경 시킬 수 있게 된다. 그러므로 final이 반드시 불변을 보장해주는 것은 아니다.

String, StringBuilder, StringBuffer

  • String : Immutable 불변한 문자 클래스. 불변하기 때문에 thread safe하다. 또한 +연산시 매번 새로운 객체를 만들어 사용하던 객체는 GC 대상이 된다. 따라서 +연산이 많이 일어난다면 메모리를 많이 사용하고, 응답속도에도 영향을 미친다. 문자열 연산이 적고 조회가 많은 멀티스레드 환경에서 좋다.
  • StringBuffer : Mutable한 문자 클래스. 더하기 연산 시에는 기존 객체를 재사용한다. 또한, 내부적으로 synchronized method를 사용해서 thread safe하다. 문자열 연산이 많고 멀티 스레드 환경에서 좋다
  • StringBuilder : Mutable한 문자 클래스. 더하기 연산 시에는 기존 객체를 재사용한다. 문자열 연산이 많고 싱글 스레드 환경에서 좋다

민감한 정보를 String 으로 저장하는 것과, char[] 또는 StringBuilder/StringBuffer 같은 클래스로 저장하는 것은 어떤 차이가 있나요?

참고

이펙티브 자바

자바 병렬 프로그래밍

Java 불변 객체(Immutable Object) 및 final을 사용해야 하는 이유

final은 불변을 보장할까?

Java Unmodifiable Collection vs Immutable 차이점

profile
부족함을 당당히 마주하는 용기

0개의 댓글