러스트의 변수 선언



임뮤터블(불변) 형:


let x = 5;

이렇게 선언된 변수에 다른 값을 재할당 시도하면 컴파일 오류 발생.



뮤터블 형:


let mut x = 5;

이제 값 재할당 가능.



타입 지정:


let x: u32 = 5;

변수 선언 시 타입을 따로 지정하지 않으면 러스트가 알아서 적절한 타입을 부여함. (type inference)



상수:


const MAX_OPINTS: u32 = 100_000;

상수에는 선택적으로 임뮤터블/뮤터블을 지정할 수 없음. 무조건 임뮤터블이며, 타입을 반드시 명시해주어야 함.



쉐도잉


let x = 5;


let x = x + 1;


let x = x * 2;

이미 선언된 변수명을 재선언 하여 사용 가능. 이렇게 하면 같은 이름의 완전히 새로운 변수가 생성되기 때문에 임뮤터블 변수였다 할지라도 새로운 값을 넣을 수 있음. 아래와 같이 변수의 타입도 새롭게 지정 가능. 이를 쉐도잉이라 함.


let spaces = "    ";    // 스트링 타입

let spaces = spaces.len()    // 넘버 타입

















'Rust' 카테고리의 다른 글

컨트롤 플로우(루프)  (0) 2018.02.15
컨트롤 플로우(if 조건문)  (0) 2018.02.15
함수의 동작  (0) 2018.02.15
데이터 타입  (0) 2018.02.14
환경 잡기  (0) 2018.02.13

윈도우 로컬 환경 잡기


1. Rust 설치 

- 아래 경로에서 다운로드 받아 설치

https://www.rust-lang.org/ko-KR/install.html



2. C++ 컴파일러 설치

- 아래 경로에서 Visual C++ 빌드 툴 설치

https://support.microsoft.com/ko-kr/help/2977003/the-latest-supported-visual-c-downloads



3. 환경변수 설정

- path 에 아래 경로 추가 (Rust 디폴트로 설치했을 경우)

%사용자경로%\.cargo\bin;



============

컴파일 시도 결과로 아래 에러가 나오면 C++ 컴파일러가 설치되지 않았기 때문이다. 컴파일러를 설치하자.

error: could not exec the linker link.exe: The system cannot find the file specified. (os error 2)


'Rust' 카테고리의 다른 글

컨트롤 플로우(루프)  (0) 2018.02.15
컨트롤 플로우(if 조건문)  (0) 2018.02.15
함수의 동작  (0) 2018.02.15
데이터 타입  (0) 2018.02.14
변수  (0) 2018.02.14

Node.js 의 쓰레딩 모델



싱글 쓰레드 이벤트 루프(libuv)


보통의 클라이언트 스크립트가 싱글 쓰레드로 동작하듯, node.js 로 싱글 쓰레드로 동작한다고 말하기는 하나, 엄밀히 말해 내부적으로는 멀티 쓰레드가 구비되어 있다. 클라이언트가 요청을 보내면 이벤트 루프 쓰레드가 요청을 받아 필요에 따라(요청의 성격에 따라) 내부의 쓰레드 풀(Internal Thread Pool)에서 대기 중인 쓰레드에 요청을 위임하여 처리하며, 필요하지 않을 때는 자신(이벤트 루프 쓰레드)이 직접 처리한다. 


이 과정은 아래와 같은 순서로 이루어진다.


1. 클라이언트가 요청을 보낸다.

2. 클라이언트의 요청이 이벤트 큐(Event Queue)에 담긴다.

3. 이벤트 루프 쓰레드(싱글 쓰레드)가 이벤트 큐에서 요청을 가져온다.

4. 이벤트 루프 쓰레드가 이벤트 큐에서 꺼낸 요청이 블로킹 IO 혹은 기타 처리에 많은 시간을 요구하는 복잡한 성격의 요청인지 확인한다.

-if: 요청이 블로킹 IO 혹은 기타 처리에 많은 시간은 요구하는 복잡한 성격의 요청(주로 파일시스템 IO, 데이터베이스, 외부 서비스 처리 등)이라면-

4.1.1. 이벤트 루프 쓰레드는 이 요청을 처리하지 않고 내부 쓰레드 풀에서 대기 중인 쓰레드에게 요청을 위임한다.

4.1.2. 요청을 위임받은 내부 쓰레드는 요청을 처리하고 이벤트 루프 쓰레드에게 응답을 전달한다.

4.1.3. 이벤트 루프 쓰레드는 응답을 요청 클라이언트에게 전달한다. (종료)

-else-

4.2.1. 요청이 논 블로킹으로 처리될 성격의 요청이라면 이벤트 루프 쓰레드는 요청을 즉각 처리한다.

4.2.1. 이벤트 루프 쓰레드는 응답을 요청 클라이언트에게 전달한다. (종료)


클라이언트의 요청은 순서대로 이벤트 큐에 쌓이며, 이벤트 루프 쓰레드는 위의 이 요청을 큐에 쌓인 순서대로, 4번 과정부터 처리한다.


여기서 알 수 있는 중요한 사실은, IO 의 의미를 엄격히 적용할 때, Node.js 는 블로킹이며, IO 처리에 있어서만 논 블로킹이다.



Node.js 쓰레딩 모델의 장점


1. 클라이언트의 동시 요청을 다루기 쉽다.

2. 프로그래밍 레벨의 동시성 처리에 관하여 고민한 필요가 없다.

3. 싱글 쓰레드 이벤트 루프 덕에 클라이언트의 요청에 대해 더 많은 쓰레드를 생성할 필요가 없다.

4. 장점 3번 으로 인해 쓰레드 관리에 대한 메모리 효율이 좋다.



Node.js 쓰레딩 모델의 주의할 점


1. 무거운 작업을 내부 쓰레드에게 위임한다고 해서 이벤트 루프 쓰레드의 처리 지연이 없는 것은 아니다. 이벤트 루프 쓰레드가 내부 쓰레드와 대화하는 부분, 즉 요청을 위임하거나 응답을(내부 쓰레드로부터) 전달받는 과정에서 CPU 인텐시브한 작업이 많다면 이벤트 루프 쓰레드의 처리는 자연히 지연된다. 그리고 이벤트 루프 쓰레드는 싱글 쓰레드이기에, 이 현상은 node.js 서버의 전체 응답 시간을 지연시킬 수 있다.

2. 이벤트 루프 쓰레드의 동작에서 에러가 발생하면 서버가 쉽게 죽어버릴 수 있다. 일반적인 클라이언트 브라우저의 스크립트가 에러를 만나면 그 자리에서 멈춰버리듯, 이벤트 루프 쓰레드도 마찬가지의 경우가 발생할 수 있다.


스프링의 트랜잭션 관리


종합적인 트랜잭션 관리는 스프링 프레임워크를 사용하는 가장 중요한 이유 중 하나이다. 스프링 프레임워크는 다음과 같은 장점을 지닌 트랜잭션 관리를 위한 일관된 추상화를 제공한다.


  • 자바 트랜잭션 API(JTA), JDBC, 하이버네이트, 자바 영속성 API(JPA), 자바 데이터 객체(JDO) 와 같은 서로 다른 트랜잭션을 아우르는 일관된 프로그래밍 모델
  • 선언적 트랜잭션 관리
  • JTA 와 같은 복잡한 트랜잭션 API 보다 간단한 프로그래밍 방식의 트랜잭션 관리 API
  • 스프링 데이터 접근 추상화를 통한 훌륭한 통합

다음 섹션은 스프링 프레임워크릐 부가 가치와 기술에 대하여 설명한다. (또한 가장 실용적인 예제와 어플리케이션 서버 통합, 그리고 공통된 문제들에 대한 솔루션에 대한 논의를 포함한다.)


  • 스프링 프레임워크의 트랜잭션 관리 모델의 장점 에서는 왜 당신이 EJB 컨테이너 관리 트랜잭션(CMT) 이나 하이버네이트와 같은 상용 로컬 트랜잭션 관리 대신 스프링 프레임워크의 트랜잭션 관리를 사용해야 하는지 설명한다.
  • 스프링 프레임워크 추상화의 이해 에서는 핵심 클래스를 선보이며 다양한 자원으로부터 DataSource 인스턴스를 획득하고 설정하는 방법에 대해 설명한다.
  • 트랜잭션을 통한 동기화 에서는 어플리케이션 코드가 어떻게 자원의 생성, 재사용, 제거를 안전하게 수행하는지 설명한다.
  • 프로그래밍 방식 트랜잭션 관리 에서는 프로그래밍 방식(프로그램 코드를 통한) 트랜잭션 관리에 대해 알아본다.
  • 트랜잭션 바운드 이벤트 에서는 당신이 트랜잭션을 이용해 어떻게 어플리케이션 이벤트를 사용할 수 있는지 설명한다.


스프링 프레임워크의 트랜잭션 관리 모델의 장점

전통적으로 자바 EE 개발자들은 트랜잭션 관리를 위한 두 가지 선택지를 가지고 있었다. 글로벌 트랜잭션과 로컬 트랜잭션이 그것인데, 둘 모두 큰 한계를 가지고 있다. 글로벌와 로컬 트랜잭션 관리에 대해서는 다음 두 섹션에서 논하며 스프링 트랜잭션 관리가 이 두 트랜잭션 관리의 한계점을 다루는 방식을 설명한다.


글로벌 트랜잭션

글로벌 트랜잭션은 다수의 트랜잭션 관련 자원들을 다룰 수 있도록 해준다. 전형적인 트랜잭션 관련 자원은 관계형 데이터베이스와 메시지 큐와 같은 것들이다. 어플케이션 서버는 JTA 라는 다루기 힘든 API 를 통해 글로벌 트랜잭션을 관리한다. 더 나아가서 JTA 의 UserTransaction 은 일반적으로 JNDI 를 통해 얻어져야 하는데, 이는 JTA 를 사용하기 위해서는 JNDI 도 필요하다는 것을 의미한다. JTA 가 보통 어플리케이션 서버 환경에서만 유효하듯이, 확실히 글로벌 트랜잭션을 사용함은 어플리케이션의 잠재적인 재사용성을 제한하게 된다.

이전에는 EJB CMT(컨테이너 관리 트랜잭션) 을 사용하는 것이 글로벌 트랜잭션을 관리하기 위해 선호되는 방법이었다. CMT 는 하나의 선언적 트랜잭션 관리 형식이다. EJB CMT 는 트랜젹선 관련 JNDI 룩업을 사용할 필요가 없었으나, EJB 자체가 JNDI 를 필요로 했다. 자바 코드를 통한 트랜잭션 관리의 필요성을 거의 없앴긴 했지만, 완전히 없애지는 못했다. 중요한 단점은 CMT 는 JTA 와 어플리케이션 서버 환경에 묶여 있었다는 점이고, 또한 비즈니스 로직을 EJB 에 구현하거나, 적어도 EJB 의 외관 뒤에 구현하는 경우에만 사용할 수 있었다. EJB 의 선언적 트랜잭션 관리를 위한 강력한 대안에도 불구하고 이러한 단점들은 너무나 큰 것이어서 EJB 는 매력적인 제안이 되지 못했다.


로컬 트랜잭션

로컬 트랜잭션은 자원 종속적이다. JDBC 커넥션과 관련된 트랜잭션이 그렇다. 로컬 트랜잭션은 사용하지 쉬울 수 있지만, 큰 약점을 가지고 있다. 여러가지 트랜잭션 자원으로 작업할 수 없다. 가령, JDBC 커넥션을 사용하는 트랜잭션 관리 코드는 JTA 글로벌 트랜잭션에서 동작하지 않는다. 왜냐하면 어플리케이션 서버는 트랜잭션 관리와 관련이 없기 때문에, 여러 자원에 대한 정확성을 보장할 수 없다. (대부분의 어플리케이션은 단일 트랜잭션 자원을 사용한다는 점은 알아둬야 한다.) 또다른 단점은 로컬 트랜잭션은 프로그래밍 모델 안으로 급속히 퍼지게 된다.


스프링 프레임워크의 일관된 프로그래밍 모델

스프링은 글로벌와 로컬 트랜잭션의 약점을 해결한다. 스프링은 어플리케이션 개발자로 하여금 어떤 환경에서든 일관된 프로그래밍 모델을 사용할 수 있도록 한다. 한 번 작성된 코드는 다른 환경의 다른 트랜잭션 관리 전략에서도 사용될 수 있다. 스프링 프레임워크는 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리를 제공한다. 대부분 선언적 트랜잭션 관리가 선호되는데, 이 방식은 거의 모든 상황에 적합하다.

프로그래밍 방식 트랜잭션 관리를 사용하면 개발자는 어떠한 트랜잭션 기반 환경에서도 동작하는 스프링 프레임워크 트랜잭션 추상화로 작업할 수 있다. 선언적 트랜잭션 관리를 사용하면 개발자는 트랜잭션 관리와 관련된 코드를 아주 조금, 혹은 전혀 작성하지 않아도 되기 때문에, 코드는 스프링 프레임워크 트랜잭션 API 혹은 그 어떤 트랜잭션 API 에도 의존성을 갖지 않는다.

트랜잭션 관리를 위한 어플리케이션 서버가 필요한가?

스프링 프레임워크의 트랜잭션 관리는 엔터프라이즈 자바 어플리케이션이 어플리케이션 서버를 필요로 할 때와 같은 전통적인 규칙을 변경한다. 

특히 EJB 를 통한 선언적 트랜잭션을 위한 어플리케이션 서버는 필요하지 않다. 사실 당신의 어플리케이션 서버가 강력한 JTA 기능을 가지고 있다고 해도, 스프링 프레임워크의 선언적 트랜잭션은 EJB CMT 보다 더 강력하고 더 생산성이 뛰어난 프로그래밍 모델을 제공한다.

보통 어플리케이션 서버의 JTA 기능이 요구되는 경우는 어플리케이션이 여러가지 자원을 다뤄야 할 때 뿐인데, 많은 어플리케이션이 이런 기능을 필요로 하진 않는다. 많은 수의 고급 어플리케이션은 대신 높은 확장성을 지닌 단일한 데이터베이스를 사용한다. (오라클 RAC 와 같은) Atomikos Transactions, JOTM 과 같은 독립형 트랜잭션 관리자는 다른 선택 사항이다. 물론 자바 메시지 서비스(JMS), 자바 EE 컨테이너 아키텍처(JCA) 와 같은 어플리케이션 서버의 다른 기능을 필요로 할 수는 있다.

스프링 프레임워크는 어플리케이션을 언제 완전한 어플리케이션 서버로 확장할지 선택할 수 있게 해준다. JDBC 커넥션과 같은 로컬 트랜잭션으로 코드를 작성하기 위한 선택이 EJB CMT 나 JTA 뿐이던 시절은 지났다. 그리고 그런 코드를 글로벌, 컨테이너 관리 트랜잭션 안에서 실행하기 위해서는 막대한 재작업이 요구된다. 스프링 프레임워크를 사용하면 코드가 아닌 설정 파일 안의 빈 정의를 조금 바꿀 뿐이다.



스프링 프레임워크 트랜잭션 추상화의 이해


스프링 트랜잭션 추상화의 핵심은 트랜잭션 전략의 개념이다. 트랜잭션 전략은 org.springframework.transaction.PlatformTransactionManager 인터페이스에 의해 정의된다.


public interface PlatformTransactionManager {


    TransactionStatus getTransaction(

            TransactionDefinition definition) throws TransactionException;


    void commit(TransactionStatus status) throws TransactionException;


    void rollback(TransactionStatus status) throws TransactionException;

}


이 인터페이스는 어플리케이션 코드에 프로그래밍 방식으로도 사용 가능하긴 하지만, 기본적으로 서비스 제공 인터페이스(SPI)이다. PlatformTransactionManager 는 하나의 인터페이스이기 때문에 필요에 따라 손쉽게 모의화 또는 스텁 될 수 있다. 이 인터페이스는 JNDI 와 같은 룩업 전략에 구애받지 않는다. PlatformTransactionManager 구현체는 스프링 프레임워크 IoC 컨테이너 안의 다른 객체들(또는 빈)과 같은 방식으로 정의된다. 이런 장점은 스프링 프레임워크 트랜잭션 추상화를 가치있게 만들고, JTA 를 사용할 때조차도 이 장점은 유효하다. 트랜잭션 관련 코드의 테스트는 JTA 를 직접 사용할 때보다 훨씬 쉬워진다.


다시 스프링의 철학을 따라서, PlatformTransactionManager 의 모든 메서드에서 발생할 수 있는 예외인 TransactionException 은 unchecked(런타임 예외)이다. 트랜잭션 인프라의 에러는 거의 대부분 치명적이다. 어플리케이션 코드가 실제로 트랜잭션 실패를 복구할 수 있는 경우에는 TransactionException 을 캐치할 것인지 선택할 수 있다. 여기서 중요한 점은 개발자에게 트랜잭션 예외 캐치를 강제하지 않고 선택권을 준다는 것이다.


getTransaction(..) 메서드는 TransactionDefinition 파라미터에 따른 TransactionStatue 객체를 반환한다. 반환된 TransactionStatus 객체는 새로운 트랜잭션을 나타내거나, 혹은 현재 호출 스택에 매칭되는 기존 트랜잭션을 나타낼 수 있다. 후자의 경우는 자바 EE 트랜잭션 컨텍스트와 마찬가지로 TransactionStatus 가 실행 쓰레드와 연관되어 있음을 의미한다.


TransactionDefinition 인터페이스는 다음 사항들을 명시한다.


  • 격리: 이 트랜잭션이 다른 트랜잭션으로부터 격리되는 정도. 예로, 이 트랜잭션이 다른 트랜잭션의 커밋되지 않은 쓰기 작업을 볼 수 있는가?
  • 전파: 보통 한 트랜잭션 범위 안에서 실행된 코드는 그 트랜잭션 안에서 동작한다. 그러나 이미 트랜잭션이 존재할 때 트랜잭션 관련 메서드가 실행될 경우 트랜잭션을 어떻게 묶을 것인지 선택할 수 있다. 예로, 기존 트랜잭션 안에서 실행할 것인가, 혹은 생성된 새로운 트랜잭션으로 실행하여 기존 트랜잭션과 분리할 것인가. 스프링은 EJB CMT 와 유사한 모든 트랜잭션 전파 옵션을 제공한다. 스프링의 트랜잭션 전파의 의미에 대해서는 트랜잭션 전파 에서 읽어볼 수 있다.
  • 타임아웃: 트랜잭션 인프라에 의하여, 이 트랜잭션 수행이 자동 타임아웃-롤백 될 때까지 얼마만큼의 시간이 주어지는가.
  • 읽기 전용 상태: 읽기 전용 트랜잭션은 트랜잭션이 데이터를 읽기는 하지만 수정할 수는 없도록 할 때 사용된다. 읽기 전용 트랜잭션은 하이너베이트를 사용할 때와 같은 몇몇 경우에 유용할 최적화가 될 수 있다.

이 설정들은 표준적인 트랜잭션 개념을 반영한다. 필요하다면 트랜잭션 격리 수준과 다른 핵심적인 트랜잭션 개념을 논한 자료를 참조하라. 스프링 프레임워크나 다른 트랜잭션 관리 솔루션을 사용하기 위해서는 이 개념들을 이해해야 한다.

TransactionStatus 인터페이스는 트랜잭션 코드가 트랜잭션 실행과 쿼리 트랜잭션 상태를 제어하기 위한 쉬운 방법을 제공한다. 그 개념은 모든 트랜잭션 API 에게 공통된 사항인 것처럼 친숙해야 한다.

public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}

스프링의 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리 중 어떤 것을 선택하느냐에 상관없이 반드시 알맞는 PlatformTransactionManager 구현체가 정의되어야 한다. 일반적으로 의존성 주입을 통해 구현체를 정의한다.

PlatformTransactionManager 구현체에는 JDBC, JTA, 하이버네이트, 기타 등등의 트랜잭션이 작동할 환경에 대한 지식이 요구된다. 다음 예제는 로컬 PlatformTransactionManager 구현체를 정의하는 방법을 보여준다. (일반적인 JDBC 가 사용되었다.)

JDBC DataSource 를 정의한다.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

다음으로 이와 관련된 PlatformTransactionManager 빈 정의는 정의된 DataSource 으로 참조를 갖는다. 

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>


JTA 를 사용하는 자바 EE 컨테이너라면 JNDI 를 통해 얻어지는 DataSource 와 스프링의 JtaTransactionManager 를 함께 사용한다. JTA 와 JNDI 룩업 버전은 다음과 같다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:jee="http://www.springframework.org/schema/jee"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/jee

        http://www.springframework.org/schema/jee/spring-jee.xsd">


    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>


    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />


    <!-- other <bean/> definitions here -->


</beans>


JtaTransactionManager 는 DataSource 나 다른 어떤 자원에 대해서도 알 필요가 없다. 컨테이너의 글로벌 트랜잭션 관리를 기반으로 하기 때문이다.


다음 예제에서 보이듯, 하이버네이트 로컬 트랜잭션 또한 쉽게 사용할 수 있다. 이 경우 하이버네이트 LocalSessionFactoryBean 을 정의해야 한다. LocalSessionFactoryBean 은 어플리케이션 코드가 하이버네이트 Session 인스턴스를 얻기 위해 사용된다. 


DataSource 빈 정의는 이전의 로컬 JDBC 예제와 유사하다. 따라서 다음 예제에선 생략한다.


non-JTA 트랜잭션 관리자가 사용하는 DataSource 가 자바 EE 컨테이너가 관리하는 JNDI 룩업에 의해 얻어진다면, 이 DataSource 는 트랜잭션과 관련이 없도록 해야한다. 트랜잭션을 관리하는 것은 자바 EE 컨테이너가 아닌 스프링 프레임워크이다.


여기서의 txManager 빈은 HibernatetransactionManager 타입이다. DataSourceTransactionManager 가 DataSource 로의 참조를 필요로 하는 것과 같이, HibernateTransactionManager 도 SessionFactory 참조를 필요도 한다.


<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">

    <property name="dataSource" ref="dataSource"/>

    <property name="mappingResources">

        <list>

            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>

        </list>

    </property>

    <property name="hibernateProperties">

        <value>

            hibernate.dialect=${hibernate.dialect}

        </value>

    </property>

</bean>


<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">

    <property name="sessionFactory" ref="sessionFactory"/>

</bean>


하이버네이트와 자바 EE 컨테이너 관리 JTA 트랜잭션을 사용할 때는 간단히 JDBC JTA 예제에서와 같은 JtaTransactionManager 를 사용할 수 있다.


<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>


JTA 를 사용한다면 데이터 접근에 JDBC, 하이버네이트, JPA 또는 다른 어떤 기술을 사용하든 트랜잭션 매니저 정의는 같아야 한다. JTA 트랜잭션은 어떠한 트랜잭션 관련 자원이든 요청할 수 있는 글로벌 트랜잭션이기 때문이다.


어떤 경우든 어플리케이션 코드는 바뀌지 않는다. 트랜잭션 관리 방식을 변경하려면 그저 설정을 바꾸기만 하면 된다. 심지어 트랜잭션이 로컬에서 글로벌로 바뀌거나 혹은 그 반대일지라도 마찬가지이다.



트랜잭션을 통한 자원 동기화


이제 트랜잭션 관리자가 어떻게 생성되며 트랜잭션으로 동기화가 필요한 관련 자원으로 어떻게 연결되는지 알아본다. (예를 들어 DataSourceTransactionManager 는 JDBC DataSource 로, HibernateTransactionManager 는 하이버네이트 SessionFactory 로, 그리고 기타 등등) 이 섹션에서는 직접적으로든 간접적으로든 JDBC, 하이버네이트, 혹은 JDO 와 같은 영속성 API 를 사용하는 어플리케이션 코드가 어떻게 자원을 생성하고 재사용하고 제거하는지 설명한다. 그리고 관련 PlatformTransactionManager 를 통해 트랜잭션 동기화가 어떻게 (선택적으로)발생하는지에 대해 논한다.



고수준 동기화 처리 방법


선호된는 방법은 스프링의 영속성 통합 API 에 기반하는 가장 높은 수준의 템플릿을 사용하거나, 또는 트랜잭션 인지 팩토리 빈이나 네이티브 자원 팩토리 관리용 프록시를 통한 네이티브 ORM API 를 사용하는 것이다. 트랙잭션 인지 솔루션은 내부적으로 자원의 생성, 재사용, 제거, 선택적 트랜잭션 동기화, 그리고 예외 매핑을 다룬다. 따라서 사용자의 데이터 접근 코드는 이러한 작업을 처리할 필요는 없지만 순수한 비 형식적 영속성 로직에만 초점을 맞출 수 있다. 일반적으로 네이티브 ORM 을 사용하거나 JdbcTemplate 를 사용한 JDBC 접근을 위한 템플릿을 취한다. 이런 솔루션들에 대해서는 이 문서의 다음 챕터들에서 자세히 다룬다.



저수준 동기화 처리 방법


저수준 처리 방법에는 DataSourceUtils (JDBC), EntityManagerFactoryUtils (JPA), SessionFactoryUtil (하이버네이트), PersistenceManagerFactoryUtil (JDO), 기타 등등과 같은 클래스들이 존재한다. 어플리케이션 코드가 네이티브 영속성 API 의 자원 타입을 직접적으로 다루길 원한다면 이런 클래스들을 사용해서 적절한 스프링 프레임워크 관리 인스턴스가 얻어지고, 트랜잭션이 (선택적으로) 동기화되며, 프로세스에서 발생하는 예외가 일관된 API 에 적절하게 매핑되도록 한다.


예를 들어, JDBC 의 경우, DataSource 의 getConnection() 메서드를 호출하는 전통적인 JDBC 접근 방법 대신 다음과 같이 스프링의 org.springframework.jdbc.datasource.DataSourceUtils 클래스를 사용한다.


Connection conn = DataSourceUtils.getConnection(dataSource);


기존 트랜잭션이 이미 동기화된 (링크된) 커넥션을 가지고 있다면, 그 인스턴스가 반환된다. 그렇지 않다면, 이 메서드 호출은 새로운 커넥션이 생성되도록 한다. 이 커넥션은 기존 트랜잭션과 (선택적으로) 동기화되고, 같은 트랜잭션 안에서 재사용된다. 언급된대로 어떠한 SQLException 이든 스프링 프레임워크의 unchecked DataAccessException 중 하나인 CannotGetJdbcConnectionException 으로 래핑된다. 이 처리법은 SQLException 에서 얻을 수 있는 정보보다 많은 정보를 제공하며 다른 지속성 기술에서도 데이터베이스 전반에 걸친 이식성을 보장한다.


이 처리 방법은 스프링 트랜잭션 관리 (트랜잭션 동기화는 선택적이다) 를 사용하지 않아도 유효하다. 때문에 이 방법은 트랜잭션 관리를 위해 스프링을 사용하든 사용하지 않든 사용할 수 있다.


물론 스프링의 JDBC, JPA, 또는 하이버네이트 지원을 한 번 사용해보면 보통 DataSourceUtils 이나 다른 헬퍼 클래스들은 선호하지 않게 된다. 왜냐하면 다른 관련 API 를 직접 사용하는 것보다 스프링 추상화를 통한 작업이 훨씬 편하기 때문이다. 예를 들어 스프링 JdbcTemplate 나 jdbc.object 패키지를 사용해서 JDBC 사용을 단순화하면 알맞는 커넥션 검색은 뒤에서 알아서 처리하기 때문에 이를 위한 특별한 코드를 작성할 필요가 없어진다.



TransactionAwareDataSourceProxy


TransactionAwareDataSourceProxy 클래스는 가장 저수준에 존재한다. 이 클래스는 타겟 DataSource 의 프록시이다. 타겟 DataSource 를 래핑하여 스프링 관리 트랜잭션을 인지하도록 한다. 이 점에 있어서는 자바 EE 서버가 제공하는 전통적인 JNDI DataSource 와 유사하다.


대부분의 경우 이 클래스의 사용은 절대 필요하거나 매력적이지는 않는데, 예외적으로 기존 코드가 반드시 표준 JDBC 인터페이스 구현체로 호출되고 전달되어야 하는 경우는 다르다. 이 경우, 기존 코드를 사용하면서 스프링 관리 트랜잭션에 참여하도록 할 수 있다. 위에서 언급한 더 높은 수준의 추상화를 사용하여 새 코드를 작성하는 편이 좋다.



선언적 트랜잭션 관리


대부분의 스프링 프레임워크 사용자는 선언적 트랜잭션 관리를 사용한다. 이 방법은 어플리케이션 코드로의 영향도가 가장 적고, 때문에 비 침습성 경량 컨테이너의 이상과 가장 일치한다.


스프링 프레임워크의 선언적 트랜잭션 관리는 스프링 관점 지향 프로그래밍 (AOP) 으로 가능하지만, 트랜잭션 측면의 코드는 스프링 프레임워크의 배포와 함께 제공되며 보일러플레이트 방식으로 사용될 수 있기 때문에 AOP 개념이 일반적으로 이 코드의 사용을 효과적으로 만들어준다고 이해할 필요는 없다. 


스프링 프레임워크의 선언적 트랜잭션 관리는 트랜잭션 동작을 독립적인 메서드 단위로 지정할 수 있다는 점에서 EJB CMT 와 유사하다. 필요하다면 setRollbackOnly() 메서드를 트랜잭션 컨텍스트 안에서 호출하는 일도 가능하다. 이 두 트랜잭션 관리의 차이는 다음과 같다.


  • EJB CMT 가 JTA 에 묶여 있었던 것과는 달리, 스프링 프레임워크의 선언적 트랜잭션 관리는 어느 환경에서나 유효하다. 간단히 설정을 바꾸는 것으로 JTA 트랜잭션, JDBC 를 이용한 로컬 트랜잭션, JPA, 하이버네이트 또는 JDO 등의 다양한 트랜잭션 환경에서 작동할 수 있다.
  • 스프링 프레임워크의 선언적 트랜잭션 관리는 어떤 클래스에나 적용할 수 있다. EJB 에서와 같은 특별한 클래스는 필요하지 않다.
  • 스프링 프레임워크는 EJB 에 존재하지 않는 선언적 롤백 규칙을 제공한다. 롤백 규칙은 프로그래밍 방식와 선언적 방식 모두 지원한다.
  • 스프링 프레임워크는 AOP 를 사용하여 트랜잭션 동작에 대한 커스터마이징을 지원한다. 가령 트랜잭션 롤백 수행 안에 특정한 동작을 지정할 수 있다. 트랜잭션 어드바이스와 함께 임의의 어드바이스를 추가할 수 있다. EJB CMT 를 사용하면 setRollbackOnly() 외에는 컨테이너의 트랜잭션 관리에 영향을 줄 수 없다.
  • 스프링 프레임워크는 고수준의 어플리케이션 서버에서 지원하는 원격 호출 간 트랜잭션 컨텍스트 전파를 지원하지 않는다. 이 기능을 원한다면 EJB 를 사용하길 권한다. 하지만 이런 기능을 사용하지 전에 신중히 검토해야 한다. 왜냐하면 보통은 트랜잭션이 원격 호출까지 이어지기를 원하지 않기 때문이다.

TransactionProxyFactoryBean 은 어디에 있는가?

스프링 2.0 버전 이상의 선언적 트랜잭션 설정은 이전 버전에서의 것과는 상당히 다르다. 주 차이점은 더이상 TransactionProxyFactoryBean 빈을 설정할 필요가 없다는 것이다.

스프링 2.0 전의 설정은 여전히 100% 유효하다. 새로운 <tx:tags/> 설정 대신 TransactionProxyFactoryBean 빈을 설정한다고 생각하면 된다.

롤백 규칙의 개념은 중요하다. 이 규칙은 어떤 예외 (그리고 throwables) 에서 자동 롤백이 수행될 것인지 지정하도록 한다. 이 규칙을 자바 코드가 아닌, 설정 안에 선언적으로 지정한다. 여전히 TransactionStatus 객체의 setRollbackOnly() 호출로 현재 트랜잭션을 롤백 할 수는 있지만 대부분은 MyApplicationException 가 던져졌을 때 항상, 반드시 롤백 하도록 지정할 수 있다. 이 옵션의 큰 장점은 비즈니스 객체가 트랜잭션 인프라에 의존하지 않는다는 것이다. 예를 들어, 비즈니스 클래스는 스프링 트랜잭션 API 나 기타 스프링 API 를 임포트 할 필요가 없다.


EJB 컨테이너가 기본적으로 시스템 예외 (주로 런타임 예외) 에 대한 트랜잭션 자동 롤백을 수행하긴 하지만, EJB CMT 는 어플리케이션 예외에 대해 자동 롤백을 수행하진 않는다 (java.rmi.RemoteException 을 제외한 checked 예외들). 스프링의 선언적 트랜잭션 관리의 기본 동작은 EJB 의 관습을 따르기 때문에 (자동 롤백은 unchecked 예외에 대해서만 수행), 롤백 동작 커스터마이징은 유용하게 사용된다.



스프링 프레임워크의 선언적 트랜잭션 구현체의 이해


@Transaction 와 @EnableTransactionManagement 어노테이션을 추가하는 것 만으로 트랜잭션이 어떻게 작동하는지 이해하기를 바랄 순 없다. 이 섹션은 트랜잭션 관련 문제가 발생할 경우 스프링 프레임워크의 선언적 트랜잭션 인프라가 내부적으로 어떻게 작동하는지 설명한다.


스프링 프레임워크의 선언적 트랜잭션 지원에 관한 가장 중요한 개념은 이 기능이 AOP 프록시에 의해 지원되며, 트랜잭션 어드바이스는 메타데이터 (현재 XML 또는 어노테이션 기반) 에 의해 작동한다는 것이다. AOP 와 트랜잭션 메타데이터의 결합은 AOP 프록시를 생성하는데, 이 프록시는 적절한 PlatformTransactionManager 구현체와TransactionInterceptor 를 함께 사용하여 메서드 호출 중심으로 트랜잭션을 구동한다.


개념상으로 트랜잭션 프록시 메서드 호출은 다음과 같다.




선언적 트랜잭션 구현체 예제


다음 인터페이스와 그 구현체를 보자. 이 예제는 특정 도메인 모델에 초점을 두지 않고 트랜잭션 사용법에 집중하기 위해 Foo 와 Bar 클래스를 플레이스홀더로 사용한다. 이 예제의 목적에 부합하기 위해 DefaultFooService 클래스가 각 구현 메서드에서 UnsupportedOperationException 인스턴스를 던지는 것이 좋다. 트랜잭션이 생성되고 UnsupportedOperationException 에 대해 롤백을 수행하는 것을 볼 수 있다.


// the service interface that we want to make transactional


package x.y.service;


public interface FooService {


    Foo getFoo(String fooName);


    Foo getFoo(String fooName, String barName);


    void insertFoo(Foo foo);


    void updateFoo(Foo foo);


}


// an implementation of the above interface


package x.y.service;


public class DefaultFooService implements FooService {


    public Foo getFoo(String fooName) {

        throw new UnsupportedOperationException();

    }


    public Foo getFoo(String fooName, String barName) {

        throw new UnsupportedOperationException();

    }


    public void insertFoo(Foo foo) {

        throw new UnsupportedOperationException();

    }


    public void updateFoo(Foo foo) {

        throw new UnsupportedOperationException();

    }


}


FooService 인터페이스의 처음 두 메서드, getFoo(String), getFoo(String, String) 은 반드시 읽기 전용 트랜잭션 컨텍스트에서 실행되어야 하고, insertFoo(Foo) 와 updateFoo(Foo) 는 반드시 읽기 쓰기 트랜잭션 컨텍스트에서 실행되어야 한다고 가정한다. 아래 설정은 다음 절에서 자세하게 설명한다.


<!-- from the file 'context.xml' -->

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- this is the service object that we want to make transactional -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->

    <tx:advice id="txAdvice" transaction-manager="txManager">

        <!-- the transactional semantics... -->

        <tx:attributes>

            <!-- all methods starting with 'get' are read-only -->

            <tx:method name="get*" read-only="true"/>

            <!-- other methods use the default transaction settings (see below) -->

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- ensure that the above transactional advice runs for any execution

        of an operation defined by the FooService interface -->

    <aop:config>

        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>

        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>

    </aop:config>


    <!-- don't forget the DataSource -->

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>

        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>

        <property name="username" value="scott"/>

        <property name="password" value="tiger"/>

    </bean>


    <!-- similarly, don't forget the PlatformTransactionManager -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"/>

    </bean>


    <!-- other <bean/> definitions here -->


</beans>


위 설정을 보자. 트랜잭션 서비스 객체인 fooService 빈을 만든다. 트랜잭션 적용 사항은 <tx:advice/> 정의에 캡슐화된다. <tx:advice/> 정의는 "get 으로 시작하는 모든 메서드는 읽기 전용 컨텍스트로 실행되고, 다른 모든 메서드는 기본 트랜잭션으로 실행된다" 를 의미한다. transaction-manager 속성은 트랜잭션을 구동하는 PlatformTransactionManager 빈의 이름을 설정한다. 여기서는 txManager 빈이 그것이다.


PlatformTransactionManager 빈의 이름이 transactionManager 라면 <tx:advice/> 의 transaction-manager 속성 설정을 생략할 수 있다. PlatformTransactionManager 빈이 다른 이름을 가질 경우 예제와 같이 transaction-manager 속성을 반드시 설정해야 한다.


<aop:config/> 정의는 txAdvice 빈에서 정의한 트랜잭션이 프로그램의 적절한 지점에서 실행되도록 보장한다. 먼저 FooService 인터페이스 (fooServiceOperation) 에 정의된 모든 메서드의 실행에 매치되는 포인트컷을 정의한다. 다음으로 advisor 를 이용해서 포인트컷과 txAdvice 를 연결한다. 이 설정은 fooServiceOperation 의 실행 시점에 txAdvice 에서 정의한 어드바이스가 구동됨을 의미한다.


공통 요건은 서비스 레이어 전체를 트랜잭션으로 묶는 것이다. 이를 위한 최선의 방법은 포인트컷 표현식을 모든 서비스 레이어에 매치되도록 변경하는 것이다. 예를 들자면 다음과 같다.


<aop:config>

    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>

    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>

</aop:config>


이제 설정을 분석했다. 이제 이 모든 설정이 실제로 무엇을 하는지 궁금할 것이다.


위 설정은 fooService 빈 정의로부터 생성된 객체의 트랜잭션 프록시를 생성한다. 이 프록시는 트랜잭션 어드바이스를 통해 설정되어 프록시의 적절한 메서드가 호출됐을 때 트랜잭션이 시작되고, 정지되고, 읽기 전용이 되는 등 트랜잭션 설정에 따른 동작을 수행한다. 위 설정에 대한 테스트를 수행하는 다음 프로그램을 보자.


public final class Boot {


    public static void main(final String[] args) throws Exception {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);

        FooService fooService = (FooService) ctx.getBean("fooService");

        fooService.insertFoo (new Foo());

    }

}


위 프로그램의 결과는 아래와 유사하다 (Log4J 아웃풋과 DefaultFooService 클래스의 insertFoo(..) 메서드 호출에 의해 던져진 UnsupportedOperationException 의 스택 트레이스. 간단히 보기 위해 중간에 잘랐음.)


<!-- the Spring container is starting up... -->

[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors


<!-- the DefaultFooService is actually proxied -->

[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]


<!-- ... the insertFoo(..) method is now being invoked on the proxy -->

[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo


<!-- the transactional advice kicks in here... -->

[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]

[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction


<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->

[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException

[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]


<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->

[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]

[DataSourceTransactionManager] - Releasing JDBC Connection after transaction

[DataSourceUtils] - Returning JDBC Connection to DataSource


Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)

<!-- AOP infrastructure stack trace elements removed for clarity -->

at $Proxy0.insertFoo(Unknown Source)

at Boot.main(Boot.java:11)



선언적 트랜잭션 롤백


이전 섹션에서 어플리케이션의 클래스, 주로 서비스 레이어 클래스에 적용하는 선언적 트랜잭션 설정 방법의 기초를 선보였다. 이 섹션에서는 간단한 선언적 방식으로 트랜잭션 롤백을 제어하는 방법에 대해 설명한다.


스프링 프레임워크의 트랜잭션 인프라에 트랜잭션이 롤백되어야 함을 알리는 좋은 방법은 현재 트랜잭션 컨텍스트에서 실행중인 코드에서 예외를 던지는 것이다. 스프링 프레임워크의 트랜잭션 인프라 코드는 호출 스택에 차오르는 처리되지 않은 예외를 잡아서 트랜잭션을 롤백할 것인지 결정한다.


기본 설정에 따르면, 스프링 프레임워크의 트랜잭션 인프라 코드는 오직 런타임, unchecked 예외만을 트랜잭션 롤백 대상으로 본다. RuntimeException 의 인스턴스나 이것의 서브클래스가 던져져야 한다는 뜻이다. (Error 또한 기본적으로 롤백 대상이다.) 트랜잭션 메서드에서 발생한 checked 예외는 기본 설정 상 롤백되지 않는다.


checked 예외를 포함하여 어떤 예외 타입에 대해 트랜잭션 롤백을 수행할지 정확히 설정할 수 있다. 다음 XML 스니핏은 어플리케이션에서 정의한 checked 예외 타임에 대한 롤백을 설정하는 방법을 보여준다.


<tx:advice id="txAdvice" transaction-manager="txManager">

    <tx:attributes>

    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>

    <tx:method name="*"/>

    </tx:attributes>

</tx:advice>


어떤 예외에 대해서는 트랜잭션 롤백 처리를 원하지 않을 경우 이를 설정할 수도 있다. 다음 예제는 스프링 프레임워크의 트랜잭션 인프라에게 처리되지 않은 InstrumentNotFoundException 이 발생해도 트랜잭션을 롤백하지 않고 커밋할 것을 지정한다.


<tx:advice id="txAdvice">

    <tx:attributes>

    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>

    <tx:method name="*"/>

    </tx:attributes>

</tx:advice>


프로그래밍 방식으로 롤백을 처리할 수도 있다. 이 방법은 아주 간단하긴 하지만 상당히 침습적으로, 어플리케이션 코드와 스프링 트랜잭션 인프라 코드의 결합도를 크게 높이게 된다.


public void resolvePosition() {

    try {

        // some business logic...

    } catch (NoProductInStockException ex) {

        // trigger rollback programmatically

        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    }

}


가능하면 선언적 롤백 설정을 사용하는 편이 좋다. 프로그래밍 방식 롤백은 반드시 필요한 경우에 사용하도록 한다. 이 방식은 깨끗한 POJO 기반 아키텍처를 구현할 때 뛰어난 사용법이 된다.



다른 빈에서의 다른 트랜잭션 설정


이런 시나리오를 생각해보자. 다수의 서비스 레이어 객체를 가지며 각각의 레이어 객체에 대해 서로 완전히 다른 트랜잭션 설정을 적용하고자 한다. 이런 설정은 개별적인 <aop:advisor/> 엘레멘트를 정의하여 다른 pointcut 과 advice-ref 속성 값을 설정하는 방법으로 가능하다.


비교해보자면, 먼저 모든 서비스 레이어 클래스는 루트 x.y.service 패키지에 정의되어 있다고 가정한다. 이 패키지 (혹은 서브패키지) 에 정의된, 이름이 Service 로 끝나는 모든 클래스 인스턴스 빈에 기본 트랜잭션 설정을 적용하기 위해 다음과 같이 작성한다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <aop:config>


        <aop:pointcut id="serviceOperation"

                expression="execution(* x.y.service..*Service.*(..))"/>


        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>


    </aop:config>


    <!-- these two beans will be transactional... -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>


    <!-- ... and these two beans won't -->

    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->

    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->


    <tx:advice id="txAdvice">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->


</beans>


다음으로 두 개의 서로 다른 빈에 각각 완전히 다른 트랜잭션 설정이 적용된 예제를 보자


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <aop:config>


        <aop:pointcut id="defaultServiceOperation"

                expression="execution(* x.y.service.*Service.*(..))"/>


        <aop:pointcut id="noTxServiceOperation"

                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>


        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>


        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>


    </aop:config>


    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- this bean will also be transactional, but with totally different transactional settings -->

    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>


    <tx:advice id="defaultTxAdvice">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <tx:advice id="noTxAdvice">

        <tx:attributes>

            <tx:method name="*" propagation="NEVER"/>

        </tx:attributes>

    </tx:advice>


    <!-- other transaction infrastructure beans such as a PlatformTransactionManager omitted... -->


</beans>



<tx:advice/> 설정


이 섹션은 <tx:advice/> 태그를 통한 다양한 트랜잭션 설정을 요약해서 설명한다. 기본 <tx:advice/> 설정은 다음과 같다.


  • 전파 설정은 REQUIRED 이다.
  • 격리 수준은 DEFAULT 이다.
  • 트랜잭션은 읽기/쓰기 이다.
  • 트랜잭션 타임아웃은 트랜잭션 기반 시스템의 기본 타임아웃 설정을 따르며, 타임아웃이 지원되지 않는다면 트랜잭션 타임아웃은 없다.
  • 모든 RuntimeException 예외에 대해서 롤백을 수행하며, checked Exception 에는 수행하지 않는다.

이 기본 설정은 변경할 수 있다. 아래의 표는 <tx:advice/> 와 <tx:attributes/> 태그 안에 위치한 <tx:method/> 태그의 다양한 속성을 정리한다.

<tx:method/> 설정

속성 

 필수 여부

 기본값

 설명

 name

 Y

 

트랜잭션 속성이 적용될 메서드 이름. 다수의 메서드에 같은 설정을 적용하기 위해 와일드카드 (*) 를 사용할 수 있다. 예로, get*, handle*, on*Event 등등

 propagation

 N

 REQUIRED

트랜잭션 전파 동작

 isolation

 N

 DEFAULT

트랜잭션 격리 수준

 timeout

 N

 -1

트랜잭션 타임아웃 값

 read-only

 N

 false

읽기 전용 트랜잭션인가?

 rollback-for

 N

 

롤백을 적용할 Exception(s). 콤마로 구분한다. 예로, com.foo.MyBusinessException,ServletException

 no-rollback-for

 N

 

롤백을 적용하지 않을 Exception(s). 콤마로 구분한다. 예로, com.foo.MyBusinessException,ServletException



@Transactional 사용하기


XML 기반 선언적 트랜잭션 설정법에 더해서 어노테이션 기반의 방법을 사용할 수 있다. 트랜잭션을 자바 소스코드에 직접 선언하면 트랜잭션 선언과 트랜잭션이 적용되는 코드가 훨씬 가깝게 된다. 트랜잭션이 적용되는 코드는 거의 항상 그런 방식으로 배포되기 때문에 지나친 결합에 의한 위험성은 크지 않다.


스프링의 어노테이션 대신 표준 javax.transaction.Transactional 어노테이션도 지원된다. 자세한 정보는 JTA 1.2 문서를 참조하라.


@Transactional 어노테이션은 사용하기 쉽다.


// the service class that we want to make transactional

@Transactional

public class DefaultFooService implements FooService {


    Foo getFoo(String fooName);


    Foo getFoo(String fooName, String barName);


    void insertFoo(Foo foo);


    void updateFoo(Foo foo);

}


위 POJO 가 스프링 IoC 컨테이너에 빈으로 정의되었다면 이 빈 인스턴스는 XML 설정을 한 줄 추가하는 것으로 트랜잭션 처리 대상이 된다.


<!-- from the file 'context.xml' -->

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <!-- this is the service object that we want to make transactional -->

    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- enable the configuration of transactional behavior based on annotations -->

    <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <!-- (this dependency is defined somewhere else) -->

        <property name="dataSource" ref="dataSource"/>

    </bean>


    <!-- other <bean/> definitions here -->


</beans>


<tx:annotation-driven/> 태그의 transaction-manager 속성도 PlatformTransactionManager 빈의 이름이 transactionManager 인 경우에 생략될 수 있다. 


자바 기반 설정을 사용한다면 @EnableTransactionManagement 어노테이션을 사용해서 동일한 효과를 얻을 수 있다. 간단히 이 어노테이션을 @Configuration 클래스에 추가하면 된다. 


메서드 가시성과 @Transactional


프록시를 사용할 때, @Transactional 어노테이션은 반드시 public 메서드에 적용되어야 한다. protected, private, package-visible 메서드에 @Transactional 어노테이션이 추가되면 에러가 발생하지는 않지만 해당 메서드에 트랜잭션 설정이 적용되지 않는다. non-public 메서드에 어노테이션을 적용하고 싶다면 어스팩트J 사용을 고려하라.


@Transactional 어노테이션은 인터페이스 정의, 인터페이스의 메서드, 클래스 정의, 클래스의 public 메서드에 적용할 수 있다. 하지만 @Transactional 어노테이션을 추가하는 것 만으로 트랜잭션을 활성화 할 수는 없다. @Transactional 어노테이션은 런타임 인프라가 @Transactional 을 인지하고 적절한 빈과 트랜잭션 동작을 설정하기 위해 사용되는 메타메이터가 된다. <tx:annotation-driven/> 엘레멘트가 트랜잭션 동작의 스위치가 된다.


@Transactional 어노테이션은 구체화된 클래스 (그리고 구체화된 클래스의 메서드) 에 적용할 것을 권한다. 인터페이스나 인터페이스의 메서드에 적용하는 것도 가능하긴 하지만, 이 때는 인터페이스 기반 프록시에서만 유효한 트랜잭션 설정이 된다. 자바 어노테이션은 인터페이스로부터 상속되지 않는다는 사실은, 클래스 기반 프록시 (proxy-target-class="true") 나 위빙 기반 어스팩트 (mode="aspectj") 를 사용할 경우 프록싱과 위빙 인프라가 트랜잭션 설정을 인식할 수 없다는 것을 의미하고, 객체는 트랜잭션 프록시로 래핑되지 않게 된다. 확실히 좋은 설정이 아니다.


기본 프록시 모드에서는 외부로부터의 프록시를 통한 메서드 호출만이 인터셉트된다. 한 객체의 메서드에서 같은 객체 (자신) 의 다른 메서드를 호출할 경우, 이 메서드에 @Transactional 어노테이션이 적용되어 있다 할지라도 실제 트랜잭션 처리는 적용되지 않는다. 또한 프록시가 원하는 대로 작동하기 위해서는 반드시 완전한 초기화가 이루어져야 하므로 초기화 코드 (@PostConstructor) 에 이 기능을 사용하지 않아야 한다.


객체 자신에 대한 호출에도 트랜잭션을 적용하려면 어스팩트J 모드 사용을 고려해야 한다. 이 경우 처음에는 프록시가 없다. 대신 @Transactional 을 모든 종류의 런타임 동작으로 바꾸기 위해서 타겟 클래스를 위빙한다 (클래스의 바이트코드가 수정된다.)


어노테이션 기반 트랜잭션 세팅

XML 속성

 어노테이션 속성

 기본값

 설명

 transaction-manager

 N/A

 transactionManager

 사용할 트랜잭션 매니저의 이름. 이름이 transactionManager 가 아닌 경우에만 필수이다.

 mode

 mode

 proxy

 기본 모드인 "proxy" 는 어노테이션이 적용된, 스프링의 AOP 프레임워크를 사용하여 프록시되기 위한 빈을 처리한다. (여기서의 프록시는 언급한대로 프록시를 통한 외부 메서드 호출에만 적용된다.) 다른 모드인 "aspectj" 는 대상 클래스를 스프링의 어스팩트J 트랜잭션 어스팩트로 위빙하는데, 클래스의 바이트코드를 수정하여 모든 종류의 메서드 호출에 트랜잭션 코드를 적용한다. 어스팩트J 로드타임 위빙이나 컴파일타임 위빙에는 spring-aspects.jar 가 필요하다.

 proxy-target-class

 proxyTargetClass

 false

 프록시 모드를 적용한다. @Transactional 어노테이션이 적용된 클래스에 어떤 타입의 트랜잭션 프록시를 생성할 것인지 설정한다. proxy-target-class 가 true 라면 클래스 기반 프록시가 생성된다. false (또는 생략)라면 표준 JDK 인터페이스 기반 프록시가 생성된다.

 order

 order

 Ordered.LOWEST_PRECEDENCE

@Transactional 어노테이션이 적용된 빈에 적용될 트랜잭션 어드바이스의 순서를 정의한다. 따로 지정하지 않으면 AOP 서브시스템이 순서를 정한다.


@EnableTransactionManagement 와 <tx:annotation-driven/> 은 정의되어 있는 어플리케이션 컨텍스트 안에서 빈의 @Transactional 을 찾는다. 이 설정을 DispatcherServlet 에 대한 WebApplicationContext 에 적용한다면 서비스가 아닌 컨트롤러 안의 빈만을 찾는다.


메서드의 트랜잭션 설정을 평가할 때는 가장 많이 얻어지는 위치가 우선권을 갖는다. 다음 예제의 DefaultFooService 클래스에는 읽기 전용 트랜잭션 설정이 클래스 레벨 어노테이션으로 지정되었다. 하지만 같은 클래스에 있는 updateFoo(Foo) 메서드의 @Transactional 어노테이션의 트랜잭션 설정이 클래스 레벨 어노테이션보다 우선한다.


@Transactional(readOnly = true)

public class DefaultFooService implements FooService {


    public Foo getFoo(String fooName) {

        // do something

    }


    // these settings have precedence for this method

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

    public void updateFoo(Foo foo) {

        // do something

    }

}



@Transactional 설정


@Transactional 어노테이션은 인터페이스, 클래스, 메소드 등에게 반드시 트랜잭션 처리를 하도록 지정하는 메타데이터이다. 예를 들어, "이 메소드가 실행되면 모든 기존 트랜잭션을 중단하고 새로운 읽기 전용 트랜잭션을 시작한다." 와 같은 의미를 가질 수 있다. @Transactional 설정의 기본 설정은 다음과 같다.


  • 전파 설정은 PROPAGATION_REQUIRED.
  • 격리 수준은 ISOLATION_DEFAULT.
  • 읽기/쓰기 트랜잭션
  • 타임아웃은 기반 트랜잭션 시스템의 기본 설정을 따른다. 타임아웃이 지원되지 않는다면 타임아웃은 없다.
  • 모든 RuntimeException 에 대해 롤백을 수행하고, checked Exception 에 대해서는 수행하지 않는다.

이 기본 설정은 변경될 수 있다. @Transactional 어노테이션의 다양한 프로퍼티 설정에 대해서는 아래 요약되어 있다.


@Transactional 설정

 프로퍼티

 타입

 설명

 value

 String 

 사용할 트랜잭션 관리자를 지정하는 선택적 구분자

 propagation

 enum: Propagation

 선택적 전파 설정.

 isolation

 enum: Isolation

 선택적 격리 수준.

 readOnly

 boolean

 읽기/쓰기 vs 읽기 전용 트랜잭션.

 timeout

 int (초)

 트잭션 타임아웃.

 rollbackFor

 Throwable 로부터 얻을 수 있는 Class 객체 배열

 롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열.

 rollbackForClassName

 Throwable 로부터 얻을 수 있는 클래스 이름 배열

 롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열.

 noRollbackFor

 Throwable 로부터 얻을 수 있는 Class 객체 배열

 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열.

 noRollbackForClassName

 Throwable 로부터 얻을 수 있는 클래스 이름 배열

 롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열.


현재는 트랜잭션의 이름을 명시적으로 설정할 수 없다. 여기서 트랜잭션의 '이름' 이란 트랜잭션 모니터와 (적용 가능한 경우) 로깅 출력에 표시되는 트랜잭션 이름을 의미한다. 선언적 트랜잭션에서 트랜잭션 이름은 항상 완전한 클래스 이름 + "." + 트랜잭션 처리 메서드 이름이 된다. 예를 들어, BusinessService 클래스의 handlePayment(..) 메서드에서 트랜잭션이 시작된다면 트랜잭션 이름은 com.foo.BusinessService.handlePayment 가 된다.



@Transactional 을 이용한 다중 트랜잭션 매니저


스프링 어플리케이션은 대부분 단인 트랜잭션 매니저로 구동되지만, 다수의 독립된 트랜잭션 매니저가 필요한 상황도 있다. @Transacional 어노테이션의 value 속성은 사용할 PlatformTransactionManager 를 선택적으로 지정할 수 있다. 이 값은 빈 이름이나 트랜잭션 매니저 빈의 qualifier 값이 지정될 수 있다. 예를 들어 qualifier 표기법을 사용한다면 자바 코드는 다음과 같다.


public class TransactionalService {


    @Transactional("order")

    public void setSomething(String name) { ... }


    @Transactional("account")

    public void doSomething() { ... }

}


위 자바 코드는 어플리케이션 컨텍스트의 다음 트랜잭션 매니저 빈 선언과 함께 설정된다.


<tx:annotation-driven/>


    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        ...

        <qualifier value="order"/>

    </bean>


    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        ...

        <qualifier value="account"/>

    </bean>


여기서 TransactionalService 의 두 메서드는 각각의 트랜잭션 매니저로 실행된다. 각 트랜잭션 매니저는 "order" 와 "account" 구분자로 구분된다. 기본 <tx:annotation-driven> 대상 빈 이름으로는 여전히 transactionManager 가 사용된다.



커스텀 숏컷 어노테이션


여러 메서드들에 같은 속성이 지정된 @Transactional 어노테이션을 사용하는 일이 많아진다면 스프링의 메타 어노테이션 지원의 커스텀 숏컷 어노테이션이 유용하게 사용될 수 있다. 예제로 다음 어노테이션을 보자.


@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Transactional("order")

public @interface OrderTx {

}


@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Transactional("account")

public @interface AccountTx {

}


그리고 위 어노테이션은 다음과 같이 사용한다.


public class TransactionalService {


    @OrderTx

    public void setSomething(String name) { ... }


    @AccountTx

    public void doSomething() { ... }

}


여기서는 트랜잭션 매니저 구분자를 예로 들었지만, 트랜잭션 전파, 롤백 규칙, 타임아웃 등 다른 설정도 이렇게 사용할 수 있다.



트랜잭션 전파


이 섹션은 스프링의 트랜잭션 전파의 의미를 설명한다. 이 섹션은 트랜잭션 전파에 대한 소개가 아니다. 스프링에서의 트랜잭션 전파에 관하여 자세히 설명한다.


스프링 관리 트랜잭션에서는 물리적 트랜잭션과 논리적 트랜잭션의 차이와 트랜잭션 전파 설정이 이 차이를 어떻게 적용하는지 알아야 한다.



Required



PROPAGATION_REQUIRED


전파 설정이 PROPAGATION_REQUIRED 일 때는 각 트랜잭션 처리 대상 메서드에 논리적 트랜잭션 범위가 생성된다. 이 논리적 트랜잭션 범위는 롤백 전용 상태를 개별적으로 결정할 수 있으며, 외부 트랜잭션 범위는 내부 트랜잭션 범위와 논리적으로 독립적이다. 물론 표준 PROPAGATION_REQUIRED 동작의 경우 이런 모든 범위는 동일한 물리적 트랜잭션으로 매핑된다. 따라서 내부 트랜잭션 범위에 설정된 롤백 전용 표시는 외부 트랜잭션의 실제 커밋에 영향을 준다.


하지만 내부 트랜잭션 범위에 롤백 전용이 설정된 경우에는 외부 트랜잭션은 롤백 여부를 스스로 결정하지 않는다. 때문에 (내부 트랜잭션 범위에 의해 자동적으로 발생한) 롤백은 예상치 못하게 발생하게 된다. 이 시점에 여기에 대응하는 UnexpectedRollbackException 이 발생한다. 이 예외는 트랜잭션 호출자로 하여금 트랜잭션이 실제로 커밋 되지 않았음에도 커밋이 된 것으로 오인하는 것을 방지하기 위한 예상된 동작이다. 그래서 내부 트랜잭션이 (외부 트랜잭션 모르게) 트랜잭션을 롤백 전용으로 표시하면 외부 호출자는 커밋을 호출한다. 외부 호출자는 UnexpectedRollbackException 을 받아서 롤백이 대신 수행되었음을 명확하게 나타내야 한다.



RequiresNew



PROPAGATION_REQUIRES_NEW


PROPAGATION_REQUIRES_NEW 는 PROPAGATION_REQUIRED 와 대조적으로, 각 트랜잭션 범위에 대해 완전하게 독립된 트랜잭션을 사용한다. 기반 물리적 트랜잭션이 서로 다르며, 때문에 독립적인 커밋이나 롤백을 수행할 수 있다. 외부 트랜잭션은 내부 트랜잭션의 롤백 상태의 영향을 받지 않는다.



Nested


PROPAGATION_NESTED 는 롤백 가능한 다수의 세이브포인트를 가진 단일 물리적 트랜잭션을 사용한다. 이런 부분적 롤백은 내부 트랜잭션 범위가 자신의 범위에 대한 롤백을 수행할 수 있도록 한다. 외부 트랜잭션은 몇몇 작업이 롤백되어도 물리적 트랜잭션을 지속할 수 있다. 이 설정은 보통 JDBC 세이브포인트에 매핑된다. 때문에 JDBC 리소스 트랜잭션에서만 유효하다.



트랜잭션 작업 조언


트랜잭션 및 기본 프로파일링 어드바이스를 모두 실행한다고 가정한다. <tx:annotation-driven/> 의 컨텍스트 안에서 이것을 어떻게 적용할까?


updateFoo(Foo) 메서드를 실행할 때 다음 동작들을 보고자 한다.


  • 프로파일링 어스팩트 시작

  • 트랜잭션 어드바이스 실행

  • 어드바이스된 객체의 메서드 실행

  • 트랜잭션 커밋

  • 프로파일링 어스팩트의 정확한 전체 트랜잭션 메서드 실행 시간 보고


이 챕터에서는 AOP 의 자세한 설명을 고려하지 않았다. AOP 설정에 대한 자세한 정보는 AOP 챕터를 참조하기 바란다. 


아래는 위에서 언급한 간단한 프로파일링 어스팩트 코드다. 어드바이스 순서는 Ordered 인터페이스를 통해 설정되었다. 


package x.y;


import org.aspectj.lang.ProceedingJoinPoint;

import org.springframework.util.StopWatch;

import org.springframework.core.Ordered;


public class SimpleProfiler implements Ordered {


    private int order;


    // allows us to control the ordering of advice

    public int getOrder() {

        return this.order;

    }


    public void setOrder(int order) {

        this.order = order;

    }


    // this method is the around advice

    public Object profile(ProceedingJoinPoint call) throws Throwable {

        Object returnValue;

        StopWatch clock = new StopWatch(getClass().getName());

        try {

            clock.start(call.toShortString());

            returnValue = call.proceed();

        } finally {

            clock.stop();

            System.out.println(clock.prettyPrint());

        }

        return returnValue;

    }

}


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- this is the aspect -->

    <bean id="profiler" class="x.y.SimpleProfiler">

        <!-- execute before the transactional advice (hence the lower order number) -->

        <property name="order" __value="1"__/>

    </bean>


    <tx:annotation-driven transaction-manager="txManager" __order="200"__/>


    <aop:config>

            <!-- this advice will execute around the transactional advice -->

            <aop:aspect id="profilingAspect" ref="profiler">

                <aop:pointcut id="serviceMethodWithReturnValue"

                        expression="execution(!void x.y..*Service.*(..))"/>

                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>

            </aop:aspect>

    </aop:config>


    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>

        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>

        <property name="username" value="scott"/>

        <property name="password" value="tiger"/>

    </bean>


    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"/>

    </bean>


</beans>


위 설정은 fooService 빈에 프로파일링과 트랜잭션 어스팩트를 원하는 순서대로 적용한다. 이런 방식으로 몇 개의 어스팩트든 추가로 설정할 수 있다.


다음 예제는 위와 같은 설정을 적용하지만 순수 XML 선언법을 사용한다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="

        http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/tx

        http://www.springframework.org/schema/tx/spring-tx.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="fooService" class="x.y.service.DefaultFooService"/>


    <!-- the profiling advice -->

    <bean id="profiler" class="x.y.SimpleProfiler">

        <!-- execute before the transactional advice (hence the lower order number) -->

        __<property name="order" value="1__"/>

    </bean>


    <aop:config>

        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>

        <!-- will execute after the profiling advice (c.f. the order attribute) -->


        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" __order="2__"/>

        <!-- order value is higher than the profiling aspect -->


        <aop:aspect id="profilingAspect" ref="profiler">

            <aop:pointcut id="serviceMethodWithReturnValue"

                    expression="execution(!void x.y..*Service.*(..))"/>

            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>

        </aop:aspect>


    </aop:config>


    <tx:advice id="txAdvice" transaction-manager="txManager">

        <tx:attributes>

            <tx:method name="get*" read-only="true"/>

            <tx:method name="*"/>

        </tx:attributes>

    </tx:advice>


    <!-- other <bean/> definitions such as a DataSource and a PlatformTransactionManager here -->


</beans>


위 설정은 fooService 빈에 프로파일링과 트랜잭션 어스팩트를 지정된 순서로 적용한다. 프로파일링 어드바이스를 트랜잭션 어드바이스 다음 혹은 이전에 실행하고 싶다면 간단히 order 프로퍼티 값을 바꾸도록 한다.



@Transactional 과 어스팩트J 사용하기


어스팩트J 를 사용해 스프링 프레임워크의 @Transactional 지원을 스프링 컨테이너 외부에서 사용할 수 있다. 먼저 클래스에 (또는 클래스의 메서드에) @Transactional 어노테이션을 설정하고 spring-aspects.jar 에 정의된 org.springframework.transaction.AnnotationTransactionAspect 를 통해 어플리케이션으로 위빙한다. 어스팩트는 반드시 트랜잭션 매니저로 설정되어야 한다. 물론 스프링 프레임워크의 IoC 컨테이너로 어스팩트의 DI 를 제어할 수 있다. 트랜잭션 관리 어스팩트를 설정하는 가장 간단한 방법은 <tx:annotation-driven/> 엘레멘트를 사용하고 mode 속성을 aspectj 로 지정하는 것이다. 여기서는 스프링 컨테이너 외부에서 구동되는 어플리케이션에 포커스를 두기 때문에 프로그래밍 방식으로 보여준다.


// construct an appropriate transaction manager

DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());


// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods

AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);


이 어스팩트를 사용할 때는 반드시 인터페이스가 아닌, 구체화된 클래스 (또는 구체화된 클래스의 메서드) 에 어노테이션을 설정해야 한다. 어스팩트J 는 어노테이션은 인터페이스로부터 상속되지 않는다는 자바의 원칙을 따른다.


클래스의 @Transactional 어노테이션은 클래스의 모든 메서드의 실행에 대한 기본 트랜잭션 처리를 적용한다.


메서드의 @Transactional 어노테이션은 클래스 어노테이션에 의해 주어진 기본 트랜잭션 처리를 적용한다. 메서드 가시성에 상관없이 모든 메서드에 적용 가능하다.


어플리케이션을 AnnotationTransactionAspect 로 위빙하기 위해서는 어플리케이션을 반드시 어스팩트J 와 함께 빌드하거나 로드 타임 위빙을 사용해야 한다. 



프로그래밍 방식 트랜잭션 관리


스프링 프레임워크가 제공하는 프로그래밍 방식 트랜잭션 관리는 두 가지 의미가 있다.


  • TransactionTemplate 사용
  • PlatformTransactionManager 구현체 직접 사용

스프링 팀은 프로그래밍 방식 트랜잭션 관리에 있어 TransactionTemplate 사용을 권장한다. 두 번째 방식은 예외 핸들링이 더 가볍다는 점 외엔 JTA UserTransaction API 를 사용하는 것과 유사하다. 


TransactionTemplate 사용하기

TransactionTemplate 는 JdbcTemplate 와 같은 다른 스프링 템플릿과 같은 접근법을 취한다. 콜백 방식을 사용해서 트랜잭션 리소스 획득, 해제와 같은 보일러플레이트 코드를 어플리케이션 코드에서 분리하고 의도에 맞는, 오로지 개발자가 원하는 동작에 집중하도록 한다.

다음 예제에서 보듯이 TransactionTemplate 사용은 스프링의 트랜잭션 인프라와 API 에 전적으로 묶이게 된다. 프로그래밍 방식이 어플리케이션 개발의 요건에 맞는지 맞지 않는지는 스스로 결정해야 한다.


반드시 트랜잭션 컨텍스트에서 실행하면서 TransactionTemplate 을 사용하는 어플리케이션 코드는 다음과 같다. TransactionCallback 구현체는 (보통 익명 내부 클래스로) 트랜잭션 컨텍스트에서 실행할 코드를 작성하도록 한다. 그리고 커스텀 TransactionCallback 의 인스턴스를 TransactionTemplate 의 execute(..) 메서드로 전달한다.


public class SimpleService implements Service {


    // single TransactionTemplate shared amongst all methods in this instance

    private final TransactionTemplate transactionTemplate;


    // use constructor-injection to supply the PlatformTransactionManager

    public SimpleService(PlatformTransactionManager transactionManager) {

        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");

        this.transactionTemplate = new TransactionTemplate(transactionManager);

    }


    public Object someServiceMethod() {

        return transactionTemplate.execute(new TransactionCallback() {

            // the code in this method executes in a transactional context

            public Object doInTransaction(TransactionStatus status) {

                updateOperation1();

                return resultOfUpdateOperation2();

            }

        });

    }

}


반환값이 없다면 TransactionCallbackWithoutResult 클래스를 사용한다.


transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {

        updateOperation1();

        updateOperation2();

    }

});


콜백 안의 코드는 파라미터로 제공된 TransactionStatus 객체의 setRollbackOnly() 메서드를 호출하여 트랜잭션을 롤백할 수 있다.


transactionTemplate.execute(new TransactionCallbackWithoutResult() {


    protected void doInTransactionWithoutResult(TransactionStatus status) {

        try {

            updateOperation1();

            updateOperation2();

        } catch (SomeBusinessExeption ex) {

            status.setRollbackOnly();

        }

    }

});



트랜잭션 설정 지정하기


프로그래밍 방식이나 설정 방식으로 TransactionTemplate 의 전파, 격리 수준, 타임아웃과 같은 트랜잭션 설정을 지정할 수 있다. TransactionTemplate 인스턴스는 기본적으로 기본 트랜잭션 설정을 가진다. 다음 예제는 프로그래밍 방식으로 커스터마이징된 TransactionTemplate 의 트랜잭션 설정을 보여준다.


public class SimpleService implements Service {


    private final TransactionTemplate transactionTemplate;


    public SimpleService(PlatformTransactionManager transactionManager) {

        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");

        this.transactionTemplate = new TransactionTemplate(transactionManager);


        // the transaction settings can be set here explicitly if so desired

        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);

        this.transactionTemplate.setTimeout(30); // 30 seconds

        // and so forth...

    }

}


다음 예제는 스프링 XML 설정을 사용한 TransactionTemplate 의 트랜잭션 설정이다. sharedTransactionTemplate 은 필요에 따라 다수의 서비스로 주입 가능하다.


<bean id="sharedTransactionTemplate"

        class="org.springframework.transaction.support.TransactionTemplate">

    <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>

    <property name="timeout" value="30"/>

</bean>


마지막으로 TransactionTemplate 클래스의 인스턴스는 쓰레드세이프하다. 인스턴스 안에 어떠한 어플리케이션 작동 상태도 유지하지 않는다. TransactionTemplate 인스턴스는 설정 상태를 유지한다. 따라서 TransactionTemplate 인스턴스는 다수의 클래스에 공유될 수 있고, 만약 다르게 설정된 TransactionTemplate 이 필요하다면 두 개의 구분된 TransactionTemplate 를 생성해야 한다.



PlatformTransactionManager 사용하기


트랜잭션 관리를 위해 org.springframework.transaction.PlatformTransactionManager 를 직접 사용할 수도 있다. 간단하게 사용중인 PlatformTransactionManager 의 구현체를 빈에 넘기면 된다. 그리고 TransactionDefinition 과 TransactionStatus 객체를 사용해서 트랜잭션을 초기화하고 롤백하고 커밋할 수 있다.


DefaultTransactionDefinition def = new DefaultTransactionDefinition();

// explicitly setting the transaction name is something that can only be done programmatically

def.setName("SomeTxName");

def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);


TransactionStatus status = txManager.getTransaction(def);

try {

    // execute your business logic here

}

catch (MyException ex) {

    txManager.rollback(status);

    throw ex;

}

txManager.commit(status);



프로그래밍 방식 vs 선언적 방식


트랜잭션 작업이 많지 않다면 프로그래밍 방식은 대개 좋은 선택이 된다. 예를 들어, 트랜잭션 작업은 몇 개의 update 가 전부인 웹 어플레케이션을 개발한다면 스프링이나 다른 기술을 사용한 트랜잭션 프록시 설정을 원치 않을 수 있다. 이런 경우에 TransactionTemplate 은 좋은 처리 방법이다. 명시적으로 트랜잭션 이름을 세팅하는 일은 오직 프로그래밍 방식 트랜잭션 관리에서만 할 수 있는 일이기도 하다.


다른 한편으로는, 어플리케이션에 수많은 트랜잭션 작업이 필요하다면 일반적으로 선언적 트랜잭션 관리가 좋다. 이 방식은 트랜잭션 관리를 비즈니스 로직과 분리하며, 설정하기 어렵지 않다. 스프링 프레임워크를 사용하면 EJB CMT 를 사용할 때보다 선언적 트랜잭션 관리 설정 비용이 크게 줄어든다.



트랜잭션 바운드 이벤트


스프링 4.2 에서는 트랜잭션의 한 단계에 이벤트 리스너를 바인딩할 수 있다. 전형적인 예로는 트랜잭션이 성공적으로 완료됐을 때의 이벤트를 핸들링하는 것이다. 리스너에게 현재 트랜잭션의 결과가 중요한 경우, 이런 이벤트 처리가 더 유연하게 사용될 수 있다.


보통의 이벤트 리스너는 @EventListener 어노테이션을 통해 등록한다. 리스너를 트랜잭션에 바인딩하려면 @TransactionalEventListener 를 사용한다. 이 어노테이션을 사용하면 기본적으로 리스너는 트랜잭션의 커밋 단계에 바인딩된다.


이제 이벤트 바인딩 개념의 예제를 보자. 한 컴포넌트가 주문 생성 이벤트를 게시하고, 성공적으로 커밋된 트랜잭션 이벤트를 핸들링하는 리스너를 정의한다고 가정한다.


@Component

public class MyComponent {


    @TransactionalEventListener

    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {

          ...

    }

}


TransactionalEventListener 어노테이션은 트랜잭션의 어느 단계에 리스너를 바인딩할지 설정하는 phase 속성을 노출한다. 유효한 트랜잭션 단계는 BEFORE_COMMIT, AFTER_COMMIT (기본값), AFTER_ROLLBACK, 그리고 완료된 트랜잭션을 종합하는 AFTER_COMPLETION (커밋 또는 롤백) 이 있다.


구동중인 트랜잭션이 없다면 리스너는 실행되지 않는다. 하지만 어노테이션의 fallbackExecution 속성을 true 로 세팅하여 동작을 오버라이딩할 수 있다.



어플리케이션 서버 별 통합


스프링의 트랜잭션 추상화는 일반적으로 어플리케이션 서버에 무관심하다. 스프링의 JtaTransactionManager 클래스는 선택적으로 JTA UserTransaction 과 TransactionManager 객체에 대한 JNDI 룩업을 수행할 수 있다. JtaTransactionManager 클래스는 TransactionManager 객체를 자동으로 탐지하는데, 이 객체는 어플리케이션 서버마다 다르다. JTA TransactionManager 에 대한 접근 권한을 갖게되면 향상된 트랜잭션을 얻게 되는데, 특히 트랜잭션 일시 중단을 지원한다. 


스프링의 JtaTransactionManager 는 자바 EE 어플리케이션 서버 구동의 표준적인 선택이며 모든 공통된 서버에서 작동한다고 알려져 있다. 트랜잭션 일시 중단과 같은 발전된 기능은 GlassFish, JBoss, Geronimo 를 포함한 많은 서버에서 작동한다. 여기에 특별한 설정은 필요하지 않다. 하지만 완전하게 지원되는 트랜잭션 일시 중단과 더 발전된 통합을 위해 스프링은 WebLogic 과 WebSphere 서버를 위한 특별한 어댑터를 제공한다. 이 어댑터들에 대해서는 다음 섹션에서 다룬다.


WebLogic 과 WebSphere 서버를 포함한 표준적인 시나리오에서는 편리하게 <tx:jta-transaction-manager/> 설정 엘레멘트 사용을 고려하라. 이 엘레멘트를 설정하면 기반이 되는 서버를 자동으로 감지하고 플랫폼에서 유효한 최선희 트랜잭션 매니저를 선택한다. 이는 서버 별 어댑터 클래스 (다음 섹션에서 다루는) 를 명시적으로 설정할 필요가 없다는 뜻이다. 어댑터 클래스는 사용으로 선택된다. 기본적으로 표준 JtaTransactionManager 가 사용된다.



IBM WebSphere


WebSphere 6.1.0.9 와 그 위의 버전에서 권장되는 스프링 JTA 트랜잭션 매니저는 WebSphereUowTransactionManager 이다. 이 특별한 어댑터는 IBM 의 UOWManager API 를 사용한다. 이 API 는 WebSphere 어플리케이션 서버 6.1.0.9 와 이후 버전에서 유효하다. 이 어댑터를 사용하면 스프링 기반 트랜잭션 일시 중단 (PROPAGATION_REQUIRES_NEW 에 의한 중단/재개) 은 공식적으로 IBM 이 지원한다.



오라클 WebLogic 서버


WebLogic 서버 9.0 또는 그 위의 버전에서 일반적으로 WebLogicJtaTransactionManager 를 사용한다. 이 특별한 WebLogic 전용 클래스는 JtaTransactionManager 의 서브클래스이다. 이 클래스는 WebLogic 관리 트랜잭션 환경에서 표준 JTA 를 넘어서 스프링 트랜잭션 정의의 모든 기능을 지원한다. 지원되는 기능은 트랜잭션 이름, 트랜잭션 별 격리 수준, 그리고 모든 경우의 트랜잭션에서의 알맞은 재개를 포함한다.



일반적인 문제의 해결책


특정 DataSource 에 대한 잘못된 트랜잭션 관리자 사용


선택한 트랜잭션 기술과 요건에 맞는 알맞은 PlatformTransactionManager 구현체를 사용하라. 제대로 사용된 스프링 프레임워크는 간단하고 포터블한 추상화를 제공한다. 글로벌 트랜잭션을 사용한다면 반드시 org.springframework.transaction.jta.JtaTransactionManager 클래스 (또는 이 클래스의 어플리케이션 서버 별 서브클래스) 를 사용하라. 그렇지 않으면 트랜잭션 인프라는 컨테이너의 DataSource 인스턴스와 같은 리소스에 대해 로컬 트랜잭션을 수행한다. 이런 로컬 트랜잭션은 의미가 없으며, 좋은 어플리케이션 서버는 이를 오류로 처리한다.



추가 정보


스프링 프레임워크의 트랜잭션 지원에 대한 정보는 여기서 더 얻을 수 있다.


  • 스프링의 분산 트랜잭션 with or without XA 는 JavaWorld 에 기재된, 스프링의 David Syer 의 스프링 어플리케이션의 7 가지 분산 트랜잭션 패턴 가이드이다. 패턴 중 3 가지는 XA 를 사용하고, 4 가지는 사용하지 않는다.
  • 자바 트랜잭션 디자인 전략 은 InfoQ 에서 판매중인 책으로, 자바 트랜잭션에 대해 훌륭하게 소개한다. 또한 스프링 프레임워크과 EJB3 를 사용한 트랜잭션 설정 방법 예제도 포함되어 있다.






이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/amdahls-law.html

!! 쉬운 이해를 위한 의역이 다소 포함되었다.



암달의 법칙은 연산의 병행 처리로 연산 속도를 얼마나 올릴 수 있는지 계산하는 데 쓰일 수 있다. 암달의 법칙은 1967년 이 이론을 발표한 Gene Amdahl 의 이름을 따서 명명되었다. 병행 또는 동시성 시스템으로 작업하는 대부분의 개발자들은 암달의 법칙을 알지 못하더라도 잠재적인 속도 향상에 대한 직관적인 느낌을 가진다. 어쨌든 암달의 법칙은 여전히 알아둘 가치가 있다.


먼저 수학적인 설명 후 다이어그램을 통해 암달의 법칙을 보여주도록 하겠다.



암달의 법칙 정의


병렬화 가능한 프로그램은 두 부분으로 나뉜다.


  • 병렬화 불가능한 부분
  • 병렬화 가능한 부분


디스크의 파일을 처리하는 프로그램이 있고 가정하자. 프로그램에는 디렉토리를 스캔하여 내부 메모리에 파일의 목록을 생성하는 부분이 있다. 각 파일 정보는 파일을 처리하는 쓰레드로 전달된다. 디렉토리를 스캔하여 파일 목록을 생성하는 부분은 병렬화 불가능한 부분이지만 파일을 처리하는 부분은 병렬화 가능한 부분이다.


순차적으로 실행되는 프로그램의 전체 수행 시간을 T 라고 한다. T 에는 병렬화 가능한 부분과 병렬화 불가능한 부분의 수행 시간이 모두 포함되어 있다. 병렬화 불가능한 부분은 B 라고 한다. 이제 병렬화 가능한 부분은  T - B 라 할 수 있다. 요약하자면 다음과 같다.


  • T = 순차적 실행의 전체 수행시간
  • B = 병렬화 불가능한 부분 실행의 전체 수행시간
  • T - B = 병렬화 가능한 부분의 전체 수행시간 (순차적으로 실행되었을 경우)


그리고 다음과 같이 표현된다.


T = B + (T-B)


병렬화 가능한 부분에 대한 부호는 없다는 점에서 위 식은 어딘가 이상해 보이기도 한다. 그러나 식에서 병렬화 가능한 부분은 T 와 B 로 표현될 수 있고, 부호의 수를 줄이면서 식은 개념적으로 축소되었다. 


병렬화 가능한 부분을 나타내는 T - B 는 프로그램을 병렬로 실행했을 때 기대할 수 있는 속도 상승 값이다. 속도가 얼마나 향상되는가는 쓰레드나 CPU 를 얼마나 사용하느냐에 달려있다. 쓰레드 또는 CPU 의 수를 N 이라 한다. 병렬화 가능한 부분의 최대 실행 속도는 다음과 같이 계산된다.


(T - B) / N


다르게 쓸 수도 있다.


(1/N) * (T - B)


위키피디아의 암달의 법칙 문서에는 위 식이 등장한다.


암달의 법칙에 의하면, 프로그램의 병렬화 가능한 부분이 N 개의 쓰레드 혹은 CPU 를 사용하여 실행될 경우 전체 수행시간은 다음과 같이 계산된다.


T(N) = B + (T - B) / N


T(N) 은 병렬 처리의 수와 전체 수행시간을 의미한다. 따라서 T 는 T(1) 으로 표현될 수 있다. 병렬 처리 1 에 대한 프로그램 전체 수행시간이다. T(1) 을 사용하면 암달의 법칙은 다음과 같이 표현된다.


T(N) = B + ( T(1) - B ) / N


의미는 여전히 같다.



계산 예제


암달의 법칙을 보다 잘 이해하기 위해 계산 예제를 하나 보도록 한다. 프로그램 전체 수행 시간은 1 으로 설정한다. 병렬화 불가능한 부분은 프로그램의 40% 을 자치한다. 수행 시간으로는 0.4 이다. 병렬화 가능한 부분은 1 - 0.4 = 0.6 이 된다.


병렬 처리의 수를 2 라고 하면(2 쓰레드 혹은 CPU) 수행 시간은 다음과 같다.


T(2) = 0.4 + ( 1 - 0.4 ) / 2
     = 0.4 + 0.6 / 2
     = 0.4 + 0.3
     = 0.7


같은 식으로 인자를 5 로 바꾸면 다음과 같다.


T(5) = 0.4 + ( 1 - 0.4 ) / 5
     = 0.4 + 0.6 / 5
     = 0.4 + 0.12
     = 0.52



그림으로 보는 암달의 법칙


쉬운 이해를 위해 암달의 법칙을 그림으로 설명해본다.


먼저, 프로그램의 병렬화 가능하지 않은 부분을 B 로 두고, 가능한 부분을 1-B 로 둔다.


Amdahls law illustrated.


상단의 선은 프로그램 전체 수행시간 T(1) 이다.


병렬 처리의 수가 2 라면 수행 시간을 다음과 같이 표현할 수 있다.


Amdahls law illustrated with a parallelization factor of 2.


병렬 처리의 수가 3 이라면 다음과 같다.


Amdahls law illustrated with a parallelization factor of 3.



알고리즘 최적화


암달의 법칙에 의하면 병렬화 가능한 부분은 하드웨어에 맡김으로써 더 빠르게 실행될 수 있다. 더 많은 수의 쓰레드/CPU 를 사용하는 것이다. 하지만 병렬화 가능하지 않는 부분의 속도 향상은 코드 최적화를 통해서 이루어질 수 있다. 따라서 병렬화 가능하지 않은 부분의 코드를 최적화 함으로써 프로그램의 속도와 병렬성을 높일 수 있다. 가능하다면 병렬화 가능하지 않는 부분의 처리를 병렬화 가능한 부분으로 옮겨서 병렬화 가능하지 않은 부분을 더 작게 만들 수도 있다.



순차적 수행의 최적화


프로그램에서 순차적으로 수행되는 부분을 최적화한다면, 최적화 후 암달의 법칙을 사용햐ㅐ 프로그램의 실행 시간을 계산해낼 수 있다. 병렬화 가능하지 않은 부분 B 와 병렬 처리의 수 O 으로 계산한다면암달의 법칙은 다음과 같다.


T(O,N) = B / O + (1 - B / O) / N


병렬화 가능하지 않은 부분의 실행은 이제 B / O 만큼의 시간을 소모한다. 고로 병렬화 가능한 부분의 실행 시간은 1 - B / O 가 된다.


B 가 0.4 이고 O 가 2, N 은 5 라면 계산은 다음과 같다.


T(2,5) = 0.4 / 2 + (1 - 0.4 / 2) / 5
       = 0.2 + (1 - 0.4 / 2) / 5
       = 0.2 + (1 - 0.2) / 5
       = 0.2 + 0.8 / 5
       = 0.2 + 0.16
       = 0.36



실행 시간 vs 향상된 속도


지금까지는 암달의 법칙을 프로그램의 최적화 또는 병렬화 후 실행 시간을 계산하는 데에 사용해왔다. 암달의 법칙은 향상된 속도, 즉 새로운 알고리즘이나프로그램이 이전 버전보다 얼마나 빨라졌는지를 계산하는 데에도 사용될 수 있다.


올드 버전의 실행 시간을 T 라고 하면, 향상된 속도는 다음과 같다.


Speedup = T / T(O,N)


실행 시간이나 향상된 속도를 계산할 때 T 은 주로 1 로 설정한다. 


Speedup = 1 / T(O,N)


T(O,N) 에 암달의 법칙을 넣으면 식은 다음과 같이 된다.


Speedup = 1 / ( B / O + (1 - B / O) / N )


B = 0.4, O = 2, N = 5 라면 계산은 다음과 같이 된다.


Speedup = 1 / ( 0.4 / 2 + (1 - 0.4 / 2) / 5)
        = 1 / ( 0.2 + (1 - 0.4 / 2) / 5)
        = 1 / ( 0.2 + (1 - 0.2) / 5 )
        = 1 / ( 0.2 + 0.8 / 5 )
        = 1 / ( 0.2 + 0.16 )
        = 1 / 0.36
        = 2.77777 ...


이 계산이 의미하는 것은, 병렬화 가능하지 않은 부분(순차적 실행)을 인자 2 로 최적화하고 병렬화 가능한 부분을 인자 5 로 병렬화하면 프로그램의 실행은 최대 2.77777 배로 빨라진다는 것이다.



계산에 그치지 말고 측정하라


암달의 법칙을 통해 프로그램의 향상된 속도를 계산해낼 수 있기는 하지만, 이 계산에 지나치게 의존해선 안된다. 실제로 알고리즘을 최적화 또는 병렬화 할 때는 다른 많은 요인들이 작용할 수 있다.


메모리의 속도, CPU 캐시 메모리, 디스크, 네트워크 카드 기타 등등의 것들이 그 제한 요인이 된다. 만약 새 알고리즘이 병렬화 되었으나 CPU 캐시 미스를 더 많이 일으킨다면 CPU 의 수 만큼의 기대했던 속도 향상을 얻을 수 없다. 알고리즘이 메모리 버스나 디스크 혹은 네트워크 카드 또는 커넥션을 지나치게 사용하게 된다면 역시 같은 결과가 발생한다.


암달의 법칙은 프로그램의 어디를 최적화 할 것인지에 대한 아이디어를 얻기 위해 사용하고, 실제로 얼마만큼의 최적화 속도 향상이 있을지는 측정을 통해 알아보아야 한다. 종종 높은 순차적 실행 알고리즘(단일 CPU)은 병렬 알고리즘보다 나은 성능을 보인다. 이유는 간단하다. 순차적 실행은 동시 실행에 따르는 (작업을 중단하고 다시 시작하는 등의)오버헤드가 없고, 단일 CPU 알고리즘은 하드웨어의 작업에 에 더 친화적일 수 있다. (CPU 파이프라인, CPU 캐시 등)







이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html



동시성에서의 논 블로킹 알고리즘이란 쓰레드간의 공유된 상태(자원)로의 접근을 서로의 중단 없이 수행하도록 하는 것이다. 보다 일반적인 용어로 말하자면, 어떤 알고리즘에서 한 쓰레드의 정지가 여기에 관련된 다른 쓰레드의 정지를 유발하지 않는다면 이 알고리즘을 논 블로킹 알고리즘이라 한다.


블로킹과 논 블로킹 동시성 알고리즘의 차이를 보다 잘 이해하기 위해서 먼저 블로킹 알고리즘을 다루고 이어서 논 블로킹 알고리즘을 다루도록 한다.



블로킹 동시성 알고리즘


다음과 같은 알고리즘을 블로킹 동시성 알고리즘이라 한다.


  • A: 쓰레드에 의해 요청된 동작을 수행 - 또는
  • B: 쓰레드가 동작을 수행할 수 있을 때까지 대기


많은 종류의 블로킹 알고리즘과 동시성 블로킹 자료구조가 있다. 예를 들어, java.util.concurrent.BlockingQueue 인터페이스의 구현체들은 모두 블로킹 자료구조이다. 쓰레드가 BlockingQueue 에 엘레멘트를 삽입하려 하는데 큐에 공간이 없다면 쓰레드는 공간이 생길 때까지 대기한다.


다음 다이어그램은 공유된 자료구조를 보호하는 블로킹 알고리즘의 동작을 보여준다.




논 블로킹 동시성 알고리즘


다음과 같은 알고리즘을 논 블로킹 동시성 알고리즘이라 한다.


  • A: 쓰레드에 의해 요청된 동작을 수행 - 또는
  • B: 요청된 동작이 수행될 수 없는 경우 이를 요청 쓰레드에게 통지


자바는 몇몇 논 블로킹 자료구조를 가지고 있다. AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference 클래스들은 모두 논 블로킹 자료구조의 예이다.


다음 다이어그램은 공유된 자료구조를 보호하는 논 블로킹 알고리즘의 동작을 보여준다.




논 블로킹 vs 블로킹


블로킹과 논 블로킹 알고리즘의 주된 차이점은 위 각 알고리즘의 동작 설명 중 두 번째 단계에 있다. 다시 말해서, 블로킹 알고리즘과 논 블로킹 알고리즘의 차이는 요청된 동작이 수행될 수 없을 때 어떻게 대응하느냐에 있다.


블로킹 알고리즘은 쓰레드가 요청한 동작이 수행 가능해질 때까지 대기한다. 논 블로킹 알고리즘은 요청된 동작이 현재 수행 불가능하다는 사실을 쓰레드에게 알린다.


블로킹 알고리즘을 사용하면 쓰레드는 요청 동작이 수행 가능해질 때까지 대기하게 될 수 있다고 했는데, 이것은 일반적으로 첫번째 스레드가 요청된 작업을 수행할 수 있게 하는 다른 쓰레드의 동작이 된다. 어떤 이유로 쓰레드가 어플리케이션의 다른 곳에서 동작이 중단되어 첫번째 스레드가 요청한 작업을 수행할 수 없는 경우 첫번째 쓰레드는 무기한, 혹은 다른 쓰레드가 필요한 동작을 수행할 때까지 중단된다.


예를 들어, 쓰레드가 꽉 찬 BlockingQueue 안에 엘레멘트를 삽입하려 하면 이 쓰레드는 다른 쓰레드가 큐에서 엘레멘트를 꺼내갈 때까지 동작이 중단된다. 만일 어떤 이유로 큐에서 엘레멘트를 꺼내가야 할 쓰레드의 동작이 어플리케이션의 다른 영역에서 중단된다면, 큐에 엘레멘트를 삽입하기 위해 대기중인 쓰레드의 중단 상태는 해제되지 못한다. 대기 상태는 무기한 지속되거나, 혹은 다른 쓰레드가 큐에서 엘레멘트를 꺼내갈 때까지 지속된다.



논 블로킹 동시성 자료구조


멀티쓰레드 시스템에서 쓰레드들은 일반적으로 어떤 종류의 자료구조를 통해 서로 협업한다. 단순한 형태의 다양한 자료구조에서부터 큐, 맵, 스택과 같은 보다 발전된 형태의 자료구조들 중 어느 것이든 이러한 자료구조로 사용될 수 있다. 다수의 쓰레드의 동시 접근에 제대로 기능하기 위해, 자료구조는 반드시 동시성 알고리즘에 의해 보호되어야 한다. 이런 알고리즘이 자료구조를 동시성 자료구조로 만드는 것이다.


동시성 자료구조를 보호하는 알고리즘이 쓰레드를 중단시키는 방법을 통한다면 이 알고리즘을 블로킹 알고리즘이라 한다. 고로, 자료구조는 블로킹 동시성 자료구조가 된다.


동시성 자료구조를 보호하는 알고리즘이 쓰레드를 중단시키지 않는 방법을 통한다면 이 알고리즘을 논 블로킹 알고리즘이라 한다. 고로, 자료구조는 논 블로킹 동시성 자료구조가 된다.


이런 각 동시성 자료구조는 특정한 방법의 쓰레드 간 협업을 지원하기 위해 설계되었다. 어떤 동시성 자료구조를 사용할 것이냐는 쓰레드 간 협업이 어떤 방식으로 이루어진 것이냐에 달려있다. 다음 섹션에서 몇가지 논 블로킹 자료구조를 다루면서 어떤 상황에 이 자료구조들이 사용될 수 있는지 설명한다. 논 블로킹 자료구조의 동작 방식에 대한 설명은 논 블로킹 자료구조의 설계와 구현에 대한 방안을 제공할 것이다.



volatile 변수


자바 volatile 변수는 변수의 값을 항상 메인 메모리에서 직접 읽히도록 한다. volatile 변수에 새 값이 대입되면 이 값은 언제나 메인 메모리로 즉시 쓰여진다. 이 동작은 다른 CPU 에서 동작하는 다른 쓰레드에게 항상 volatile 변수의 최신 값이 읽히도록 보장한다. volatile 변수의 값은 CPU 캐시가 아닌 메인 메모리로부터 직접 읽히게 된다.


volatile 변수는 논 블로킹이다. volatile 변수로의 쓰기는 원자적 연산이고, 다른 쓰레드가 끼어들 수 없다. 하지만 volatile 변수라 할지라도 연속적인 읽기-값 변경-쓰기 동작은 원자적이지 않다. 다음 코드는 둘 이상의 쓰레드에 의해 수행된다면 여전히 경합을 유발한다.


volatile myVar = 0;

...
int temp = myVar;
temp++;
myVar = temp;


처음에 volatile 변수인 myVar 의 값은 메인 메모리로부터 읽어들여 temp 변수로 대입된다. 다음으로 temp 변수의 값은 1 증가된다. 그리고 temp 변수의 값은 myVar 로 다시 대입되어, 값은 메인 메모리로 저장된다.


두 쓰레드가 이 코드를 실행하여 똑같이 myVar 을 읽고, 값을 1 증가시키고, 값을 메인 메모리로 저장한다면, myVar 의 증가되는 값은 2 가 아닌, 1 이 될 여지가 있다. (예: 두 쓰레드가 값 19 를 읽어 각각 값을 증가시키고 저장하지만 결과값은 21 이 아닌 20 이 된다.) 


위와 같은 코드를 작성할 일은 아마도 없겠지만, 위 코드는 사실 아래 코드와 동일하다.


myVar++;


위 코드가 실행되면 myVar 의 값은 CPU 레지스터 또는 CPU 캐시로 읽히고, 1 증가되고, CPU 레지스터나 CPU 캐시에서 다시 메인 메모리로 저장된다.



The Single Writer Case


공유 변수로 쓰기를 수행하는 쓰레드가 하나이고 이 변수를 읽는 쓰레드가 다수일 때는 어떠한 경합도 발생하지 않는다. 읽기 쓰레드가 몇 개이든 아무런 문제가 되지 않는다. 따라서 쓰기 쓰레드가 하나인 경우라면 언제든 volatile 변수를 사용해도 좋다.


경합 조건은 다수의 쓰레드가 같은 공유 변수에 대해 읽기-값 변경-쓰기 순서의 연산을 수행할 때 발생한다. 읽기-값 변경-쓰기 연산을 수행하는 쓰레드가 하나 뿐이고, 나머지 모든 쓰레드는 읽기 연산만을 수행한다면 경합 조건은 발생하지 않는다.


다음은 synchronized 를 사용하지 않고도 동시성을 유지하는 single writer counter 이다.


public class SingleWriterCounter { private volatile long count = 0; /** * 오직 한 쓰레드만 이 메소드를 호출해야 함. * 그렇지 않으면 경합 조건을 유발할 수 있음. */ public void inc() { this.count++; } /** * 다수의 읽기 쓰레드가 이 메소드를 호출할 수 있음. * @return */ public long count() { return this.count; } }


inc() 메소드를 호출하는 쓰레드가 하나라는 전제에 한하여 SingleWriterCounter 클래스의 인스턴스는 다수의 쓰레드가 접근할 수 있다. 이 말은 inc() 메소드를 호출하는 쓰레드가 한 시점에 하나여야 한다는 뜻이 아니다. 시점을 막론하고 오직 한 쓰레드만이 inc() 를 호출하여야 한다. count() 메소드는 다수의 쓰레드에 의해 호출될 수 있으며, 이는 어떠한 경합 조건도 유발하지 않는다.


다음 다이어그램은 다수의 쓰레드가 volatile 변수 count 로 어떻게 접근하는지 보여준다.




volatile 기반의 더 발전된 자료구조


volatile 변수로의 쓰기 쓰레드가 하나이고 읽기 쓰레드가 둘 이상인 경우에 volatile 변수의 조합을 사용하는 자료구조를 만들 수 있다. 각 volatile 변수는 각각의, 서로 다른 쓰기 쓰레드가 접근할 수 있다(volatile 변수 하나당 하나의 쓰기 쓰레드이다. 즉 한 volatile 변수로의 쓰기 쓰레드는 역시 반드시 하나여야 한다.) 이런 구조를 가진 자료구조를 사용하면 쓰레드들이 논 블로킹 방식으로 서로 정보를 주고받을 수 있다. 


다음은 두 개의 쓰기 카운터를 가진 간단한 클래스이다. 어떻게 구현되었나 보자.


public class DoubleWriterCounter { private volatile long countA = 0; private volatile long countB = 0; /** * 오직 한 쓰레드만이 이 메소드를 호출해야 함. * 그렇지 않으면 경합 조건을 유발할 수 있음. */ public void incA() { this.countA++; } /** * 오직 한 쓰레드만이 이 메소드를 호출해야 함. * 그렇지 않으면 경합 조건을 유발할 수 있음. */ public void incB() { this.countB++; } /** * 다수의 읽기 쓰레드가 이 메소드를 호출할 수 있음. */ public long countA() { return this.countA; } /** * 다수의 읽기 쓰레드가 이 메소드를 호출할 수 있음. */ public long countB() { return this.countB; } }


DoubleWriterCounter 클래스는 이제 두 volatile 변수를 가진다. 그리고 각각의 증가, 읽기 메소드를 따로 가진다. incA() 메소드를 호출하는 쓰레드는 하나여야 하고, 마찬가지로 incB() 를 호출하는 쓰레드도 하나여야 한다. incA() 호출 쓰레드와 incB() 호출 쓰레드는 달라도 된다. countA() 와 countB() 메소드 호출은 어떤 쓰레드든 상관없이 가능하다. 여기에 경합 조건은 없다.


DoubleWriterCounter 클래스는 두 쓰레드간의 협업에 사용될 수 있다. 두 카운트 변수는 생산-소비 형태로 사용된다. 다음 다이어그램은 위와 같은 자료구조를 이용한 두 쓰레드간의 협업을 나타낸다.




눈치 빠른 독자들은 두 개의 SingleWriterCounter 인스턴스를 사용함으로 DoubleWriteCounter 를 사용하는 것과 같은 효과를 얻을 수 있다는 사실을 알 것이다. 필요에 따라 SingleWriterCounter 인스턴스의 수를 얼마든지 늘려서 사용할 수 있다.



컴페어 스왑을 통한 낙관적 락


다중 쓰기 쓰레드들의 공유 변수로의 접근이 꼭 필요한 상황이라면 volatile 변수만으로는 충분하지 못하다. 타 쓰레드의 접근을 배제할 수 있는 방법이 필요하다. 일단 synchronized 블록을 통한 상호 배제를 보자.


public class SynchronizedCounter {
    long count = 0;

    public void inc() {
        synchronized(this) {
            count++;
        }
    }

    public long count() {
        synchronized(this) {
            return this.count;
        }
    }
}


inc() 와 count() 메소드는 synchronized 블록을 사용한다. 우리는 이러한 방식(synchronized 블록과 wait()-notify() 호출 등등)의 동기화를 피하고자 한다.


synchronized 블록을 사용하는 대신 우리는 자바의 원자적 변수를 사용한다. 여기서는 AtomicLong 이 그것이다. AtomicLong 을 사용하면 위 클래스가 어떻게 변하는지 보자.


import java.util.concurrent.atomic.AtomicLong;

public class AtomicCounter {
    private AtomicLong count = new AtomicLong(0);

    public void inc() {
        boolean updated = false;
        while(!updated){
            long prevCount = this.count.get();
            updated = this.count.compareAndSet(prevCount, prevCount + 1);
        }
    }

    public long count() {
        return this.count.get();
    }
}


AtomicCounter 는 SynchronizedCounter 와 마찬가지로 쓰레드 세이프하다. 여기서 흥미로운 점은 inc() 메소드의 구현이다. inc() 메소드는 더이상 synchronized 블록을 사용하지 않는다. 대신 다음과 같은 코드가 있다.


boolean updated = false;
while(!updated){
    long prevCount = this.count.get();
    updated = this.count.compareAndSet(prevCount, prevCount + 1);
}


위 코드는 원자적 연산은 아니다. 즉 두 개의 서로 다른 쓰레드가 동시에 inc() 메소드를 호출하여 long prevCount = this.count.get(); 코드를 실행하는 일이 가능하다. 두 쓰레드 모두 count 가 증가하기 전의 값을 얻을 수 있다. 여기까진 어떠한 경합도 발생하지 않는다.


여기서 중요한 코드는 while 루프 안의 두 번째 라인에 있다. compareAndSet() 메소드 호출은 원자적 연산이다. 이 메소드는 AtomicLong 내부의 기존 값과 내부의 값이라 예상되는 값을 비교하여 두 값이 일치한다면 새로운 값을 세팅한다. 이 동작의 수행은 일반적으로 CPU 의 컴페어 스왑 명령을 직접 사용한다. 때문에 synchronized 블록은 필요치 않으며, 쓰레드를 중단할 일도 없다. 덕분에 쓰레드 중단에 따르는 오버헤드는 발생하지 않는다.


AtomicLong 의 내부 값이 20 이라고 하면, 두 쓰레드는 이 값을 읽는다. 그리고 compareAndSet(20, 20 + 1) 을 호출한다. compareAndSet() 메소드는 원자적 연산이므로, 실행은 순차적으로 이루어진다(한 시점에 한 번).


첫 번째 쓰레드는 예상 값 20(counter 의 이전 값)을 AtomicLong 의 내부 값과 비교한다. 두 값은 일치하고, AtomicLong 은 내부 값을 21 로 세팅한다(20 + 1). updated 변수는 true 가 되고 while 루프는 정지된다.


이제 두 번째 쓰레드를 보자. 쓰레드는 compareAndSet(20, 20 + 1) 을 호출한다. AtomicLong 의 내부 값은 더이상 20 이 아니다. 이 호출은 실패한다. AtomicLong 의 내부 값은 21 로 세팅되지 않는다. updated 변수는 false 가 되고, while 루프는 계속된다. 이번엔 내부 값 21 을 읽어서 1 증가된 값인 22 를 세팅하려 시도한다. 이 과정에서 다른 쓰레드의 개입이 없다면 이 시도는 성공하고 AtomicLong 의 값은 22 가 된다.



왜 낙관적 락인가?


이전 섹션의 코드는 낙관적 락이라 불린다. 낙관적 락은 종종 비관적 락이라 불리는, 전통적인 락과는 차이가 있다. 전통적인 락은 synchronized 블록이나 기타 락을 사용해 공유 메모리로의 접근을 차단한다. 이런 방식은 쓰레드의 중단을 일으킨다.


낙관적 락은 쓰레드의 접근을 차단하지 않고, 모든 쓰레드가 공유 메모리의 복제본을 생성한다. 쓰레드는 자신이 가진 본사본의 데이터를 수정할 수 있으며, 수정된 데이터를 공유 메모리로 쓰기 시도할 수 있다. 다른 쓰레드가 이 데이터를 변경하지 않았다면 컴페어 스왑 연산을 통해 쓰기 쓰레드는 공유 메모리의 데이터를 변경할 수 있다. 만약 다른 쓰레드가 먼저 데이터를 변경했다면, 쓰기 쓰레드는 변경된 새로운 값을 읽어서 다시 변경을 시도한다.


이것이 낙관적 락이라 불리는 이유는, 쓰레드가 데이터의 복제본을 얻어서 데이터를 변경하고 이 변경을 적용하는 동작이, 이 과정에서 다른 쓰레드가 데이터를 변경하지 않았다는 낙관적인 추정 하에 이루어지기 때문이다. 이 추정이 맞는다면 쓰레드는 락 없이 공유 메모리의 데이터를 변경하는 작업에 성공한다. 추정이 틀린다면 쓰레드의 변경 작업은 실패하지만, 여전히 락은 없다.


낙관적 락은 공유 메모리의 경합이 낮거나 중간 수준일 때 가장 잘 동작하는 경향이 있다. 경합이 매우 높으면, 쓰레드는 많은 CPU 싸이클을 공유 메모리를 복제하고 변경하는 데에 낭비하게 되고 공유 메모리로 데이터의 변경을 쓰는 데에 실패하게 된다. 이런 경우에는 경합을 낮추는 방향으로 코드를 재설계할 것을 권한다.



낙관적 락은 논 블로킹


여기서 소개한 낙관적 락 메카니즘은 논 블로킹이다. 한 쓰레드가 공유 메모리의 복제본을 얻어 데이터를 변경하는 과정에서 어떤 이유로 정지상태가 되더라도 같은 공유 메모리로 접근하는 다른 쓰레드들이 정지되는 일은 없다.


전통적인 락/언락 패러다임에서는 한 쓰레드가 락을 걸면 이 락은 락을 건 쓰레드에 의해 해제될 때까지 다른 쓰레드들의 접근을 차단한다(정지상태). 락을 건 쓰레드가 어떤 이유로 정지상태가 되면, 락은 매우 오랜 시간동안 해제되지 않은 상태로 유지된다. -무기한 지속될 수 있다.



교체될 수 없는 자료구조(Non-swappable Data Structures)


컴페어 스왑을 통한 낙관적 락은 공유 자료구조에 알맞게 동작한다. 이 공유 자료구조는 단일 컴페어 스왑 연산에서 전체 자료구조가 새로운 자료구조로 교체될 수 있는(swappable) 성질을 가진다. 그러나 전체 자료구조를 변경된 복제본으로 교체하는 일이 언제나 가능한 것은 아니다.


공유 자료구조가 큐라고 해보자. 이 큐에 엘레멘트를 넣거나 빼려고 하는 각 쓰레드는 먼저 큐의 전체 복제복을 가지고 이 복제본의 데이터를 변경한다. 이 동작은 AtomicReference 를 통해 이루어진다. 참조를 복제하고, 큐를 복제하여 수정하고, AtomicReference 안의 참조를 새로 생성된 큐와 교체한다.


하지만 자료구조의 크기가 클 경우 이러한 복제 동작은 많은 메모리와 CPU 싸이클을 요구한다. 어플리케이션의 메모리 사용량을 증가시키고, 많은 시간이 낭비된다. 이는 결국 어플리케이션의 성능에 영향을 미치는데, 특히 자료구조에 대한 경합이 높을 경우 이 영향은 더욱 심해진다. 뿐만 아니라, 쓰레드가 자료구조를 복제하고 수정하는 시간이 길수록 다른 쓰레드가 중간에 자료구조를 수정했을 가능성이 높아진다. 알다시피 다른 쓰레드가 공유 자료구조를 복제하여 수정하면 자료구조가 복제된 이후로 다른 쓰레드들은 복제-수정 작업을 다시 시작해야 한다. 그리고 이것은 성능과 메모리 소비량에 더욱 큰 영향을 미친다.


다음 섹션에서는 복제-수정이 아닌, 동시에 수정할 수 있는 논 블로킹 자료구조의 구현 방안에 대하여 소개한다.



변경 예정 공유하기


쓰레드는 전체 자료구조를 복제하고 수정하는 대신, 공유 자료구조의 변경 예정을 공유할 수 있다. 공유 자료구조를 변경하려는 쓰레드의 프로세스는 다음과 같다.


  1. 다른 쓰레드가 변경 예정을 공유 자료구조로 전달했는지 확인한다.
  2. 공유 자료구조로 변경 예정을 전달한 쓰레드가 없다면, 변경 예정을 만든다. 그리고 이 예정을 공유 자료구조로 전달한다(컴페어 스왑)
  3. 공유 자료구조의 변경을 수행한다.
  4. 다른 쓰레드에게 예정된 변경이 수행되었음을 알리기 위해 변경 예정으로의 참조를 제거한다.


보다시피 두 번째 스탭에서 다른 쓰레드의 변경 예정 전달은 차단된다. 이로 인해 두 번째 스탭은 공유 자료구조에 락을 건 것과 같이 동작한다. 한 쓰레드가 변경 예정을 성공적으로 전달했다면, 공유 자료구조에 변경이 수행될 때까지 다른 쓰레드는 변경 예정을 전달할 수 없다.


쓰레드가 변경 예정을 전달한 뒤 다른 작업을 수행하던 중 동작이 정지된다면, 공유 자료구조는 락이 걸린다. 공유 자료구조는 자료구조를 사용하는 쓰레드를 직접 정지시키지 않는다. 공유 자료구조에 변경 예정을 전달할 수 없음을 인지한 쓰레드는 다른 작업을 수행한다. 물론 이 동작은 필요에 따라 바꿀 수 있다.



완료될 수 있는 변경 예정


공유 자료구조로 전달된 변경 예정은 자료구조에 락을 걸 수 있다. 변경 예정 객체는 다른 쓰레드가 변경을 완료했음을 알 수 있는 충분한 정보를 가지고 있어야 한다. 따라서 변경 예정을 전달한 쓰레드가 변경을 완료하지 못하면 다른 쓰레드가 이를 대신하여 그 변경을 완료하고 다른 쓰레드가 공유 자료구조를 사용할 수 있도록 유지할 수 있다.


다음 다이어그램은 여기서 설명한 논 블로킹 알고리즘의 설계를 보여준다.




변경은 반드시 하나 이상의 컴페어 스왑 연산으로 수행되어야 한다. 두 쓰레드가 예정된 변경을 완료하려 시도하면 그 중 한 쓰레드만이 컴페어 스왑 연산을 완료할 수 있다. 그리고 컴페어 스왑 연산이 완료된 직후의 다음 시도는 실패한다.



A-B-A 문제


위 다이어그램의 알고리즘은 A-B-A 문제에 부딪힐 수 있다. A-B-A 문제란 어떤 변수의 값이 A 에서 B 로 변경되었는데 다시 A 로 돌아가는 상황을 가리킨다. 다른 쓰레드는 변수가 실제로 변경되었는지 감지할 수 없다.


쓰레드 A 가 진행중인 변경을 확인하고 데이터를 복제한 뒤 쓰레드 스케쥴러에 의해 동작이 중단되면, 그 사이 쓰레드 B 는 공유 자료구조에 접근할 수 있다. 쓰레드 B 가 공유 자료구조를 완전히 변경한 뒤 변경 예정을 삭제하면, 쓰레드 A 에게는 자신이 데이터를 복제한 이후로 공유 자료구조에 어떠한 변경도 발생하지 않았던 것처럼 보이게 된다. 그러나 변경은 발생하였고, 쓰레드 A 는 자신이 복제했었던 데이터(쓰레드 B 가 데이터를 변경하기 전)를 기준으로 자신의 변경을 수행한다.


다음 다이어그램이 위와 같은 A-B-A 문제를 보여준다.




A-B-A 문제의 해결책


A-B-A 문제의 해결책으로 널리 알려진 방법은 데이터 변경 시에 변경 예정 객체의 포인터만 교체하는 것이 아니라, 포인터와 카운터를 함께 교체하는 것이다. 이 교체는 단일 컴페어 스왑 연산을 사용한다. 이 방법은 C 와 C++ 과 같은 포인터를 지원하는 언어에서 구현 가능하다. 현재 변경 객체의 포인터가 '진행중인 변경 없음' 을 나타낸다 해도, 포인터+카운터에서 카운터가 증가되어 변경 사실을 인지시켜준다. 


자바에서 참조와 카운터를 한 변수로 묶는 일은 불가능하다. 대신 자바는 AtomicStampedReference 클래스를 제공한다. 이 클래스는 컴페어 스왑 연산을 사용해서 참조와 스탬프를 교체할 수 있다.



논 블로킹 알고리즘 템플릿


아래 코드는 논 블로킹 알고리즘의 구현 방법을 보여주는 코드 템플릿이다. 이 템플릿은 본문에서 설명된 내용을 근거로 한다.


NOTE: 나는 논 블로킹 알고리즘에 있어 전문가가 아니기 때문에, 이 템플릿에는 몇 가지 오류가 있을 수 있다. 이 템플릿을 당신의 논 블로킹 알고리즘 구현물의 기반으로 삼지 않도록 하라. 이 템플릿은 논 블로킹 알고리즘이 어떤 식으로 구현될 수 있는지에 대한 방향을 제공할 뿐이다. 논 블로킹 알고리즘을 스스로 구현하고 싶다면 실제 논 블로킹 알고리즘 구현체를 두고 연구해야 한다.


import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicStampedReference; public class NonblockingTemplate { public static class IntendedModification { public AtomicBoolean completed = new AtomicBoolean(false); } private AtomicStampedReference<IntendedModification> ongoingMod = new AtomicStampedReference<IntendedModification>(null, 0); // 자료구조의 상태를 선언한다. public void modify() { while(!attemptModifyASR()); } public boolean attemptModifyASR(){ boolean modified = false; IntendedModification currentlyOngoingMod = ongoingMod.getReference(); int stamp = ongoingMod.getStamp(); if(currentlyOngoingMod == null){ //변경 예정을 만들기 위한 자료구조 상태를 복제한다. //변경 예정을 준비한다. IntendedModification newMod = new IntendedModification(); boolean modSubmitted = ongoingMod.compareAndSet(null, newMod, stamp, stamp + 1); if(modSubmitted){ //컴페어 스왑 연산을 통해 변경을 완료한다. //note: 다른 쓰레드가 컴페어 스왑 연산의 완료를 도울 수 있기 때문에, //CAS 는 실패할 수 있음. modified = true; } } else { //진행중인 변경 작업을 완료하려고 시도하므로, 이 쓰레의 접근을 허용하기 위해 //자료구조는 해제된다. modified = false; } return modified; } }



논 블로킹 알고리즘 구현의 어려움


논 블로킹 알고리즘을 제대로 설계하고 구현하기란 난이도가 상당한 일이다. 논 블로킹 알고리즘을 구현하려 한다면, 직접 해보기에 앞서 다른 사람이 구현한 기존의 논 블로킹 알고리즘을 참고하기 바란다.


자바에는 이미 몇가지 논 블로킹 구현체가 있고(ex: ConcurrentLinkedQueue), 앞으로 선보일 자바에서 더 추가될 예정이다.


자바의 내장 논 블로킹 자료구조에 더하여 당신이 사용할 수 있는 몇가지 오픈소스 논 블로킹 자료구조가 있다. 예로, LMAX Disrupter(큐 성격의 자료구조), Cliff Click 의 논 블로킹 해시맵이 그것이다. 자바 컨커런시 레퍼런스 페이지에서 더 많은 자료를 찾아볼 수 있다.



논 블로킹 알고리즘의 이점


블로킹 알고리즘과 비교하여 논 블로킹 알고리즘의 몇가지 이점이 있다.



선택


논 블로킹 알고리즘의 첫 번째 이점은, 쓰레드가 요청된 작업을 수행할 수 없는 경우 어떤 동작을 취할 지 선택할 수 있다는 것이다. 그저 동작을 정지하고 대기하는 대신, 쓰레드는 무엇을 할지 선택할 수 있다. 물론 쓰레드가 할 일이 없는 경우도 있을 수 있다. 이런 경우에는 스스로 동작을 정지하고 대기하도록 할 수 있다. 이렇게 함으로 CPU 자원은 는 다른 작업을 위해 쓰일 수 있을 것이다. 어쨌든 적어도 쓰레드는 선택권을 갖는다.


단일 CPU 시스템에서 요청된 동작을 수행할 수 없는 쓰레드를 정지시키는 일은 합리적이다. 이렇게 하면 다른 쓰레드가 CPU 를 사용하여 작업을 수행할 수 있다. 하지만 단일 CPU 시스템에서도 블로킹 알고리즘은 데드락이나 기아상태, 기타 다른 동시성 문제를 야기할 수 있다.



노 데드락


논 블로킹 알고리즘의 두 번째 이점은, 한 쓰레드의 정지가 다른 쓰레드의 정지를 일으키지 않는다는 것이다. 이 말은 데드락이 발생하지 않음을 의미한다. 두 쓰레드가 서로의 락이 해제되기를 기다리는 상황은 발생하지 않는다. 요청된 동작을 수행할 수 없는 쓰레드라도 정지되어 대기 상태로 들어가지 않기 때문이다. 논 블로킹 알고리즘에서도 라이브 락은 발생할 수 있다. 두 쓰레드가 요청된 동작을 수행하려 하지만 다른 쓰레드의 동작 수행으로 인해 이 동작이 수행 불가능한 상황이다. 두 쓰레드는 수행 불가능을 통보받지만 계속해서 수행을 시도한다.



노 쓰레드 서스펜션


쓰레드를 정지하고 활성화하는 일에는 비용이 든다. 운영체제와 쓰레드 라이브러리가 발전해가면서 줄어들어들긴 했지만, 쓰레드 정지와 활성화를 위한 비용은 여전히 크다.


쓰레드는 동작이 차단되면서 정지 상태가 된다. 따라서 쓰레드 정지-활성화 오버헤드를 일으킨다. 논 블로킹 알고리즘에서 쓰레드는 정지되지 않기 때문에, 이 오버헤드는 발생하지 않는다. CPU 는 컨택스트 스위칭에 사용될 잠재적인 시간 비용을 실제 비즈니스 로직을 수행하는 데에 사용할 수 있다는 뜻이다.


멀티 CPU 시스템에서 블로킹 알고리즘은 전체적인 성능에 있어 큰 영향을 미칠 수 있다. CPU A 에서 동작하는 쓰레드가 CPU B 의 쓰레드를 기다리며 정지될 수 있다. 이는 어플리케이션의 병행성을 낮추게 된다. 물론 CPU A 는 다른 쓰레드가 동작하도록 조정할 수 있을 것이다. 하지만 쓰레드의 정지-활성화라는 컨텍스트 스위칭 비용은 비싸다. 쓰레드가 정지되지 않는 편이 낫다.



쓰레드 지연 감소


여기서 지연이란 요청된 동작이 수행 가능해진 시점부터 실제로 쓰레드가 동작을 수행하는 시점까지의 시간을 의미한다. 논 블로킹 알고리즘에서 쓰레드는정지되지 않기 때문에 쓰레드 활성화에 드는 비싸고 느린 비용을 지불하지 않는다. 이는 어떤 요청 동작이 수행 가능해지면 쓰레드는 보다 빠르게 응답을 줄 수 있다는 의미이다. 따라서 응답 지연은 감소한다.


논 블로킹 알고리즘은 요청 동작이 수행 가능해질 때까지 바쁜 대기(busy-waiting)를 통해 지연을 낮춘다. 물론 논 블로킹 자료구조에 대한 쓰레드 경합이 높아지면 CPU 는 바쁜 대기에 많은 싸이클을 소모하게 된다. 이 사실은 염두에 두어야 한다. 당신의 자료구조가 높은 쓰레드 경합을 가진다면 논 블로킹 알고리즘은 최선이 아닐 수 있다. 그러나 쓰레드 경합을 낮추도록 어플리케이션을 재설계하는 방법도 존재한다.



'Java > Concurrency' 카테고리의 다른 글

암달의 법칙(Amdahl's Law)  (1) 2017.06.17
동기화 장치 해부(Anatomy of a Synchronizer)  (0) 2017.05.25
컴페어 스왑(Compare and Swap)  (0) 2017.05.24
쓰레드 풀(Thread Pools)  (0) 2017.05.22
블로킹 큐(Blocking Queues)  (3) 2017.05.22
이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/anatomy-of-a-synchronizer.html



많은 동기화 장치(락, 세마포어, 블로킹 큐, 기타 등등)에 기능적 차이가 있더라도, 이 동기화 장치들의 내부 설계는 모두 유사하게 이루어져 있다. 다시 말해서 이 장치들은 내부적으로 같은(또는 유사한) 기본 부분들로 구성되어 있다. 이 기본 부분들에 대한 지식은 동기화 장치를 설계할 때 큰 도움이 될 수 있다. 이 글은 이 기본 부분들에 대해 다룬다.


※ 본문의 내용은 2004년 봄 Jakob Jenkov, Toke Johansen, Lars Bjørn 코펜하겐 IT 대학 이학 석사과정 프로젝트 결과물의 일부분이다. 이 프로젝트를 진행하는 동안 우리는 Doug Lea 에게 프로젝트 관련 연구에 대한 자문을 구했다. 흥미롭게도 그는 이 프로젝트와는 독립적으로, Java 5 의 동시성 유틸리티 개발 과정에서 비슷한 결론을 내놓았다.  Doug Lea 의 연구는 책 'Java Concurrency in Practice' 에 설명되어 있다. 또한 이 책은 '동기화 장치 해부' 챕터를 담고 있는데, 챕터의 내용은 본문의 그것과 유사하지만 완전히 같지는 않다.


모두는 아닐지라도, 대부분의 동기화 장치의 목적은 코드의 특정한 부분(크리티컬 섹션)을 쓰레드들의 동시 접근으로부터 보호하는 데에 있다. 이를 위해 동기화 장치는 다음의 각 부분들을 필요로 한다.


  1. 상태
  2. 접근 조건
  3. 상태 변경
  4. 통지 전략
  5. 확인-설정 메소드
  6. 설정 메소드


이 모든 부분들을 정확히 똑같이 가지고 있는 동기화 장치가 있을 수도 있지만, 모든 동기화 장치가 이 부분들을 모두 가지고 있는 것은 아니다. 보통 동기화 장치에는 이들 중 하나 이상의 부분들이 있다.



상태


동기화 장치의 상태는 쓰레드가 접근 권한을 획득할 수 있는지 결정하기 위한 접근 조건에 사용된다. Lock 에서의 상태는 boolean 타입으로, Lock 이 잠겼는지 잠기지 않았는지를 나타낸다. BoundedSemaphore 는 내부에 카운터(int) 와 최대치(int) 라는 상태를 둔다. 카운터는 세마포어를 획득한 쓰레드의 수이고, 최대치는 쓰레드가 획득 가능한 최대(제한) 세마포어의 수이다. 블로킹 큐에서는 큐의 엘레멘트 목록과 큐 최대 크기(int) 를 상태로 둔다.


다음은 Lock 과 BoundedSemaphore 의 코드 일부이다. 상태 코드는 볼드 처리되었다.


public class Lock{

  //state is kept here
  private boolean isLocked = false; 

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}
public class BoundedSemaphore {

  //state is kept here
      private int signals = 0;
      private int bound   = 0;
      
  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  public synchronized void take() throws InterruptedException{
    while(this.signals == bound) wait();
    this.signal++;
    this.notify();
  }
  ...
}



접근 조건


접근 조건은 확인-설정 메소드를 호출하는 쓰레드가 상태를 설정할 수 있도록 허용할지를 판단하는 조건이다. 접근 조건은 일반적으로 동기화 장치의 상태에 기반을 둔다. 보통 while 루프 안에서 접근 조건을 이용하여 상태가 조건에 맞는지를 확인하는 방법으로 Spurious Wakeups 에 대응한다. 접근 조건의 확인 결과는 true 또는 false 로 나타낸다.


Lock 에서의 접근 조건은 간단히 isLocked 멤버 변수의 값을 확인하는 것이다. BoundedSemaphore 에서의 접근 조건은 세마포어를 '획득하는지' 혹은 '해제하는지' 에 따른 두 가지 접근 조건이 있다. 쓰레드가 세마포어를 획득하려 하면 signals 변수의 값이 획득 상한값에 다다르지 않았는지 확인한다. 쓰레드가 세마포어를 해제하려 하면 signals 변수의 값이 0 이 아닌지 확인한다.


다음은 LockBoundedSemaphore 의 코드 일부이다. 접근 조건은 볼드 처리되었다. while 루프 안에서 접근 조건이 어떤식으로 작동하는지 알 수 있다.


public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    //access condition
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  ...
}
public class BoundedSemaphore {
  private int signals = 0;
  private int bound   = 0;

  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  public synchronized void take() throws InterruptedException{
    //access condition
    while(this.signals == bound) wait();
    this.signals++;
    this.notify();
  }

  public synchronized void release() throws InterruptedException{
    //access condition
    while(this.signals == 0) wait();
    this.signals--;
    this.notify();
  }
}



상태 변경


쓰레드가 크리티컬 섹션으로의 접근을 획득하면 다른 쓰레드들의 접근을 막기 위해 동기화 장치의 상태가 변경되어야 한다. 다시 말해서, 상태는 현재 쓰레드가 크리티컬 섹션 안에서 실행되고 있음을 나타내야 하고, 이것은 동일한 크리티컬 섹션에 접근하려 하는 다른 쓰레드들의 접근 조건에 영향을 미치도록 한다.


Lock 에서의 상태 변경은 isLocked = true; 코드이다. 세마포어에서의 상태 변경은 signals--; 또는 signals++; 코드가 있다.


다음은 상태 변경을 보여주는 코드 일부이다. 상태 변경은 볼드 처리되었다.


public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    //state change
    isLocked = true;
  }

  public synchronized void unlock(){
    //state change
    isLocked = false;
    notify();
  }
}
public class BoundedSemaphore {
  private int signals = 0;
  private int bound   = 0;

  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  public synchronized void take() throws InterruptedException{
    while(this.signals == bound) wait();
    //state change
    this.signals++;
    this.notify();
  }

  public synchronized void release() throws InterruptedException{
    while(this.signals == 0) wait();
    //state change
    this.signals--;
    this.notify();
  }
}



통지 전략


쓰레드가 동기화 장치의 상태를 변경하면, 때로는 대기 상태에 있는 다른 쓰레드들에게 변경 사실을 알릴 필요가 있다. 아마 이 상태 변경은 다른 쓰레드들의 접근 조건을 true 로 만들어 접근이 가능하게끔 해줄 것이다.


일반적인 통지 전략은 세 가지로 분류된다.


  1. 모든 대기 쓰레드에게 통지
  2. N 개의 대기 쓰레드 중 임의의 1 개 쓰레드에게 통지
  3. N 개의 대기 쓰레드 중 지정된 1 개 쓰레드에게 통지


모든 대기 쓰레드에게 통지하기는 상당히 쉽다. 대기 쓰레드들은 동일한 객체의 wait() 메소드를 호출한다. 이 객체의 notifyAll() 을 호출함으로써 대기 쓰레드들을 모두 깨울 수 있다.


임의의 1 개 쓰레드를 깨우기 또한 어렵지 않다. 대기 쓰레드들이 wait() 을 호출한 객체의 notify() 를 호출하면 된다. notify() 메소드는 대기중인 쓰레드들 중 어떤 쓰레드를 깨울지에 대한 보장이 전혀 없기 때문에 '임의의 1 개 쓰레드' 라는 요건을 충족한다.


때때로 대기 쓰레드 중 임의의 쓰레드가 아닌, 특정한 쓰레드를 깨워야 할 필요가 있다. 가령, 대기 쓰레드들의 접근 획득에 있어 특정한 순서가 보장되어야 할 때이다. 대기 상태로 들어간 순서 또는 별도로 지정된 우선순위로 쓰레드들의 접근 획득 순서를 조정해야 하는 것이다. 이런 경우에는 대기 쓰레드는 자신만의 구분된 객체의 wait() 를 호출해야 한다. 그리고 쓰레를 깨울 때는 대상 쓰레드가 wait() 을 호출한 객체의 nofity() 를 호출하여 정확히 해당 쓰레드를 깨우도록 한다. 관련 예제는 기아상태와 공정성 에서 찾을 수 있다.


다음은 통지 전략 코드의 일부이다. 통지 코드(임의의 1 개 쓰레드에게 통지)는 볼드 처리되었다.


public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      //wait strategy - related to notification strategy
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify(); //notification strategy
  }
}



확인-설정 메소드


대부분의 경우 동기화 장치는 두 가지 유형의 메소드가 있다. 첫 번째 유형은 확인-설정 메소드이다(두 번째는 설정 메소드인데, 다음 섹션에서 다룬다.). 확확인-설정이란 이 메소드를 호출하는 쓰레드는 동기화 장치의 내부 상태가 접근 조건을 충족하는지 확인한다. 상태가 조건을 충족한다고 확인되면, 쓰레드는 동기화 장치의 내부 상태에 쓰레드가 접근을 획득했음을 나타내도록 설정한다.


주로 이 상태 설정은 다른 쓰레드들의 접근을 막기 위해 접근 조건이 false 가 되도록 한다. 간혹 다른 경우도 있는데, 예를 들자면 읽기/쓰기 락이 그렇다. 읽기/쓰기 락에서 읽기 접근 쓰레드는 락의 상태를 변경하여 자신의 접근을 표시하지만, 이것이 다른 쓰레드들의 접근을 막지는 않는다. 쓰기 접근이 없는 한 다른 쓰레드들의 읽기 접근도 허용된다.


확인-설정 메소드에서 반드시 지켜져야 할 점은 확인-설정 연산이 반드시 원자적으로 실행되어야 한다는 것이다. 확인-설정 메소드가 실행될 때 '확인' 과 '설정' 사이에 다른 쓰레드가 끼어들어서는 안된다.


확인-설정 메소드의 흐름은 주로 다음 순서를 따른다.


  1. 필요하다면 확인 전에 상태를 설정한다.
  2. 상태를 접근 조건에 대해 확인한다.
  3. 접근 조건을 충족하지 못한다면, 대기한다.
  4. 접근 조건을 충족한다면, 상태를 설정한다. 그리고 필요하다면 대기 쓰레드에게 통지한다.


ReadWriteLock 클래스의 lockWrite() 메소드가 확인-설정 메소드의 예시가 될 수 있다. lockWrite() 호출 쓰레드는 확인 전에 상태를 설정한다(writeRequest++). 다음으로 canGrantWriteAccess() 메소드에서 내부 상태를 접근 조건에 대해 확인한다. 조건을 충족한다면 내부 상태를 설정하고 메소드는 종료된다. 아래 예제에서는 대기 쓰레드에게 통지하는 부분은 없다.


public class ReadWriteLock{
    private Map<Thread, Integer> readingThreads =
        new HashMap<Thread, Integer>();

    private int writeAccesses    = 0;
    private int writeRequests    = 0;
    private Thread writingThread = null;

    ...

    
        public synchronized void lockWrite() throws InterruptedException{
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while(! canGrantWriteAccess(callingThread)){
        wait();
        }
        writeRequests--;
        writeAccesses++;
        writingThread = callingThread;
        }
    

    ...
}


아래 BoundedSemaphore 클래스에는 두 개의 확인-설정 메소드가 있다. take() 와 release() 가 그것이다. 이 두 메소드는 저마다 내부 상태를 확인하고 설정한다.


public class BoundedSemaphore {
  private int signals = 0;
  private int bound   = 0;

  public BoundedSemaphore(int upperBound){
    this.bound = upperBound;
  }

  
      public synchronized void take() throws InterruptedException{
      while(this.signals == bound) wait();
      this.signals++;
      this.notify();
      }

      public synchronized void release() throws InterruptedException{
      while(this.signals == 0) wait();
      this.signals--;
      this.notify();
      }
  
}



설정 메소드


설정 메소드는 동기화 장치에서 자주 등장하는 메소드의 두 번째 유형이다. 설정 메소드는 동기화 장치의 내부 상태를 설정하는데, 여기에 확인 작업은 없다. 설정 메소드의 전형적인 예제는 Lock 클래스의 unlock() 메소드이다. 락을 소유한 메소드는 Lock 이 해제되었는지 확인할 필요 없이, 언제나 락을 해제할 수 있다. 


설정 메소드의 흐름은 주로 다음 순서를 따른다.


  1. 내부 상태를 설정한다.
  2. 대기 쓰레드에게 이를 알린다.


다음은 unlock() 메소드 예제이다.


public class Lock{

  private boolean isLocked = false;
  
      public synchronized void unlock(){
      isLocked = false;
      notify();
      }
  
}


'Java > Concurrency' 카테고리의 다른 글

암달의 법칙(Amdahl's Law)  (1) 2017.06.17
논 블로킹 알고리즘(Non-blocking Algorithms)  (2) 2017.05.27
컴페어 스왑(Compare and Swap)  (0) 2017.05.24
쓰레드 풀(Thread Pools)  (0) 2017.05.22
블로킹 큐(Blocking Queues)  (3) 2017.05.22
이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/compare-and-swap.html



컴페어 스왑은 동시성 알고리즘을 설계할 때 사용되는 기술이다. 기본적으로 컴페어 스왑은 변수의 예상 값과 실제 값을 비교하여 둘의 값이 일치하면 실제 값을 새로운 값으로 교체한다. 약간 복잡하게 들릴 수 있지만 사실 한 번 이해하면 그리 어렵지 않다. 자세히 다가가보자.



컴페어 스왑은 어디에 쓰이는가


여러 프로그램들과 동시성 알고리즘에서 대단히 널리 발견되는 패턴은 '체크-액트' 이다. 체크-액트 패턴은 변수의 값을 확인하여 이 값을 전제로 다음 동작을 수행한다. 


class MyLock {

    private boolean locked = false;

    public boolean lock() {
        if(!locked) {
            locked = true;
            return true;
        }
        return false;
    }
}


실제 멀트쓰레드 어플리케이션에서 이 코드를 사용하려면 많은 문제가 있겠지만 여기서는 넘어가도록 하자.


코드에서 보이듯 lock() 메소드는 먼저 locked 멤버변수의 값을 확인하여 값이 false 라면(체크) 값을 locked 을 true 로 바꾸고 true 를 반환한다(액트).


다수의 쓰레드가 같은 MyLock 인스턴스에 접근한다면 lock() 메소드는 제대로 동작하지 않을 것이다. 쓰레드A 가 변수 locked 의 값이 false 인 것을 확인했다. 그리고 그와 동시와 쓰레드B 가 똑같이 변수 locked 의 값이 false 인 것을 확인했다. 따라서 쓰레드A 와 쓰레드B 모두 locked 를 false 로 확인했고, 두 쓰레드 모두 이 값을 전제로 다음 동작을 수행할 것이다.


멀티쓰레드 어플리케이션에서 '체크-액트' 연산이 제대로 수행되려면 연산의 원자성이 필요하다. 여기서의 원자성이란 '체크' 와 '액트' 의 동작이 분리되지 않은 하나의 원자적인 코드 블록으로 실행되어야 함을 의미한다. 원자적인 코드 블록을 실행하는 쓰레드는 코드 실행의 시작과 끝까지 다른 쓰레드의 간섭을 받지 않는다. 다른 쓰레드들은 원자적인 코드 블록의 실행에 개입할 수 없다(끼어들거나 동시에 실행할 수 없다).


다음 코드는 위 예제의 lock() 메소드에 synchronized 를 사용했다. 이제 lock() 메소드는 원자성을 갖는다.


class MyLock {

    private boolean locked = false;

    public synchronized boolean lock() {
        if(!locked) {
            locked = true;
            return true;
        }
        return false;
    }
}


이제 lock() 메소드는 동기화되었다. 하나의 MyLock 인스턴스의 lock() 메소드는 한 시점에 오직 한 쓰레드만이 실행할 수 있다. lock() 메소드는 사실상 원자적이다.


이 원자적인 lock() 메소드는 실제로 '컴페어 스왑' 의 한 예라고 볼 수 있다. lock() 메소드는 변수 locked 의 예상값 false 로 비교하여 실제 값이 false 이면 변수의 값을 true 로 바꾼다.



원자적 연산으로서의 컴페어 스왑


현대의 CPU는 원자적 컴페어 스왑 연산을 빌트인으로 지원한다. Java 5 부터 java.util.concurrent 패키지의 새로운 원자적 연산 클래스들을 통해 이러한 기능을 사용할 수 있다.


다음 코드는 AtomicBoolean 클래스를 사용하여 lock() 메소드를 구현하였다.


public static class MyLock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public boolean lock() {
        return locked.compareAndSet(false, true);
    }

}


변수 locked 의 타입은 더이상 boolean 이 아닌, AtomicBoolean 이다. 이 클래스는 AtomicBoolean 인스턴스의 값을 예상값과 비교하여 두 값이 일치하면 실제 값을 새 값으로 세팅하는 compareAndSet() 메소드를 가지고 있다. 예제에서는 AtomicBoolean locked 의 값을 false 로 비교하여 false 가 맞다면 AtomicBoolean locked 의 값을 새 값인 true 로 세팅한다.


compareAndSwap() 메소드는 값이 새 값으로 바뀌었으면 true 를, 바뀌지 않았으면 false 를 반환한다.


컴페어 스왑을 직접 구현하기보다는 Java 5 부터 추가된 컴페어 스왑 기능을 사용하는 것이 좋다. Java 5+ 에 내장된 컴페어 스왑 클래스들은 어플리케이션이 구동되는 CPU의 기본 컴페어 스왑 기능을 사용하기 때문에 보다 빠른 컴페어 스왑 연산이 가능하다.


이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/thread-pools.html




쓰레드 풀은 동시에 가동하는 쓰레드 수의 최대치에 제한을 둘 때 유용하다. 새 쓰레드를 생성하는 일은 성능에 있어 오버헤드가 따른다. 그리고 각 쓰레드에는 각각의 스택을 위한 메모리가 할당되어야 한다. 


작업을 실행할 때마다 새 쓰레드를 생성하여 시작하는 일 대신, 쓰레드 풀에 작업을 넘기는 방법으로 작업을 실행할 수 있다. 쓰레드 풀은 놀고 있는 쓰레드가 생기면 즉시 작업을 쓰레드에 할당하여 실행되도록 한다. 작업은 내부적으로 블로킹 큐에 저장되며, 쓰레드 풀의 쓰레드들은 여기서 작업을 빼서 실행한다. 큐에 새로운 작업이 들어오면 놀고 있는 쓰레드가 큐에서 작업을 빼서 실행한다. 이를 두고 쓰레드에 작업이 할당되었다고 하며, 작업이 할당되지 않은 쓰레드들은 큐에 새 작업이 들어올 때까지 대기 상태로 유지된다.


스레드 풀은 멀티쓰레드 서버에 주로 사용된다. 네트워크를 통해 서버로 도착하는 커넥션들은 각각 하나의 작업으로 포장되어 쓰레드 풀에 넘겨진다. 쓰레드들은 이렇게 넘겨받은 커넥션들의 요청을 동시에 처리한다. 이에 대해서는 나중에 자바로 멀티쓰레드 서버를 구현하는 방법에 대해 자세히 살펴볼 것이다. 


Java 5 는 java.util.concurrent 패키지에 쓰레드 풀을 포함한다. 이 쓰레드 풀 클래스에 대한 정보는 java.util.concurrent.ExecutorService(원문) 에서 읽어볼 수 있다. 쓰레드 풀을 직접 구현할 필요는 없지만, 여기서도 역시 쓰레드 풀 구현에 대해 자세히 알아보는 것은 도움이 되리라 본다.


아래는 간단한 쓰레드 풀 구현이다. 여기서의 BlockingQueue 블로킹 큐 튜토리얼에서 등장한 구현체를 사용했음을 알아두기 바란다. 실제로 쓰레드 풀을 구현할 때는 자바의 빌트인 블로킹 큐를 사용하도록 한다.


public class ThreadPool {

    private BlockingQueue taskQueue = null;
    private List<PoolThread> threads = new ArrayList<PoolThread>();
    private boolean isStopped = false;

    public ThreadPool(int noOfThreads, int maxNoOfTasks){
        taskQueue = new BlockingQueue(maxNoOfTasks);

        for(int i=0; i<noOfThreads; i++){
            threads.add(new PoolThread(taskQueue));
        }
        for(PoolThread thread : threads){
            thread.start();
        }
    }

    public synchronized void  execute(Runnable task) throws Exception{
        if(this.isStopped) throw
            new IllegalStateException("ThreadPool is stopped");

        this.taskQueue.enqueue(task);
    }

    public synchronized void stop(){
        this.isStopped = true;
        for(PoolThread thread : threads){
           thread.doStop();
        }
    }

}
public class PoolThread extends Thread {

    private BlockingQueue taskQueue = null;
    private boolean       isStopped = false;

    public PoolThread(BlockingQueue queue){
        taskQueue = queue;
    }

    public void run(){
        while(!isStopped()){
            try{
                Runnable runnable = (Runnable) taskQueue.dequeue();
                runnable.run();
            } catch(Exception e){
                //log or otherwise report exception,
                //but keep pool thread alive.
            }
        }
    }

    public synchronized void doStop(){
        isStopped = true;
        this.interrupt(); //break pool thread out of dequeue() call.
    }

    public synchronized boolean isStopped(){
        return isStopped;
    }
}


쓰레드 풀 구현은 두 파트로 나뉜다. ThreadPool 클래스는 쓰레드 풀 사용을 위한 공용 인터페이스이다. PoolThread 클래스는 작업을 실행하기 위한 쓰레드를 구현한다.


작업을 실행하기 위해서는 ThreadPool.execute(Runnable r) 이 호출된다. 이 메소드는 Runnable 구현체를 파라미터로 받는다. 이 Runnable 은 내부의 블로킹 큐에 삽입되고, 작업 쓰레드가 가져가기를 기다린다.


Runnable 은 작업이 할당되지 않는 PoolThread 에 의해 큐에서 빠져나와 실행된다. 이 과정은 PoolThread.run() 메소드에서 볼 수 있다. 실행이 완료되면 PoolThread 는 루프를 돌며 큐에 실행할 작업이 있는지 계속 확인한다. 이 루프는 사용자에 의해 쓰레드 풀이 정지될 때까지 지속된다.


ThreadPool 을 정지하기 위해서는 ThreadPool.stop() 이 호출된다. 이 메소드는 풀 내부의 isStopped 변수에 쓰레드 풀이 정지되었음을 표시한다. 그리고 작업 쓰레드 각각에 doStop() 을 호출하여 더이상 새 작업을 실행하지 않도록 한다. 풀이 정지된 다음 execute() 메소드가 호출되면 IllegalStateException 을 던져 작업이 실행될 수 없음을 알린다.


작업 쓰레드들은 자신이 현재 실행중인 작업이 완료된 다음 정지된다. PoolThread.doStop() 메소드를 보면 this.interrupt() 호출이 있다. 이 호출은 taskQueue.dequeue() 호출 대기 상태로 들어간 쓰레드의 대기 상태를 풀어버리고 InterruptedException 과 함께 dequeue() 호출을 빠져나가도록 한다. 이 예외는 PoolThread.run() 에서 캐치된다. 그리고 while 루프의 조건문에서 isStopped 의 값을 확인한다. 쓰레드 풀이 정지되어 isStopped 는 이제 true 이다. PoolThread.run() 은 종료되고 쓰레드는 죽게 된다.




'Java > Concurrency' 카테고리의 다른 글

동기화 장치 해부(Anatomy of a Synchronizer)  (0) 2017.05.25
컴페어 스왑(Compare and Swap)  (0) 2017.05.24
블로킹 큐(Blocking Queues)  (3) 2017.05.22
세마포어(Semophores)  (0) 2017.05.22
재진입 락아웃(Reentrance Lockout)  (0) 2017.05.21
이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/blocking-queues.html



블로킹 큐란 특정 상황에 쓰레드를 대기하도록 하는 큐이다. 큐에서 엘레멘트를 빼려고 시도했는데(디큐) 큐가 비어있다거나, 큐에 엘레멘트를 넣으려 했는데(인큐) 큐에 넣을 수 있는 공간이 없다거나 할 때 디큐/인큐 호출 쓰레드를 대기하도록 한다. 비어있는 큐에서 엘레멘트를 빼려고 시도하는 쓰레드의 대기 상태는 다른 쓰레드가 이 큐에 엘레멘트를 넣을 때까지 지속된다. 꽉 찬 큐에 엘레멘트를 넣으려 시도하는 쓰레드의 대기 상태는 다른 쓰레드가 큐에서 엘레멘트를 빼거나 큐를 정리하여(clean) 큐의 공간이 확보될 때까지 지속된다. 


아래는 한 블로킹 큐를 둔 두 쓰레드의 상호 동작을 보여준다.


Java 5 는 java.util.concurrent 패키지에 블로킹 큐를 포함한다. 이 블로킹 큐 클래스에 대한 내용은 java.util.concurrent.BlockingQueue(원문) 튜토리얼에서 볼 수 있다. Java 5 가 블로킹 큐 구현체를 제공하긴 하지만, 역시 그 기반 이론을 아는 일은 가치가 있다.



블로킹 큐 구현


블로킹 큐의 구현은 바운디드 세마포어와 유사하다. 간단한 블로킹 큐 구현을 보자.


public class BlockingQueue {

  private List queue = new LinkedList();
  private int  limit = 10;

  public BlockingQueue(int limit){
    this.limit = limit;
  }


  public synchronized void enqueue(Object item)
  throws InterruptedException  {
    while(this.queue.size() == this.limit) {
      wait();
    }
    if(this.queue.size() == 0) {
      notifyAll();
    }
    this.queue.add(item);
  }


  public synchronized Object dequeue()
  throws InterruptedException{
    while(this.queue.size() == 0){
      wait();
    }
    if(this.queue.size() == this.limit){
      notifyAll();
    }

    return this.queue.remove(0);
  }

}
    


큐 크기가 크기 제한에 다다르면 enqueue() 와 dequeue() 에서 notifyAll() 이 호출된다. 큐 크기가 제한에 다다르지 않은 상태로 enqueue() 나 dequeue() 가 호출되면 쓰레드는 대기하지 않는다.

'Java > Concurrency' 카테고리의 다른 글

컴페어 스왑(Compare and Swap)  (0) 2017.05.24
쓰레드 풀(Thread Pools)  (0) 2017.05.22
세마포어(Semophores)  (0) 2017.05.22
재진입 락아웃(Reentrance Lockout)  (0) 2017.05.21
읽기/쓰기 락(Read/Write Locks in Java)  (0) 2017.05.16

+ Recent posts