Java 비동기 로직을 동기 처리하기 with ChatGPT

@ZungTa · 2024-01-02 화요일 · 3 min read

Problem
Solution
result 변수 값 할당 대기
더 편리한 방법
코드 결과

Problem

Spring Boot에서 Redis를 API처럼 사용하는 기능을 구성하고 있었다.

나는 NodeJS에서 Closure 사용하듯이 평소처럼 로컬 변수를 새로 생성한 인스턴스의 함수에서 사용하려고 했다.

public String dummyCommand() {
  String result;

  MessageListener messageListener = new MessageListener() {
    @Override
    public void onMessage(Message message, byte[] pattern) {
      result = message.toString();
      redisMessageListenerContainerForBsDs.removeMessageListener(this);
    }
  };

  redisMessageListenerContainerForBsDs.addMessageListener(messageListener,
      new ChannelTopic("result"));

  return result;

}

그랬더니 result에 message.toString()값을 대입하려고 하는 부분에 이런 오류가 발생했다.

Variable 'result' is accessed from within inner class, needs to be final or effectively final

그리고 무시하고 실행 했더니 이런 오류가 발생했다.

error: variable result might not have been initialized
    return result;
           ^

이 오류의 원인은 두 가지이다.

  1. result 변수가 초기화되지 않았고 값이 할당되는 부분인 onMessage가 비동기적으로 실행되기 때문에 result 변수가 초기화 되기 전에 dummyCommand 함수가 값을 반환해버리기 때문이다.
  2. 로컬 변수인 result를 inner class 안에서 접근하는 것도 문제가 되어서 final로 선언하라고 하는 것 같다.

1번 문제에 대해서는 반복문을 사용해서 1초마다 result 값을 검사해서 null이 아니면 반환하려고 하긴 했었다. 근데 어차피 inner class에서 변수에 값 할당이 안되는 상황이라 불가능했다.

Solution

ChaptGPT의 답변을 통해 해결책을 마련할 수 있었고 해당 해결책을 분할하여 생각해보며 적용했다.

또한 AtomicReference, CountDownLatch 이 두 가지 새로운 키워드에 대해서 나중에 따로 학습해 볼 예정이다.

result 변수 값 할당 대기

result를 AtomicReference로 바꿔서 사용하면 된다.

값 할당은 set, 값 조회는 get으로 하면 된다.

그리고 return을 하기 전에 무한 루프로 result를 체크하면 된다.

참고로 이렇게 return을 지연시키는 이유는 dummyCommand 함수가 Service 레이어의 함수. 즉 저 함수의 return이 API의 응답이 되기 때문이다.

public String dummyCommand() {
  AtomicReference<String> result = new AtomicReference<>();

  MessageListener messageListener = new MessageListener() {
    @Override
    public void onMessage(Message message, byte[] pattern) {
      result.set(message.toString());
			...
    }
  };

  ...

  while (true) {
    if (result.get() != null) {
      break;
    }
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  return result.get();
}

더 편리한 방법

무한 루프를 돌리는 방법보다 더 깔끔한 방법이다.

CountDownLatch를 이용해서 await을 해주면 latch의 카운트가 0이 될 때까지 기다리는 것 같다.

public String dummyCommand() {
  CountDownLatch latch = new CountDownLatch(1);
	...

  MessageListener messageListener = new MessageListener() {
    @Override
    public void onMessage(Message message, byte[] pattern) {
	    ...
      latch.countDown();
    }
  };
	...

  try {
    latch.await();
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }

  return result.get();
}

코드 결과

public String dummyCommand() {
  CountDownLatch latch = new CountDownLatch(1);
  AtomicReference<String> result = new AtomicReference<>();

  MessageListener messageListener = new MessageListener() {
    @Override
    public void onMessage(Message message, byte[] pattern) {
      result.set(message.toString());
      redisMessageListenerContainerForBsDs.removeMessageListener(this);
      latch.countDown();
    }
  };

  redisMessageListenerContainerForBsDs.addMessageListener(messageListener,
      new ChannelTopic("result"));

  try {
    latch.await();
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }

  return result.get();

}
@ZungTa
I'm a backend developer
© ZungTa Devlog, Built with Gatsby