대범하게

[클린코드] 25일차/26일차/27일차 - 동시성 본문

알아두면쓸데있는신기한잡학사전/독서일지

[클린코드] 25일차/26일차/27일차 - 동시성

대범하게 2023. 11. 30. 00:10
반응형

[클린코드] 25일차 - 동시성

클린코드 25일차 (p. 226 ~ 229 (13장) )

클린코드 26일차 (p.  230 ~ 237 (13장) )

클린코드 27일차 (p.  237 ~ 244 (13장) )

13장 동시성

이 장에서는 여러 스레드를 동시에 돌리는 이유와 어려움 그리고 대처방법에 대해 이야기한다.

그리고 마지막으로 동시성을 테스트하는 방법과 문제점을 논한다.

 

0. 동시성이란?

싱글 코어에서 멀티 스레드를 동작시켜 동시에 실행되는 것 같이 보이는 것이다.

 

1. 동시성이 필요한 이유는 무엇인가?

동시성은 결합을 없애는 전략이다.

 

즉, 무엇what 과 언제when 를 분리하는 전략이다.

 

단일 스레드는 무엇을 언제 실행했는지 정지점을 정한 후 디버깅할 경우 파악할 수 있다.

 

디버깅하는 경우 좋지만 작업 효율이 좋지 못한 경우도 있다.

 

어떤 시스템은 응답 시간과 작업 처리량 개선이라는 요구사항으로 인해 동시성 구현이 불가피하다.

 

예를 들면)

 

카카오 알림톡을 유저 100명에게 보내는 경우 (1명에게 보내는 시간이 1초)

 

단일 스레드로 작업을 진행하는 경우 100초가 걸릴 것이다. 즉, 100번째 사람은 100초 뒤에 알림톡이 올 것이다.

 

멀티 스레드(스레드 수 = 3)로 이 작업을 진행하면 3개씩 보낼 수 있기 때문에 33초정도가 걸린다.

 

많은 사용자를 동시에 처리하면 시스템 응답 시간을 높일 수 있다.

 

*웹 애플리케이션이 표준으로 사용하는 '서블릿Servlet' 모델

서블릿은 웹 혹은 EJB 컨테이너 아래서 돌아가며, 이들 컨테이너는 동시성을 부분적으로 관리한다.

웹 요청이 들어올 때마다 웹 서버는 비동기식으로 서블릿을 실행한다.즉 각 서블릿 스레드는 다른 서블릿 스레드와 무관하게 돌아간다.

 

2. 동시성에 대한 미신과 오해

1 ) 동시성은 항상 성능을 높여준다.

아니다.  때로 성능을 높여준다.

 

- 대기 시간이 아주 길어 여러 스레드가 프로세스를 공유하는 경우

- 여러 프로세스가 동시에 처리할 독립적인 계산이 충분히 많은 경우

 

2) 동시성을 구현해도 설계는 변하지 않는다.

 

일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라지며

단일 스레드 시스템과 다중 스레드 시스템의 설계를 판이하게 다르다.

 

3 ) 스프링 컨테이너를 사용하면 동시성을 이해할 필요가 없다.

 

실제로 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지 알아야만 한다.


아래는 타당한 말들 ..

 

1 ) 동시성은 다소 부하를 유발한다.

 

성능 측면에서 부하가 걸리며, 코드도 더 짜야한다.

 

2 ) 일반적으로 동시성 버그는 재현하기 어렵다. 

 

단순 로직 문제면 확인하기 쉽지만 그렇지 않은 경우,

그래서 진짜 결함으로 간주되지 않고, 일회성 버그로 여겨 무시하기 쉽다.

 

3. 동시성을 구현하기 어려운 이유

public class X {
    private int lastIdUsed = 7;
 
    public int getNextId() {
        return ++lastIdUsed;
    }
}

 

위 X의 getNextId()를 x, y 2개의 스레드가 동시에 호출하면 결과는 어떻게 될까?

 

1. x가 8, y가 9를 받는다. lastIdUsed는 10이 된다.

2. x가 9, y가 8을 받는다. lastIdUsed는 10이 된다.

3. x가 8, y가 8을 받는다. lastIdUsed는 8이 된다. (?)

 

1, 2는 우리가 원하는 답이다.

 

3은 뭐지? 동시에lastIdUsed가 7일 때 호출된 경우다. 동시성 보장이 안 되어있기 때문에 이런 상황이 발생한다.

 

정확히 답하려면 JIT(Just-In-Time) 컴파일러가 바이트 코드를 처리하는 방식과 자바 메모리 모델이 원자로 간주하는 최소 단위를 알아야 한다. 간단하게 답하자면, 바이트 코드만 고려했을 때, 두 스레드가 getNextId 메서드를 실행하는 잠재적인 경로는 최대 12,870개 달한다...

 

결론 : 자바 코드 한 줄을 거쳐가는 경로는 수없이 많은데, 그 중 예상하지 못한 일부 경로에서 생각지도 못한 문제가 발생할 수 있기 때문에 동시성 구현하기가 어렵다.

 

4. 동시성 방어 원칙

그렇다면 예상치 못한 경로에서 에러나는 걸 어떻게 방지할 수 있을까 ? 에 대해 알아보자.

 

1 ) 단일 책임 원칙 Single Responsibility Principle, SRP

 

SRP = 메소드, 클래스, 컴포넌트를 변경할 이유는 1가지여야 한다. 책임이 1개여야 한다.

 

- 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.

- 동시성 코드는 다른 코드에서 겪는 난관과 다르며... 훨씬 어렵다.

- 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다.

 

권장사항 : 동시성 코드는 다른 코드와 분리하라.

 

 

2 ) 자료 범위를 제한하라. 

 

위 예에서 객체 하나를 공유하고 동일 필드를 수정하는 경우 두 스레드가 서로 간섭하므로 예상치 못한 결과를 내놨다.

 

이 문제를 해결하려면 공유 객체를 사용하는 코드 내 임계 영역critical section을 synchronized 키워드로 보호하라.

public class X {
    private int lastIdUsed = 7;
 
 	// synchronized 추가
    public int getNextId() {
        return ++lastIdUsed;
    }
}

 

물론 공유 자료를 임계 영역에 잘 넣어둔다면 괜찮겠지만 실수가 발생할 수 있는 코드기 때문에 항상 주의를 기울여야 한다.

 

권장사항 : 자료를 캡슐화 encapsulation하라. 공유 자료를 최대한 줄여라.

 

3 ) 자료 사본을 사용하라.

 

공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.

 

공유 자료를 복사해서 대체할 수 있는 경우 사용하면 된다.

 

복사 vs 임계 영역에 대한 부하가 걱정이라면 테스트를 해보자.

 

임계 영역 대신 객체를 복사해서 사용하자.

 

 

4 ) 스레드는 가능한 독립적으로 구현하라.

 

모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.

 

권장사항 : 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.

 

 

5 ) 라이브러리를 이해하라.

 

개발하다보면 동시성이 필요한 경우 java.util.concurrent에 있는 클래스를 사용하라 라는 말을 자주 듣는다.

 

자바에선 이미 동시성을 위한 클래스들이 있다. 그걸 사용하자.

 

권장사항 : 언어가 제공하는 클래스를 검토하라.

자바에서는 java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks를 익혀라.

 

 

6 ) 실행 모델을 이해하라.

 

기본 용어

- 한정된 자원(Bound Resource) : 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적.

예 ) 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼

 

- 상호 배제(Mutual Exclustion) : 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킴.

 

- 기아(Starvation) : 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다림.

예) 항상 짧은 스레드에게 우선순위를 준다면, 짧은 스레드가 지속적으로 이어질 경우, 긴 스레드가 기아 상태에 빠진다.

 

- 데드락(Deadlock) : 여러 스레드가 서로 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다.

 

- 라이브락(Livelock) : 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.

 

다중 스레드 프로그래밍에서 사용하는 실행 모델

- 생산자 - 소비자 

- 읽기 - 쓰기

- 식사하는 철학자들

 

권장사항 : 위에서 설명한 기본 알고리즘과 각 해법을 이해하라.

 

 

7) 동기화하는 메서드 사이에 존재하는 의존성을 이해하라.

 

synchronized가 붙은 메서드 사이에 의존성을 두지말자.

 

권장사항 : 공유 객체 하나에는 메서드 하나만 사용하라.

 

만약 정말 공유 객체 하나에 여러 메서드가 필요한 상황이라면, 

- 클라이언트에서 잠금 - 

- 서버에서 잠금 - 

- 연결 서버 - 

 

 

8 ) 동기화 하는 부분을 작게 만들어라.

 

synchronized를 사용하는 메서드를 잘게 쪼개자.

 

너무 큰 단위로 하면 성능에 문제가 생긴다.

 

 

9 ) 올바른 종료 코드는 구현하기 어렵다.

 

깔끔하게 종료하는 코드를 올바르게 구현하긴 어렵다.

 

예를들면 데드락이 있다.

 

이런 문제를 풀기 위한 알고리즘은 기존에 나와있는 방식을 사용하자. 어렵다.

 

10 ) 스레드 코드 테스트를 잘하자

 

멀티 스레드 환경에선 이해가 안 가는, 재현이 정말 어려운 에러들이 많이 발생한다. 이를 초기에 잡으려면 테스트를 잘 해야한다.

 

권장사항 : 문제를 노출하는 테스트 케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘거가면 절대로 안 된다.

 

구체적인 지침

 

- 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라.

 

멀티 스레드 환경에선 가끔씩 알 수 없는 에러가 발생한다.

 

수천, 수백만번에 1번씩 나는 에러라도 일회성 문제로 치부하지말고 원인과 해결책을 찾아보자.

 

- 다중 스레드를 개발하기 전 단일 스레드부터 제대로 돌게 만들자.

 

다중 스레드가 필요한 이유는 어차피 효율 때문인다.

 

근데 멀티 스레드로 개발하는 건 위에서도 말했지만 어렵다.

 

그러기에 단일 스레드로 먼저 잘 돌아가는 코드를 개발 후 다중 스레드로 전환하자.

 

- 다양한 환경에서 돌려보자.

 

스레드 수를 1개, 2개, 3개, 그 이상으로 테스트 해보자.

실환경, 테스트환경에서 돌려본다.

테스트 코드를 빨리, 천천히, 다양한 속도로 돌려보자.

 

- 스레드 수를 쉽게 조절할 수 있게 코드를 작성하자.

 

처음부터 적절한 스레드 수를 알 수 없다.

 

환경변수로 스레드 수를 운영 중에도 변경할 수 있게 하는 방법을 고민해보자.

 

- 프로세서 수보다 많은 스레드를 돌려보자.

 

프로세서 수보다 많은 스레드를 돌려 스와핑을 유도해서 임계 영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾아보자.

 

- 다른 플랫폼에서 돌려보자.

 

운영체제마다 다른 스레드 정책이 걱정된다면 다른 플랫폼에 돌려보자.

 

- 코드에 보조 코드를 넣어 강제로 실패를 일으키게 해보자.

 

wait, yield, priority 등과 같은 스레드를 조작할 수 있는 메서드들을 중간중간에 넣어 돌려보자.

 

예상밖에 문제가 보일지도 모른다.

 

결론

멀티 스레드 코드는 올바로 구현하기 정말 어렵다. 

 

무엇보다 먼저, SRP를 준수한다 == 단일 책임 원칙. 즉, 스레드 코드는 최대한 집약 & 작아야함.

 

동시성 오류를 일으키는 잠정적인 원인을 이해하고 있자.

 

스레드 코드는 많은 플랫폼에서 많은 설정으로 반복해서 계속 테스트 해야한다.

Comments