[인프런워밍업스터디_BE_0기] 다섯 번째 과제!

오늘의 과제는 주어진 코드를 최대한 깔끔하게 리팩터링 하는 것이다. 주어진 코드는 다음과 같다. 

public class Main {

    public static void main(String[] args) {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        int a = scanner.nextInt();

        int r1 = 0, r2 = 0, r3= 0, r4 = 0, r5 = 0, r6 = 0;

        for (int i = 0; i < a; i++) {
            double b = Math.random() * 6;
            if (b >= 0 && b < 1) {
                r1++;
            } else if (b >= 1 && b < 2) {
                r2++;
            } else if (b >= 2 && b < 3) {
                r3++;
            } else if (b >= 3 && b < 4) {
                r4++;
            } else if (b >= 4 && b < 5) {
                r5++;
            } else if (b >= 5 && b < 6) {
                r6++;
            }
        }

        System.out.printf("1은 %d번 나왔습니다.\n", r1);
        System.out.printf("2은 %d번 나왔습니다.\n", r2);
        System.out.printf("3은 %d번 나왔습니다.\n", r3);
        System.out.printf("4은 %d번 나왔습니다.\n", r4);
        System.out.printf("5은 %d번 나왔습니다.\n", r5);
        System.out.printf("6은 %d번 나왔습니다.\n", r6);
    }
}

 

1. 코드분석

우선 코드를 정확하게 이해해야 리팩터링을 할 수 있을 것이다. 각 코드가 어떤 기능을 하고 있는지 주석을 달아보자!

public class Main {

    public static void main(String[] args) {
        // 시행 횟수를 입력 받는다.
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        int a = scanner.nextInt();

        // 숫자 별로 나온 횟수를 저장할 변수를 선언한다.
        int r1 = 0, r2 = 0, r3= 0, r4 = 0, r5 = 0, r6 = 0;

        // 입력받은 시행 횟수만큼 주사위를 던지고 변수에 저장한다.
        for (int i = 0; i < a; i++) {
            double b = Math.random() * 6;
            if (b >= 0 && b < 1) {
                r1++;
            } else if (b >= 1 && b < 2) {
                r2++;
            } else if (b >= 2 && b < 3) {
                r3++;
            } else if (b >= 3 && b < 4) {
                r4++;
            } else if (b >= 4 && b < 5) {
                r5++;
            } else if (b >= 5 && b < 6) {
                r6++;
            }
        }

        // 결과를 출력한다.
        System.out.printf("1은 %d번 나왔습니다.\n", r1);
        System.out.printf("2은 %d번 나왔습니다.\n", r2);
        System.out.printf("3은 %d번 나왔습니다.\n", r3);
        System.out.printf("4은 %d번 나왔습니다.\n", r4);
        System.out.printf("5은 %d번 나왔습니다.\n", r5);
        System.out.printf("6은 %d번 나왔습니다.\n", r6);
    }
}

 

2. 변수명 리팩터링

주석을 달고 나니 어떻게 분리해야할지 조금 감이 잡히는 것 같다. 다음으로 의도를 알 수 없는 변수의 이름을 변경해주자!

public class Main {

    public static void main(String[] args) {
        // 시행 횟수를 입력 받는다.
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        int trialsNumber = scanner.nextInt();

        // 숫자 별로 나온 횟수를 저장할 변수를 선언한다.
        int count1 = 0, count2 = 0, count3= 0, count4 = 0, count5 = 0, count6 = 0;

        // 입력받은 시행 횟수만큼 주사위를 던지고 변수에 저장한다.
        for (int i = 0; i < trialsNumber; i++) {
            double numberOfDice = Math.random() * 6;
            if (numberOfDice >= 0 && numberOfDice < 1) {
                count1++;
            } else if (numberOfDice >= 1 && numberOfDice < 2) {
                count2++;
            } else if (numberOfDice >= 2 && numberOfDice < 3) {
                count3++;
            } else if (numberOfDice >= 3 && numberOfDice < 4) {
                count4++;
            } else if (numberOfDice >= 4 && numberOfDice < 5) {
                count5++;
            } else if (numberOfDice >= 5 && numberOfDice < 6) {
                count6++;
            }
        }

        // 결과를 출력한다.
        System.out.printf("1은 %d번 나왔습니다.\n", count1);
        System.out.printf("2은 %d번 나왔습니다.\n", count2);
        System.out.printf("3은 %d번 나왔습니다.\n", count3);
        System.out.printf("4은 %d번 나왔습니다.\n", count4);
        System.out.printf("5은 %d번 나왔습니다.\n", count5);
        System.out.printf("6은 %d번 나왔습니다.\n", count6);
    }
}

 

3. 메서드 추출

자! 이제 위에서부터 차근차근 기능을 분리해보자.

우선 시행횟수를 입력받는 부분은 메서드로 추출할 수 있을 것 같다.

public class Main {

    public static void main(String[] args) {
        // 시행 횟수를 입력 받는다.
        int trialsNumber = getTrialsNumber();

        // 숫자 별로 나온 횟수를 저장할 변수를 선언한다.
        int count1 = 0, count2 = 0, count3= 0, count4 = 0, count5 = 0, count6 = 0;

        // 입력받은 시행 횟수만큼 주사위를 던지고 변수에 저장한다.
        for (int i = 0; i < trialsNumber; i++) {
            double numberOfDice = Math.random() * 6;
            if (numberOfDice >= 0 && numberOfDice < 1) {
                count1++;
            } else if (numberOfDice >= 1 && numberOfDice < 2) {
                count2++;
            } else if (numberOfDice >= 2 && numberOfDice < 3) {
                count3++;
            } else if (numberOfDice >= 3 && numberOfDice < 4) {
                count4++;
            } else if (numberOfDice >= 4 && numberOfDice < 5) {
                count5++;
            } else if (numberOfDice >= 5 && numberOfDice < 6) {
                count6++;
            }
        }

        // 결과를 출력한다.
        System.out.printf("1은 %d번 나왔습니다.\n", count1);
        System.out.printf("2은 %d번 나왔습니다.\n", count2);
        System.out.printf("3은 %d번 나왔습니다.\n", count3);
        System.out.printf("4은 %d번 나왔습니다.\n", count4);
        System.out.printf("5은 %d번 나왔습니다.\n", count5);
        System.out.printf("6은 %d번 나왔습니다.\n", count6);
    }

    private static int getTrialsNumber() {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        return scanner.nextInt();
    }
}

 

4. 클래스 분리 후 메서드 추출

다음으로 변수를 6개 선언하고 다음 로직에서 계속 해당 변수를 사용하고 있다. 그렇다면 변수를 포함해서 그 아래 기능까지 클래스로 묶고 싶다! 그리고 각 기능을 메서드로 추출해보자!

public class Main {

    public static void main(String[] args) {
        // 시행 횟수를 입력 받는다.
        int trialsNumber = getTrialsNumber();

        // 숫자 별로 나온 횟수를 저장할 변수를 선언한다. -> 주사위 객체를 선언한다.
        Dice dice = new Dice();

        // 입력받은 시행 횟수만큼 주사위를 던지고 변수에 저장한다.
        dice.rollTheDice(trialsNumber);

        // 결과를 출력한다.
        dice.printStat();
    }

    private static class Dice {
        private int count1 = 0;
        private int count2 = 0;
        private int count3 = 0;
        private int count4 = 0;
        private int count5 = 0;
        private int count6 = 0;

        public void rollTheDice(int trialsNumber) {
            for (int i = 0; i < trialsNumber; i++) {
                double numberOfDice = Math.random() * 6;
                if (numberOfDice >= 0 && numberOfDice < 1) {
                    count1++;
                } else if (numberOfDice >= 1 && numberOfDice < 2) {
                    count2++;
                } else if (numberOfDice >= 2 && numberOfDice < 3) {
                    count3++;
                } else if (numberOfDice >= 3 && numberOfDice < 4) {
                    count4++;
                } else if (numberOfDice >= 4 && numberOfDice < 5) {
                    count5++;
                } else if (numberOfDice >= 5 && numberOfDice < 6) {
                    count6++;
                }
            }
        }

        public void printStat() {
            System.out.printf("1은 %d번 나왔습니다.\n", count1);
            System.out.printf("2은 %d번 나왔습니다.\n", count2);
            System.out.printf("3은 %d번 나왔습니다.\n", count3);
            System.out.printf("4은 %d번 나왔습니다.\n", count4);
            System.out.printf("5은 %d번 나왔습니다.\n", count5);
            System.out.printf("6은 %d번 나왔습니다.\n", count6);
        }
    }

    private static int getTrialsNumber() {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        return scanner.nextInt();
    }
}

 

5. 다중 조건문 제거

이제 메인은 4줄의 코드로 어느정도 정리된 것 같으니 새로 만든 클래스 내의 메서드들을 우선 정리하고 싶다.

rollTheDice 메서드를 정리할 방법이 없을까 생각하다보니 주사위의 숫자는 1~6의 정수로 이루어져 있으니까 배열로 표현할 수 있을 것 같다!

public class Main {

    // 생략....

    private static class Dice {
        private final int[] diceCount = {0, 0, 0, 0, 0, 0};

        public void rollTheDice(int trialsNumber) {
            for (int i = 0; i < trialsNumber; i++) {
                int numberOfDice = (int) Math.floor(Math.random() * 6);

                diceCount[numberOfDice]++;
            }
        }

        public void printStat() {
            System.out.printf("1은 %d번 나왔습니다.\n", diceCount[0]);
            System.out.printf("2은 %d번 나왔습니다.\n", diceCount[1]);
            System.out.printf("3은 %d번 나왔습니다.\n", diceCount[2]);
            System.out.printf("4은 %d번 나왔습니다.\n", diceCount[3]);
            System.out.printf("5은 %d번 나왔습니다.\n", diceCount[4]);
            System.out.printf("6은 %d번 나왔습니다.\n", diceCount[5]);
        }
    }

    private static int getTrialsNumber() {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        return scanner.nextInt();
    }
}

 

6. 반복된 기능을 반복문으로 변경

훨씬 깔끔해진 것 같다! 결과를 출력해주는 printStat은 동일한 형식의 문자열을 출력하니 반복문을 사용하면 될 것 같다.

public class Main {

    // 생략....

    private static class Dice {
        private final int[] diceCount = {0, 0, 0, 0, 0, 0};

        public void rollTheDice(int trialsNumber) {
            for (int i = 0; i < trialsNumber; i++) {
                int numberOfDice = (int) Math.floor(Math.random() * 6);

                diceCount[numberOfDice]++;
            }
        }

        public void printStat() {
            for (int i = 0; i < diceCount.length; i++) {
                System.out.printf("%d은 %d번 나왔습니다.\n", i+1, diceCount[i]);
            }
        }
    }

    private static int getTrialsNumber() {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        return scanner.nextInt();
    }
}

 

7. 역할에 맞도록 다시 클래스 분리

이제 Dice 클래스에서 더 정리할 부분이 없는지 다시 한 번 살펴보니까 Dice 라는 클래스 이름은 주사위를 의미 하는데 클래스 내의 기능들은 주사위를 던진 횟수를 저장하고 출력하는게 주요 기능으로 보인다. 그렇다면 클래스 이름을 바꿀까..? 생각해봤지만 '주사위를 던지는' 기능은 Dice와 어울리는 것 같다.. 그러니 다시 클래스를 분리해보자.

public class Main {

    public static void main(String[] args) {
        // 시행 횟수를 입력 받는다.
        int trialsNumber = getTrialsNumber();

        // 숫자 별로 나온 횟수를 저장할 변수를 선언한다.
        Dice dice = new Dice();
        DiceStat diceStat = new DiceStat();

        // 입력받은 시행 횟수만큼 주사위를 던지고 변수에 저장한다.
        for (int i = 0; i < trialsNumber; i++) {
            int numberOfDice = dice.rollTheDice();

            diceStat.countDiceNumber(numberOfDice);
        }

        // 결과를 출력한다.
        diceStat.printStat();
    }

    private static class Dice {
        private static int rollOneDice() {
            return (int) Math.floor(Math.random() * 6);
        }
    }

    private static class DiceStat {
        private final int[] diceNumberCount = {0, 0, 0, 0, 0, 0};

        public void countDiceNumber(int numberOfDice) {
            diceNumberCount[numberOfDice]++;
        }

        public void printStat() {
            for (int i = 0; i < diceNumberCount.length; i++) {
                System.out.printf("%d은 %d번 나왔습니다.\n", i+1, diceNumberCount[i]);
            }
        }
    }

    private static int getTrialsNumber() {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        return scanner.nextInt();
    }
}

클래스를 분리 하면서 rollTheDice 메서드도 각 클래스의 역할에 맞게 분리하였다. 그리고 이를 호출해주는 클래스도 그에 맞게 변경하였다. 추가적으로 diceCount라는 이름이 주사위의 숫자가 몇 번 나왔는지 저장하는 변수라는 의미를 잘 전달하지 못 하는 것 같아. diceNumberCount로 변경하였다.

 

메인문의 길이는 좀 더 길어졌지만 각 클래스가 자신의 역할을 더 명확히 표현하게 된 것 같으니 만족!

더 정리할 부분이 있는지 찾아보면서 다음과 같은 고민이 있었지만 이정도 수정으로 마무리지었다.

 

  1. 시행횟수를 입력받는 메서드를 Dice 클래스에 넣어야할까?

-> 시행횟수를 입력받는 행위는 주사위 라는 도메인과 상관이 없는 것 같으니 PASS!

 

  1. trialsNumber 변수와 numberOfDice 변수는 한 번만 사용되고 있는데 변수에 메서드를 인라인할까?

-> trialsNumber는 반복문에서 반복 횟수를 지정할 때 쓰고 있다. 이곳으로 인라인하면 사용자에게 입력받는 메서드라는 파악이 힘들 것 같으니 PASS!

-> countDiceNumber 메서드는 주사위의 숫자를 받아 저장한다. rollOneDece 메서드는 주사위를 굴린다는 행위를 잘 표현하고 있지만 어떤 주사위 숫자가 나왔는지를 표현하지는 않으니 메서드이름을 변경할까 고민해봤지만 행위를 그대로 표현하고 numberOfDice 변수로 주사위를 굴려 나온 숫자를 표현하기로 했다!

8. 패키지 생성 후 클래스 이동

이제 dice 패키지를 만들고 Main 내부에 만들 클래스를 dice 패키지로 옮기도록 하자!

 

9. 추가 과제 수행!!

현재 코드는 주사위가 1~6까지만 있다는 가정으로 작성되었다.

따라서 주사위가 1~12까지 있거나 1~20까지 있다면 많은 코드를 수정해야한다.

주사위의 숫자 범위가 달라지더라도 코드를 적게 수정할 수 있도록 고민해보자!

주사위의 숫자 범위가 달라졌을 때 내가 리팩터링한 코드에서 수정할 부분은 두 군데이다.

  1. Dice의 rollOneDice 내 상수 6

  2. DiceStat의 diceNumberCount 크기

이걸 이렇게 바꾸자.

  1. 생성자로 받을 수도 있겠지만 현재 요구사항에서 주사위의 숫자 범위는 가변형이 아니니 주사위 면의 수를 Dice 내 전역 상수로 선언하자!

     

  2. DiceStat의 diceNumberCount 크기를 DiceStat 생성 시에 Parameter로 받도록 변경하자! 이 때 배열의 크기는 1보다 작을 수 없으니 예외처리도 해주도록 하자!

 

이렇게 바꾸면 주사위의 숫자 범위가 변경되어도 Dice 객체의 상수값 한개만 변경해주면 된다.

최종 수정본은 다음과 같다!

 

[메인]

public class Main {

    public static void main(String[] args) {
        int trialsNumber = getTrialsNumber();

        Dice dice = new Dice();
        DiceStat diceStat = new DiceStat(dice.getDiceSize());

        for (int i = 0; i < trialsNumber; i++) {
            int numberOfDice = dice.rollOneDice();

            diceStat.countDiceNumber(numberOfDice);
        }

        diceStat.printStat();
    }

    private static int getTrialsNumber() {
        System.out.println("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        return scanner.nextInt();
    }
}

 

[Dice]

public class Dice {
    private static final int DICE_SIZE = 6;

    public int rollOneDice() {
        return (int) Math.floor(Math.random() * DICE_SIZE);
    }

    public int getDiceSize() {
        return DICE_SIZE;
    }
}

 

[DiceStat]

public class DiceStat {
    private final int[] diceNumberCount;

    public DiceStat(int diceSize) {
        if (diceSize < 1) {
            throw new IllegalArgumentException("주사위의 면은 1개 이상이어야 합니다.");
        }
        this.diceNumberCount = new int[diceSize];
    }

    public void countDiceNumber(int numberOfDice) {
        diceNumberCount[numberOfDice]++;
    }

    public void printStat() {
        for (int i = 0; i < diceNumberCount.length; i++) {
            System.out.printf("%d은 %d번 나왔습니다.\n", i+1, diceNumberCount[i]);
        }
    }
}

댓글을 작성해보세요.

채널톡 아이콘