이 장에서 다룰 내용
•
확장성 대 성능
•
점진적인 확장성
•
데이터베이스 규칙 위반
•
보다 원활한 병렬화
•
모놀리스의 진실
•
과거의 웹사이트는 데이터베이스로 텍스트 파일을 사용했었다.
◦
무언가를 쓸 때 텍스트 파일을 잠궈야 했으며 정보를 제공할 때 역시, 잠궈야 했다.
◦
이는 심각한 성능 저하를 일으키는데, 하드웨어 때문이 아니고 데이터 구조와 병렬화에 대한 결정으로 기인한 것이었다.
•
이것이 확장성의 핵심이다. 성능만으로는 시스템을 확장할 수가 없다. 점점 더 많은 사용자를 만족시키기 위해서는 모든 측면을 고려한 설계가 필요하다.
•
시스템 관점에서 확장성은 더 많은 하드웨어를 투입하여 시스템을 더 빠르게 만들 수 있는 능력을 의미한다.
◦
프로그래밍 관점에서 확장성 좋은 코드는 수요가 증가하는 상황에서도 응답성을 일정하기 유지할 수 있다.
•
확장성 또한 더 큰 목표를 향하면서도 구체적이고 작은 단계부터 점진적으로 풀어가는 것이 가장 좋다.
•
확장할 수 있는 코드를 만들기 위한 첫 번째 단계는 확장성을 방해하는 잘못된 코드를 제거하는 것이다.
1. 잠금을 사용하지 마라
•
프로그래밍에서의 잠금은 Thread-Safe한 코드를 작성하기 위한 기능이다.
◦
Thread-Safe이란 코드 조각 2개 이상인 스레드에서 동시에 호출되어도 일관되게 작동하는 것을 의미한다.
•
잠금은 다른 스레드를 멈추고 특정 조건에서 기다리게 할 수 있다. 이는 일관성을 제공하기는 하지만 동시에 확장성에서는 큰 문제가 될 수 있다.
2. 불일치를 수용하라
•
데이터베이스는 잠금, 트랜잭션, 원자 카운터, 트랜잭션 로그, 페이지 체크섬, 스냅샷 등과 같이 불일치를 방지하는 수 많은 기능을 제공한다.
•
신뢰성은 흑백의 개념이 아니다. 성능과 확장성이 향상될 수 있다면 어느 정도는 희생을 감수할 수 잇다.
◦
NoSQL은 외래 키나 트랜잭션과 같이 전통적인 관계형 데이터베이스에서 추구하는 어떠한 일관성을 무시하고 그 대가로 성능, 확장성을 얻겠다는 철학에서 출발했다.
⇒ NOLOCK이란 구문을 사용한 예시를 설명하는데, 처음봐서 알아보니 mssql에서 사용하는 쿼리힌트였다.
3. 데이터베이스 연결을 캐시하지 마라
•
데이터베이스에 대한 단일 커넥션을 하나 생성한 후, 이를 다른 코드에서 공유하는 것은 범하기 쉬운 잘못된 아이디어다.
◦
쿼리를 실행하기 전에 앞서 복잡한 절차들, 커넥션 생성, 세션 연결 등의 절차를 무시할 수 있어 합리적이라고 생각할 수 있지만 데이터베이스에 대한 연결이 하나인 경우, 병렬적으로 쿼리를 수행할 수 없어 효과적이지 못하다.
•
현재의 프레임워크들은 대부분, 연결을 획득 후, 쿼리 수명동안만 이를 유지하고 다시 반환하는 식으로 확장성을 구현하고 있다.
1. ORM의 형태로
•
최신 객체 관계 매핑 도구(ORM)들은 Entity 프레임워크와 같이 완전히 다른 복잡한 추상화 집합을 제공하며 데이터베이스의 구체성에 의존하지 않게 해준다.
◦
모든 것이 자동으로 이루어지므로 연결이 언제 열리고 반환되는지 신경쓰지 않아도 된다.
4. 스레드를 사용하지 마라
•
확장성은 병렬화뿐만 아니라 리소스를 절약하는 것이기도 하다. 전체 메모리 이상으로 확장할 수 없고 CPU 사용량 100% 이상으로 확장할 수 없다.
•
스레드 풀에는 대개 시스템의 CPU 코어 수보다 더 많은 스레드가 있는데, 이는 스레드가 종종 I/O와 같은 다른 작업이 끝날 때까지 대기해야 하기 때문이다.
◦
위 그림에선 CPU 1은 스레드 1의 입출력 장치의 처리가 진행되는 동안 스레드 5의 작업을 처리한다.
•
이는 CPU 코어 수만큼의 스레드를 갖는 것보단 낫지만 CPU를 최대로 활용하지는 못한다.
◦
운영 체제는 스레드에 짦은 실행 시간을 제공한 다음 CPU 코어를 다른 스레드에 양보하여 모든 스레드가 적당한 시간 안에 실행될 수 있도록 하는데, 이를 선점형 스케쥴링이라고 한다.
•
입출력 대기 시간을 활용하는 더 정확한 방법은 비동기식 입출력을 사용하는 것이다. 비동기 입출력은 명시적이기 때문에 의식적이고 정확하게 CPU가 이런 흐름을 따라 처리되겠구나를 가늠할 수 있다.
1. 비동키 코드의 주의사항
•
코드를 비동기로 만들 때는 특정한 사항을 염두에 두어야한다.
◦
I/O 작업이 없다면 비동기는 필요 없다.
▪
비동기식 프로그래밍은 I/O 바운드 작업과 함께 사용할 때만 확장성에 도움을 줄 수 있다.
▪
CPU 바운드 작업에서 비동기를 사용하면 단일 스레드에서 병렬로 실행되는 I/O 작업과 달리 작업을 위한 별도의 스레드가 필요하기 때문에 확장성에 도움이 되지 않는다.
◦
동기화와 비동기화를 섞지 마라
▪
동기화된 컨텍스트에서 비동기 함수를 안전하게 호출하는 것은 어려운 기술이다.
▪
동기 맥락의 컨텍스트에 비동기 코드를 함께 사용하지마라. 설정이 복잡하기 때문에 보통 프레임워크만이 이를 수행한다.
2. 비동기를 이용한 멀티스레딩
•
비동기 I/O는 리소스를 덜 소모하기 때문에 I/O가 많이 필요한 코드에서 멀티스레딩보다 더 나은 확장성을 제공한다.
•
그러나 멀티스레딩과 비동기는 서로 배타적이지 않기 때문에 두 가지를 동시에 가질 수 있고 멀티스레드 코드를 작성하기 위해 비동기 프로그래밍 구문을 사용할 수도 있다.
5. 모놀리스를 존중하라
•
마이크로서비스의 개념은 간단하다.
•
코드를 자체적으로 호스팅이 가능한 프로젝트별로 분산하여 향후 해당 프로젝트를 별도의 서버에 배포하는 것으로 확장성을 확보하는 것이다.
•
모롤리스는 마이크로서비스와 대치되는 개념이다. 그렇다고 확장성이 떨어지는 개념도 아니다. 앱을 분할해야하는 상황이 오기 전까지 모놀리스는 훌륭한 구성안이 되어준다.
•
마이크로서비스로 새로운 프로젝트를 복잡하게 시작하지 말아라. 단점이 장점보다 작아질 때만 사용할 것을 고려하라.
6. 요약
•
다단계 다이어트 프로그램처럼 점진적으로 확장성에 접근하라. 작은 개선을 통해 궁극적으로 더 나은 확장 가능한 시스템을 구축할 수 있다.
•
확장성의 가장 큰 블록 중 하나는 잠금이다. 이들과 함께 살 수 없고, 이들 없이는 살 수도 없다. 대로는 이것이 불필요하다는 것을 인정해야 한다.
•
코드 확장성을 높이기 위해 수동으로 잠금을 획득하는 것보다 잠금이 없는 데이터 구조나 동시성 데이터 구조를 선호하라.
•
다중 스레드 환경에서 리소스 초기화를 안전하게 수행하며 성능을 최적화하고 싶다면 Double-Checked Locking을 사용하라.
•
더 나은 확장성을 위해 불일치를 활용하는 법을 배워라. 비즈니스에 적합한 수준의 불일치 타입을 선택하고 확장 가능한 코드로 만들 기회로 활용하라.
•
ORM은 일반적으로 귀찮은 작업이지만, 여러분이 생각하지 못한 최적화로 확장성이 더 뛰어난 앱을 만들 수도 있다.
•
뛰어난 확장성이 필요한 모든 I/O 바인딩 코드에 비동기 I/O를 사용하여 사용 가능한 스레드를 보존하고 CPU 사용량을 최적화하라.
•
CPU 바운드 작업의 병렬화를 위해 멀티스레딩을 사용하라. 그러나 비동기식 프로그래밍 구조와 함께 멀티스레딩을 사용하는 경우라면 비동기식 I/O를 통한 확장성의 이점을 기대하지 마라.
•
마이크로서비스 아키텍처에 대한 설계 논의가 끝나기 전에 모놀리스 아키텍처는 전 세계 투어를 끝낼 것이다.