본문 바로가기
Blog/TIL

[240621] 동시성 테스트, BT

by 코젼 2024. 6. 21.
728x90
반응형

🔶동시성 테스트

🔶CompletableFuture

🔶트리

🔶이진탐색트리 BST


목차

    / 오늘의 TIL /


    비동기

    비동기(asynchronous)는 한 가지 작업이 끝날 때까지 다른 작업을 기다리지 않고 동시에 여러 작업을 처리할 수 있는 방식을 의미합니다. 주로 다음과 같은 상황에서 사용됩니다:

     

    1. 성능 향상: 특히 네트워크 호출이나 파일 입출력 등의 작업에서는 대기 시간이 발생할 수 있습니다. 이 때 비동기 방식을 사용하면 대기 시간 동안 다른 작업을 수행하여 전체 시스템의 성능을 향상시킬 수 있습니다.

    2. 응답성 향상: 사용자 인터페이스에서 비동기 방식을 사용하면 UI가 멈추지 않고 여러 작업을 동시에 처리할 수 있어 사용자 경험을 개선할 수 있습니다.

    3. 자원 활용: 멀티코어 프로세서 시스템에서는 비동기 작업을 사용하여 여러 코어를 효율적으로 활용할 수 있습니다.

     

    비동기 작업은 다음과 같은 방식으로 구현될 수 있습니다:

     

    콜백(callback): 작업이 완료되면 특정 함수(콜백 함수)가 호출되어 결과를 처리합니다.

    퓨처(future): 작업이 완료될 때까지 대기하고, 완료되면 결과를 가져오는 방식입니다.

    프로미스(promise): 퓨처와 유사하지만 미래의 값을 나타내는 대신 값을 생성하는 프로세스를 나타냅니다.

    CompletableFuture: Java에서 제공하는 기능으로, 비동기적으로 실행할 작업을 구성하고 조합할 수 있는 방법을 제공합니다.

     

    비동기 작업을 잘 활용하면 시스템의 응답성과 성능을 향상시킬 수 있지만, 동기적 작업보다 복잡성이 높을 수 있으며, 적절한 처리와 예외 처리가 필요합니다.

     

    비동기 작업의 구현 방식에 따라 스레드 사용 여부가 달라집니다:

     

    1. 단일 스레드 환경: 일부 비동기 프로그래밍 모델은 단일 스레드에서 동작합니다. 이 경우 비동기 작업들은 단일 스레드에서 순차적으로 실행되지만, 이벤트 루프(Event Loop)와 같은 메커니즘을 통해 비동기 작업의 완료를 기다리지 않고 다음 작업을 수행할 수 있습니다. JavaScript의 Node.js 환경이 이에 해당합니다.

    2. 멀티 스레드 환경: 다른 비동기 프로그래밍 모델들은 멀티 스레드 환경에서 동작합니다. 이 경우 여러 개의 스레드가 동시에 실행되어 병렬로 여러 작업을 처리할 수 있습니다. Java의 CompletableFuture나 Python의 asyncio 라이브러리에서 제공하는 비동기 작업은 이에 해당합니다.

     

    간단히 정리하면, 단일 스레드 환경에서는 비동기 작업이 하나의 스레드에서 실행되며 이벤트 루프를 통해 비동기 작업의 완료를 기다리지 않고 다음 작업을 처리할 수 있습니다. 반면 멀티 스레드 환경에서는 여러 개의 스레드가 동시에 실행되어 병렬로 여러 작업을 처리할 수 있습니다.


    동시성 테스트

    메모리 Stub 을 사용하고 있어서 @Transactional 을 사용할 수 없는 상황인 경우

    ExecutorService 를 사용하여 병렬 실행할 수 있다.

    @SpringBootTest
    class Test {
      @Autowired
      private final UserService userService;
      
      @Test
      void 동시성테스트() {
        // given
        userService.chargePoint(1, 100000);
        // when
        CompletableFuture.allOf(
          CompletableFuture.runAsync(() -> {
            userService.usePoint(1, 10000);
          }),
          CompletableFuture.runAsync(() -> {
            userService.chargePoint(1, 4000);
          }),
          CompletableFuture.runAsync(() -> {
            userService.usePoint(1, 100);
          })
        ).join() // 제일 오래 끝나는거 끝날떄까지 기다려줌. = 내가 비동기/병렬로 실행한 함수가 전부 끝남을 보장.
          
        // Thread.sleep(); // 야 ? 위에서 하나가 뭔가 문제가 있어가지고 이거보다 오래 돌았어 그럼 테스트 보장이 안되잖아.
        
        // then
        UserPoint userPoint = userService.getBy(1);
        // 수식으로 검증해서 테스트 작성자의 오류도 줄인다.
        assertThat(userPoint.getPoint()).isEqualTo(100000 - 10000 + 4000 - 100);
      }
    }

    ExecutorService

    각각의 작업을 별도의 스레드로 실행하고, CompletableFuture.allOf() 를 사용하여 모든 작업이 완료될 때까지 기다린다.

    ExecutorService executorService = Executors.newFixedThreadPool(3);
    
    CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
        // userService.usePoint(1, 10000);
    }, executorService);
    
    CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
        // userService.chargePoint(1, 4000);
    }, executorService);
    
    CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> {
        // userService.usePoint(1, 100);
    }, executorService);
    
    CompletableFuture.allOf(future1, future2, future3).join();

     

    CompletableFuture

    • allOf()
      • 병렬로 실행하고, 모든 CompletableFuture의 완료를 기다린다.
      • 모든 CompletableFuture가 완료될 때까지 기다리는 새로운 CompletableFuture를 반환한다.
      • 배열의 순서대로 결과가 완료되지 않아서 실행 순서가 보장되지 않는다.
    • runAsync()
      • 결과를 반환하지 않는 비동기 작업을 정의할 때 사용됨 
      • Runnable을 인자로 받는다.
      • Runnable은 결과를 반환하지 않고 단순히 수행하는 역할을 한다.
    • supplyAsync()
      • 결과를 반환하는 비동기 작업을 정의할 때 사용됨 
      • 비동기로 실행되는 작업을 시작하는 메서드 
      • CompletableFuture가 완료되면 이전 CompletableFuture의 결과를 받아서 처리하는 함수를 제공한다.
      • thenApply()
        • CompletableFuture의 결과를 가공하거나 변환할 때 사용한다.
        • 이전 CompletableFuture가 정상적으로 값을 리턴하면 해당 값을 인자로 받아 함수를 실행하고, 그 결과를 새로운 CompletableFuture에 저장해서 반환한다.

    -> 따라서 supplyAsync()는 비동기로 작업을 시작하고, thenApply()는 이전 작업의 결과를 받아서 다음 작업을 수행한다.

    이들 메서드는 CompletableFuture의 연결된 작업들을 조립하여 복잡한 비동기 흐름을 구성하는 데 사용된다.

     

    Exception

    CompletableFuture의 join 메서드를 실행하면서 생긴 exception은 CompletionException 이고, 검증할 수 있다.

        @Test
        @DisplayName("잔여 포인트보다 사용하려는 포인트가 많은 경우")
        void pointGreaterThanBalanceTest() {
    
            //given
            long id = 1L;
            userPointTable.insertOrUpdate(id, 100L);
    
            //비동기 처리
            CompletableFuture<Void> future = CompletableFuture.allOf(
                    CompletableFuture.supplyAsync(() -> {
                        //100 포인트 충전
                        return pointService.charge(id, 100L);
                    }).thenApply(result -> {
                        //3000 포인트 사용
                        return pointService.use(id, 3000L);
                    })
            );
    
            //join 호출 시, 발생한 exception catch
            Throwable throwable = catchThrowable(future::join);
    
            //exception 발생 검증
            assertThat(throwable)
                    .isInstanceOf(CompletionException.class)
                    .hasCauseInstanceOf(IllegalArgumentException.class);;
        }

    트리

    https://school.programmers.co.kr/learn/courses/30/lessons/42892

     

    import java.util.*;
    
    class Solution {
        private static class Node {
            int x, y, num; //노드의 좌표, 번호 저장
            Node left, right; //노드의 왼쪽, 오른쪽 자식 노드
    
            public Node(int num, int x, int y) {
                this.num = num;
                this.x = x;
                this.y = y;
            }
        }
    
        //이진 트리 생성 메서드
        private static Node makeBT(int[][] nodeinfo) {
            //각 노드에 대한 좌표, 번호를 배열에 저장
            Node[] nodes = new Node[nodeinfo.length];
            for (int i = 0; i < nodeinfo.length; i++) {
                nodes[i] = new Node(i+1, nodeinfo[i][0], nodeinfo[i][1]);
            }
    
            //y 기준으로 내림차순 정렬, y가 같다면 x 기준으로 오름차순 정렬
            Arrays.sort(nodes, ((o1, o2) -> {
                if (o1.y == o2.y) return Integer.compare(o1.x, o2.x);
                return Integer.compare(o2.y, o1.y);
            }));
    
            Node root = nodes[0]; //루트 노드
    
            for (int i = 1; i < nodes.length; i++) {
                Node parent = root;
                while (true) {
                    //부모 노드의 x좌표가 더 크면 왼쪽으로
                    if (nodes[i].x < parent.x) {
                        if (parent.left == null) {
                            parent.left = nodes[i];
                            break;
                        } else parent = parent.left; //left 서브 트리 노드를 parent로 변경하고 null이 나올 때까지 진행
                    }
                    //부모 노드의 x좌표가 더 작거나 같으면 오른쪽으로
                    else {
                        if (parent.right == null) {
                            parent.right = nodes[i];
                            break;
                        } else parent = parent.right; //right 서브 트리 노드를 parent로 변경하고 null이 나올 때까지 진행
                    }
                }
            }
            return nodes[0];
        }
    
        //전위 순회 메서드
        private static void preOrder(Node curr, ArrayList<Integer> answer) {
            if (curr == null) return; //리프 노드 종료
            answer.add(curr.num);
            preOrder(curr.left, answer);
            preOrder(curr.right, answer);
        }
        
        //후위 순회 메서드
        private static void postOrder(Node curr, ArrayList<Integer> answer) {
            if (curr == null) return; //리프 노드 종료
            postOrder(curr.left, answer);
            postOrder(curr.right, answer);
            answer.add(curr.num);
        }
    
        /**
         * @param nodeinfo 이진트리 노드 좌표 배열
         * @return 전위 순회, 후위 순회한 결과
         */
        public static int[][] solution(int[][] nodeinfo) {
            Node root = makeBT(nodeinfo); //이진 트리 생성
            ArrayList<Integer> preOrderList = new ArrayList<>();
            preOrder(root, preOrderList); //전위 순회
            ArrayList<Integer> postOrderList = new ArrayList<>();
            postOrder(root, postOrderList); //후위 순회
            
            //결과 반환
            int[][] answer = new int[2][nodeinfo.length];
            answer[0] = preOrderList.stream().mapToInt(Integer::intValue).toArray();
            answer[1] = postOrderList.stream().mapToInt(Integer::intValue).toArray();
            
            return answer;
        }
    }

     

    이진 탐색 트리 (BST)

    • 노드의 왼쪽 서브 트리에는 루트 노드의 키보다 작은 키가 있는 노드만 포함된다.
    • 노드의 오른쪽 서브 트리에는 루트 노드의 키보다 큰 키가 있는 노드만 포함된다.
    • 중복된 키를 허용하지 않는다.
    728x90
    반응형

    'Blog > TIL' 카테고리의 다른 글

    [240905] 코딩 테스트 풀이  (0) 2024.09.05
    [240626] JS 문법  (0) 2024.06.26
    [240620] ArgumentMatchers  (0) 2024.06.20
    [240619] Mockito  (0) 2024.06.19
    [240618] 트리 + 해시맵  (0) 2024.06.18

    댓글