동시성 문제
- 여러 프로세스나 스레드가 동일한 데이터를 조작하는 경우 필연적으로 발생한다.
- 문제의 원인을 미리 알기 어렵다.
- 테스트하기도 어렵다.
오프라인 동시성
- 여러 데이터베이스 트랜잭션에 걸쳐 조작되는 데이터에 대한 동시성 제어
손실된 업데이트
- 같은 파일을 작업하는 경우 뒤에 파일을 업데이트한 사람의 내용으로 덮어쓰기가 되어 버리는 경우
일관성 없는 읽기
- 올바르지만 한편으로 올바르지 않은 정보의 두 조각을 읽은 경우 발생한다.
- 동시성 문제는 정확성 충족만으로는 충분하지 않다.
- 동시 작업이 얼마나 많이 진행될 수 있는지를 나타내는 활동성 또한 충족해야 한다.
실행 컨텍스트
- 외부 세계와 상호작용하는 관점에서 중요한 두 가지 컨텍스트로 요청과 세션이 있다.
- 운영체제와 관련된 두 가지 중요한 용어는 프로세스와 스레드이다.
- 프로세스 : 사용하는 내부 데이터에 대한 다단계 격리를 제공하는 대규모 실행 컨텍스트이다.
- 스레드 : 한 프로세스 내에서 여러 스레드로 작동할 수 있게 구성된 소규모 활성 에이전트이다.
- 이론상으로는 각 세션은 전체 수명 동안 한 프로세스와 독점적 관계를 맺는다.
- 프로세스는 서로 확실하게 격리되므로 동시성 충돌을 줄이는데 유리하다.
- 그러나 실제로 이렇게 작동하는 서버 툴은 없다. 프로세스를 시작하려면 많은 자원이 필요하기 때문이다.
- 그렇지만, 한 프로세스로 한 번에 한 요청만 처리하게 하는 시스템은 흔하고 이렇게 해도 동시성 문제를 예방할 수 있다.
격리와 불변성
- 격리와 불변성이라는 해결책이 중요하다.
- 격리를 통해 데이터를 분리해서 하나의 활성 에이전트만 데이터에 접근할 수 있게 해야 한다.
- 훌륭한 동시성 설계란 격리 환경을 만드는 방법을 찾고 이 환경 내에서 최대한 많은 프로그래밍 작업을 수행하는 것이다.
- 동시성 문제는 공유하는 데이터가 수정될 수 있을 때만 발생한다.
- 따라서 변경 불가능한 데이터를 인식할 수 있으면 동시성 충동을 예방할 수 있다.
- 하지만 모든 데이터를 변경 불가능하게 만들 수는 없다.
- 일부 데이터를 변경 불가능하게 만들거나 최소한 거의 항상 불가능하게 만들면 동시성 문제에 대한 우려가 줄고 데이터를 광범위하게 공유할 수 있다.
- 다른 방법으로는 애플리케이션에서 데이터를 읽기만 하는 부분을 분리하고 이곳에서 데이터의 복사본을 사용하게 해서 모든 동시성 제어로부터 자유롭게 작업하는 것이다.
낙관적 동시성 제어와 비관적 동시성 제어
- 그러면 격리할 수 없는 변경 가능한 데이터는 어떻게 해야 할까?
낙관점 잠금
- 두 사용자 모두 자유롭게 파일을 복사하고 편집할 수 있다.
- 변경 내용에서 충돌을 발생시키고 어떻게 해결할지 결정하게 만든다.
- 충돌 감지
비관적 잠금
- 먼저 파일을 체크아웃하면 다른 사용자가 파일을 편집할 수 없게 된다.
- 충돌 예방
- 단점 : 동시성 제한, 활동성 저하, 교착 상태 발생
- 교착 상태를 해결하려면 교착 상태가 발생하면 감지하는 소프트웨어를 사용하는 것이다.
- 희생자 한 명을 선택해 해당 사용자의 작업과 잠금을 포기한다.
- 교착 상태는 감지가 어렵고 희생자에게 큰 피해를 준다.
- 모든 잠금에 시간 제한을 두는 방식도 있다.
- 교착 상태 예방 방법 기법으로는 작업을 시작할 때 필요한 잠금을 모두 얻게 하고 추가 잠금을 얻지 못하게 하는 것이다.
- 잠금을 얻는 순서에 대한 규칙을 지정할 수 있다. 예) 알파벳순으로 파일에 대한 잠금을 얻게 한다.
- 또한 누군가 가지고 있는 잠금을 얻으려고 하면 자동으로 얻으려는 사람이 희생자가 되는 방법도 있다.
트랜잭션
- 동시성을 처리하는 가장 중요한 툴이다.
- 시작점과 끝점이 명확하게 정의된 일련의 작업이다.
- 전체를 실행하거나 실행하지 않는 양자택일 방식으로 수행해야 한다.
- ACID 속성을 가진다
- 원자성 : 수행되는 각 작업의 단계는 모두 성공이거나 롤백돼야 한다.
- 일관성 : 시스템의 자원은 트랜잭션의 시작과 완료 시점에 모두 일관성 있고 손상되지 않은 상태여야 한다.
- 격리성 : 트랜잭션의 결과는 트랜잭션이 성공적으로 커밋하기 전까지 열려 있는 다른 트랜잭션에서 볼 수 없어야 한다.
- 지속성 : 커밋된 트랜잭션의 결과는 영구적이어야 한다.
- 여러 요청에 걸치는 긴 트랜잭션은 만들지 말아야 한다.
- 요청이 시작될 때 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션을 끝내는 방법이 일반적이다.
- 다른 방법은 트랜잭션을 최대한 늦게 여는 것이다. (지연 트랜잭션)
- 트랜잭션을 열고 처음 쓰기를 수행할 때까지 긴 시간 지연이 발생하는 경우 이 기법으로 활동성을 개선할 수 있다.
- 그러나 트랜잭션을 시작하기 전까지는 동시성 제어가 안된다.
활동성을 위한 트랜잭션 격리성 저하
직렬화 가능 트랜잭션
- 완전한 격리가 가능한 경우 트랜잭션을 직렬화 가능
- 여러 트랜잭션을 동시에 실행해도 순서대로 실행했을 떼와 같은 결과를 얻을 수 있다.
- SQL 표준에서 제공하는 가장 강력한 트랜잭션 시스템이다. 그 아래 수준에서는 각각 특정한 종류의 일관성 없는 읽기가 발생할 수 있다.
직렬화 가능 아래의 첫 번째 격리 수준 : 팬텀이 허용되는 반복 가능 읽기
- 팬텀이란 컬렉션에 여러 요소를 추가했을 때 읽는 쪽에서 일부 요소만 볼 수 있는 현상이다.
그 아래 격리 수준 : 반복 불가능 읽기가 허용되는 커밋된 읽기
-
가장 낮은 격리 수준 : 더티 읽기가 허용되는 커밋되지 않은 읽기
- 커밋되지 않은 읽기에서는 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있다.
비지니스 트랜잭션과 시스템 트랜잭션
- 시스템 트랜잭션은 비즈니스 시스템의 사용자에게는 전혀 의미가 없다.
- 비즈니스 트랜잭션의 예)
- 은행 시스템의 사용자에게 트랜잭션은 로그인, 계정 선택, 이체 내역 설정, 그리고 최종적으로 사용자가 확인 버튼을 눌러 금액을 이체하는 과정을 포함한다.
- ACID 속성을 제공해야 한다.
- 사용자가 금액을 이체하기 전에 취소하면 이전 화면에서 수행한 모든 변경은 취소돼야 한다.
- 이체 내역을 설정하는 과정 자체는 확인 버튼을 누르기 전까지 계좌의 잔액에 영향을 미치지 않아야 한다.
- ACID 속성을 지원하는 확실한 방법은 비즈니스 트랜잭션을 한 시스템 트랜잭션 안에서 실행하는 것이다.
- 하지만 비즈니스 트랜잭션을 사용하면 긴 시스템 트랜잭션이 되고 잘 처리되지 않을 수 있다.
- 이럴땐 데이터베이스의 동시성 요건이 중간 정도라면 긴 트랜잭션을 사용을 권장한다.
- 긴 트랜잭션을 사용하면 데이터베이스가 주요 병목 구간이 되기 때문에 애플리케이션 확장성에 제약이 생긴다.
- 따라서 여러 엔터프라이즈 애플리케이션에서는 긴 트랜잭션을 짧은 트랜잭션으로 분리한다.
- 여러 시스템 트랜잭션 사이에서 비즈니스 트랜잭션의 ACID 특성을 스스로 처리해야한다.
- 오프라인 동시성 문제도 해결해야 한다.
- 비즈니스 트랜잭션이 트랜잭션 리소스(데이터베이스)와 상호작용할 때마다 해당 리소스의 무결성을 유지하기 위해 시스템 트랜잭션 안에서 이 상호작용을 수행해야 한다.
- 원자성, 지속성이 가장 쉽게 지원할 수 있다.
- 사용자가 저장을 선택할 때 비즈니스 트랜잭션의 커밋 단계를 하나의 시스템 트랜잭션 안에서 실행하는 방법으로 지원이 가능하다.
- 격리 속성이 적용하기 가장 까다롭다.
- 격리의 실패는 일관성의 실패로 이어진다.
- 애플리케이션이 단일 트랜잭션 안에서 일관성을 유지하기 위해 해야 하는 역할은 모든 사용 가능한 비즈니스 규칙을 적용하는 것이다.
- 그리고 여러 트랜잭션에 걸쳐 일관성을 유지하기 위해 해야 하는 역할은 세션이 다른 세션의 변경 내용을 망치고 레코드 집합이 잘못된 상태가 되어 사용자의 작업이 손실되지 않도록 방지하는 것이다.
- 일관성 없는 읽기라는 문제도 있다.
- 여러 시스템 트랜잭션에서 데이터를 읽은 경우 데이터가 일관되게 유지된다는 보장이 없다.
- 다른 읽기 결과는 메모리 내 데이터에서 일관성 문제를 일으켜 애플리케이션 실패가 일어날 수도 있다.
- 비즈니스 트랜잭션은 세션과 밀접한 연관이 있다.
- 사용자 관점에서 각 세션은 일련의 비즈니스 트랜잭션이므로 일반적으로 모든 비즈니스 트랜잭션은 단일 클라이언트 세션에서 실행된다고 간주한다.
오프라인 동시성 제어를 위한 패턴
- 가능하면 트랜잭션 시스템이 최대한 동시성 문제를 맡아서 처리하게 해야 한다.
- 비즈니스와 시스템 트랜잭션의 불일치는 종종 생긴다. 이를 처리하는 패턴은 다음과 같다.
1. 낙관적 오프라인 잠금
- 여러 비즈니스 트랜잭션에 걸쳐 낙관적 동시성 제어를 사용한다.
- 쉽고 활동성이 가장 높다.
- 하지만 비즈니스 트랜잭션이 실패한 경우를 커밋할 때 돼서야 알 수 있다.
- 이 늦은 발견이 낙관적 오프라인 잠금의 한계이다.
2. 비관적 오프라인 잠금
- 문제가 있으면 조기에 알 수 있다.
- 프로그래밍이 어렵고 활동성이 제한된다.
- 이러한 두 접근 방식을 사용할 때 모든 객체의 잠금을 직접 관리하지 않으면 복잡도를 크게 낮출 수 있다.
- 굵은 입자 잠금을 사용하면 객체 그룹의 동시성을 함께 관리할 수 있다.
- 암시적 잠금을 사용하면 애플리케이션 개발자의 부담을 크게 덜 수 있다.
- 비관적 오프라인 잠금과 굵은 입자 잠금을 사용하려면 도메인에 대한 정보를 충분히 얻어야 한다.
애플리케이션 서버 동시성
- 동시성의 다른 형태로 애플리케이션 서버 자체의 프로세스 동시성이 있다.
- 서버가 여러 동시 요청을 어떻게 처리하며, 애플리케이션 설계에 어떤 영향을 미치는지 고려한다.
- 잠금과 동기화 블록을 사용해 명시적 다중 스레드 프로그래밍을 구현하기란 매우 어렵다.
- 찾아내기 아주 어려운 결함이 쉽게 발생하며, 동시성 버그는 재현하기가 거의 불가능하다.
- 따라서 사용을 가급적 최소화해야 한다.
- 세션별 프로세스 : 가장 간단한 방법으로 각 세션이 각자의 프로세스에서 실행된다.
- 장점 : 각 프로세스의 상태가 다른 프로세스로부터 완전히 격리된다. 따라서 애플리케이션 프로그래머가 다중 스레드에 대해 신경 쓸 필요가 전혀 없다.
- 메모리 격리 관점에서는 각 요청별 새로운 프로세스를 시작하는 방법과 한 프로세스를 요청 간에 유휴 상태로 유지되는 세션에 연결하는 방법이 거의 동일한 효과가 있다.
- 단점 : 프로세스가 리소스를 너무 많이 소모한다.
- 그래서 다른 세션의 여러 요청을 순차적으로 처리할 수 있도록 프로세스의 풀을 만들어 효율을 높이는 방법이 나왔다. (요청별 프로세스)
- 요청별 프로세스를 이용하면 일정 수의 세션을 처리하는 데 훨씬 적은 수가 사용된다.
- 격리도 거의 같은 수준으로 유지되어 다중 스레드 문제가 많이 발생하지 않는다.
- 다만 요청별 프로세스의 주의점은 사용된 모든 리소스를 반환해야 한다는 것이다.
- 아파치 mod-perl을 비롯해 여러 대규모 트랜잭션 처리 시스템에서 이 체계를 적용하고 있다.
- 요청별 프로세스 방식도 일정 수준의 부하를 처리하려면 많은 프로세스를 실행해야 한다.
- 이런 경우 한 프로세스가 여러 스레드를 실행하게 하면 처리량을 개선할 수 있다. (요청별 스레드)
- 요청별 스레드 방식에서 각 요청은 프로세스 내의 한 스레드에 의해 처리된다.
- 스레드는 프로세스보다 리소스를 적게 사용하면서 많은 요청을 처리해 서버의 효율을 높일 수 있다.
- 다만, 문제는 스레드가 서로 격리되지 않기 때문에 어떤 스레드든지 접근 가능한 데이터를 변경할 수 있다.
- 가장 권하는 방식은 요청별 프로세스다.
- 효율은 낮지만 확장성이 충분하고 견고하다.
- 한 스레드가 잘 못 되면 전체 프로세스가 잘 못 될 수 있다.
- 요청별 스레드를 사용할 때 격리된 지역을 만들고 이를 활용해야 한다.
- 스레드가 요청을 처리하기 시작할 때 다른 스레드가 볼 수 없는 곳에 새로운 객체를 만들고 이를 활용하는 게 일반적인 방법이다.
- 객체 생성 프로세스에 비용이 많이 든다는 걱정은 객체를 풀링하는 방법으로 해결했다.
- 하지만 문제는 풀링된 객체에 대한 접근을 어떤 방식으로든 동기화해야 한다는 것이다.
- 객체 생성 비용은 사용하는 가상 시스템과 메모리 관리 전략에 따라 크게 바뀐다.
- 각 세션마다 새로운 객체를 만들면 많은 동시성 버그를 예방하고 확장성을 개선할 수 있다.
- 이 전술에서 피해야 하는 영역 : 정적 클래스 기반 변수나 전역 변수.
- 이러한 변수에 대한 접근은 모두 동기화해야 한다.
- 싱글턴의 경우도 마찬가지다.
- 전역 메모리와 비슷한 기능이 필요하면 레지스트리를 사용한다.
- 레지스트리는 정적 변수와 비슷하지만 스레드별 저장소를 사용하도록 되어 있다.
'Study > 엔터프라이즈 애플리케이션 아키텍처 패턴' 카테고리의 다른 글
제12장 객체-관계형 구조 패턴 : 연관 테이블 매핑 (0) | 2023.03.28 |
---|---|
09장 - 도메인 논리 패턴 (0) | 2023.02.20 |
03장 - 관계형 데이터베이스 매핑 02 - 데이터 읽기 / 구조적 매핑 패턴 (0) | 2023.02.08 |
03장 - 관계형 데이터베이스 매핑 01 - 아키텍처 패턴 / 동작 문제 (0) | 2023.02.07 |
02장 - 도메인 논리 구성 (0) | 2023.02.07 |
동시성 문제
- 여러 프로세스나 스레드가 동일한 데이터를 조작하는 경우 필연적으로 발생한다.
- 문제의 원인을 미리 알기 어렵다.
- 테스트하기도 어렵다.
오프라인 동시성
- 여러 데이터베이스 트랜잭션에 걸쳐 조작되는 데이터에 대한 동시성 제어
손실된 업데이트
- 같은 파일을 작업하는 경우 뒤에 파일을 업데이트한 사람의 내용으로 덮어쓰기가 되어 버리는 경우
일관성 없는 읽기
- 올바르지만 한편으로 올바르지 않은 정보의 두 조각을 읽은 경우 발생한다.
- 동시성 문제는 정확성 충족만으로는 충분하지 않다.
- 동시 작업이 얼마나 많이 진행될 수 있는지를 나타내는 활동성 또한 충족해야 한다.
실행 컨텍스트
- 외부 세계와 상호작용하는 관점에서 중요한 두 가지 컨텍스트로 요청과 세션이 있다.
- 운영체제와 관련된 두 가지 중요한 용어는 프로세스와 스레드이다.
- 프로세스 : 사용하는 내부 데이터에 대한 다단계 격리를 제공하는 대규모 실행 컨텍스트이다.
- 스레드 : 한 프로세스 내에서 여러 스레드로 작동할 수 있게 구성된 소규모 활성 에이전트이다.
- 이론상으로는 각 세션은 전체 수명 동안 한 프로세스와 독점적 관계를 맺는다.
- 프로세스는 서로 확실하게 격리되므로 동시성 충돌을 줄이는데 유리하다.
- 그러나 실제로 이렇게 작동하는 서버 툴은 없다. 프로세스를 시작하려면 많은 자원이 필요하기 때문이다.
- 그렇지만, 한 프로세스로 한 번에 한 요청만 처리하게 하는 시스템은 흔하고 이렇게 해도 동시성 문제를 예방할 수 있다.
격리와 불변성
- 격리와 불변성이라는 해결책이 중요하다.
- 격리를 통해 데이터를 분리해서 하나의 활성 에이전트만 데이터에 접근할 수 있게 해야 한다.
- 훌륭한 동시성 설계란 격리 환경을 만드는 방법을 찾고 이 환경 내에서 최대한 많은 프로그래밍 작업을 수행하는 것이다.
- 동시성 문제는 공유하는 데이터가 수정될 수 있을 때만 발생한다.
- 따라서 변경 불가능한 데이터를 인식할 수 있으면 동시성 충동을 예방할 수 있다.
- 하지만 모든 데이터를 변경 불가능하게 만들 수는 없다.
- 일부 데이터를 변경 불가능하게 만들거나 최소한 거의 항상 불가능하게 만들면 동시성 문제에 대한 우려가 줄고 데이터를 광범위하게 공유할 수 있다.
- 다른 방법으로는 애플리케이션에서 데이터를 읽기만 하는 부분을 분리하고 이곳에서 데이터의 복사본을 사용하게 해서 모든 동시성 제어로부터 자유롭게 작업하는 것이다.
낙관적 동시성 제어와 비관적 동시성 제어
- 그러면 격리할 수 없는 변경 가능한 데이터는 어떻게 해야 할까?
낙관점 잠금
- 두 사용자 모두 자유롭게 파일을 복사하고 편집할 수 있다.
- 변경 내용에서 충돌을 발생시키고 어떻게 해결할지 결정하게 만든다.
- 충돌 감지
비관적 잠금
- 먼저 파일을 체크아웃하면 다른 사용자가 파일을 편집할 수 없게 된다.
- 충돌 예방
- 단점 : 동시성 제한, 활동성 저하, 교착 상태 발생
- 교착 상태를 해결하려면 교착 상태가 발생하면 감지하는 소프트웨어를 사용하는 것이다.
- 희생자 한 명을 선택해 해당 사용자의 작업과 잠금을 포기한다.
- 교착 상태는 감지가 어렵고 희생자에게 큰 피해를 준다.
- 모든 잠금에 시간 제한을 두는 방식도 있다.
- 교착 상태 예방 방법 기법으로는 작업을 시작할 때 필요한 잠금을 모두 얻게 하고 추가 잠금을 얻지 못하게 하는 것이다.
- 잠금을 얻는 순서에 대한 규칙을 지정할 수 있다. 예) 알파벳순으로 파일에 대한 잠금을 얻게 한다.
- 또한 누군가 가지고 있는 잠금을 얻으려고 하면 자동으로 얻으려는 사람이 희생자가 되는 방법도 있다.
트랜잭션
- 동시성을 처리하는 가장 중요한 툴이다.
- 시작점과 끝점이 명확하게 정의된 일련의 작업이다.
- 전체를 실행하거나 실행하지 않는 양자택일 방식으로 수행해야 한다.
- ACID 속성을 가진다
- 원자성 : 수행되는 각 작업의 단계는 모두 성공이거나 롤백돼야 한다.
- 일관성 : 시스템의 자원은 트랜잭션의 시작과 완료 시점에 모두 일관성 있고 손상되지 않은 상태여야 한다.
- 격리성 : 트랜잭션의 결과는 트랜잭션이 성공적으로 커밋하기 전까지 열려 있는 다른 트랜잭션에서 볼 수 없어야 한다.
- 지속성 : 커밋된 트랜잭션의 결과는 영구적이어야 한다.
- 여러 요청에 걸치는 긴 트랜잭션은 만들지 말아야 한다.
- 요청이 시작될 때 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션을 끝내는 방법이 일반적이다.
- 다른 방법은 트랜잭션을 최대한 늦게 여는 것이다. (지연 트랜잭션)
- 트랜잭션을 열고 처음 쓰기를 수행할 때까지 긴 시간 지연이 발생하는 경우 이 기법으로 활동성을 개선할 수 있다.
- 그러나 트랜잭션을 시작하기 전까지는 동시성 제어가 안된다.
활동성을 위한 트랜잭션 격리성 저하
직렬화 가능 트랜잭션
- 완전한 격리가 가능한 경우 트랜잭션을 직렬화 가능
- 여러 트랜잭션을 동시에 실행해도 순서대로 실행했을 떼와 같은 결과를 얻을 수 있다.
- SQL 표준에서 제공하는 가장 강력한 트랜잭션 시스템이다. 그 아래 수준에서는 각각 특정한 종류의 일관성 없는 읽기가 발생할 수 있다.
직렬화 가능 아래의 첫 번째 격리 수준 : 팬텀이 허용되는 반복 가능 읽기
- 팬텀이란 컬렉션에 여러 요소를 추가했을 때 읽는 쪽에서 일부 요소만 볼 수 있는 현상이다.
그 아래 격리 수준 : 반복 불가능 읽기가 허용되는 커밋된 읽기
-
가장 낮은 격리 수준 : 더티 읽기가 허용되는 커밋되지 않은 읽기
- 커밋되지 않은 읽기에서는 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있다.
비지니스 트랜잭션과 시스템 트랜잭션
- 시스템 트랜잭션은 비즈니스 시스템의 사용자에게는 전혀 의미가 없다.
- 비즈니스 트랜잭션의 예)
- 은행 시스템의 사용자에게 트랜잭션은 로그인, 계정 선택, 이체 내역 설정, 그리고 최종적으로 사용자가 확인 버튼을 눌러 금액을 이체하는 과정을 포함한다.
- ACID 속성을 제공해야 한다.
- 사용자가 금액을 이체하기 전에 취소하면 이전 화면에서 수행한 모든 변경은 취소돼야 한다.
- 이체 내역을 설정하는 과정 자체는 확인 버튼을 누르기 전까지 계좌의 잔액에 영향을 미치지 않아야 한다.
- ACID 속성을 지원하는 확실한 방법은 비즈니스 트랜잭션을 한 시스템 트랜잭션 안에서 실행하는 것이다.
- 하지만 비즈니스 트랜잭션을 사용하면 긴 시스템 트랜잭션이 되고 잘 처리되지 않을 수 있다.
- 이럴땐 데이터베이스의 동시성 요건이 중간 정도라면 긴 트랜잭션을 사용을 권장한다.
- 긴 트랜잭션을 사용하면 데이터베이스가 주요 병목 구간이 되기 때문에 애플리케이션 확장성에 제약이 생긴다.
- 따라서 여러 엔터프라이즈 애플리케이션에서는 긴 트랜잭션을 짧은 트랜잭션으로 분리한다.
- 여러 시스템 트랜잭션 사이에서 비즈니스 트랜잭션의 ACID 특성을 스스로 처리해야한다.
- 오프라인 동시성 문제도 해결해야 한다.
- 비즈니스 트랜잭션이 트랜잭션 리소스(데이터베이스)와 상호작용할 때마다 해당 리소스의 무결성을 유지하기 위해 시스템 트랜잭션 안에서 이 상호작용을 수행해야 한다.
- 원자성, 지속성이 가장 쉽게 지원할 수 있다.
- 사용자가 저장을 선택할 때 비즈니스 트랜잭션의 커밋 단계를 하나의 시스템 트랜잭션 안에서 실행하는 방법으로 지원이 가능하다.
- 격리 속성이 적용하기 가장 까다롭다.
- 격리의 실패는 일관성의 실패로 이어진다.
- 애플리케이션이 단일 트랜잭션 안에서 일관성을 유지하기 위해 해야 하는 역할은 모든 사용 가능한 비즈니스 규칙을 적용하는 것이다.
- 그리고 여러 트랜잭션에 걸쳐 일관성을 유지하기 위해 해야 하는 역할은 세션이 다른 세션의 변경 내용을 망치고 레코드 집합이 잘못된 상태가 되어 사용자의 작업이 손실되지 않도록 방지하는 것이다.
- 일관성 없는 읽기라는 문제도 있다.
- 여러 시스템 트랜잭션에서 데이터를 읽은 경우 데이터가 일관되게 유지된다는 보장이 없다.
- 다른 읽기 결과는 메모리 내 데이터에서 일관성 문제를 일으켜 애플리케이션 실패가 일어날 수도 있다.
- 비즈니스 트랜잭션은 세션과 밀접한 연관이 있다.
- 사용자 관점에서 각 세션은 일련의 비즈니스 트랜잭션이므로 일반적으로 모든 비즈니스 트랜잭션은 단일 클라이언트 세션에서 실행된다고 간주한다.
오프라인 동시성 제어를 위한 패턴
- 가능하면 트랜잭션 시스템이 최대한 동시성 문제를 맡아서 처리하게 해야 한다.
- 비즈니스와 시스템 트랜잭션의 불일치는 종종 생긴다. 이를 처리하는 패턴은 다음과 같다.
1. 낙관적 오프라인 잠금
- 여러 비즈니스 트랜잭션에 걸쳐 낙관적 동시성 제어를 사용한다.
- 쉽고 활동성이 가장 높다.
- 하지만 비즈니스 트랜잭션이 실패한 경우를 커밋할 때 돼서야 알 수 있다.
- 이 늦은 발견이 낙관적 오프라인 잠금의 한계이다.
2. 비관적 오프라인 잠금
- 문제가 있으면 조기에 알 수 있다.
- 프로그래밍이 어렵고 활동성이 제한된다.
- 이러한 두 접근 방식을 사용할 때 모든 객체의 잠금을 직접 관리하지 않으면 복잡도를 크게 낮출 수 있다.
- 굵은 입자 잠금을 사용하면 객체 그룹의 동시성을 함께 관리할 수 있다.
- 암시적 잠금을 사용하면 애플리케이션 개발자의 부담을 크게 덜 수 있다.
- 비관적 오프라인 잠금과 굵은 입자 잠금을 사용하려면 도메인에 대한 정보를 충분히 얻어야 한다.
애플리케이션 서버 동시성
- 동시성의 다른 형태로 애플리케이션 서버 자체의 프로세스 동시성이 있다.
- 서버가 여러 동시 요청을 어떻게 처리하며, 애플리케이션 설계에 어떤 영향을 미치는지 고려한다.
- 잠금과 동기화 블록을 사용해 명시적 다중 스레드 프로그래밍을 구현하기란 매우 어렵다.
- 찾아내기 아주 어려운 결함이 쉽게 발생하며, 동시성 버그는 재현하기가 거의 불가능하다.
- 따라서 사용을 가급적 최소화해야 한다.
- 세션별 프로세스 : 가장 간단한 방법으로 각 세션이 각자의 프로세스에서 실행된다.
- 장점 : 각 프로세스의 상태가 다른 프로세스로부터 완전히 격리된다. 따라서 애플리케이션 프로그래머가 다중 스레드에 대해 신경 쓸 필요가 전혀 없다.
- 메모리 격리 관점에서는 각 요청별 새로운 프로세스를 시작하는 방법과 한 프로세스를 요청 간에 유휴 상태로 유지되는 세션에 연결하는 방법이 거의 동일한 효과가 있다.
- 단점 : 프로세스가 리소스를 너무 많이 소모한다.
- 그래서 다른 세션의 여러 요청을 순차적으로 처리할 수 있도록 프로세스의 풀을 만들어 효율을 높이는 방법이 나왔다. (요청별 프로세스)
- 요청별 프로세스를 이용하면 일정 수의 세션을 처리하는 데 훨씬 적은 수가 사용된다.
- 격리도 거의 같은 수준으로 유지되어 다중 스레드 문제가 많이 발생하지 않는다.
- 다만 요청별 프로세스의 주의점은 사용된 모든 리소스를 반환해야 한다는 것이다.
- 아파치 mod-perl을 비롯해 여러 대규모 트랜잭션 처리 시스템에서 이 체계를 적용하고 있다.
- 요청별 프로세스 방식도 일정 수준의 부하를 처리하려면 많은 프로세스를 실행해야 한다.
- 이런 경우 한 프로세스가 여러 스레드를 실행하게 하면 처리량을 개선할 수 있다. (요청별 스레드)
- 요청별 스레드 방식에서 각 요청은 프로세스 내의 한 스레드에 의해 처리된다.
- 스레드는 프로세스보다 리소스를 적게 사용하면서 많은 요청을 처리해 서버의 효율을 높일 수 있다.
- 다만, 문제는 스레드가 서로 격리되지 않기 때문에 어떤 스레드든지 접근 가능한 데이터를 변경할 수 있다.
- 가장 권하는 방식은 요청별 프로세스다.
- 효율은 낮지만 확장성이 충분하고 견고하다.
- 한 스레드가 잘 못 되면 전체 프로세스가 잘 못 될 수 있다.
- 요청별 스레드를 사용할 때 격리된 지역을 만들고 이를 활용해야 한다.
- 스레드가 요청을 처리하기 시작할 때 다른 스레드가 볼 수 없는 곳에 새로운 객체를 만들고 이를 활용하는 게 일반적인 방법이다.
- 객체 생성 프로세스에 비용이 많이 든다는 걱정은 객체를 풀링하는 방법으로 해결했다.
- 하지만 문제는 풀링된 객체에 대한 접근을 어떤 방식으로든 동기화해야 한다는 것이다.
- 객체 생성 비용은 사용하는 가상 시스템과 메모리 관리 전략에 따라 크게 바뀐다.
- 각 세션마다 새로운 객체를 만들면 많은 동시성 버그를 예방하고 확장성을 개선할 수 있다.
- 이 전술에서 피해야 하는 영역 : 정적 클래스 기반 변수나 전역 변수.
- 이러한 변수에 대한 접근은 모두 동기화해야 한다.
- 싱글턴의 경우도 마찬가지다.
- 전역 메모리와 비슷한 기능이 필요하면 레지스트리를 사용한다.
- 레지스트리는 정적 변수와 비슷하지만 스레드별 저장소를 사용하도록 되어 있다.
'Study > 엔터프라이즈 애플리케이션 아키텍처 패턴' 카테고리의 다른 글
제12장 객체-관계형 구조 패턴 : 연관 테이블 매핑 (0) | 2023.03.28 |
---|---|
09장 - 도메인 논리 패턴 (0) | 2023.02.20 |
03장 - 관계형 데이터베이스 매핑 02 - 데이터 읽기 / 구조적 매핑 패턴 (0) | 2023.02.08 |
03장 - 관계형 데이터베이스 매핑 01 - 아키텍처 패턴 / 동작 문제 (0) | 2023.02.07 |
02장 - 도메인 논리 구성 (0) | 2023.02.07 |