블랙잭 카드게임 리팩토링

뫄뫄(ahk)·2022년 11월 8일
0
post-thumbnail

리팩토링의 흔적..

원래 deck은 String 타입의 배열이었다.
(deck은 블랙잭 카드게임에 쓰이는 카드를 저장하는 공간으로, 총 208개의 String(카드종류+숫자/문자)을 저장)

public class Deck{
    String[] deck = new String[kind.length * num.length * round];
    private Deck(){
    	int deckNum = 0;

		for(int i=0; i<4; i++){
    		for(int j=0; j<kind.length; j++){
        		for(int k=0; k<num.length; k++){
            		deck[deckNum++] = kind[j]+num[k];
     	       }
         	}
    	}
            deckNum = 0;
	}
    
    ...
    public String hit(){
        int ranNum = 0;
        String returnCard = "";
        do{
            ranNum = (int)(Math.random() * deck.length);
            returnCard = deck[ranNum];
            if(!"".equals(returnCard)){
                deck[ranNum] = "";
                break;
            }
        } while(true);
        return returnCard;
    }
}

처음에는 게임도중 카드를 저장하는 공간의 길이를 수정할 필요가 없어보였다. 또 블랙잭 게임을 하는데 52장의 카드 4팩을 사용해서 카드의 중복이 생기기 때문에 Set은 당연히 사용못하고, List도 (블랙잭 게임에서 hit을 할 때) 길이를 수정하면 요소들을 옮겨야하는데 자원이 많이 사용될거라 생각해 deck을 String 배열로 정했다.

하지만 여기에 과연 do-while문이 사용되는게 맞을까 싶어 다시 자바의 정석 collection 파트를 뒤적이던 중 갑자기 유투브에서 찾아본 블랙잭 게임 영상이 떠올랐다.
이미지 출처 : https://www.gamblingsites.org/blog/can-you-count-cards-with-continuous-shuffling-machine/

딜러는 게임이 시작되면 새 카드 덱을 꺼내어 그대로 위의 사진과 같은 기계에 집어 넣은 뒤, 한 장씩 꺼낸다. 카드는 이미 이 기계 안에서 섞여 새롭게 정렬되었기 때문에 한장씩 차례로 꺼내도 문제가 없다.

그래서 이 기계의 역할을 shuffle()과 hit()이 그대로 나눠서 하면 되겠다는 생각에 다시 아래처럼 리팩토링했다.

public class Deck{
  List<String> deck = new ArrayList<>(kind.length * num.length * round);

  private Deck(){
      for(int i=0; i<4; i++){
          for(int j=0; j<kind.length; j++){
              for(int k=0; k<num.length; k++){
                  deck.add(kind[j]+num[k]);
              }
          }
      }
  }
  ...
  public void shuffle(){
      int ranNum = 0;
      String temp = "";

      for(int i=0; i<deck.size(); i++){
          ranNum = (int)(Math.random() * deck.size());
          temp = deck.get(ranNum);
          deck2.set(ranNum, deck.get(i));
          deck2.set(i, temp);
      }
  }

  public String hit(){
      String returnCard = deck2.get(deck2.size()-1);
      deck2.remove(deck2.size()-1);
      return returnCard;
  }
}

리팩토링의 성능(시간)을 확인해보자

두 코드 간의 성능을 확인하기 위해 각각 test를 10번 돌려 보았다.
전후가 각각 0.0481s, 0.0497s로 평균 시간이 비슷한 것을 확인할 수 있었다.
더 돌려보니 어느게 더 빠른지 확인하는게 의미가 없어보였으나, 리팩토링 이후가 더 빨리 돌아갈 것으로 생각했던 나에게는 예상 외의 결과였다.

어디서 시간이 걸렸는지 확인을 해보니,

deck.add(kind[j]+num[k]);

shuffle()이나 hit()을 실행하는데는 시간이 거의 안걸렸고 (0ms에 수렴..)
Deck을 생성하며 arrayList의 add()를 사용할 때 시간이 대부분 걸렸다.

ArrayList는 capacity이상의 데이터가 저장되려면 capacity를 늘리는게 아니라, 더 큰 arrayList를 생성해서 거기에 데이터를 복붙하는 형식으로 크기를 늘린다. 그걸 감안하고 ArrayList를 생성할 때 생성자에 initialCapacity를 딱 데이터 갯수대로 넣었지만

List<String> deck = new ArrayList<>(kind.length * num.length * round);

그래도 데이터를 저장하는데 시간이 오래 걸린 것이다. 이는 배열에 값을 저장할 때도 마찬가지였다.

결론은..

작은 어플리케이션이라 성능에는 큰 차이가 없었지만 조금 더 논리적으로 코드를 짤 수 있었다. 실제 사물을 모방하는 것이 코딩에 도움이 된다.

profile
NONONONONONOYes!

0개의 댓글