프레임워크/Spring

[Spring, Java] 멀티스레드 적용기

멍토 2021. 8. 25.

서비스를 만들며 가장 컸던 문제점은 중간지점을 찾기위한 탐색시간이 오래걸린다는 부분이다.

내부적으로 살펴보면 탐색지점을 찾기위해 추천할만한 역들의 리스트를 뽑고, 외부 API를 통해서 출발점과 도착역까지 걸리는 시간을 구하는 부분에서 시간이 오래걸리는 부분이었다.

모든 알고리즘을 적용하고 나니 걸렸던 시간은 대략 1분이었다.

정확한 지점을 찾기위한 로직이 추가되다 보니 시간이 많이 늘어나게 된것이다.

그렇지만 1분이라는 시간... 사용자 입장에서는 짧은시간이 아니다.

이러한 문제로 인해 여러가지 의견이 나오게 되었다.

  1. 정확하지 않아도 괜찮으니 로직을 빼서 시간을 단축하자.
  2. 우리 서비스의 아이덴티티인데 정확하지 않은것보다 낫지않냐?

그래서 둘의 타협점을 찾기위해 하루 정도의 시간을 두고 최적화를 해보고 그래도 느리다면 로직을 빼기로 했다.

이때 적용을 해본것이 멀티스레드이다.

우리 서비스에서 시간이 많이걸리는 부분이 api를 요청하고 응답이 오는걸 기다리는 시간이 긴다는데 착안하여 여러 스레드로 요청을 동시에 보내서 기다리는 시간을 줄이는데 있었다.

private void calculateSource(Points points,
        Map<Point, Map<Point, PathResult>> responsesFromPoint, UtilityResponse response) {
        for (Point source : points.getPoints()) {
              Map<Point, PathResult> responses
                = responsesFromPoint.getOrDefault(source, new HashMap<>());
            Point target = new Point(response.getX(), response.getY());

            PathResult pathResult = pathResultRedisRepository
                .findById(source.toString() + target)
                .orElseGet(() -> saveRedisCachePathResult(source, target, response.getPlaceName()));
            responses.put(target, pathResult);
            responsesFromPoint.put(source, responses);
        }
}

적용하기 전 코드이다. 받아와서 Map에 매핑을 하고있다.

처음에는 단순하게 저기있는 For문을 parallelStream을 이용하여 처리했더니 에러가 발생했다.

내부에서 getOrDefault로 new를 하는데 여러 스레드에서 같이 접근하는 문제가 발생한 것이다.

따라서 미리 위에서 for문을 통해서 미리 생성을 하고 parallelStream처리를 하였으나 두번째 문제가 발생하였다.

map에 데이터를 넣는것 역시 동기화 보장이 안되는 것이다.

인터넷을 통해 찾아보니 ConCurrentHaspMap이라는 것을 이용하면 동기화가 보장이 된다고 했지만 적용해보았지만 왜인지 되지 않았다.

여러가지 방법으로 삽질을 하다가 하루가 지나가게 되었다.

다음날 아침 오전까지만 해본다고 하고 자기전에 생각난 방법으로 처리를 해보았다.

동기화가 문제라면 steam에서 제공하는 map을 통해서 새로운 객체를 생성하고 List로 받으면 되지 않을까?

새로운 객체를 만들기 시작했다.

public class PathTransferResult {

    private final Point source;
    private final Point target;
    private final PathResult pathResult;

    public PathTransferResult(Point source, Point target,
        PathResult pathResult) {
        this.source = source;
        this.target = target;
        this.pathResult = pathResult;
    }

    public Point getSource() {
        return source;
    }

    public Point getTarget() {
        return target;
    }

    public PathResult getPathResult() {
        return pathResult;
    }
}

 

private void calculateSource(Points points,
        Map<Point, Map<Point, PathResult>> responsesFromPoint, UtilityResponse response) {
              final Point target = new Point(response.getX(), response.getY());

        List<PathTransferResult> pathTransferResults = points.getPoints().parallelStream()
            .map(source -> new PathTransferResult(
                source,
                target,
                pathResultRedisRepository.findById(source.toString() + target)
                .orElseGet(() -> saveRedisCachePathResult(source, target, response.getPlaceName())))
            )
            .collect(Collectors.toList());

        for (PathTransferResult pathTransferResult : pathTransferResults) {
              Map<Point, PathResult> responses
              = responsesFromPoint.getOrDefault(pathTransferResult.getSource(), new HashMap<>());
            responses.put(pathTransferResult.getTarget(), pathTransferResult.getPathResult());
            responsesFromPoint.put(pathTransferResult.getSource(), responses);

 

위에서 먼저 parallelStream을 통해 처리하여 추가한 객체로 변환한 뒤 모든 처리가 끝나면 map에 넣는 방식으로 변경하였더니 정상적으로 동작했다.

성능측정을 해보니 기존에 걸리던 시간이 1분이었던 시간이 18~23초로 줄어들었다.

 

앞으로 어떻게 해야 시간을 더 단축할 수 있을지 고민하는 것이 과제인 것 같다.

서비스 주소 : https://seeyouthere.co.kr/

댓글

💲 광고입니다.