[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 클린코드 (Day5)

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 클린코드 (Day5)

진도표 5일차와 연결됩니다

우리는 <클린 코드>라는 개념을 배웠습니다. <클린 코드>에 대한 감각을 익히기 위해서는 어떤 코드가 좋은 코드이고, 어떤 코드가 좋지 않은 코드인지 이론적인 배경을 학습하는 것도 중요할 뿐 아니라, 다양한 코드를 읽어 보며 어떤 부분이 읽기 쉬웠는지, 어떤 부분이 읽기 어려웠는지, 읽기 어려운 부분은 어떻게 고치면 좋을지 경험해보는 과정이 필요합니다.

이번 과제는 제시된 코드를 읽어보며, 코드를 더 좋은 코드로 고쳐나가는 과정입니다. 구글에 “클린 코드” 혹은 “클린 코드 정리”를 키워드로 검색해보면, 이론적인 배경을 충분히 찾아보실 수 있습니다. 🙂 그러한 내용들을 보며 제시된 코드를 더 좋은 코드로 바꿔보세요! (코드를 바꿀 때 왜 바뀐 코드가 더 좋은 코드인지 다른 사람에게 설명하신다고 생각해보시면 더욱 좋습니다.)

 

[제시된 코드]

  • 여러 함수로 나누어도 좋습니다! 🙂

  • 여러 클래스로 나누어도 좋습니다! 🙂

image

image


클린코드

오늘이 벌써 5일차의 날이 밝았다. 이번에는 클린코드가 무엇인지, 어떤 코드가 좋은 코드이며, 어떠한 코드가 안 좋은 코드인지 알아보는 시간을 가졌다. 그리고 또한 모든 비즈니스 로직을 가지고 있는 하나의 controller 클래스를 service와 repository 레이어로 분리함으로 '단일책임의 원칙'을 지킬 수 있었다. 하지만 아직 더 궁금하고 공부하고 싶어서, 2개의 유튜브 영상을 시청하였다. 하나의 영상은 코치님이 올리신 영상이고 하나는 토스에서 올린 영상이였는데 먼저 시청을 해보기로 하였다. 영상 url은 아래 참고에 달아두기로 하겠다.

 

과제 전, 영상 학습

 

코치님 영상

이 영상의 핵심은 왜 엔티티에서 setter 를 지양해야 하는지에 대한 영상이었습니다. 먼저 결론부터 이야기 해보면 setter는 지양하자라는 말이다. 그 이유는 아래와 같다.

 

📚Setter 지양 이유

1. 변경의도가 파악하기 어렵다: 어느 엔티티에 setter가 있을 때 이 setter를 메서드에 묶어서 사용하는것은 함수의 이름으로도 확인이 가능하며, 이 안의 setter들이 함께 처리된다. 또한 코드가 몇 천줄 이상이 되었고 여기서 추가 요구사항이 있을 때 코드를 추적해서 변경해야 하는데 메서드로 묶으면 한 곳만 수정을 해주면 되겠지만 setter를 직접 해주면 어마 무시한 코드의 수정이 일어날 것이다. 즉, 메서드로 묶었을 때 코드의 응집성이 좋아진다.

2. 객체 일관성 유지를 할 수 없다.

3. 1번과 연관되지만 메서드로 묶지 않고 setter를 직접 사용하면 생산성 차이가 발생한다.

위의 내용은 나에게 와 닿는 부분에 대해서 이야기를 했고 자세한 부분을 원하면 영상에 직접 들어가서 확인 바란다.

 

토스 영상 (feat. 진유림님)

토스 SLASH21에서 진유림님이 발표한 영상을 참고해보았다. 이 영상에서는 지뢰코드를 예시로 들고 있다.

🙋🏻 지뢰코드란?

회사에서 '이거 건들지 않는게 좋을꺼에요.'의 코드들이 있을 것이다. 이런 코드들은 흐름 파악이 어렵고 도메인 맥락 표현이 안되어 있으며 동료에게 물어봐야 알 수 있는 코드를 뜻한다. 이 코드는 개발할 때 병목현상이 발생하고 유지보수할 때 많은 시간이 들고 심하면 기능추가도 되지 않을 뿐더러 성능도 떨어질 수 있다.

그래서 클린코드를 도입하여 이런 코드의 유지보수 시간을 단축(코드 파악 단축, 디버깅 시간 단축, 코드리뷰 단축)할 수 있다.

 

안일한 코드 함정

지뢰코드는 하나의 목적인 코드가 흩어져서 확인할려면 스크롤을 왔다갔다 해야하는 코드를 뜻한다. 우리 회사의 몇개의 코드들이 생각나는 부분이였다. 이런 이유가 된 것은 하나의 함수에 여러가지 일을 하기 때문이다. 그래서 세부구현을 모두 읽어야 함수의 역할을 알게 된다. 그래서 단일책임원칙등 이런 클린코드 개념들이 나왔다. 하지만 이 코드는 처음부터 지저분한 코드들은 아니었을 것이다. 즉, 지뢰코드도 그 당시에는 좋은 코드였을 것이다. 그러다가 이런저런 기능들을 무작정 추가가 되버리고 지뢰코드로 변화된 것일 것이다.

 

📚 결론

클린코드는 단순히 짧은 코드가 아니다. 원하는 로직을 빠르게 찾을 수 있는 코드를 의미한다.

또한 선언적 프로그래밍으로 가되, 명령형 프로그래밍 기법을 적절히 사용한다.

핵심 기능만 보이게 하고 일부 세부기능은 일단 숨기자.


블로그 글

클린코드에 대해 구글링을 한 결과, 여러 블로그들이 나왔다. 그 중에, 어느 블로그에 정리가 잘 되어 있어서 일부만 발췌해서 정리해보았다. 자세한 글은 직접 가셔서 읽어보는 것을 추천한다.

 

📚클린코드 정리

1. 객체의 생성에도 유의미한 이름을 사용하라

2. 함수는 하나의 역할만 해야한다.

3. 명령과 조회를 분리하라(Command와 Query의 분리)

4. 오류코드 보다는 예외를 활용하자

5. 여러 예외가 발생하는 경우 Wrapper 클래스로 감싸자

6. 테스트 코드의 작성

7. 변경하기 쉬운 클래스 + 클래스 응집도

8. 디미터 법칙

 

🙋🏻 디미터 법칙: 디미터의 법칙은 어떤 모듈이 호출하는 객체의 속사정을 몰라야 한다는 것이다. 그렇기에 객체는 자료를 숨기고 함수를 공개해야 한다. 만약 자료를 그대로 노출하면 내부 구조가 드러나 결합도가 높아지게 된다.


과제

그럼 과제에서 제공한 코드를 클린코드의 원칙을 최대한 지키며 리팩토링 해보는 과정을 보여주겠다.

 

package me.sungbin;

package me.sungbin;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        System.out.print("숫자를 입력하세요 : ");
        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);
    }
}

 

Step0. 코드 분석

일단 먼저 눈에 고쳐야 할 부분이 보이는데 바로 아래와 같다.

 

1. 변수명 개선: r1, r2, r3... 이런 부분과 단순 알파벳 하나로 되어 있는 부분을 고쳐야 할 것 같다.

변수를 이렇게 작성하여 코딩하면 위의 코드가 나중에 몇 천줄이 되고 유지보수를 할 때 이 변수는 대체 어떤 변수인지 파악이 어렵기 때문이다. 그래서 유의미한 이름으로 작성을 해보는 것이 좋을 것 같다.

2. 매직 넘버 제거: 주사위 면 값을 상수로 정의한다. 이유는 나중에 이런 매직넘버가 여러 코드에 흩어져 있다고 가정하자. 그런데 어느날 주사위가 육면체가 아닌 36면체로 변경해야한다는 것이다. 그러면 이 매직넘버를 하나하나 찾아서 바꿔주야 하지만 상수로만 정의하면 상수 값만 바꾸면 되기 때문이다.

3. 중복 제거: 주사위를 굴리는 로직과 출력 부분을 함수로 분리. 이것은 개발자면 당연하다고 느끼는 부분일 것이다. 현재 출력하는 부분과 주사위 굴리는 조건문과 증가 연산자 부분이 뭔가 중복되는 것 같다. 그래서 이 부분도 함수로 변경할 예정이다.

4. 책임 분리: 주사위 굴리기와 결과 출력을 분리. 이는 위에서 설명한 클린코드의 단일책임의 원칙으로 분리하는 것이 유지보수성에 좋을 것이다. 왜 좋은지는 두 영상과 블로그를 참조하자.

5. 배열사용: 주사위 눈금 횟수를 배열로 사용하는 것은 코드 가독성을 높이고 중복을 줄이며 유지 보수를 쉽게 만든다.

 

그러면 정리해보겠다. 클린 코드 원칙에 따라, 현재 코드는 여러 가지 방법으로 개선될 수 있다. 먼저, 'r1', 'r2', 'r3', 'r4', 'r5', 'r6'와 같은 변수명은 의미를 명확하게 전달하지 않는다. 더 명확한 변수명을 사용할 수 있습니다. 또한, 각각의 조건문은 매직 넘버(magic number)를 사용하고 있는데, 이는 읽는 사람이 코드의 의도를 바로 이해하기 어렵게 만듭니다. 상수를 사용하여 이러한 숫자에 이름을 붙여 가독성을 높일 수 있습니다.

다음으로, 주사위의 각 면을 계산하는 부분은 반복되는 구조를 가지고 있으므로 이를 함수로 분리하여 중복을 줄이고 코드의 재사용성을 높일 수 있습니다. 또한, 현재 코드는 주사위를 굴린 후 결과를 출력하는 두 가지 작업을 동시에 수행하고 있습니다. 이를 분리하여 한 함수는 주사위를 굴리는 로직을, 다른 함수는 결과를 출력하는 로직을 담당하게 하는 것이 좋습니다.

마지막으로, 주사위의 각 면이 나온 횟수를 저장하는 배열을 사용하면 변수를 줄일 수 있고, for 루프 안에서의 조건문을 간소화할 수 있습니다. 이렇게 코드를 개선하면 유지 관리가 용이해지고 다른 개발자가 이해하기 쉬운 코드가 됩니다.

 

🙋🏻 매직넘버란? 의미 있는 이름의 상수로 대체될 수 있는 숫자

 

Step1. 리팩토링 첫 단계

위의 부분을 적용한 코드는 아래와 같다.

package me.sungbin.step1;

import java.util.Scanner;

/**
 * @author : rovert
 * @packageName : me.sungbin.step1
 * @fileName : Main
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class Main {

    private static final int SIDES_OF_DICE = 6;

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

        int[] counts = rollDice(rolls);
        printResults(counts);
    }

    /**
     * 주사위를 굴리는 로직이 들어가는 메서드
     * @param numberOfRolls
     * @return
     */
    private static int[] rollDice(int numberOfRolls) {
        int[] counts = new int[SIDES_OF_DICE];
        for (int i = 0; i < numberOfRolls; i++) {
            int result = (int) (Math.random() * SIDES_OF_DICE);
            counts[result]++;
        }
        return counts;
    }

    /**
     * 출력기능을 담당하는 메서드
     * @param counts
     */
    private static void printResults(int[] counts) {
        for (int i = 0; i < counts.length; i++) {
            System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]);
        }
    }
}
  • SIDES_OF_DIE 상수를 통해 주사위 면의 수를 명확하게 합니다.

  • rollDice 함수는 주사위를 굴리는 작업만 수행하고, 그 결과를 int[] 배열에 저장합니다.

  • printResults 함수는 주사위의 각 면이 나온 횟수를 출력합니다.

  • int[] counts 배열은 0부터 5까지 각 면이 나온 횟수를 저장합니다. 배열의 인덱스는 주사위 면의 숫자에 해당합니다.

  • Scanner 객체를 종료하는 부분이 누락되어 추가하였다.

  • 불필요한 Exception 던지는 부분을 제거한다.

코드를 구조화함으로 각 부분의 책임이 명확해지고, 재사용성과 유지보수성이 향상된다.

 

Step2. 리팩토링 2단계 (feat. 객체지향)

위와 같이 static method로 분리하면 객체지향적이지 않은 것 같다는 생각을 했고, 하나의 파일에 모든 로직이 있게 되는 셈인 것 같았다. 그래서 객체지향적으로 작성을 위해 여러 파일들을 만들어 분리하기로 하였다. 적용한 코드는 아래와 같다.

 

Dice.java

package me.sungbin.step2;

/**
 * @author : rovert
 * @packageName : me.sungbin.step2
 * @fileName : Dice
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class Dice {
    private final int sides;

    public Dice(int sides) {
        this.sides = sides;
    }

    /**
     * 주사위 면의 숫자 구하기
     * @return
     */
    public int roll() {
        return (int) (Math.random() * sides);
    }

    public int getSides() {
        return sides;
    }
}

 

DiceRollHandler.java

package me.sungbin.step2;

/**
 * @author : rovert
 * @packageName : me.sungbin.step2
 * @fileName : DiceRollHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class DiceRollHandler {

    private final Dice dice;

    private final int[] counts;

    public DiceRollHandler(Dice dice, int numberOfRolls) {
        this.dice = dice;
        this.counts = new int[dice.getSides()];
        for (int i = 0; i < numberOfRolls; i++) {
            this.counts[dice.roll()]++;
        }
    }

    public void rollAll() {
        for (int i = 0; i < counts.length; i++) {
            counts[dice.roll()]++;
        }
    }

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

 

DiceGame.java

package me.sungbin.step2;

import java.util.Scanner;

/**
 * @author : rovert
 * @packageName : me.sungbin.step2
 * @fileName : Main
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class DiceGame {

    private static final int SIDES_OF_DICE = 6;
    public static void main(String[] args) {
        System.out.print("숫자를 입력하세요 : ");
        Scanner scanner = new Scanner(System.in);
        int rolls = scanner.nextInt();
        scanner.close();

        Dice dice = new Dice(SIDES_OF_DICE); // 정해진 면의 수를 가진 주사위 객체 생성
        DiceRollHandler handler = new DiceRollHandler(dice, rolls); // 주사위를 던지는 이벤트에 대한 핸들러 객체 생성
        handler.rollAll(); // 주사위 던지기 이벤트 비즈니스 로직
        handler.printResults(); // 출력
    }
}

이 구조에서는 Dice 클래스가 주사위 자체를 나타내고, DiceRollHandler 클래스가 주사위를 굴리고 결과를 추적하는 역할을 한다. DiceGame 클래스의 main 메서드는 게임을 시작하는 진입점이다.

  • Dice 클래스는 주사위의 기능(굴리기)과 속성(면의 수)을 캡슐화한다.

  • DiceRollHandler 클래스는 주사위를 여러 번 굴리는 로직을 책임지고, 각 면이 나온 횟수를 저장합니다.

  • 주사위 게임의 진행과 결과 출력은 DiceRollHandler 클래스에서 담당한다.

  • Main클래스로 되어있는 부분을 이름을 변경하였다. 일단 Main이라는 클래스 명은 이 프로젝트가 무엇인지 알기 어렵기 하기 때문이다.

     

이렇게 클래스를 분리하면 코드의 각 부분이 명확한 책임을 가지게 되어 유지보수가 용이하고, 다른 주사위 게임에 Dice 클래스나 DiceRollHandler 클래스를 재사용할 수 있는 가능성이 생깁니다.

Step3. 리팩토링 3단계

다음으로 코드를 더 클린하게 만들기 위해 여러 가지 최적화와 리팩토링을 수행해보겠다.

 

1. 메소드 분리와 책임의 명확화: 각 메서드가 하나의 작업만 수행하도록 만들어 코드의 가독성을 높인다.

2. 입출력 분리: 사용자 입력과 출력을 처리하는 별도의 클래스를 만들어 로직과 UI를 분리한다.

3. 예외 처리 개선: Scanner를 사용할 때 발생할 수 있는 예외를 적절히 처리합니다.

 

Dice.java

package me.sungbin.step3;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : Dice
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class Dice {
    private final int sides;

    public Dice(int sides) {
        this.sides = sides;
    }

    public int roll() {
        return (int) (Math.random() * sides);
    }

    public int getSides() {
        return sides;
    }
}

DiceRollHandler.java

package me.sungbin.step3;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : DiceRollHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class DiceRollHandler {
    private final Dice dice;
    private final int[] counts;

    public DiceRollHandler(Dice dice) {
        this.dice = dice;
        this.counts = new int[dice.getSides()];
    }

    public void rollAll(int numberOfRolls) {
        for (int i = 0; i < numberOfRolls; i++) {
            counts[dice.roll()]++;
        }
    }

    public int[] getCounts() {
        return counts;
    }
}

InputHandler.java

package me.sungbin.step3;

import java.util.Scanner;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : InputHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class InputHandler {
    public static int getNumberOfRolls() {
        System.out.print("숫자를 입력하세요 : ");
        try (Scanner scanner = new Scanner(System.in)) {
            return scanner.nextInt();
        } catch (Exception e) {
            throw new IllegalArgumentException("유효하지 않은 입력입니다.");
        }
    }
}

OutputHandler.java

package me.sungbin.step3;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : OutputHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class OutputHandler {
    public static void printResults(int[] counts) {
        for (int i = 0; i < counts.length; i++) {
            System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]);
        }
    }
}

DiceGame.java

package me.sungbin.step3;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : DiceGame
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class DiceGame {
    private static final int SIDES_OF_DIE = 6;

    public static void main(String[] args) {
        int rolls = InputHandler.getNumberOfRolls();
        Dice dice = new Dice(SIDES_OF_DIE);
        DiceRollHandler roller = new DiceRollHandler(dice);
        roller.rollAll(rolls);
        OutputHandler.printResults(roller.getCounts());
    }
}

이러한 리팩토링을 통해 코드의 가독성과 관리 가능성이 향상되었다. 입력과 출력을 담당하는 InputHandlerOutputHandler 클래스는 각각의 책임을 분명히 하며, DiceGame은 게임의 로직만을 담당하게 된다. 예외 처리를 통해 프로그램의 예외를 처리할 수 있게 하며, 견고해진 프로그램이 될 것 같다.

 

Step4. 한걸음 더! 적용 (feat. 디자인 패턴)

코치님이 한걸음 더에서 주사위의 숫자범위가 달라지더라도 코드를 적게 수정할 수 있도록 해보시라고 하셨다. 그래서 한번 고민을 해보았다. 지금도 주사위 최대 수를 상수로 빼두었기에 충분하다고 생각은 들었다. 또한 디자인패턴을 적용을 해봄으로 더 견고한 코드를 작성해보았다.

 

DiceRollStrategy.java

package me.sungbin.step4;

/**
 * @author : rovert
 * @packageName : me.sungbin.step4
 * @fileName : DiceRollStrategy
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public interface DiceRollStrategy {
    int roll();
    int getSides(); // 면 수를 반환하는 메소드 추가
}

 

Dice.java

package me.sungbin.step4;

/**
 * @author : rovert
 * @packageName : me.sungbin.step4
 * @fileName : Dice
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE 			AUTHOR			 NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class Dice {
    private final DiceRollStrategy strategy;

    public Dice(DiceRollStrategy strategy) {
        this.strategy = strategy;
    }

    public int roll() {
        return strategy.roll();
    }

    // 주사위의 면 수를 반환하는 메소드 추가
    public int getSides() {
        return this.strategy.getSides();
    }
}

 

StandardDiceRollStrategy.java

package me.sungbin.step4;

/**
 * @author : rovert
 * @packageName : me.sungbin.step4
 * @fileName : StandardDiceRollStrategy
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class StandardDiceRollStrategy implements DiceRollStrategy {

    private final int sides;

    public StandardDiceRollStrategy(int sides) {
        this.sides = sides;
    }

    @Override
    public int roll() {
        return (int) (Math.random() * sides) + 1;
    }

    @Override
    public int getSides() {
        return this.sides;
    }
}

 

InputHandler.java

package me.sungbin.step4;

import java.util.Scanner;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : InputHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class InputHandler {
    public static int getNumberOfRolls() {
        System.out.print("숫자를 입력하세요 : ");
        try (Scanner scanner = new Scanner(System.in)) {
            return scanner.nextInt();
        } catch (Exception e) {
            throw new IllegalArgumentException("유효하지 않은 입력입니다.");
        }
    }
}

 

OutputHandler.java

package me.sungbin.step4;

/**
 * @author : rovert
 * @packageName : me.sungbin.step3
 * @fileName : OutputHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class OutputHandler {
    public static void printResults(int[] counts) {
        for (int i = 0; i < counts.length; i++) {
            System.out.printf("%d은 %d번 나왔습니다.\n", i + 1, counts[i]);
        }
    }
}

 

DiceRollHandler.java

package me.sungbin.step4;

/**
 * @author : rovert
 * @packageName : me.sungbin.step4
 * @fileName : DiceRollHandler
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class DiceRollHandler {
    private final Dice dice;
    private final int[] counts;

    public DiceRollHandler(Dice dice) {
        this.dice = dice;
        this.counts = new int[dice.getSides()]; // 주사위 면 수에 맞는 크기로 배열 초기화
    }

    public void rollAll(int numberOfRolls) {
        for (int i = 0; i < numberOfRolls; i++) {
            int result = dice.roll() - 1; // 0부터 시작하는 배열 인덱스에 맞추기 위해 1을 뺌
            counts[result]++;
        }
    }

    public int[] getCounts() {
        return counts;
    }
}

 

DiceGame.java

package me.sungbin.step4;

/**
 * @author : rovert
 * @packageName : me.sungbin.step4
 * @fileName : DiceGame
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
public class DiceGame {
    private static final int SIDES_OF_DIE = 6; // 기본값, 필요에 따라 변경 가능

    public static void main(String[] args) {
        int rolls = InputHandler.getNumberOfRolls();
        DiceRollStrategy strategy = new StandardDiceRollStrategy(SIDES_OF_DIE); // 전략 선택
        Dice dice = new Dice(strategy);
        DiceRollHandler roller = new DiceRollHandler(dice);
        roller.rollAll(rolls);
        OutputHandler.printResults(roller.getCounts());
    }
}

 

Step5. 테스트 코드

이제 참고한 블로그에 따르면 테스트코드도 반드시 필요하다고 한다. 그럼 마지막으로 테스트 코드를 작성하고 마무리하자. 테스트 코드는 주요 로직들을 테스트하였다.

 

package me.sungbin.step4;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author : rovert
 * @packageName : me.sungbin.step4
 * @fileName : DiceRollHandlerTest
 * @date : 2/23/24
 * @description :
 * ===========================================================
 * DATE           AUTHOR        NOTE
 * -----------------------------------------------------------
 * 2/23/24       rovert         최초 생성
 */
class DiceRollHandlerTest {

    @Test
    @DisplayName("Dice 클래스의 주사위 롤링 기능이 1부터 6 사이의 값을 정확히 생성하는지 검증")
    public void testDiceRoll() {
        DiceRollStrategy strategy = new StandardDiceRollStrategy(6);
        Dice dice = new Dice(strategy);
        int result = dice.roll();

        assertTrue(result >= 1 && result <= 6);
    }

    @Test
    @DisplayName("StandardDiceRollStrategy 클래스를 통해 생성된 주사위 값이 1부터 6 사이인지 확인")
    public void testStandardDiceRollStrategy() {
        StandardDiceRollStrategy strategy = new StandardDiceRollStrategy(6);
        int result = strategy.roll();
        assertTrue(result >= 1 && result <= 6);
    }
}

회고

이렇게 클린코드로 코드를 리팩토링 해보니, 많은 것을 찾아보고 나 자신이 발전한 느낌이 들었다. 이런 부분을 개인적으로도 자주 연습해봐야겠다.

 

📚 참고

https://www.youtube.com/watch?v=5P7OZceQ69Q
https://www.youtube.com/watch?v=edWbHp_k_9Y

https://mangkyu.tistory.com/132

댓글을 작성해보세요.

  • jd
    jd

    안녕하세요 성빈님 주다애 입니다.5일차 과제 수고하셨습니다!
    리뷰 시작하겠습니다.

     

    1. 개인적으로 DiceRollStrategy 인터페이스를 놓고 StandardDiceRollStrategy가 구현하도록 만든 부분이 눈에 띄었습니다! (이런 방식이 전략 패턴 맞을까요..?) 프로그램 확장성 면에서 좋을 것 같다는 생각을 했습니다.

    2. Dice 객체를 만들어서 값을 저장하도록 한 부분도 좋았습니다. 저는 이번 프로그램 규모가 작아서 그냥 메소드들로만 처리를 했는데 Dice 클래스를 만드신 부분이 더 객체 지향적일 것 같네요.

    3. 패키지를 나누는 것은 어떨까요? 저는 주로 view, controller.. 이렇게 패키지를 나누어서 안에 클래스를 저장합니다. 나중에 프로그램에 클래스가 추가된다면 패키지를 나누는 것이 더 관리하기 편할 것 같아요!

    4. DiceGamemain 메소드가 하는 일이 많은 것 같습니다. 의존성 주입 부분을 메소드로 빼는 것은 어떨지 의견이 궁금합니다. 다른 방법도 있으시면 그것도 궁금합니다.

    5. // 면 수를 반환하는 메소드 추가와 같은 주석은 필수적이지는 않다고 생각합니다.

    코드 보면서 많이 배워갑니다! 고생하셨습니다😀


    양성빈
    양성빈

    전략패턴 맞습니다!
    main 부분이 하는 역할이 많다고 느껴졌는데 의존성 주입 부분에 대해서는 전혀 생각을 못했었네요!
    피드백 감사합니다!

  • 김영림
    김영림
    public class Dice {
        private final int sides;
    
        public Dice(int sides) {
            this.sides = sides;
        }
    
        public int roll() {
            return (int) (Math.random() * sides);
        }
    
        public int getSides() {
            return sides;
        }
    }

    오오 이렇게 Dice 클래스 안에서 roll를 관리하니까 훨씬 더 객체지향?스러워졌네요
    덕분에 많은 생각을 하고갑니다ㅎㅎ


    양성빈
    양성빈

    칭찬 감사합니다~ 아직 많이 부족해서.. 같이 성장해요!

  • 영후이
    영후이

    Step 4. 의 Dice.java 파일이 잘못 올라가 있는거 같습니다! image양질의 포스트 잘보고갑니다! 🙇‍♂


    양성빈
    양성빈

    와~ 정말 감사합니다! 방금 수정 완료했습니다! 피드백 감사합니다!