본문 바로가기
Clean Code

[클린 코드] 13장 - 동시성 (Concurrency)

by 노력남자 2022. 9. 5.
반응형

동시성이란?

 

멀티 스레드를 사용하는 프로그램을 말한다.

 

동시성이 필요한 이유?

 

동시성은 결합을 없애는 작업이다.

 

무엇과 언제를 분리하는 전략을 말한다.

 

단일 스레드는 무엇을 언제 실행하는지 예상할 수 있다.

 

디버깅하기엔 좋지만 작업 효율이 좋지 않은 경우가 있다.

 

그럴 때 멀티 스레드를 사용한다. 무엇과 언제를 분리시켜 효율을 증대시킨다.

 

예)

 

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

 

단일 스레드로 이 작업을 진행하는 경우 1명씩 차근차근 보내야 하기 때문에 100초가 걸린다.

 

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

 

 

동시성의 미신과 오해

 

위 예제만 보면 아마 동시성이 엄청 좋아보일지 모른다.

 

아니다.

 

미신과 오해를 하나씩 알아보자.

 

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

 

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

 

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

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

 

추가로

 

- 동시성을 사용할 환경인지? 받아주는 쪽에서 동시성을 받을 여력이 되는지도 봐야한다.

 

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

 

단일 스레드와 멀티 스레드 시스템은 설계가 아예 다르다.

 

무엇과 언제를 분리하면 시스템 구조가 달라진다.

 

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

 

언제나 그렇듯 이해를 해야 한다. 몰라도 개발은 할 수 있지만 모르면 모르고 지나가기 때문에 아는 게 좋다.

 

오해 중에 사실도 있다.

 

1. 멀티 스레드 시스템은 부하가 많을 것이다.

 

맞다. 한번에 많은 요청, 많은 작업을 하려니 당연히 부하가 많다.

 

2. 동시성은 복잡하다.

 

복잡하다. 고려해야 할 사항이 많다.

 

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

 

단순 로직 문제면 확인하기 쉽지만.

 

그게 아닌 경우가 가끔 있어서 재현하기 어려운 경우가 있다.

 

4. 단일 -> 멀티 스레드로 설계를 바꾸는 경우 근본적인 설계 전략을 고민해야 한다.

 

이건 좀 케바케인 거 같다.

 

내가 개발해보면 엄청나게 근본적인 설계까지 바꿔야 하는 경우가 있지 않았다.

 

일단 책에선 그렇다니 가끔 그런 경우가 있나보다.

 

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

 

public class Sequence {
    private int seq = 7;

    public int getNextSeq() {
        return ++seq;
    }
}

 

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

 

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

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

3. x가 8, y가 8을 받는다. seq는 9가 된다. (?)

 

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

 

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

 

자바에서 사용하는 JIT(Just-In-Time) 컴파일러가 getNextSeq를 실행하는 잠재적인 경로는 12,870 ~ 2,704,156개란다.

 

정확한 건 책 뒷편에 있는데 알고싶지 않다..

 

결론 : 여튼 하나의 소스에선 여러 경로가 있는데 예상하지 못한 일부 경로에서 생각지도 못한 문제가 발생할 수 있기 때문에 동시성 구현하기가 어렵다.

 

동시성 방어 원칙

 

그렇다면 위와 같이 우리가 생각하지도 못한 경로에서 에러나는 걸 어떻게 방지할 수 있을까?에 대해 알아보자.

 

1. SRP(Single Responsibility Principle)를 지키자

 

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

 

- 동시성도 하나의 책임이다.

- 동시성 코드는 복잡하고 어렵다.

- 독자적인 개발, 변경, 조율 주기가 있다.

 

다른 코드와 분리하자.

 

2. 공유 자료를 최대한 줄이자.

 

Sequnce의 getNextSeq에서 문제가 됐던 게 바로 seq를 공유 자료로 사용했기 때문인다.

 

getNextSeq 메소드를 임계영역으로 정하고 synchronized 를 붙혀주면 해결할 수 있다.

 

public class Sequence {
    private int seq = 7;

    // synchronized 추가
    public synchronized int getNextSeq() {
        return ++seq;
    }
}

 

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

 

안 쓸 수 있으면 안 쓰는 방향으로 가자.

 

3. 자료 사본을 사용하라

 

임계 영역을 사용하지 않을 거면 사본을 사용하는 것도 한 가지 방법이다.

 

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

 

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

 

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

 

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

 

2, 3에서 말한 거와 같은 맥락인 거 같다.

 

각 스레드가 독립적으로 도는 것처럼 공유 객체를 사용하지 말자는 내용이다.

 

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

 

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

 

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

 

6. 실행 모델을 이해하라

 

위키에 정리가 잘 되어있어 링크로 대체한다.

 

생산자-소비자

독자-저자

식사하는 철학자들

잠자는 이발사

 

위 실행 모델을 이해하자

 

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

 

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

 

정말 필요하다면

 

클라이언트에서 잠금 - 클라이언트에서 첫 번째 메서드를 호출 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지.

 

서버에서 잠금 - 서버에다 "서버를 자금고 모든 메서드를 호출 후 잠금을 해제하는" 메서드를 구현. 클라이언트에서 이 메서드 호출

 

연결 서버 - 중간 서버를 둬서 중간 서버에서 잠금을 수행

 

을 사용하자. (클라이언트에서 잠근이나 서버에서 잠금이나 똑같은 말 아닌가 싶다... 이해가 안 간다.)

 

8. 동기화 부분을 작게 만들자

 

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

 

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

 

9. 올바른 종료 코드는 구현이 어렵다

 

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

 

예를들면 데드락이 있다.

 

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

 

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

 

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

 

하나씩 알아보자.

 

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

 

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

 

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

 

- 멀티 스레드로 개발하기 전에 단일 스레드 환경에서 돌아가게 먼저 개발하자.

 

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

 

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

 

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

 

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

 

  • 스레드 수를 1개, 2개, 3개, 그 이상으로 테스트 해보자.
  • 실환경, 테스트환경에서 돌려본다.
  • 테스트 코드를 빨리, 천천히, 다양한 속도로 돌려보자.

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

정리

멀티 스레드로 구현하긴 어렵다.

 

빨라야 할 거 같은데 빠르지 않은 경우가 엄청 많다.

 

위에 적어놓은 문제들과 미신과 오해를 잘 보고 이해하자.

 

동시성 관련해서 이렇게 짧게 정리한다는 게 맞나싶다.

 

부록에 동시성 부록이 있다. 한번 읽어보자 자세하게 나와있다.

 

나중에 기회가 되면 정리해보겠다.

반응형

댓글