[워밍업 클럽 BE-0기 백엔드] 과제 5일차 - 코드 리팩토링 과제
오늘은 주사위를 던져서 숫자별로 나온 횟수를 기록하고 결과를 출력하는 절차지향적으로 작성된 코드를 리팩토링 해야합니다. 절차지향적으로 짠 코드가 모두 나쁜 것은 아니지만, 이번 스터디에서 우리는 객체지향 패러다임에 근거해 등장한 자바라는 언어와 그 언어가 가진 패러다임을 극대화한 스프링이라는 프레임워크를 공부하고 있으므로 객체지향적으로 깔끔하게 코드를 정리할 수 있는 역량을 길러야 합니다.
주어진 코드는 아래와 같습니다.
public class Assignment {
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("1은 %d번 나왔습니다.\n", r2);
System.out.printf("1은 %d번 나왔습니다.\n", r3);
System.out.printf("1은 %d번 나왔습니다.\n", r4);
System.out.printf("1은 %d번 나왔습니다.\n", r5);
System.out.printf("1은 %d번 나왔습니다.\n", r6);
}
}
객체지향적으로 코드를 리팩토링하기 위해서는 여러 명령문들이 어떠한 역할을 하고있는지 구분하는 것이 중요한 것 같습니다. 역할을 구분해야 역할과 책임에 따른 클래스, 메소드 단위로 구분하기가 쉬워지기 때문입니다. 저는 우선 두 개의 클래스를 사용하려고 합니다.
단순히 주사위를 굴리고 결과를 저장하고, 결과를 출력하는 책임을 갖고 있는 메서드를 가진 클래스
그리고 그 클래스를 실행하는 클래스
본디 자바의 main
은 프로그램 실행의 진입점으로서 반드시 어딘가에 만들어져 있어야 합니다. 그리고 우리는 스프링 부트를 켜서 기본으로 생성되어있는 클래스를 보시면
@SpringBootApplication
public class LibraryAppApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryAppApplication.class, args);
}
}
이렇게 단순히 스프링 애플리케이션을 실행하는 main
메서드와 명렁문만이 있습니다.
그래서, 저는 아래처럼 만들었습니다.
public class DiceApplication {
public static void main(String[] args) {
DiceRoller diceRoller = new DiceRoller();
diceRoller.rollDice();
diceRoller.printResult();
}
}
이 클래스에선 단순히 하나의 일만 합니다. DiceRoller 즉, 주사위를 굴리고 그에 따른 결과를 출력하는 책임을 갖고 있는 클래스를 생성하고, 일을 시키기만 합니다! 저는 예전에 유튜브에서 TDA, Tell, Don't Ask(요청하지말고, 시켜라) 라는 개념을 본 적이 있었는데 그 원칙에 최대한 맞춰보기 위해 위와 같이 작성했습니다.
그리고 DiceRoller라는 클래스는 아래와 같이 작성했습니다.
public class DiceRoller {
private int[] results = new int[6];
public void rollDice() {
System.out.print("숫자를 입력하세요 : ");
Scanner scanner = new Scanner(System.in);
int rolls = scanner.nextInt();
generateRandomRolls(rolls);
}
private void generateRandomRolls(int rolls) {
for (int i = 0; i < rolls; i++) {
int result = (int) (Math.random() * 6); // 0 to 5
results[result]++;
}
}
public void printResults() {
for (int i = 0; i < results.length; i++) {
System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, results[i]);
}
}
}
이 클래스는 초기 주사위 숫자 크기와 결과를 담고있는 배열을 선언하고 있구요. 다음 3개의 메서드를 갖고 있습니다.
숫자를 입력받고 입력받은 숫자만큼 주사위를 굴리도록 '시키는' 메서드
넘겨받은 숫자만큼 주사위를 굴리고 기록하는 메서드
그리고 그 결과를 출력하는 메서드.
물론 여기서 핵심 로직은 주사위를 굴리고 결과를 기록하는 것입니다. 더 나누려면 나눌 수 있습니다. 입출력 부분을 함께 수행하는 클래스로 말이죠. 하지만 여기서는 사이즈가 그렇게 크지 않으므로 입출력 및, 주사위 굴리고 결과를 기록하는 메서드를 한 클래스안에 두었습니다. 그리고 Math.random()
은 그 결과를 double
로 반환하는 까닭에 기존의 코드는
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.println()
을 System.out.print()
로 바꾸어 주었습니다.
그러면 한 걸음 더! 에 있는 문제를 해결해봅시다. 지금 저는 주사위의 숫자가 1부터 6까지 있다고만 가정했습니다. 하지만, 어느날 주사위라는 것 자체가 규격이 바뀌었다고 가정한다면 제가 작성한 코드는 오작동하는 코드입니다. 왜냐면
private int[] results = new int[6];
int result = (int) (Math.random() * 6)
이렇게 배열 크기를 주사위가 1부터 6까지 있다는 가정하에 직접 넣어주었기 때문입니다. 그래서 저는 필드에서 선언한 즉시 초기화를 하기보다는, 생성자로 초기화하는 것을 선택했습니다. 그러면 코드는 아래와 같이 바뀔 수 있습니다.
public class DiceApplication {
public static void main(String[] args) {
DiceRoller diceRoller = DiceRoller.makeDice();
diceRoller.rollDice();
diceRoller.printResults();
}
}
class DiceRoller {
private int[] results;
private int sides;
public DiceRoller(int sides) {
this.sides = sides;
this.results = new int[sides];
}
public static DiceRoller makeDice() {
Scanner scanner = new Scanner(System.in);
System.out.print("주사위 면의 수를 입력하세요 : ");
int sides = scanner.nextInt();
return new DiceRoller(sides);
}
public void rollDice() {
System.out.print("굴릴 횟수를 입력하세요 : ");
Scanner scanner = new Scanner(System.in);
int rolls = scanner.nextInt();
generateRandomRolls(rolls);
}
private void generateRandomRolls(int rolls) {
for (int i = 0; i < rolls; i++) {
int result = (int)(Math.random() * sides);
results[result]++;
}
}
public void printResults() {
for (int i = 0; i < sides; i++) {
System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, results[i]);
}
}
}
저는 스태틱 메서드를 사용해서 DiceRoller 객체를 생성하도록 하고 그렇게 생성된 주사위를 굴리고, 결과를 출력하게 끔 했습니다. 실행 결과는 아래와 같습니다.
어떤가요? 많이 클린해졌나요? 누군가는 '작성해야 하는 코드 양이 더 많아졌는데?' 라고 생각하실 순 있겠지만 내부 동작과 어떤 흐름으로 프로그램이 실행되는 지에 대한 '명확성' 부분에서 저는 좀 더 나아졌다고 생각합니다. 저의 리팩토링이 어떠했는지에 대해서 냉철한 평가를 내려주셔도 좋고 조금 더 개선할 부분이 있다면 알려주세요!
감사합니다!
댓글을 작성해보세요.