반응형

Chapter07 병렬 데이터 처리와 성능

자바의 병렬

자바 7 이전의 버전에서는 병렬의 처리가 어려웠다.
분할을 위한 스레드 할당 → 동기화 추가 → 결과 합치기의 과정을 통해 병렬 처리가 실행되는데, 각 병렬처리를 한 후에 스레드를 합칠 때 동기화를 적절히 이뤄줘야 교착 상태를 피할 수 있었다.
7버전 부터 지원하기 시작한 Fork/Join 프레임워크를 활용하면 해당 문제를 쉽고 효율적으로 해결할 수 있다. 이 Fork/Join 프레임워크를 활용한 스트림 API와 병렬 처리에 대해서 배워보자.


병렬 스트림

  • 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림
  • 멀티코어 프로세서가 각각의 청크를 처리하도록 한다
  • Collections.parallelStream() or Arrays.stream().parallel()
    • Pipeline에있는 parallel 상태에 true가 저장된다.
    • 이후 파이프라인 처리할 때 처리할 때 parallel 체크를 통해 해결한다.
  • 병렬 처리를 고려할 때는 성능 벤치마킹을 하는 것을 권장
  • 성능 최적화
    • 기본형(primitive) 타입의 경우 기본형 특화 스트림 권장 - 오토박싱의 이유
    • 일반적으로 순차적인 처리가 필요한 스트림의 경우 병렬 비용이 더욱 비싸 느릴 가능성이 높음
    • 순차 처리가 필요없는 경우 findAny같은 순서에 상관 없는 쇼트 서킷, unordered 같은 메서드 사용
    • 하나의 요소를 처리하는 데 드는 비용이 비싸다면 고려
    • 스트림 자료구조의 특성을 확인 ex) LinkedList vs ArrayList 일반적으로 좋음
    • 병합 과정의 비용을 생각해야 한다
    | 소스 | 분해성 |
    | --- | --- |
    | ArrayList | Excellent |
    | LinkedList | Bad |
    | IntStream.range | Excellent |
    | Stream.iterate | Bad |
    | HashSet | Good |
    | TreeSet | Good |
    | Stream.generate | iterate보단 낫다 |

parallel처리 관련 이미지

Untitled

Untitled

Untitled

포크/조인 프레임워크

  • 병렬 작업할 때 사용

  • 재귀적으로 큰 작업을 작은 작업으로 분할한 후 서브태스크의 결과를 합쳐서 전체 결과를 만듦

  • 스레드 풀(ForkJoinPool)의 작업자 스레드에 분산 할당하는 ExecutorService

    • ExecutorService를 왜 언급하는가?

      RecursiveTask를 실제로 활용했을 때 해당 추상 클래스의 부모인 ForkJoinTask에서 ForkJoinPool을 활용해서 실제 활용하고있기 때문

      RecursiveTask → ForkJoinTask에서 사용하는 ForkJoinPoolExcutorService

      Untitled

      Untitled

  • 분할 정복(divide-and-conquer) 알고리즘의 병렬화

  • compute() 메서드 오버라이딩 해서 구현

  • 병렬처리 시 주의점

    • join 호출은 두 서브태스크가 모두 시작된 다음에 해야한다.
    • ForkJoinPool의 invoke메서드는 병렬 계산 시작하는 시점에서 한번만 사용
    • 한쪽은 fork() 한쪽은 compute() ⇒ 같은 스레드 재사용 피하기 위함
    • 디버깅이 어렵다 ⇒ 스택이 아닌 다른 스레드 이기 때문
    • 병렬 처리가 무조건 빠르지 않다.
  • 작업 훔치기(work stealing) 특성을 갖고있음

  • ForkJoin을 활용한 RecursiveTask 예시

import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

public class ForkJoinSumCalculator extends RecursiveTask<Long> {

    //THRESHOLD 이상의 값 까지만 분해
  public static final long THRESHOLD = 10_000;

  private final long[] numbers;
  private final int start;
  private final int end;

  public ForkJoinSumCalculator(long[] numbers) {
    this(numbers, 0, numbers.length);
  }

  private ForkJoinSumCalculator(long[] numbers, int start, int end) {
    this.numbers = numbers;
    this.start = start;
    this.end = end;
  }

  @Override
  protected Long compute() {
    int length = end - start; //해당 태스크에서 더할 배열의 길이
    if (length <= THRESHOLD) {
      return computeSequentially();
    }
        //각 태스크 분리 left, right
    ForkJoinSumCalculator leftTask =
        new ForkJoinSumCalculator(numbers, start, start + length / 2);
        //생성한 태스크 비동기 실행
    leftTask.fork();
    ForkJoinSumCalculator rightTask =
        new ForkJoinSumCalculator(numbers, start + length / 2, end);
        //두번째 태스크 동기 실행
    Long rightResult = rightTask.compute();
        //비동기 실행했던 left의 결과를 읽거나 처리완료 후 읽기까지 대기
    Long leftResult = leftTask.join();
    return leftResult + rightResult;
  }

    //가장 작은 단위일 때 작은 단위로 쪼갠 태스크의 결과를 계산
  private long computeSequentially() {
    long sum = 0;
    for (int i = start; i < end; i++) {
      sum += numbers[i];
    }
    return sum;
  }

    public static long forkJoinSum(long n) {
    long[] numbers = LongStream.rangeClosed(1, n).toArray();
    ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
    return FORK_JOIN_POOL.invoke(task);
  }
}

//호출 방법
ForkJoinSumCalculator.forkJoinSum(long n));

Spliterator 인터페이스

  • 구성 요소
    • booleaen tryAdvance(Consumer<? super T> action)
      • 요소를 하나씩 순차적으로 돌며 요소가 남아있는지 확인
    • Spliterator trySplit()
      • 일부 요소를 분할해서 새로운 Spliterator 생성
    • long estimateSize()
      • 탐색해야 할 요소 수 정보 제공(RecursiveTask의 THRESHOLD 역할로 보임)
    • int characteristics()
      • 다양한 특성 집합을 포함(ORDERED, DISTINCT 그 외 다수)

7장을 마치며

  • 내부 반복을 통해 명시적으로 스트림 병렬처리
  • 병렬처리가 무조건 빠르지 않음 특성과 병렬처리 후의 결과 등을 잘 확인하여 고려할 것
  • 병렬 처리에는 포크/조인 프레임워크를 활용한다
  • Spliterator를 통해 병렬 처리를 직접 정의할 수 있다.
반응형
반응형

빌더는 Capacity와 count를 갖고있다. 빌더 안에서 문자열을 합쳐도 StringBuilder의 capacity를 넘지 않는다면 new 연산을 하지 않음

이유는 아래 설명을 통해 풀어가보겠다

StringBuilder : 기존의 capacity일 때는 새로운 메모리에 문자열을 추가하는 것이 아닌 기존의 메모리에 담기 때문에.

String은 불변하고, 불변성의 특징을 갖기 때문에 문자열을 추가할 때는 두개를 서로 붙여서 새로운 메모리를 할당하는 new 연산이 들어감

추가로 StringBuilder와 기능은 동일한 StringBuffer를 사용한다면 성능이 약간 낮아지는 대신에 Thread-Safe하다는 것까지 체크하면 좋을 것 같다.

문자열 합칠 때 비교

  • String : 무조건 new String(a + b);
  • StringBuilder : a.length+b.length < capacity ? StringBuilder(a + b) : new StringBuilder(a + b);

String클래스의 배열 final byte[]

 

StringBuilder는 추상클래스AbstractStringBuilder를 상속받고, 해당 클래스에서 char[]배열은 final선언이 안되어있음

 

때문에 동작은 아래와 같음

 

결론 : 문자열을 더할 때는 String 대신 StringBuilder나 StringBuffer를 사용하자.

참고 사이트 : https://cjh5414.github.io/why-StringBuffer-and-StringBuilder-are-better-than-String/

https://readystory.tistory.com/141

반응형
반응형

공부하며 가볍게 정리

람다


람다식은 함수(메서드)를 간단한 식으로 표현하는 것

람다 작성 방법 : 메서드의 이름과 반환타입을 제거하고 →를 블록{} 앞에 추가한다

ex)

int max(int a, int b){
	return a > b ? a : b;
}
(int a, int b) -> {
	return a > b ? a : b;
}

람다식을 활용하여 사용할 때는 함수형 인터페이스를 통해서 람다를 구현할 수 있어야한다.

interface를 자체적으로 선언해서 활용해도 되고, 기존에 있는 Functions 패키지에 들어있는 여러 클래스의 의미를 명확히 아는 것이 좋을 것 같다. 그 이유는 자바에서 지원하는 내부 클래스에는 함수형 인터페이스를 매개로 동작하는 메서드들이 있는데 그 메서드를 사용하기 위해서는 함수형 인터페이스가 어떻게 활용되는지 의미라도 파악을 해야 사용할 수 있을 것이기 때문이다.

함수형 인터페이스로는 대표적으로 네가지가 있고 바로 아래 쓴 메서드가 해당 함수를 실행하는 메서드이다.

  • Supplier<T> - 공급자 : 매개변수는 없고 반환값만 있다
    • get()
  • Consumer<T> - 사용자 : 매개변수는 있고, 반환값이 없다
    • accept()
  • Function<T, R> - 함수 : 일반적인 함수형태, 하나의 매개변수를 받아 결과를 반환
    • apply()
  • Predicate<T> - 매개변수 하나를 받아서 boolean타입으로 반환한다
    • test()

T는 제네릭타입을 뜻하고, R은 리턴타입을 뜻한다.

추가로 Bi가 붙은 형태가 있는데 다른 방법은 똑같고 매개변수가 두개 들어간다는 의미이다.

 

스트림


스트림은 jdk1.8버전부터 도입이 되었는데, 도입 이유는 Collection 인터페이스의 자식인 List, Set, Map과 배열등은 이전 버전까지는 같은 메서드여도 동작하는 형태가 달랐다. 때문에 이를 통일된 방식으로 처리를 하기 위해 표준화해서 등장한 것이다.

ex) 배열, List, Set, Map의 정렬방식이 서로 다름

장점으로는 데이터 소스를 추상화하여 표준화 된 방식으로 사용이 가능하고, 또 원본의 코드를 수정하지 않기 때문에 코드의 재사용성이 높아진다. 또한 간결하게 표현이 가능해 가독성이 높아진다.

단점으로는 단순한 로직에서 데이터의 양이 적다면 순환하는데 드는 비용이 기본형의 래퍼클래스로 인해 더 크다는 것을 인지할 수 있다.(박싱 언박싱 반복)

스트림은 중간연산, 최종연산이 있고, 유의해야 할 점으로는 스트림으로 변환하면 1회만 사용되고 버려지는데, 이러한 특징으로 인해 최종연산을 실행하면 해당 스트림이 사라지는 것을 인지하고 데이터 처리를 해야한다.

중간연산 : 순차적이지 않고 상황에 맞게 커스텀된다.(스트림의 지연연산특징)

최종연산 : 중간연산을 상황에 맞게 활용 이후 출력된 데이터를 최종처리할 연산을 작성

 

Optional<T> - T타입 객체의 래퍼클래스

  • 해당 객체가 null인지 판단을 하기 때문에 nullSafe하다
  • null체크를 하는데 드는 비용이 줄어든다(if문, try catch 사용필요x)

 

 

반응형
반응형

이번 프로젝트에서 리더보드를 구현하는 부분을 맡게 되었다.

리더보드에서는 해당 유저의 엔티티를 통해 하나의 값을 기준으로 내림차순으로 나열하는 방식이였는데, 때문에 JPA를 통해서 findAll로 받고 java에서 Arrays.sort형식으로 구현하려 했었는데 막상 구현하려고 하니 DB에서 데이터를 findAll로 받아서 Java로 구현한다면

"자바를 통해 다시한번 해당 데이터를 처리해야하기에 메모리에 손해가 있지 않을까?"

라는 의문을 시작으로 그렇다면 DB를 사용해서 정렬을 활용하면 더 빠를까?라는 생각을 통해 기본적으로 pk값인 id는 인덱스가 걸려있어서 속도걱정을 안해도 되겠지만 다른 특정한 값에 인덱스를 넣지 않고 orderByDesc를 해야하는 상황이였기에 이게 빠른지 저게 빠른지 모르는 상태였다.

현재는 프로젝트가 대규모 데이터를 갖고있지는 않기에 얼마나 성능적인 면에서 차이가 있을지는 모르겠어서 내가 할 수 있는 최대한의 방법인 검색을 통해 데이터를 수집해 보았고, 정리를 해 보았다.

  • 해당 데이터가 DB측에서 정리가 가능게 되어있는 가공된 데이터들 orderByDesc, Asc 등등 예외사항이 나올 수 없는 경우에 일반적으로 DB에서 처리하는 것이 빠르고 안정적
  • 위에서 언급했던 자바에서 해당 데이터를 한번 더 처리해야하기에 O(2n)의 시간복잡도를 가지게 될 것이라 생각한다.

 

GPT를 통한 답변도 비슷하게 나왔다.

JPA로 정렬하고 가져오는 것과 JPA의 findAll을 통해서 리스트를 가져온 다음 Java로 메서드를 작성해서 정렬하는것 어느게 더 빨라?
JPA
(Java Persistence API
)를 이용한 데이터베이스 쿼리와 Java의 메서드를 이용한 데이터 정렬은 각각 장단점이 있습니다
다음은 주요 차이점과 고려사항입니다

JPA로 정렬하고 가져오는 방법:
성능 : 데이터베이스에서 최적화된 정렬 알고리즘을 이용하기 때문에 대체로 빠릅니다
네트워크 부하 : 필요한 데이터만 가져올 수 있어서 네트워크 트래픽이 줄어듭니다
메모리 사용량 : 필요한 데이터만 메모리에 로드되므로 효율적입니다

Java로 메서드를 작성해서 정렬하는 방법
성능 : Java에서 처리하는 정렬은 데이터베이스의 최적화된 알고리즘보다 일반적으로 느립니다
네트워크 부하 : 모든 데이터를 가져와야 하므로 네트워크 트래픽이 증가할 수 있습니다
메모리 사용량 : 모든 데이터를 메모리에 로드해야 하므로 메모리 사용량이 늘어납니다

이러한 차이점을 고려하면 일반적으로는 JPA를 이용해서 데이터베이스에서 정렬하고 가져오는 것이 더 효율적입니다
그러나 상황에 따라 다를 수 있으므로
성능 테스트를 통해 가장 적합한 방법을 선택하는 것이 좋습니다

결론은 데이터를 추가로 가공할 것이 아니면 자바에서 findAll로 받아올 필요가 없을 것이라 정리가 되었다.

다음에 정렬할 일이 있다면 더 신중하고 정확하게 어느걸 고를지 판단할 수 있을듯 한 느낌😊

반응형

'호기심 천국 > Spring' 카테고리의 다른 글

스프링 시큐리티 1 (인증)  (0) 2023.09.26
스프링부트 final로 클래스 생성자주입  (0) 2023.07.27
반응형

현재 알고리즘 공부를 하고있는데 계속 시간초과가 나서 Scanner를 못쓰게 됐다...

앞으로 많은 문제들이 이러한 제약을 갖고있을 듯 하여 이제서야 공부하게 됐다.

https://spody.tistory.com/60

 

1920번 수 찾기 자바로 풀어 본 짧은 글

1920번 수 찾기 1920번: 수 찾기 (acmicpc.net) 1920번: 수 찾기 첫째 줄에 자연수 N(1 ≤ N ≤ 100,000)이 주어진다. 다음 줄에는 N개의 정수 A[1], A[2], …, A[N]이 주어진다. 다음 줄에는 M(1 ≤ M ≤ 100,000)이 주

spody.tistory.com

정렬과 이분탐색 모두 사용하는데 시간도 얼마 없는 문제.... 여기서 막혀버렸다

때문에 공부 조금 하고 정리할 겸 올리는 글....

사전준비

1
2
3
4
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
cs
1
public static void main(String[] args) throws NumberFormatException, IOException{
cs

main에는 이렇게 IOExcption을 해줘야한다.

BufferedReader는 Scanner에 비해서 속도가 훨씬 빠르다.

이유는 입력된 데이터가 버퍼를 거쳐 전달돼서 데이터 처리 효율이 올라간다.

때문에 데이터가 많이 들어갈 때는 이 클래스를 활용해서 해결해야한다.

또 StringTokenizer를 사용하는 이유는 BufferedReader는 한 줄씩 입력을 받는 readLine을 사용하게 되는데

"10 5 3 1 10" 등 값을 띄어쓰기로 구분 해 입력받는다 이러한 한 줄을 처리하기 위해 String을 token간격으로 끊어 사용한다는 의미의

StringTokenizer를 사용해 해당 값을 처리해준다.

기본적으로 StringTokenizer을 사용하면 띄어쓰기 별로 10 5 3 1 10 이 값에서 nextToken()메소드를 사용할 때마다

순차적으로 꺼낸다.

ex)

1
2
3
4
5
6
7
8
9
10
11
import java.io.BufferedReader;
 
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] a = new int[n];
 
StringTokenizer st = new StringTokenizer(br.readLine());
 
for(int i = 0; i < n; i++) {
    a[i] = Integer.parseInt(st.nextToken());
}
cs

input : 5

           10 5 3 1 10

이 경우

a[0] = token(10)

a[1] = token(5)

a[2] = token(3)

a[3] = token(1)

a[4] = token(10)

이런 식이다.

형태를 잘 기억해두자.

 

반응형
반응형

nextint() 입력 후 nextLine()을 사용 시에 입력관련 오류가 생긴다.

 

이유는 콘솔 입력시에 엔터키를 입력 했을 때만 지금까지 눌렀던 내용이 버퍼에 전달되어 저장이 되는데, nextInt의 메서드

는 int값만 가져간 후 구분자를 받지 않는다.

때문에 이후 \n의 구분자를 통해 입력을 구분하는 nextLine메서드를 사용할 경우 해당 부분에서 문제가 생긴다. nextLine의 첫번째 입력을''\n의 상태로 입력을 받는 것이다.

 

아래는 해당 부분의 코드 예제이다.

1
2
3
4
5
6
7
8
9
        Scanner sc = new Scanner(System.in);
        int n;
        n = sc.nextInt();
        
        String[] s = new String[n];
            
        for(int i = 0; i < n; i++) {    
            s[i] = sc.nextLine(); // 이 부분에서 s[0]에 ''가 저장이된다.
        }
cs

이러한 오류를 해결할 방법은 알아본 바로는 두가지가 있는데,

 

첫번째는 

1
2
3
4
5
6
7
8
9
10
        Scanner sc = new Scanner(System.in);
        int n;
        n = sc.nextInt();
        sc.nextLine();//해당부분 줄     
        
        String[] s = new String[n];
            
        for(int i = 0; i < n; i++) {    
            s[i] = sc.nextLine(); // 이 부분에서 s[0]에 ''가 저장이된다.
        }
cs

이렇게 nextLine()을 한줄 추가해서 방지하는 것과

 

 

1
2
3
4
5
6
7
8
9
        Scanner sc = new Scanner(System.in);
        int n;
        n = n = Integer.parseInt(sc.nextLine());//nextLine을 사용해서 int형식 
        
        String[] s = new String[n];
            
        for(int i = 0; i < n; i++) {    
            s[i] = sc.nextLine(); // 이 부분에서 s[0]에 ''가 저장이된다.
        }
cs

위와 같이 아예 nextLine()을 통해 int형을 입력받는 방법이다.

 

가장 깔끔한 방법은 두번째 방법인 듯 하다.

 

문제풀다가 멘붕와서 서치한 내용을 토대로 정리 완!

 

요약 : nextInt에는 \n의 개행문자가 들어가지 않음. 때문에 다음 nextLine사용 시 해당 enter(\n)구분자를 가져가기에
nextLine을 바로 아랫줄에 넣어 초기화 시켜주거나 Integer.parseInt(sc.nextLine());의 형식으로 입력을 받아야 함

반응형

'공부 > Java' 카테고리의 다른 글

우테코 프리코스 6기 1주차 숫자야구 회고  (0) 2023.10.26
자바의정석 ch14 람다와 스트림  (0) 2023.09.18
BufferedReader, StringTokenizer  (0) 2023.01.14

+ Recent posts