블랙잭 게임 구현을 하면서 '카드덱'이라는 객체를 사용하게 되었다.
나는 실제 (카지노에서의)규칙을 적용해서 여러벌의 카드덱을 사용하는 방식으로 구현했는데
그 과정에서 new Card()를 통해 계속해서 새로운 카드를 생성하는 방법을 사용했다.
그런데 리뷰어로부터 "카드가 만들어질 수 있는 경우의 수는 52개이고, 매번 새 인스턴스를 생성하는것은 낭비일 수 있다. 카드 인스턴스를 캐싱해보는건 어떨까?" 라는 피드백을 받았다.
생각해보면 new Card(3, 다이아몬드), new Card(3, 다이아몬드) 처럼 같은 카드가 발급될 경우 굳이 새로운 객체를 만들어서 넘겨줄 필요가 없다. 미리 생성되어있는 총 52개의 카드 중 하나를 선택해서 전달해주면 된다.
❯ 캐시란 무엇일까?
The cache is a hardware or software component embedded in an application or device memory that automatically and temporarily stores data consumed by the user to reduce the data retrieval time and effort the next time the application or device is accessed. - 출처: https://www.spiceworks.com/tech/tech-101/articles/what-is-cache/
Cache란, 애플리케이션이나 장치에 접근할 때 검색 시간과 노력을 줄이기 위해 정보(데이터)를 일시적으로 저장하는 장소를 의미한다.
Caching이란, 캐시를 사용하는것(행위)을 의미한다. 데이터를 조회할 때 원본 데이터가 아니라 캐시에서 찾아오는 방식을 예로 들 수 있다.
코드를 살펴보기 전에, 카드 덱을 생성하고 섞은 뒤 한장씩 뽑아주는 방식이 아니라 인덱스를 통해 카드를 조합했던 이유는 다음과 같다.
카드 한 세트는 총 52장의 카드로 제한되지만 실제 (카지노에서) 블랙잭 게임은 최소 4개 ~ 8개 정도의 덱을 사용한다고 한다.
그래서 중복 카드를 허용하기로 하고 랜덤으로 카드를 생성해서 전달하기 위해 인덱스 방식을 사용했다.
(처음 캐싱의 의도를 잘못 이해해서 카드를 52장으로 제한했다가 다시 복수 덱을 사용하는 방식으로 변경하기도 했다.😅)
-> 추후 코드를 수정하면서 카드 번호/모양 랜덤 선택 방식을 인덱스를 통하지 않고 enum클래스 내부에서 static 메서드를 통해 추출하는것으로 변경했다.
기존 코드를 먼저 살펴보자.
카드를 뽑아주는 CardGenerator이다. 카드 모양과 카드 형태를 enum으로 정의해놓고 랜덤숫자를 생성(IndexGenerator)해서 해당 숫자에 해당하는 enum값을 카드 모양, 카드 숫자로 사용한다.
카드 뽑기를 할 때 마다 결정된 카드 모양, 카드 숫자를 가지고 new Card(cardNumber, cardShape)를 생성해주고 있다.
public class CardGenerator {
private final IndexGenerator indexGenerator;
public CardGenerator(IndexGenerator indexGenerator) {
this.indexGenerator = indexGenerator;
}
public Card drawCard() {
int numberIndex = indexGenerator.generate(CardNumber.values().length);
CardNumber cardNumber = CardNumber.generate(numberIndex);
int shapeIndex = indexGenerator.generate(CardShape.values().length);
CardShape cardShape = CardShape.generate(shapeIndex);
return new Card(cardNumber, cardShape);
}
}
❯ 카드 덱에 대해 캐싱을 어떻게 적용해볼 수 있을까?
우선 클래스 역할을 고려해 CardGenerator 대신 CardDeck이라는 클래스로 이름을 변경하였다. (덱에서 뽑는다는 느낌!)
그래고 캐시의 의미(데이터를 일시적으로 저장하는 장소)처럼 카드 덱을 생성해서 미리 저장해놓을 곳이 필요하다.
static 키워드를 사용해 생성된 카드덱(List<Card>)을 초기화 하였다.
이를 통해 카드 목록은 static 영역에 저장되며 프로그램이 실행되는 동안 수명이 유지된다.
또한 카드 덱은 변경되지 않아야하는 값이므로 unmodifiableList()를 통해 불변을 보장해주었다.
public class CardDeck {
private static final List<Card> CACHE = Collections.unmodifiableList(new ArrayList<>(generate()));
private static List<Card> generate() {
return Arrays.stream(CardNumber.values())
.map(CardDeck::generateCardsByCardNumber)
.flatMap(List::stream)
.toList();
}
private static List<Card> generateCardsByCardNumber(CardNumber cardNumber) {
return Arrays.stream(CardShape.values())
.map(cardShape -> new Card(cardNumber, cardShape))
.toList();
}
public static Card drawCard() {
CardNumber cardNumber = CardNumber.generate();
CardShape cardShape = CardShape.generate();
return CACHE.stream()
.filter(card -> card.getCardNumber() == cardNumber && card.getCardShape() == cardShape)
.findFirst()
.orElseThrow(() -> new NoSuchElementException("해당되는 카드가 없습니다."));
}
}
❯ 생성된(캐싱된) 덱에서 카드를 어떻게 뽑을 수 있을까?
기존 메서드와 유사하게, 카드 모양과 카드 숫자를 통해 카드를 "찾아"올 수 있다.
drawCard()를 했을 때 new Card()로 새로운 카드를 생성해서 전달해주는것이 아니라 CACHE에서 해당되는 카드를 찾아서 리턴해주는것으로 변경되었다.
❯ 정리
이번에 코드를 구현하면서 어떨 때 캐싱을 사용하는것이 좋을지도 한번 생각해보게 되었다.
이번 과제에서 '카드'는 같은 모양과 숫자를 가진다면 동일한 특성을 갖는다.
이처럼 인스턴스별 특성이 중요한것이 아니라 VO와 같이 '값'이 의미있는 경우 캐싱으로 구현해도 좋을 것 같다.
어짜피 같은 값을 갖는 객체라면, 매번 새로운 인스턴스를 생성하지 않고 캐싱되어있는 데이터를 사용하여 불필요한 메모리 낭비를 예방할 수 있다.
참고자료
[Tecoble] 반복적으로 사용되는 인스턴스 캐싱하기
'우아한테크코스 > 레벨 1 - Java' 카테고리의 다른 글
[GitHub] 코드리뷰가 익숙하지 않은 분들을 위한 GitHub에서 코드리뷰 하는법 (0) | 2024.03.30 |
---|---|
[JAVA] 상태 패턴(State Pattern) with BlackJack (21) | 2024.03.19 |
[JAVA] VO(Value Object) vs DTO(Data Transfer Object) with record (13) | 2024.03.05 |
[JAVA] static 키워드와 static factory method (4) | 2024.02.29 |
[JAVA] 함수형 인터페이스 개념과 예시 (0) | 2024.02.21 |