본문 바로가기

JAVA

FOR문은 효율적일까? ExecutorService 사용법

반응형

for문을 통해 많은 데이터를 처리해야 하는 메서드가 있다고 생각해보자.

관리자가 100만명의 회원에게 알림 메시지를 전송해야 하는 경우 인덱스 순서대로 전송한다면

시간도 오래 걸리고, 처음 메시지를 받는 사람의 시간과 마지막 메시지를 받는 사람의 시간 차이는 클 것이다.

 

이를 해소하기 위해 알아보게 된게 ExecutorService 이다.



ExecutorService
비동기 모드에서 작업 실행을 단순화하는 JDK API
병렬 작업 시 여러 개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA 라이브러리

Thread
프로세스안에서 실질적으로 작업을 실행하는 단위

 

물리적 아키텍쳐에서의 소프트웨어적(자바) 스레드

1코어 1스레드 CPU 쓰던 시절에도 자바 스레드는 수십 개씩 띄우는 게 가능했었다. 하드웨어적 스레드가 1개인데 자바 스레드를 수십 개 띄우는 것이 가능하다니, 무언가 말이 이상하지 않은가? 자바 스레드가 무엇인지를 알기 전에, 여기서 잠깐 병렬성(Parallelism)과 동시성(Concurrency)이라는 개념을 집고 가자. 병렬성은 작업들이 병렬로 실행되는 성질을 의미하는 것이다. 위에서 언급한 하드웨어적 스레드는 병렬성을 가지고 있다. 동시성은 여러개의 작업들이 짧은 시간내에 번갈아 가면서 병렬로 처리되는 것처럼 보이도록 실행되는 성질을 의미하는 것이다. 자바 스레드는 동시성을 가지고 있기 때문에 하드웨어적 스레드 개수보다 더 많은 스레드를 생성하여 하드웨어적 스레드를 번갈아 가면서 사용하여 작업을 처리한다. 예를 들어, 다음 그림은 N개의 자바 스레드가 CPU가 2개인 4코어 8스레드를 번갈아 가면서 사용함을 의미한다.

위의 그림에서 어느 순간에 병렬로 실행되는 자바 스레드의 개수는 최대 8개이지만, N개의 모든 자바 스레드가 번갈아 가면서 실행되므로 병렬로 실행되고 있는 것처럼 보일것이다. 이와 같은 스레드를 소프트웨어적 스레드라고 한다. 자바 스레드(소프트웨어적 스레드)는 동시성의 성질을 가지고 있기 때문에, 자바에서는 멀티 스레드와 관련된 프로그래밍을 병렬성 프로그래밍이라 하지 않고 동시성 프로그래밍이라고 한다.

동시성을 지원하면 컨텍스트 스위칭 등 추가적인 비용이 발생함에도 불구하고, 자바는 왜 소프트웨어적 스레드를 채택했을까? 위의 예제에서 사용한 CPU가 2개인 4코어 8스레드를 예로 들어보자. 현재 총 16개의 작업이 있다고 가정을 해보자. 8개는 오래 걸리는 작업, 나머지 8개는 짧은 시간을 필요로 하는 작업이다. 최악의 경우 8개의 오래 걸리는 작업이 동시에 처리될 수도 있는데, 이 경우 나머지 8개의 작업은 처리하는데 짧은 시간이 걸리는 데에도 불구하고 현재 처리중인 8개의 작업이 다 끝날때 까지 기다려야 한다. 만약 짧은 시간을 필요로 하는 작업을 처리한 결과가 필요한 또 다른 작업이 있다면, 이 작업들은 8개의 오래 걸리는 작업 때문에 많은 시간을 기다려야 한다. 이를 방지하기 위해 작업을 번갈아 가면서 처리하는 소프트웨어적 스레드를 채택했을 것이다.

 

 


위 글은 참고용으로 작성했다. 내가 이해한 바로 다시 설명하자면 아래와 같다.

 

동시성 : Alice의 작업이 실행되는 동안 Bob과 Charlie의 작업도 동시에 실행 가능

public class Main {
    public static void main(String[] args) {
        List<String> users = new ArrayList<>();
        users.add("Alice");
        users.add("Bob");
        users.add("Charlie");
        
        for (String user : users) {
            new Thread(() -> {
                System.out.println("Processing user: " + user);
                // 각 사용자에 대한 작업 수행
            }).start();
        }
    }
}

이 FOR문의 경우 동시성은 갖고있지만 스레드 자체는 하나이기 때문에 병렬성은 갖지 않음

FOR문에 list size가 10000개씩 있더라도 병렬처리를 해주지 않는다면 병렬성을 갖지 않음

이 경우 병렬성을 갖는 ExecutorService 를 통해 해소가 가능!

@Service
public class FCMNotificationService {

private final FirebaseMessaging firebaseMessaging;
private final UserRepository userRepository;

private static final int BATCH_SIZE = 100; // 배치 크기
private ExecutorService executorService;

public FCMNotificationService(UserRepository userRepository, 
FirebaseMessaging firebaseMessaging) {
this.userRepository = userRepository;
this.firebaseMessaging =firebaseMessaging;
**this.executorService = Executors.newFixedThreadPool(10);** // 최대 10개의 병렬 작업
}

public void sendNotifications(List<FCMNotificationRequestDto> dtoList) {
for (int i = 0; i < dtoList.size(); i += BATCH_SIZE) {
final int start = i;
final int end = Math.min(i + BATCH_SIZE, dtoList.size());
System.out.println("Processing batch: " + start + " to " + (end - 1));
**executorService.execute(() -> {
try {

String threadName = Thread.currentThread().getName();
System.out.println("Executing in thread: " + threadName);

sendBatchNotifications(dtoList.subList(start, end));
} catch (FirebaseMessagingException e) {
throw new RuntimeException(e);
}
});
}
shutdown();** // 스레드 풀 종료
**}**

private void sendBatchNotifications(List<FCMNotificationRequestDto> dtoList) throws FirebaseMessagingException {

for (FCMNotificationRequestDto dto : dtoList) {
sendAdminNotificationByToken(dto);
}
}

public void shutdown() {
// 스레드 풀 종료
**executorService.shutdown();**
}

  

/**
* 관리자 전용 대량 알림 전송 시 병렬  메서드
* @param requestDto
* @throws FirebaseMessagingException
*/
public void sendAdminNotificationByToken(FCMNotificationRequestDto requestDto) throws FirebaseMessagingException {
Notification notification = Notification.builder().setTitle(requestDto.getTitle()).setBody(requestDto.getBody()).setImage(requestDto.getImage()).build();
Message message = Message.builder().setToken(requestDto.getFirebaseToken()).setNotification(notification).putData("link", requestDto.getClickAction()).build();
firebaseMessaging.send(message);
}

}
  • this.executorService = Executors.newFixedThreadPool(10); // 생성할 스레드 개수
  • executorService.execute(() -> { ... }); // 각 스레드마다 할 일
  • executorService.shutdown(); // 각 스레드가 끝나면 종료

 


DB 저장을 대량으로 해야하는 경우 JPA를 통해 saveAll 메서드를 사용하거나

jdbcTemplate을 통해 대량 저장 코드를 만든 적이 있었지만

병렬성에 대한 개념을 먼저 알았더라면 더 좋은 코드를 작성할 수 있었을 것 같다.

 

 

반응형

'JAVA' 카테고리의 다른 글

StringBuilder 를 왜 쓸까?  (0) 2024.01.07
ajax 데이터 제어 @RequestParam @RequestBody  (0) 2022.08.21
int 와 Integer 의 차이점  (0) 2022.03.23
싱글톤  (0) 2022.03.02