현재 우리 회사가 메일 발송을 위해 사용하는 서비스는 , Microsoft office365이다.
한 계정으로 메일 발송을 할 수 있는 Thread의 개수가 정해져 있다고 공식문서에 나와 있어서, 메일을 발송하는 코드에서도, 비슷한 정도의 Thread를 통해 요청을 보내기를 해야겠다 생각이 들어서, 기존의 메일서버를 수정하게 되었다.
내가 생각한 방법은 Multi-Thread 기반으로 들어가는 Request 요청을 하나의 Event Queue에 담고, 일괄적으로 전송하는 방법을 생각했다.
메일을 발송하는 워크 쓰레드를, 공식문서에 나온 쓰레드 개수만큼 두고, office 365 단일계정을 통해 메일 발송을 요청을 할 생각이다.
이전에 구축했던 메일 서비스에 많은 요청이 들어올 때, 비동기적으로 여러 Thread에서 메일 요청을 보내게 구현이 되어있었다.
우선 개념적으로 여러 Thread 에서 데이터를 한곳에 모으기 위해 Thread-safe한 큐가 필요했고, 몇 가지 조사를 해보니, Java.util.concurrent package에 제공하는 LinkedBlockingQueue를 사용하였다.
내가 생각한대로 구현이 끝난 후, 내가 사용한 자료구조에 대해 좀 더 구체적인 조사가 필요한 거 같아서, LinkedBlocking Queue에 대해 공부해보았더니, 들어는 봤지만, 확실하게 이해하고 있지 않은 많은 부분들이 존재했다.
또한 동시성 이슈에 관해 깊은 이해가 부족하다고 생각이 들어 그와 관련해서 공부를 깊게 해보도록하자..
Index
- 동시성 문제에 관한 용어정리 (1)
- 공유 자원처리 방법 (1)
- 자바에서 프로그래밍에서 동시성 처리하는 법 (2)
- 자료구조를 직접 간단하게 만들어보자 (2)
여러 자원에 대해 프로세스 혹은 스레드가 동시에 접근하면서 발생하는 문제입니다.
가장 간단한 예를 들어보도록 하겠습니다.
각 쓰레드의 작업은, 공유된 자원 (정수) 에 +1 을 하는 작업입니다.
이 작업을 여러 쓰레드로 만들어서 돌려보면
Thread test1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 10000 ; i++){
System.out.println("static num :" + staticNum);
staticNum++;
}
}
});
Thread test2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 10000; i++){
System.out.println("static num :" + staticNum);
staticNum++;
}
}
});
test1.start();
test2.start();
test1.join();
test2.join();
System.out.println(staticNum);

다음과 같이 20000이 나오는것이 아니라, 동시성 이슈가 발생해서 제대로 공유된 자원에 대한 처리가 진행되지 않는것을 볼 수 있습니다.
간단한 예시입니다.. 실제로는 훨씬 복잡한 이야기들이 있지만 이러한 문제가 있을 수 있겠구나 생각하면 좋을거 같습니다.
동시성 문제에서 자주 언급되는 용어들을 정리해보겠습니다
여러 프로세스들이 공유 자원에 동시에 접근하려는 상황을 경쟁 상태라고 한다.
어떤 프로세스(쓰레드)가 마지막으로 데이터에 접근했는지에 따라 데이터 상태가 달라지게 된다. 경쟁 상태의 문제를 해결하기 위해 프로세스(쓰레드)는 동기화 처리가 필요하다. (쓰레드의 순차적 실행)
임계 영역은 공유자원이 접근되는 부분을 의미한다. 즉 코드의 일부분 (작동의 일부분)이다.
이 임계 영역에 의해 생성되는 다음과 같은 개념들을 알아보자.
'조금만 기다리면 바로 쓸 수 있는데 굳이 Context Switching으로 부하를 줄 필요가 있나?'라는 컨셉으로 개발된 것으로 Critical Section에 진입이 불가능할 때 컨텍스트 스위칭을 하지 않고 잠시 루프를 돌면서 재시도를 하는것을 말합니다.
스핀 락(Spin lock)은 임계 구역에 진입이 불가능할 때 진입이 가능할 때까지 루프를 돌면서 재 시도하는 방식으로 구현된 락을 가리킵니다.
임계 구역 진입 전까진 루프를 계속 돌고 있기 때문에 busy waiting이 발생하게 됩니다.
여기서 Busy Waiting 이란? ( 바쁘게 기다린다는 것은 무한 루프를 돌면서 최대한 다른 스레드에게 CPU를 양보하지 않는 것이다. )
Lock-UnLock 과정이 아주 짧아서 락하는 경우가 드문 경우 유용하다고 합니다.
(계속해서 확인하기 때문에 이런 특징이 있습니다.)
Lock이 곧 사용가능해질 경우 Context Switching을 줄여 CPU의 부담을 덜어준다.하지만, 만약 어떤 스레드가 Lock을 오랫동안 유지한다면 오히려 CPU 시간을 많이 소모할 가능성이 있습니다.
하나의 CPU나 하나의 코어만 있는 경우에는 유용하지 않다.그 이유는 만약 다른 스레드가 Lock을 가지고 있고 그 스레드가 Lock을 풀어 주려면 싱글 CPU 사용률 100%를 만드는 상황이 발생하므로 주의해야합니다.
(공유자원 접근 쓰레드 수를 두어 여러쓰레드로 접근이 가능하게 하는방식)
세마포어는 동기화를 위해 wait와 signal이라는 2개의 atomic operations를 사용한다.
wait를 호출하면 세마포어의 카운트를 1줄이고, 세마포어의 카운트가 0보다 작거나 같아질 경우에 락이 실행됩니.
#cf) 이렇게 보면 뮤텍스와 세마포어가 비슷해보이지만, 차이가 존재합니다.
뮤텍스는 Locking 메커니즘으로 락을 걸은 쓰레드만이 임계 영역을 나갈때 락을 해제할 수 있다.
하지만 세마포어는 Signaling 메커니즘으로 락을 걸지 않은 쓰레드도 signal을 사용해 락을 해제할 수 있다.
세마포어의 카운트를 1로 설정하면 뮤텍스처럼 활용할 수 있다.
여기까지는 개념적인글이었다. 다음에는, Java 에서 이러한 동시성 문제를 어떻게 해결하는지 이야기 해보도록 하겠습니다 🙂
https://www.youtube.com/watch?v=vp0Gckz3z64
https://www.youtube.com/watch?v=gTkvX2Awj6g
https://7357.tistory.com/339