JAVA/JAVA | Spring 학습기록

[JAVA] Cache를 이용한 재사용으로 성능을 높이자

kth990303 2022. 3. 16. 12:40
반응형

Cache(캐시)란, 자주 사용하는 데이터를 복사해놓은 임시 장소를 의미한다.

알고리즘에서 Dynamic Programming (DP)를 공부했다면 이해하기 수월할 수 있다.

 

나는 Cache를 이용하여 성능을 높이는 방법을 알고리즘 문제풀이에서만 사용해왔고,

실제 JAVA 개발에선 사용해본 적이 거의 없었는데, 이번에 우아한테크코스 미션을 통해서 캐싱을 할 기회가 생겨서 기록해보려 한다!


블랙잭 미션에서 캐싱이 필요한 이유

블랙잭 룰을 알고 있는가?

딜러와 플레이어들이 카드 (2~10, J, K, Q, A)를 가지고 카드들의 점수 합과 카드 개수로 승부를 겨루는 게임이다.

블랙잭에서의 카드 개수는 총 52개 (4개의 문양 * 13개의 숫자/알파벳)으로 이루어져 있으며,

참가자들은 이 카드들을 랜덤으로 나누어 가지게 된 후, 추가로 카드 수령(HIT)을 희망할 경우, 카드를 받을 수 있다. 

따라서 각 플레이어들과 딜러는 카드를 필수적으로 갖게 된다.

 

이 때 블랙잭 게임이 여러 번 진행된다면, 진행될 때마다 새로운 덱을 생성해줘야 하기 때문에 똑같은 52개의 카드가 매번 생성되는데, 카드를 캐싱해주어 재사용해주면 성능을 올릴 수 있지 않을까 생각해볼 수 있다.

캐싱하기 전, 매 게임이 진행될 때마다 카드 52개를 만들어주었다.


카드를 캐싱해보자

그렇다면 캐싱을 하기 위해선 어떻게 해야될까?

그 전 코드에서는 52장의 카드를 만들기 위해 Card를 객체로 생성해주어 힙 메모리에 저장해주었었다.

그리고 게임이 끝나면 Card 객체의 수명은 끝나며,

재실행될 때 다시 객체를 할당해주어 힙 메모리에 저장해주는 것의 반복이었다.

 

그렇다면 static을 사용해보면 어떨까?

static 키워드를 사용하면 인스턴스를 생성할 필요 없이 프로그램 실행 전반에 걸쳐 수명이 유지된다.

private static final List<Card> CARDS;

static {
    CARDS = Arrays.stream(Suit.values())
            .flatMap(suit -> Arrays.stream(Denomination.values())
                    .map(denomination -> new Card(suit, denomination)))
            .collect(Collectors.toUnmodifiableList());
}

위와 같이 CARDS 52장을 담는 static final 상수를 생성해주고, static 블록으로 초기화를 해주자.

 

이제, static으로 메모리에 올라간 카드들을 이용하기 위해 정적 팩토리 메서드를 사용할 것이다.

카드를 생성할 때, new 키워드를 이용한 생성자를 사용하지 않고 캐싱한 값들을 재사용하기 위해 static factory 메서드를 사용하는 것이다.

private Card(Suit suit, Denomination denomination) {
    this.suit = suit;
    this.denomination = denomination;
}

public static Card from(Suit suit, Denomination denomination) {
    return CARDS.stream()
            .filter(card -> card.suit == suit && card.denomination == denomination)
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException(ERROR_MESSAGE_INVALID_CARD));
}

생성자의 접근제어자는 private으로 변경해주어 static 블록에서 52장의 카드를 초기화해줄 때에만 사용해주자.

정적 팩토리 메서드명의 관례에 따라 from으로 지정해주었다.


캐싱한 카드들을 재사용할 땐 복사본을 넘겨주자

이제, 매 게임마다 52장의 카드를 생성해주는 덱을 만들어주는 클래스에서 사용하게 할 수 있게

CARDS 복사본을 넘겨주는 메서드도 만들어주자.

public static List<Card> initializeDeck() {
    return List.copyOf(CARDS);
}

이 메서드를 이용하여 Deck 객체에서 카드들을 이용할 수 있도록 만들었다.

deck 클래스의 create 메서드

위에서 만든 initializeDeck() 메서드를 이용해주어 52장의 카드들을 받고,

섞지 않고 나누어주면 형평성에 문제가 생길 수 있으니 Collections.shuffle 메서드를 이용해주었다.

그리고 test를 돌려본 결과~

test failed

이럴수가... 테스트가 실패했다!

원인이 무엇일까?

 

이는 final로 지정된 List를 copyOf로 깊은 복사해주었기 때문에, shuffle할 때 UnsupportedOperationException에러가 발생한 것이다.

따라서 List.copyOf(CARDS)로 복사본을 넘겨주지 말고 new ArrayList<>(CARDS)로 복사본을 넘겨주도록 하자.

public static List<Card> initializeDeck() {
    return new ArrayList<>(CARDS);
}

tests passed

테스트가 성공하는 것을 확인할 수 있다.

만약 복사본을 넘겨주었는데 위와 같이 UnsupportedOperationException이 발생한다든지, 다른 에러가 발생한다면

제대로 캐싱이 되었는지, 그리고 복사본을 올바른 방법으로 넘겨주는지 확인해주자.

 

도움된 글: https://stackoverflow.com/questions/21854353/why-does-collections-sort-throw-unsupported-operation-exception-while-sorting-by


지난 미션에서도 캐싱을 사용하긴 했지만,

이번 미션에서도 많이 헤맸을 뿐 아니라, 복사본을 넘겨주는 과정에서 에러가 발생했기 때문에 복습 겸 기억해두려는 목적으로 포스팅을 작성해보았다 :)

 

많이 사용되는 도메인이라면 (특히 VO 라면) 캐싱을 한 번 고려해보는 것도 좋을 듯하다~

반응형