[인프런워밍업스터디_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로 변경하였다.
메인문의 길이는 좀 더 길어졌지만 각 클래스가 자신의 역할을 더 명확히 표현하게 된 것 같으니 만족!
더 정리할 부분이 있는지 찾아보면서 다음과 같은 고민이 있었지만 이정도 수정으로 마무리지었다.
시행횟수를 입력받는 메서드를 Dice 클래스에 넣어야할까?
-> 시행횟수를 입력받는 행위는 주사위 라는 도메인과 상관이 없는 것 같으니 PASS!
trialsNumber 변수와 numberOfDice 변수는 한 번만 사용되고 있는데 변수에 메서드를 인라인할까?
-> trialsNumber는 반복문에서 반복 횟수를 지정할 때 쓰고 있다. 이곳으로 인라인하면 사용자에게 입력받는 메서드라는 파악이 힘들 것 같으니 PASS!
-> countDiceNumber 메서드는 주사위의 숫자를 받아 저장한다. rollOneDece 메서드는 주사위를 굴린다는 행위를 잘 표현하고 있지만 어떤 주사위 숫자가 나왔는지를 표현하지는 않으니 메서드이름을 변경할까 고민해봤지만 행위를 그대로 표현하고 numberOfDice 변수로 주사위를 굴려 나온 숫자를 표현하기로 했다!
8. 패키지 생성 후 클래스 이동
이제 dice 패키지를 만들고 Main 내부에 만들 클래스를 dice 패키지로 옮기도록 하자!
9. 추가 과제 수행!!
현재 코드는 주사위가 1~6까지만 있다는 가정으로 작성되었다.
따라서 주사위가 1~12까지 있거나 1~20까지 있다면 많은 코드를 수정해야한다.
주사위의 숫자 범위가 달라지더라도 코드를 적게 수정할 수 있도록 고민해보자!
주사위의 숫자 범위가 달라졌을 때 내가 리팩터링한 코드에서 수정할 부분은 두 군데이다.
Dice의 rollOneDice 내 상수 6
DiceStat의 diceNumberCount 크기
이걸 이렇게 바꾸자.
생성자로 받을 수도 있겠지만 현재 요구사항에서 주사위의 숫자 범위는 가변형이 아니니 주사위 면의 수를 Dice 내 전역 상수로 선언하자!
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]);
}
}
}
댓글을 작성해보세요.