동시성 테스트 재현하다가…

머리로는 알겠는데 가슴은 왜 지맘대로인지

개발을 하다보면 동시성 에러를 마주한적이 한번이라도 있을것입니다. 저 또한 이러한 동시성 문제들을 여러번 마주한적이 있는데요. 에러와 코드를 보면서 읽다보면 대충 어떤 부분이 동시에 실행되면서 생긴 에러인지 가늠할 수 있었을겁니다. 그래도 확실하게 내 눈으로 확인을 해보고싶은 개발자 분들이 계실겁니다.

머리로는알겠는데

하지만 저와 같이 아직 미숙한 분들은 이것을 재현하는게 너무 어려울겁니다. 저의 경우에는 확인해보기 위해 해당하는 코드에 break point를 걸고 테스트를 해보려고 했는데요. 첫번째 요청은 해당 코드에서 스레드가 잘 멈추었지만 왜인지 그 이후 아무리 요청을 해도 애플리케이션에서 요청을 안받고 대기하게 만들더라구요..

저는 이 시점에서 바보같은 결론을 맺었습니다. “아 디버그모드에서는 스레드를 하나밖에 이용을 못하는구나.. “

그리고는 빠르게 동시 요청할 수 있는 툴(jmeter, vegeta 등)을 찾아다녔으며 해당 코드에 Thread.sleep 등을 걸어놓고 브레이크 포인트 없이 진행을 하고는 했었답니다..

답답

어? 이런게 있었어??

그러다가 제가 잘못된 디버깅 옵션을 사용하고 있었음을 거의 최근에 깨닳았죠..

상상도못한정체

브레이크 포인트 위에서 우클릭을 하면 놀랍게도(?) 아래와 같은 화면이 나오는데요. 아래화면에서 주목해야 할 부분은 Suspend입니다. 옵션이 All ,Thread 두 가지가 있습니다. 대략 설명하자면 All을 선택하게 되면 어떠한 스레드가 해당 브레이크포인트에 도착하고 나서부터는 해당 스레드만 컨트롤할 수 있다는 것이며 다른 나머지 스레드들은 모두 일시중지 상태가 됩니다. Thread를 선택하게 되면 하나의 스레드가 아닌 해당 브레이크 포인트에 걸리는 모든 스레드들이 해당 라인에서 일시중지 상태가 됩니다. 예시 화면으로 아래의 두번째 사진과 같이 2개의 스레드가 해당 라인에서 멈춰있음을 알 수 있죠.

브레이크포인트 옵션

두개의 스레드

위에서 간단하게 설명을 했으니 이걸 코드로 만들어서 실습을 해보는 시간을 가져봅시다.

life-is-real-practice

인생은 실전이야..

동시성 테스트를 위해 스위치를 on/off 시키는 클래스를 만들어보죠. 아래와 같이 Switch 클래스를 만들텐데 실습을 위해 버그를 포함하는 코드이니 이번 실습 이후에는 잊어주세요..

public class Switch {
    private static final Logger logger = LoggerFactory.getLogger("Switch");
    private boolean light = false;

    private static Switch aSwitch = new Switch();

    private Switch () {}

    public static Switch getInstance() {
        return aSwitch;
    }

    public void switching() {
        boolean cur = light;
        logger.info("{}", cur ? "ON" : "OFF");
        light = !cur;
        logger.info("{}", light ? "ON" : "OFF");
    }

    public boolean getLight() {
        return light;
    }
}

switching 메서드를 호출하면 현재 상태를 cur 변수에 저장하고 이 변수 값의 반대를 light 에 업데이트해줍니다. 그리고 테스트코드를 작성하여 테스트를 진행해보도록 합시다.

public class SwitchTest {

    @Test
    public void 꺼진_스위치_켰다_끄기() {
        // 스레드풀 생성 (스레드 개수:2개)
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        Switch aSwitch = Switch.getInstance();

        // async running
        CompletableFuture<Void> ret1 = CompletableFuture.runAsync(() -> aSwitch.switching(), executorService); // false -> true
        CompletableFuture<Void> ret2 = CompletableFuture.runAsync(() -> aSwitch.switching(), executorService); // true -> false

        CompletableFuture.allOf(ret1, ret2).join(); // blocking..

        assertFalse(aSwitch.getLight()); // false
    }
}

테스트 제목과 같이 스위칭하는 액션을 두번할것입니다. 스위칭을 두번하게되면 첫번째 액션에서 off -> on이 될 것이고, 두번째 액션에서 on -> off로 되면서 최종적으로는 꺼짐 상태가 되겠지요?? 하지만 실제로 실행시켜보면 아래와 같은 로그와 테스트 실패를 보게 됩니다. 이런 현상을 눈으로 확인해보기 위해 디버깅을 해보도록 합시다.

로그확인

클래스의 다음 라인에 브레이크 포인트를 걸어놓고 suspend를 Thread로 설정해봅시다.

옵션조정

그리고 테스트를 디버그모드로 실행합니다.

디버그모드실행

그러면 다음과 같이 두 개의 스레드가 동일한 라인에서 멈춰있는 것을 볼수 있습니다.

두개의스레드가걸려있다

간단하게 예제를 만들어서 어떻게 테스트를 하는건지 살펴보았는데요. 너무 어거지로 만든 예제이다 보니 불만이 많으시겠지만 이러한 방식으로 동시성 테스트를 해볼수 있었다는 것에만 알아주시면 감사하겠습니다. 포스팅은 여기에서 마무리하도록 하겠습니다! 읽어주셔서 감사합니다~!!