공유 중인 가변 데이터는 동기화해 사용하라


아이템 선정 이유

레벨1 마지막 체스 미션의 코드를 가져와서 스프링 환경에 적용하는 과정에서 쓰레드와 관련된 문제에 직면한 경험이 있다. 처음 체스 코드는 기존 레벨1 선택 요구사항만을 고려하여 다음과 같이 ChessService 를 구성하였다.

public class ChessService {

  private final ChessGameDao chessGameDao;
  private final PieceDao pieceDao;
  private ChessGame chessGame;
}

어떤 client에서 서버로 요청을 보내면 커넥션이 연결되고 WAS는 서블릿 객체를 호출해준다. 이 때의 호출은 쓰레드가 해주고, 따라서 동시처리가 필요하게 되면 쓰레드를 추가로 생성해주게 되어야한다. 즉 멀티쓰레드 환경이 된다. 그래서 레벨2 웹 체스 미션 의 요구사항인 동시에 여러 게임 하기 를 만족시킬 수 없었다.

그 이유는 무엇일까? ChessService 에서 체스게임 진행 상황인 ChessGame을 상태로 가졌기 때문이다. 이 chessGame 이라는 인스턴스 변수(데이터)에 대해서는 각 요청(쓰레드)에서 동시에 접근할 수 있게 된다.

이러한 문제상황과 관련하여 아이템78: 공유 중인 가변 데이터는 동기화해 사용하라 가 적절해보여 스터디 주제로 선정하게 되었다.


배타적 실행

자바에서는 synchronized를 사용해서 어떤 메소드나 블록을 어느 한 순간에 하나의 쓰레드만 접근해 수행할 수 있도록 보장한다. 즉, 어떤 한 쓰레드가 '가변 데이터'에 접근해 변경하는 중이라서 상태가 일관되지 않은 순간에 다른 쓰레드가 보지 못하게 막는 용도로 사용한다.

어떤 데이터에 접근하는 쓰레드는 lock을 건다. lock을 걸었기 때문에 어느 한 순간에 그 데이터에 접근할 수 있는 쓰레드는 하나뿐이게 된다. 즉, 데이터를 하나의 일관된 상태에서 다른 일관된 상태를 변환시키면서 동시에 다른 어떤 쓰레드도 그 데이터의 상태가 일관되지 않은 순간에 접근을 막을 수있게 되는 것이다.

동기화의 또 다른 기능

동기화에는 배타적 실행 이외에 또 다른 중요한 기능이 하나 더 있다. 동기화 없이는 한 쓰레드가 발생시킨 변화를 다른 쓰레드에서 확인하지 못할 수 있다. 즉, 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메소드나 블록에 들어간 쓰레드가 같은 lock의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.

원자적(Atomic)

언어 명세상 long과 double 외의 변수를 읽고 쓰는 동작은 원자적(atomic)임을 보장한다. (원자적이라는 것은 기계어 명령어 한 번으로 처리가 수행됨을 말한다.)

즉, long 과 double 이외의 타입에 대해서는 여러 쓰레드가 하나의 같은 변수에 동기화없이 접근해도 정상적으로 원하는 값을 온전히 읽어옴을 보장한다는 뜻이다. 하지만 자바 언어 명세에서 '수정이 완전히 반영된' 값을 얻는 것은 보장하지만, 다른 쓰레드에서 그 값이 '보이는가'는 보장하지 않는다.

즉, 동기화는 배타적 실행 뿐 아니라 쓰레드 사이의 안정적인 통신에 꼭 필요하다.

volatile

배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.