마틴 파울러의 엔터프라이즈 애플리케이션 아키텍처 패턴을 읽고 정리한다.
1부 이야기에서는 다양한 개념과 발전 과정을 말한다.
작성 시점은 20년 전으로 당시와 지금의 용어 & 개념이 다를 수 있고, 더 이상 사용 안 하는 것도 있을 수도 있다.
자세한 내용이 궁금하면 읽어보는 걸 권한다.
시작하면서
동시성은 개발에서 가장 까다로운 문제다. 여러 프로세스나 스레드가 동일한 데이터를 조작하는 경우 필연적으로 동시성 문제가 발생한다.
동시성 문제를 해결하기 어려운 이유는 모든 원인을 미리 알기 어렵기 때문이다.
이를 위해 트랜잭션이라는 동시성의 여러 측면을 예방하는 프레임워크를 제공한다.
개발자가 해결해야 할 두 번째 동시성 문제는 다중 스레드를 지원하는 애플리케이션 서버 시스템에 대한 것이다.
이런 문제를 제대로 이해하려면 동시성의 개념 몇 가지를 이해해야 한다.
동시성 문제
가장 간단한 개념으로 손실된 업데이트(lost update)가 있다. 손실된 업데이트란 두 명이 하나의 데이터를 조회하고 동시에 수정 작업을 했을 때 누군가 먼저 작업을 처리하고 나서 뒤늦게 한 명이 처리할 경우, 먼저 작업한 사람의 작업 내용이 뒤의 내용으로 덮어 써진 경우를 말한다.
일관성 없는 읽기(inconsistent read)는 올바르지만 한편으로 올바르지 않은 정보를 읽을 때 발생한다. A라는 주문건에 대해서, 관리자가 배송을 하기 위해 해당 주문의 배송정보를 확인하는 할 때, 고객이 그 사이에 배송 정보를 변경한다. 관리자는 변경되기 전 정보가 올바르다 생각하고 배송보내기로 한다. 이런 상황 때문에 데이터에 일관성이 없어 일관성 없는 읽기라 한다.
위의 두 가지 문제는 정확성(또는 안정성)을 위반하는 잘못된 동작을 유발하지만, 여러 명이 동시에 동일한 데이터를 사용하지 않으면 애초에 발생하지 않는다. 이를 방지하기 위해 한 명만 사용하게 허용하면 정확성은 유지되지만 활동성은 저하된다.
모든 동시성 프로그래밍의 핵심적 문제는 정확성을 충족하는 것만으로 부족하며, 동시 작업이 진행되는지 나타내는 활동성(liveness)도 충족해야 한다.
실행 컨텍스트
외부 세계와 상호작용하는 관점에서 중요한 두 가지 컨텍스트는 요청과 세션이 있다.
요청(request)은 소프트웨어가 작업하고 선택적으로 응답을 보내는 외부 세계로부터의 단일 호출에 해당된다.
세션(sesseion)은 클라이언트와 서버 간에 오랫동안 실행되는 상호작용이다. 사용자의 사이트 접속이나 로그인으로 시작해 사이트를 벗어나거나 로그아웃하면 끝난다.
운영체제와 관련된 중요한 두 가지 용어로 프로세스(process)와 스레드(thread)가 있다.
프로세스(process)는 사용되는 내부 데이터에 대한 다단계 격리를 제공하는 대규모 실행 컨텍스트다.
스레드(thread)는 한 프로세스 내에서 여러 스레드로 작동할 수 있게 구성된 소규모 활성 에이전트다.
데이터베이스를 처리할 때는 트랜잭션(transaction)이라는 중요한 컨텍스트가 있다.
트랜잭션을 이용하면 클라이언트의 여러 요청을 하나의 요청처럼 처리할 수 있다.
트랜잭션에는 애플리케이션에서 데이터베이스로 수행되는 시스템 트랜잭션과 사용자에게서 애플리케이션으로 수행되는 비즈니스 트랜잭션이 있다.
격리와 불변성
엔터프라이즈 애플리케이션에서는 격리와 불변성이라는 두 가지 해결책이 중요했다.
동시성 문제는 둘 이상의 활성 에이전트가 동일한 데이터에 접근하여 수정할 때 발생한다. 이를 해결하려면 격리를 통해 데이터를 분리하여 하나의 활성 에이전트만 접근하게 한다.
다른 방법으로는 변경 불가능한(immutable) 데이터로 예방하거나 애플리케이션에서 데이터 읽는 부분을 분리하고 데이터 복사본을 이용해 동시성 제어로부터 자유롭게 작업하는 것이다.
낙관적 동시성 제어와 비관적 동시성 제어
데이터를 격리할 수 없어 변경되는 경우에는 낙관적 동시성 제어와 비관적 동시성 제어로 두 가지로 제어한다.
낙관적 잠금(optimistic locking)을 사용하면 동시에 데이터를 접근하고 수정할 수 있다. 대신, 변경 내용을 확인하여 충돌을 알려준다.
비관적 잠금(pessimistic locking)을 사용하면 먼저 접근한 사람만 수정을 허용하고 다른 사람은 수정을 막는다.
즉, 낙관적 잠금은 충돌을 감지하고 비관적 잠금은 충돌을 예방한다.
두 가지 방법을 선택하는 중요한 기준을 충돌의 빈도와 심각도다. 접근이 많고 심각도가 낮으면 낙관적 잠금을 선택하고 충돌의 결과가 심각하면 비관적 잠금을 사용한다.
일관성 없는 읽기 예방
일관성 없는 읽기를 예방하는 여러 방법이 존재한다. 한 가지는 변경사항을 업데이트 전에 시스템에서 한번 더 확인하는 수동 작업이 있다.
낙관적 잠금의 충돌 감지 기능은 데이터에 버전 표식을 넣어 시스템에 손실된 업데이트가 있는지 버전 표식을 비교하여 확인한다.
또 다른 방법은 임시 읽기(temporal read)를 이용한다.
교착 상태
비관적 잠금 기법에서 발생하는 문제로 교착 상태(dead lock)가 있다. 교착 상태는 기본적으로 잠금을 가진 사용자가 다른 잠금을 얻으려 할 때 발생하며, 감지하기 어렵고 큰 피해를 준다.
해결하는 방법으로 시간제한과 감지 기법이 있다. 시간제한은 모든 잠금에 시간제한을 두고, 시간이 초과되면 잠금과 작업이 모두 손실되는 방법이다. 감지 기법은 교착 상태를 감지하는 소프트웨어를 감지한다.
트랜잭션
트랜잭션은 동시성을 처리하는 가장 중요한 툴이며, 시작점과 끝점이 명확하게 정의된 일련의 작업이고 관련된 모든 자원은 일관된 상태로 유지된다.
ACID
트랜잭션은 다음과 같은 속성을 가진다
- 원자성(Atomicity) : 트랜잭션의 경계 안에서 수행되는 각 작업은 모두 완료되거나 롤백되어야 한다.
- 일관성(Consistency) : 시스템의 자원은 트랜잭션의 시작과 종료에 모두 일관성 있고 손상되지 않아야 한다.
- 격리성(Isolation) : 개별 트랜잭션의 결과는 성공적으로 커밋되기 전까지 다른 트랜잭션에서 볼 수 없다.
- 지속성(Durability) : 커밋된 결과는 영구적으로 유지되어야 한다.
트랜잭션 리소스
트랜잭션에 대한 기술적 논의에서는 트랜잭션으로 동시성을 제어할 수 있는 모든 대상을 나타내는 데 "트랜잭션 리소스" 용어를 사용한다.
트랜잭션 리소스 용어는 복잡해서 데이터베이스라고 대체한다.
긴 트래잭션(long transaction)은 여러 요청에 걸친 트랜잭션을 말한다.
보통 요청이 시작할 때 트랜잭션이 시작하고 요청이 끝날 때 트랜잭션이 끝난다. 이런 요청 트랜잭션(request transaction)은 여러 환경에서 메서드로 트랜잭션을 지정하는 방법으로 지원한다.
다른 방법으로는 트랜잭션을 늦게 여는 것으로 지연 트랜잭션(late transaction)이라고 한다.
트랜잭션을 사용할 때 어떤 리소스가 잠기는지 주의해야 한다. 여러 트랜잭션이 동일한 테이블에 접근이 가능한데 작업과 관련된 행이 잠기면 잠금 수준이 올라가 다른 트랜잭션까지 영향을 끼치게 된다.
활동성을 위한 트랜잭션 격리성 저하
여러 트랜잭션을 동시에 실행해도 순서대로 실행했을 때와 동일한 결과를 얻는 경우 직렬화 가능 트랜잭션이라 말한다.
대부분의 트랜잭션 시스템은 SQL 표준으로 네 가지 격리 수준을 정의한다.
격리 수준 | 더티 읽기 | 반복 불가능 읽기 | 패턴 |
커밋되지 않은 읽기 | 예 | 예 | 예 |
커밋된 읽기 | 아니요 | 예 | 예 |
반복 가능 읽기 | 아니요 | 아니요 | 예 |
직렬화 가능 | 아니요 | 아니요 | 아니요 |
비즈니스 트랜잭션과 시스템 트랜잭션
지금까지 알아본 트랜잭션은 시스템 트랜잭션이며, RDBMS 시스템과 트랜잭션 모니터의 지원을 받아 운영된다.
시스템 트랜잭션은 비즈니스 시스템 사용자에게 의미가 없다. 예를 들면 온라인 은행 시스템에 사용자에게 트랜잭션은 로그인, 계정 선택, 이체 내역 설정, 이체 확인 버튼까지의 과정을 포함한다. 이를 비즈니스 트랜잭션(business transaction)이라 하며, 시스템 트랜잭션과 마찬가지로 ACID를 제공해야 한다.
ACID를 제공하는 방법은 비즈니스 트랜잭션을 하나의 시스템 트랜잭션 안에서 실행하는 것이다.
오프라인 동시성 제어를 위한 패턴
비즈니스 트랜잭션과 시스템 트랜잭션 간의 불일치를 해결하기 위한 패턴을 제시한다.
낙관점 오프라인 잠금 : 여러 비즈니스 트랜잭션에 걸쳐 낙관적 동시성 제어를 사용하는 방법, 한계점은 실패/성공 여부를 커밋할 때 알 수 있음
비관적 오프라인 잠금 : 낙관적 오프라인 잠금의 대안법으로 문제를 조기에 알아내지만 프로그래밍이 어렵고 활동성이 제한
굶은 입자 잠금 : 객체 그룹의 동시성을 함께 관리
암시적 잠금 : 사용자가 직접 잠금을 관리할 필요가 없음
애플리케이션 서버 동시성
동시성의 다른 형태로 애플리케이션 서버 자체의 프로세스 동시성이 있으며, 애플리케이션 서버 동시성은 트랜잭션과 관계가 없어 트랜잭션 도움을 받을 수 없어 다른 방식으로 해결해야 한다.
세션별 프로세스(process-per-session)는 가장 간단한 방법으로 각 프로세스의 상태가 다른 프로세스와 격리되어 신경 쓸 일이 없다는 것이다. 문제점은 프로세스가 리소스를 많이 잡아먹는다.
요청 별 프로세스(process-per-request)는 프로세스의 풀을 사용하는 방식으로 여러 요청을 순차적으로 처리한다. 대신 요청이 끝나면 사용된 리소스를 반환해야 한다.
요청별 쓰레드(thread-per-request)는 많은 요청을 프로세스의 여러 스레드로 처리하는 방식으로 프로세스보다 리소스를 적게 먹는 스레드를 활 영하여 서버의 효율을 높일 수 있다. 문제는 프로세스 내부에 있는 스레드끼리 격리가 되지 않는다.
'서적 > 엔터프라이즈 애플리케이션 아키텍처 패턴' 카테고리의 다른 글
1부 이야기 - 분산 전략 (0) | 2021.08.02 |
---|---|
1부 이야기 - 세션 상태 (0) | 2021.07.28 |
1부 이야기 - 웹 프레젠테이션 (0) | 2021.07.26 |
1부 이야기 - 관계형 데이터베이스 매핑 (0) | 2021.07.26 |
1부 이야기 - 도메인 논리 구성 (0) | 2021.07.26 |