반응형

1. 개요

  • TDD 공부에 들어가기 전, 코드 리팩토링 연습을 위해 '객체지향 생활체조 9가지 원칙'을 공부하였다. 이론만 숙지하고 넘기기에는 너무 중요한 내용인것 같아 실습을 진행하였다.
  • 실습 프로그램은 초간단 홀짝게임이며, 각각의 원칙을 하나씩 적용하여 리팩토링 해나갔다.
  • 코드는 피드백을 받을 여건이 되지 않아 오로지 필자의 생각만으로 짜여져있다. 도둑놈 심보인걸 잘 알지만... 눈살이 찌뿌려지는 부분이 있다면 독자분들께서 댓글을 달아주셨으면 좋겠다. 피드백을 적극 수용할 자신이 있다.

1. 한 메서드에 오직 한 단계의 들여쓰기만 한다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.

3) 1차 코드

    public void holjjakVer1() {
        System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

        Scanner in = new Scanner(System.in);

        while(true) {

            int input = in.nextInt();

            Random random = new Random();
            int randomNumber = random.nextInt(9)+1;

            if(input > 0 && input <3) {

                int result = randomNumber%2;

                if(result == 0 && input == 2) {
                    System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
                }else if(result == 1 && input == 1) {
                    System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
                }else {
                    System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
                }

            }else {
                System.out.println("종료합니다.");
                break;
            }
        }
        in.close();
    }
  • 제 1 법칙을 생각하지 않고 평소처럼 프로그래밍 했다.

4) 리팩토링 코드

    public void holjjakVer2() {

        System.out.println("개같은 홀짝 게임!");
        System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

        Scanner in = new Scanner(System.in);
        int input = 0;

        while(inputCheck(input = in.nextInt())) {
            int randomNumber = makeRandomNumber();
            check(input, randomNumber);
        }

        in.close();
    }

    /**
     * @title 입력값 체크
     * @param input
     * @desc 사용자가 입력한 값을 체크한다. 
     * 1,2 일 경우 홀짝 게임을 계속 진행하고, 그 외일 경우 종료한다.
     */
    public boolean inputCheck(int input) {

        if(input == 1 || input == 2) {
            return true;
        }else {
            System.out.println("종료되었습니다.");
            return false;
        }
    }

    /**
     * @title 난수 생성
     * @param 
     * @dsec 1에서 10까지의 난수를 생성한다.
     */
    public int makeRandomNumber() {

        Random random = new Random();
        return random.nextInt(9)+1;
    }

    /**
     * @title 홀짝 체크
     * @param input, randomNumber
     * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
     */
    public void check(int input, int randomNumber) {

        int result = randomNumber%2;

        if(result == 0 && input == 2) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
        }else if(result == 1 && input == 1) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
        }else {
            System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
        }

    }
  1. 제 1법칙을 준수하고자 하니 자연스럽게 메서드가 분리되었다.
  2. 메서드를 분리하다 보니 자연스럽게 기능별로 분리하게 되었다.
  3. 결합도가 낮아지고, 응집도가 높아지니 코드 수정이 편해짐을 느꼈다.

5) 회고

몇줄 안되는 간단한 프로그램을 구현하고, 1 법칙을 적용시키는데에도 많은 시간이 걸렸다. 1법칙을 적용해본 결과, 메서드를 늘리는 것 자체가 가독성을 저해한다고 생각했는데 그 반대였다. 가독성은 좋아지고 메서드가 기능별로 분리되어 있어 기능에 집중하여 코드를 짤 수 있었다. 집중도도 높아지는 것을 느꼈다.

1법칙 적용 전에는 이렇게 간단한 코드로 모든 법칙을 적용할 수 있을까라는 의구심이 있었지만, 지금은 법칙을 적용시켜나갈때마다 어떻게 변할까 기대가 된다.

 


2. Else_Switch_키워드를_사용하지_않는다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.

3) 기존 코드

    public void holjjakVer2() {

        System.out.println("안신나는 홀짝 게임!");
        System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

        Scanner in = new Scanner(System.in);
        int input = 0;

        while(inputCheck(input = in.nextInt())) {
            int randomNumber = makeRandomNumber();
            check(input, randomNumber);
        }

        in.close();
    }

    /**
     * @title 입력값 체크
     * @param input
     * @desc 사용자가 입력한 값을 체크한다. 
     * 1,2 일 경우 홀짝 게임을 계속 진행하고, 그 외일 경우 종료한다.
     */
    public boolean inputCheck(int input) {

        if(input == 1 || input == 2) {
            return true;
        }else {
            System.out.println("종료되었습니다.");
            return false;
        }
    }

    /**
     * @title 난수 생성
     * @param 
     * @dsec 1에서 10까지의 난수를 생성한다.
     */
    public int makeRandomNumber() {

        Random random = new Random();
        return random.nextInt(9)+1;
    }

    /**
     * @title 홀짝 체크
     * @param input, randomNumber
     * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
     */
    public void check(int input, int randomNumber) {

        int result = randomNumber%2;

        if(result == 0 && input == 2) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
        }else if(result == 1 && input == 1) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
        }else {
            System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
        }

    }

4) 리팩토링 코드

   public void holjjak() {

        System.out.println("안신나는 홀짝 게임!");
        System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

        Scanner in = new Scanner(System.in);
        int input = 0;

        while(inputCheck(input = in.nextInt())) {
            int randomNumber = makeRandomNumber();
            check(input, randomNumber);
        }

        in.close();
    }

    /**
     * @title 입력값 체크
     * @param input
     * @desc 사용자가 입력한 값을 체크한다. 
     * 1,2 일 경우 홀짝 게임을 계속 진행하고, 그 외일 경우 종료한다.
     */
    public boolean inputCheck(int input) {

        if(input == 1 || input == 2) {
            return true;
        }

        System.out.println("종료되었습니다.");
        return false;
    }

    /**
     * @title 난수 생성
     * @param 
     * @dsec 1에서 10까지의 난수를 생성한다.
     */
    public int makeRandomNumber() {

        Random random = new Random();
        return random.nextInt(9)+1;
    }

    /**
     * @title 홀짝 체크
     * @param input, randomNumber
     * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
     */
    public void check(int input, int randomNumber) {

        int result = randomNumber%2;

        if(result == 0 && input == 2) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
            return;
        }
        if(result == 1 && input == 1) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
            return;
        }

        System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
    }
  1. inputCheck 메서드의 else를 제거하였다. 제거하고 보니 애초에 else는 불필요했다.
  • before
    public boolean inputCheck(int input) {

        if(input == 1 || input == 2) {
            return true;
        }else {
            System.out.println("종료되었습니다.");
            return false;
        }
    }
  • after
    public boolean inputCheck(int input) {

        if(input == 1 || input == 2) {
            return true;
        }

        System.out.println("종료되었습니다.");
        return false;
    }
  1. check 메서드의 else if, else를 제거하고 return하는 형식으로 수정했다. 마찬가지로 return이 있으니 else가 필요없었다.
  2. else if 대신 if-return 구문을 사용하니 코드가 분리되는 느낌(?)을 받았다. 말로 설명하긴 힘든데 else if는 상위 if문과 엮여있는 느낌이랄까... 그리고 else if문은 else{ if(조건){ 로직 }} 형식의 메커니즘이었다. 내부적으로 불필요한 else문을 실행하고, 객체지향 생활체조 1법칙을 위반하는 것이기도 했기에 고민없이 if문으로 대체하였다. 가독성면에서도 훨씬 좋아보인다.
  • before
    public void check(int input, int randomNumber) {

        int result = randomNumber%2;

        if(result == 0 && input == 2) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
        }else if(result == 1 && input == 1) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
        }else {
            System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
        }

    }
  • after
    public void check(int input, int randomNumber) {

        int result = randomNumber%2;

        if(result == 0 && input == 2) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
            return;
        }
        if(result == 1 && input == 1) {
            System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
            return;
        }

        System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
    }

5) 회고

역시 제2 법칙도 적용할게 있었다. else 문과 else if문보다 if문으로 처리하는 것이 가독성과 코드 품질면에서 득이 있었다. 또한, 조건절이 들어가는 모든 코드에 별 생각없이 else if, else를 사용했던 이유를 생각해봤는데, 이유를 생각해보지 않았던게 이유였다. 이제 문법을 사용할 때 사용하는 이유를 먼저 생각해보는 습관을 가져야겠다.

 


3. 모든 원시값과 문자열을 포장(wrap)한다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.

3) 기존 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            int input = 0;

            while(inputCheck(input = in.nextInt())) {
                int randomNumber = makeRandomNumber();
                check(input, randomNumber);
            }

            in.close();
        }

        /**
         * @title 입력값 체크
         * @param input
         * @desc 사용자가 입력한 값을 체크한다. 
         * 1,2 일 경우 홀짝 게임을 계속 진행하고, 그 외일 경우 종료한다.
         */
        public boolean inputCheck(int input) {

            if(input == 1 || input == 2) {
                return true;
            }

            System.out.println("종료되었습니다.");
            return false;
        }

        /**
         * @title 난수 생성
         * @param 
         * @dsec 1에서 10까지의 난수를 생성한다.
         */
        public int makeRandomNumber() {

            Random random = new Random();
            return random.nextInt(9)+1;
        }

        /**
         * @title 홀짝 체크
         * @param input, randomNumber
         * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
         */
        public void check(int input, int randomNumber) {

            int result = randomNumber%2;

            if(result == 0 && input == 2) {
                System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
                return;
            }
            if(result == 1 && input == 1) {
                System.out.println("개수는 "+ randomNumber +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber +"! 틀렸습니다.");
        }
    }

4) 리팩토링 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");
            Scanner in = new Scanner(System.in);

            InputValue inputValue = null;

            while((inputValue = new InputValue(in.nextInt())).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                check(inputValue, randomNumber);
            }

            in.close();
        }

        /**
        * @title 홀짝 체크
        * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
        */
        public void check(InputValue inputValue, RandomNumber randomNumber) {

            if((!randomNumber.isHol() && !inputValue.isHol()) || (randomNumber.isHol() && inputValue.isHol())) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        }
    }

    /**
    * @title 사용자 입력 값 클래스
    * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
    */
    class InputValue{

        int inputValue;

        // 생성자
        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(this.inputValue == 1 || this.inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        // 홀 여부 체크
        public boolean isHol() {
            if(inputValue == 1) {
                return true; 
            }
            return false;
        }
    }

    /**
    * @title 랜덤 값 클래스
    * @desc 랜덤 값을 관리하는 Wrapper 클래스
    */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            this.randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return this.randomNumber;
        }

        // 홀 여부 체크
        public boolean isHol() {

            if(this.randomNumber % 2 == 1) {
                return true;
            }
            return false;
        }
    }ㅒ

1. 사용자의 입력 값을 관리하는 InputValue 클래스를 생성하였다. 입력 값에 대한 체크, 홀수 여부를 체크하는 메서드 또한 위 클래스로 이관하여 입력 값에 대한 모든 책임을 지도록 하였다.

2. 랜덤 값을 관리하는 RandomNumber 클래스를 생성하였다. 랜덤 값에 생성, 조회, 홀수 여부를 체크하는 메서드또한 위 클래스로 이관하여 랜덤 값에 대한 모든 책임을 지도록 하였다.

3. check 메서드의 홀 짝 체크 대한 if문 2개가 조건만 다르고 내용은 동일하여 하나의 조건절로 병합하였다. 조건절 또한 isHol 메서드를 사용하여 가독성을 높였다.

5) 회고

제 3법칙 적용 전 이 법칙을 왜 써야하는지 이해가 잘 되지 않아 관련 내용을 머릿속에 정리한 후 작업하였다.

1. 상태나 정보를 갖는 변수값은 비지니스적으로 의미있는 값이다. 이를 단순 변수로 처리하는것 보다 책임을 위임할 수 있는 클래스 형태로 관리하는 것이 좋다.
2. 원시타입을 포장함으로써 메서드의 시그니처가 명확해진다. 클래스로 포장한 타입의 메서드를 파라미터로 사용하게 되면 메소드를 설명하기 더욱 쉬워진다. 아래 두 메서드를 비교해보면 아래의 메서드 시그니처가 더욱 명확함을 알 수 있다. 출처 : https://limdingdong.tistory.com/9
public void evaluateCustomerCreditRate(int score)
public void evaluateCustomerCreditRate(CreditScore creditScore)

1번은 확실히 이해가 가지만 2번의 경우 확실히 이해가 가지 않는다. Wrapper 클래스로 관리하는게 시그니처를 명확하게한다? 그로인한 장점도 크게 와닿지 않는다. 이 내용은 계속 리팩토링을 해 나가며 이해하도록 노력해야겠다.

시그니처를 명확하게 한다는 뜻은 메서드 명과 파라미터가 명확해진다는 뜻. 여기서 말하는 명확의 대상은 파라미터를 의미한다. 원시 타입의 파라미터는 그 값에 대한 검증이 이루어지지 않았다. 위 코드를 예로 들어보면 evaluateCustomerCreditRate의 score는 어떤 값이든 들어올 수 있다. 마이너스든, 큰숫자든 말이다. 하지만 CreditScore WraaperClass는 특정 범위의 값을 가진 값으로 시그니처를 명확하게 할 수 있다. 예를들어 CreditScore의 score를 모든 int가 아닌 자연수로 한정하려면 score에 대한 WrapperClass를 아래와 같이 만들면 된다. 시그니처가 명확해지며 CreditScore 생성자만 확인한다면 된다.

class CreditScore{

    int score;

    public CreditScore(int score) {

        this.score = score;
    }

    public void validationScore(int score){
        if(score < 0) {
            throw new RuntimeException("신용점수가 0보다 작습니다.");
        }
    }
}

WrapperClass가 아닌 일반 변수로 관리한다면 검증 로직을 모두 서비스단에 적용해야하며, 관련 부분 모두 동일하게 적용해야한다. 만약 한곳이라도 누락시킨다면 버그가 발생할 수 있게 된다.

 


4. 한 줄에 점을 하나만 찍는다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.
  8. 객체지향 생활체조 9법칙의 4법칙 '한 줄에 점을 하나만 찍는다.'을 무조건 준수한다.

3) 기존 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");
            Scanner in = new Scanner(System.in);

            InputValue inputValue = null;

            while((inputValue = new InputValue(in.nextInt())).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                check(inputValue, randomNumber);
            }

            in.close();
        }

        /**
        * @title 홀짝 체크
        * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
        */
        public void check(InputValue inputValue, RandomNumber randomNumber) {

            if((!randomNumber.isHol() && !inputValue.isHol()) || (randomNumber.isHol() && inputValue.isHol())) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        }
    }

    /**
    * @title 사용자 입력 값 클래스
    * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
    */
    class InputValue{

        int inputValue;

        // 생성자
        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(this.inputValue == 1 || this.inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        // 홀 여부 체크
        public boolean isHol() {
            if(inputValue == 1) {
                return true; 
            }
            return false;
        }
    }

    /**
    * @title 랜덤 값 클래스
    * @desc 랜덤 값을 관리하는 Wrapper 클래스
    */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            this.randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return this.randomNumber;
        }

        // 홀 여부 체크
        public boolean isHol() {

            if(this.randomNumber % 2 == 1) {
                return true;
            }
            return false;
        }
    }

4) 리팩토링 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            InputValue inputValue = null;

            while((inputValue = new InputValue(in)).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                check(inputValue, randomNumber);
            }

            in.close();
        }

        /**
        * @title 홀짝 체크
        * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
        */
        public void check(InputValue inputValue, RandomNumber randomNumber) {
            int input = inputValue.getInputValue();

            if(input == randomNumber.getHoljjak()) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        }
    }

    /**
    * @title 사용자 입력 값 클래스
    * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
    */
    class InputValue{

        int inputValue;

        InputValue(Scanner in){
            this.inputValue = in.nextInt();
        }

        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(inputValue == 1 || inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        public int getInputValue() {
            return inputValue;
        }
    }

    /**
    * @title 랜덤 값 클래스
    * @desc 랜덤 값을 관리하는 Wrapper 클래스
    */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return randomNumber;
        }

        // 홀/짝 조회
        public int getHoljjak() {
            if(randomNumber % 2 == 1) {
                return 1;
            }
            return 2;
        }
    }

1. while 조건절에 4 법칙을 적용하기 위해 InputValue 클래스에 Scanner를 받는 생성자를 만들고 조건절을 수정하였다. 복잡해보이던 조건절이 한단계 단순해졌다.

before

    while((inputValue = new InputValue(in.nextInt())).inputCheck()) {

after

    while((inputValue = new InputValue(in)).inputCheck())  { // 수정
    ...

    InputValue(Scanner in){ // 추가
        this.inputValue = in.nextInt();
    }

2. 불필요한 this 구문을 제거하였다. 한 줄에 하나의 점만 찍는에 대한 목적과 거리가 먼 생성자의 this는 남겨두었다.

before

    if(this.inputValue == 1 || this.inputValue == 2) { 
    ... 

after

    if(inputValue == 1 || inputValue == 2) {
    ...

3. InputValue와 RandomNumber 클래스의 isHol 메서드를 제거하고 RandomNumber 클래스에 홀/짝에 대한 숫자 값을 조회하는 getHoljjak 메서드를 추가하였다. 두 곳에 홀 여부를 판단하는 메서드를 넣으니 홀 여부 판단에 대한 책임이 늘어났고, 홀짝 체크를 홀 여부로 판단할 필요가 없다고 느꼈기 때문이다. 이로써 조건절또한 훨씬 간단해졌다.

before

    // InputValue 클래스의 isHol
     public boolean isHol() {

        if(this.randomNumber % 2 == 1) {
            return true;
        }
        return false;
    }
    // RandomNumber 클래스의 isHol
    public boolean isHol() {
        if(inputValue == 1) {
            return true; 
        }
        return false;
    }
    // check 메서드
    public void check(InputValue inputValue, RandomNumber randomNumber) {

        if((!randomNumber.isHol() && !inputValue.isHol()) || (randomNumber.isHol() && inputValue.isHol())) {
            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
            return;
        }

        System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
    }

after

    // isHol 메서드를 제거하고 RandomNumber 클래스에 getHoljjak 메서드 생성
    public int getHoljjak() {
        if(randomNumber % 2 == 1) {
            return 1; //홀
        }
        return 2; //짝
    }

    public void check(InputValue inputValue, RandomNumber randomNumber) {
        int input = inputValue.getInputValue();

        if(input == randomNumber.getHoljjak()) { // 조건절 수정
            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
            return;
        }

        System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
    }

5) 회고

점이 많다는 건 단순 가독성 문제 뿐 아닌 높은 결합도, 책임의 분산, 불필요한 코드일 수 있음을 알려주는 좌표점과 같은 의미였다. 법칙을 적용해나갈수록 코드에게 미안함을 느낀다. 겔겔


5. 줄여쓰지 않는다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.
  8. 객체지향 생활체조 9법칙의 4법칙 '한 줄에 점을 하나만 찍는다.'을 무조건 준수한다.
  9. 객체지향 생활체조 9법칙의 5법칙 '줄여쓰지 않는다.을 무조건 준수한다.

3) 기존 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            InputValue inputValue = null;

            while((inputValue = new InputValue(in)).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                check(inputValue, randomNumber);
            }

            in.close();
        }

        /**
         * @title 홀짝 체크
         * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
         */
        public void check(InputValue inputValue, RandomNumber randomNumber) {
            int input = inputValue.getInputValue();

            if(input == randomNumber.getHoljjak()) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        }
    }

    /**
     * @title 사용자 입력 값 클래스
     * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
     */
    class InputValue{

        int inputValue;

        InputValue(Scanner in){
            inputValue = in.nextInt();
        }

        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(inputValue == 1 || inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        public int getInputValue() {
            return inputValue;
        }
    }

    /**
     * @title 랜덤 값 클래스
     * @desc 랜덤 값을 관리하는 Wrapper 클래스
     */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return randomNumber;
        }

        // 홀/짝 조회
        public int getHoljjak() {
            if(randomNumber % 2 == 1) {
                return 1;
            }
            return 2;
        }
    }

4) 리팩토링 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            InputValue inputValue = null;

            while((inputValue = new InputValue(in)).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                holjjakCheck(inputValue, randomNumber);
            }

            in.close();
        }

        /**
         * @title 홀짝 체크
         * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
         */
        public void holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
            int input = inputValue.getInputValue();

            if(input == randomNumber.getHoljjak()) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        }
    }

    /**
     * @title 사용자 입력 값 클래스
     * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
     */
    class InputValue{

        int inputValue;

        InputValue(Scanner in){
            inputValue = in.nextInt();
        }

        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(inputValue == 1 || inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        public int getInputValue() {
            return inputValue;
        }
    }

    /**
     * @title 랜덤 값 클래스
     * @desc 랜덤 값을 관리하는 Wrapper 클래스
     */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return randomNumber;
        }

        // 홀/짝 조회
        public int getHoljjak() {
            if(randomNumber % 2 == 1) {
                return 1;
            }
            return 2;
        }
    }

1. 변수나 메서드 명을 줄여쓰는 이유는 이름이 너무 길고 복잡하기 때문인데, 이 의미는 곧 그만큼 로직이 복잡하고 중요도가 높다는 반증일 수 있다. 이를 줄여쓴다면 이름으로써 전달하고자 하는 값의 의미를 파악하기 어려워진다. 불필요하게 긴 이름을 축약해야겠지만, 분명한 의미를 표현하지 못하는 이름은 목적에 맞게 명확한 이름을 정해주자. 위 코드에서 check 메서드가 정확히 무엇을 체크하는지 그 의미를 파악하기 어려워 holjjakCheck로 변경하였다.

before

    public void check(InputValue inputValue, RandomNumber randomNumber)

after

    public void holjjakCheck(InputValue inputValue, RandomNumber randomNumber)

5) 회고

변수나 메서드 명을 정할 때에는 목적에 맞게 명확한 이름을 정해주고, 코드에 긴 이름이 보인다면 곧장 로직만 보지 않고 이름의 의미를 먼저 파악하는 습관을 길러봐야겠다.

 


6. 모든 엔티티를 작게 유지한다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.
  8. 객체지향 생활체조 9법칙의 4법칙 '한 줄에 점을 하나만 찍는다.'을 무조건 준수한다.
  9. 객체지향 생활체조 9법칙의 5법칙 '줄여쓰지 않는다.'을 무조건 준수한다.
  10. 객체지향 생활체조 9법칙의 6법칙 '모든 엔티티를 작게 유지한다.'을 무조건 준수한다.

3) 회고

이 법칙의 '작게' 라는 기준이 모호하여 찾아본 결과, 책에서 부가적으로 설명하고 있으며 내용은 다음과 같다.

50줄 이상 되는 클래스와 10개 파일 이상 되는 패키지는 없어야한다.

긴 파일은 읽고, 이해하고, 재사용하기 어렵다는 의미이다. 홀짝은 너무 단순한 프로그램이라 그런지 최초 작성한 코드 또한 50줄이 채 되지 않았으며, 현재 3개의 클래스. 각 20줄 정도로 유지하고 있어 리팩토링하지 않았다. 이 법칙은 객체지향 생활체조 원칙을 잘 지켜라는 의미 또한 내포하고 있지 않나 싶다. 겔겔

 


7. 두개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.
  8. 객체지향 생활체조 9법칙의 4법칙 '한 줄에 점을 하나만 찍는다.'을 무조건 준수한다.
  9. 객체지향 생활체조 9법칙의 5법칙 '줄여쓰지 않는다.을 무조건 준수한다.
  10. 객체지향 생활체조 9법칙의 6법칙 '모든 엔티티를 작게 유지한다.'을 무조건 준수한다.
  11. 객체지향 생활체조 9법칙의 7법칙 '두개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.'을 무조건 준수한다.

3) 회고

'두개'는 클래스 분리를 강조하기 위함이다. 변수가 두 개 이상이라는 의미는 상태 정보가 두개 이상일 수 있고, 이는 곧 wrap 클래스로 분리(3원칙)할 여지가 있다는 의미이기도 하다.

3 원칙을 적용했을 때 자연스럽게 7 법칙도 적용이 되어 리팩토링을 하지 않았다. 복잡한 프로그램을 구현할 때 이 원칙을 지키는 것은 정말 어려울 것 같다.

 


8. 일급 컬렉션을 사용한다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.
  8. 객체지향 생활체조 9법칙의 4법칙 '한 줄에 점을 하나만 찍는다.'을 무조건 준수한다.
  9. 객체지향 생활체조 9법칙의 5법칙 '줄여쓰지 않는다.을 무조건 준수한다.
  10. 객체지향 생활체조 9법칙의 6법칙 '모든 엔티티를 작게 유지한다.'을 무조건 준수한다.
  11. 객체지향 생활체조 9법칙의 7법칙 '두개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.'을 무조건 준수한다.
  12. 객체지향 생활체조 9법칙의 8법칙 '일급 컬렉션을 사용한다.'을 무조건 준수한다.
  13. 홀짝 종료 시 사용자의 정답률을 표시한다. 형식은 다음과 같다 '정답률은 56%(게임 횟수/정답 횟수) 입니다.'

3) 기존 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            InputValue inputValue = null;

            while((inputValue = new InputValue(in)).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                holjjakCheck(inputValue, randomNumber);
            }

            in.close();
        }

        /**
         * @title 홀짝 체크
         * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
         */
        public void holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
            int input = inputValue.getInputValue();

            if(input == randomNumber.getHoljjak()) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        }
    }

    /**
     * @title 사용자 입력 값 클래스
     * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
     */
    class InputValue{

        int inputValue;

        InputValue(Scanner in){
            inputValue = in.nextInt();
        }

        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(inputValue == 1 || inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        public int getInputValue() {
            return inputValue;
        }
    }

    /**
     * @title 랜덤 값 클래스
     * @desc 랜덤 값을 관리하는 Wrapper 클래스
     */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return randomNumber;
        }

        // 홀/짝 조회
        public int getHoljjak() {
            if(randomNumber % 2 == 1) {
                return 1;
            }
            return 2;
        }
    }

4) 리팩토링 코드

    class Holjjak{

    public void holjjak() {

        System.out.println("안신나는 홀짝 게임!");
        System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

        Scanner in = new Scanner(System.in);
        InputValue inputValue = null;
        HoljjakScore score = new HoljjakScore();

        while((inputValue = new InputValue(in)).inputCheck()) {
            RandomNumber randomNumber = new RandomNumber();
            score.addResult(holjjakCheck(inputValue, randomNumber));
        }
        score.printScoreInfo();
        in.close();
    }

    /**
     * @title 홀짝 체크
     * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
     */
    public boolean holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
        int input = inputValue.getInputValue();

        if(input == randomNumber.getHoljjak()) {
            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
            return true;
        }

        System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        return false;
    }
}

/**
 * @title 사용자 입력 값 클래스
 * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
 */
class InputValue{

    int inputValue;

    InputValue(Scanner in){
        inputValue = in.nextInt();
    }

    InputValue(int inputValue){
        this.inputValue = inputValue;
    }

    // 입력값 체크
    public boolean inputCheck() {

        if(inputValue == 1 || inputValue == 2) {
            return true;
        }
        System.out.println("종료되었습니다.");
        return false;
    }

    public int getInputValue() {
        return inputValue;
    }
}

/**
 * @title 랜덤 값 클래스
 * @desc 랜덤 값을 관리하는 Wrapper 클래스
 */
class RandomNumber{

    int randomNumber;

    // 난수 생성 생성자
    RandomNumber(){

        Random random = new Random();
        randomNumber = random.nextInt(9)+1;
    }

    // 랜덤 값 조회
    public int getRandomNumber() {

        return randomNumber;
    }

    // 홀/짝 조회
    public int getHoljjak() {
        if(randomNumber % 2 == 1) {
            return 1;
        }
        return 2;
    }
}

/**
 * @title 홀짝 결과 클래스
 * @desc 홀짝 결과를 관리하는 일급 컬렉션
 *
 */
class HoljjakScore{

    List<Boolean> scoreBoard;

    HoljjakScore(){
        scoreBoard = new ArrayList<Boolean>();
    }

    // 홀짝 결과 추가
    public void addResult(boolean result){
        scoreBoard.add(result);
    }

    // 홀짝 결과 출력
    public void printScoreInfo() {
        int correctCnt = Collections.frequency(scoreBoard, true);
        int totalCnt = scoreBoard.size();
        double correctPercent = (double)correctCnt/(double)totalCnt*100;

        if(totalCnt != 0) {
            System.out.println("정답률은 "+String.format("%.1f",correctPercent)+"%("+correctCnt+","+totalCnt+")입니다.");
        }
    }
}

 

1. 요구사항 11, 12를 준수하기 위해 홀짝 결과 클래스(HoljjakScore.java)를 일급 컬렉션으로 구현하였다.

class HoljjakScore{

    List<Boolean> scoreBoard;

    HoljjakScore(){
        scoreBoard = new ArrayList<Boolean>();
    }

    // 홀짝 결과 추가
    public void addResult(boolean result){
        scoreBoard.add(result);
    }

    // 홀짝 결과 출력
    public void printScoreInfo() {
        int correctCnt = Collections.frequency(scoreBoard, true);
        int totalCnt = scoreBoard.size();
        double correctPercent = (double)correctCnt/(double)totalCnt*100;

        if(totalCnt != 0) {
            System.out.println("정답률은 "+String.format("%.1f",correctPercent)+"%("+correctCnt+","+totalCnt+")입니다.");
        }
    }
}

 

2. 홀짝 결과를 추가하는 HoljjakScore.addResult는 holjjakCheck 메서드 안에서 호출하려 했으나 메서드 파라미터가 추가되고, 결합도가 높아지기(홀짝 체크 + 결과 관리) 에 holjjakCheck 메서드에서 결과값을 return하고, 외부에서 HoljjakScore.addResult를 호출하는 방향으로 구현하였다. 홀짝 게임을 종료할 경우 printScoreInfo() 메서드를 호출하여 결과값을 출력하였다.

before

    public void holjjak() {

        ...

        while((inputValue = new InputValue(in)).inputCheck()) {
            RandomNumber randomNumber = new RandomNumber();
            holjjakCheck(inputValue, randomNumber);
        }

        ...
    }

    public void holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
        int input = inputValue.getInputValue();

        if(input == randomNumber.getHoljjak()) {
            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
            return;
        }

        System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
    }

after

    public void holjjak() {

        ...

        HoljjakScore score = new HoljjakScore();

        while((inputValue = new InputValue(in)).inputCheck()) {
            RandomNumber randomNumber = new RandomNumber();
            score.addResult(holjjakCheck(inputValue, randomNumber));
        }
        score.printScoreInfo();

        ...
    }

    public boolean holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
        int input = inputValue.getInputValue();

        if(input == randomNumber.getHoljjak()) {
            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
            return true;
        }

        System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
        return false;
    }

5) 회고

일급 컬렉션이란 컬렉션 외에 다른 멤버변수를 포함하지 않는 클래스이다. 여기서 컬렉션은 Collection, Map 인터페이스를 상속받는 클래스를 의미하며 대표적으로 List, HashMap, HashSet이 있다.

이번 리팩토링에서 일급 컬렉션을 사용하였기에 클래스 형태로 홀짝 결과를 관리할 수 있게 되었다. 일급 컬렉션을 사용하지 않았다면 로직이 어땠을까? 1. List는 holjjak 메서드 최상단에 생성, 2. while 문 안에서 List.add 메서드를 호출하여 결과 추가, 3. Holjjak 클래스 내에 결과 리스트를 받아 출력하는 메서드 생성. 와 같은 작업을 진행했을 것이다.

일급컬렉션을 사용함으로 얻을 수 있는 이점은 다음과 같다.

1. 상태와 행위를 관리할 수 있다.

결과에 대한 상태와 추가, 조회, 출력, 삭제, 검증과 같은 행위를 하나의 클래스에서 관리할 수 있다. 만약 결과 초기화, 결과 검증 등의 기능이 추가된다고 가정해보자. 일급 컬렉션을 사용하지 않는다면 관련 기능이 Holjjack 클래스의 메서드로 추가될 것이다. 만약 다른 클래스에서 결과 관련 기능이 필요하다면 해당 클래스에도 메서드가 추가될 것이다. 결과 관련 로직이 여러 클래스로 분산될 수 있다.

2. 컬렉션에 대한 이름을 지정할 수 있다.

일급 컬렉션을 사용하지 않는다면 이름은 변수 명으로 설정될 것이다. 이 변수명은 개발자에 따라, 클래스에 따라 달라질것이다. 이는 곧 검색이 어려워진다. 홀짝 결과에 대한 요구사항이 추가된다면 관련 코드들을 모두 뒤져야한다. 클래스 명으로 이름을 지정한다면 단순히 클래스 명으로 검색하면 되니 유지보수 요청에 빠르게 대처할 수 있을것이다.

3. 불변을 보장한다.

변수로 선언된 컬렉션은 데이터 추가, 삭제가 자유롭다. 일급 컬렉션을 사용한다면 클래스 메서드를 생성하지 않는 한 추가, 삭제가 불가능하기에 불변성을 보장할 수 있다.

4. 비지니스에 종속적인 자료구조를 생성할 수 있다.

생성되는 컬렉션을 비지니스에 종속되는 자료구조로 생성 가능하다. 즉, 단순히 자료구조를 생성하는게 아니라 비지니스 로직을 녹인 자료구조를 생성할 수 있다는 뜻이다.

중복되는 이름이 있으면 안되고, 5자리 이하의 영어이름만을 관리하는 자동차의 객체를 예시로 들어보자. 해당 조건들을 서비스 메서드에서 구현을 한다면 자동차 객체리스트가 들어간 모든 장소에서 해당 자동차 리스트에 대한 검증 코드가 들어가야 하는 문제점이 발생한다. 이러한 문제는 아래의 코드와 같이 일급 컬렉션 생성자에 비지니스 로직을 넣어 생성하면 해결이 가능하다.

    public class Cars {

        private List<Car> carList;

        public Cars(List<Car> carList) {
            validateCarName(carList); // 5자리 이하의 영어이름이 아닐 경우 Exception
            validateDuplicateName(carList); // 이름이 중복될 경우 Exception
            this.carList = carList; // 비지니스에 종속적인 자료구조 생성
        }    
}

이번 규칙을 적용하기 위해 요구사항 12번을 추가하였다. 기존 요구사항으로는 일급컬렉션을 생성할 여지가 없었고, 단어가 너무 생소하여 직접 경험해봐야겠다고 생각했기 때문이다.

현재 내가 맡은 모든 프로젝트의 컬렉션은 99퍼센트 이상이 변수로 선언되어 있다. 일급 컬렉션의 이점 모두가 너무 중요한데 이를 몰랐던 무지에 대한 부끄러움과 이 규칙을 이해했다는 감동을 동시에 느꼈다. 다음은 마지막 9 규칙을 적용할 차례이다. 마지막까지 집중력을 잃지말자!

 

 


9. getter/setter/property를 쓰지 않는다

1) 프로그램명

  • 홀짝 게임

2) 요구사항

  1. 사용자가 1(홀) 또는 2(짝)을 입력하여 홀짝을 맞춘다.
  2. 홀짝 게임에 사용되는 돌의 수는 1~10개 이며, 매번 달라진다.
  3. 홀짝 결과는 사용된 돌의 개수와 맞춤 여부이다.
  4. 사용자가 1(홀) 또는 2(짝)을 입력하지 않을 때까지 게임은 계속된다.
  5. 객체지향 생활체조 9법칙의 1법칙 '한 메서드에 오직 한 단계의 들여쓰기만 한다'을 무조건 준수한다.
  6. 객체지향 생활체조 9법칙의 2법칙 'Else_Switch_키워드를_사용하지_않는다'을 무조건 준수한다.
  7. 객체지향 생활체조 9법칙의 3법칙 '모든 원시값과 문자열을 포장(wrap)한다.'을 무조건 준수한다.
  8. 객체지향 생활체조 9법칙의 4법칙 '한 줄에 점을 하나만 찍는다.'을 무조건 준수한다.
  9. 객체지향 생활체조 9법칙의 5법칙 '줄여쓰지 않는다.을 무조건 준수한다.
  10. 객체지향 생활체조 9법칙의 6법칙 '모든 엔티티를 작게 유지한다.'을 무조건 준수한다.
  11. 객체지향 생활체조 9법칙의 7법칙 '두개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.'을 무조건 준수한다.
  12. 객체지향 생활체조 9법칙의 8법칙 '일급 컬렉션을 사용한다.'을 무조건 준수한다.
  13. 홀짝 종료 시 사용자의 정답률을 표시한다. 형식은 다음과 같다 '정답률은 56%(게임 횟수/정답 횟수) 입니다.'
  14. 객체지향 생활체조 9법칙의 9법칙 'getter/setter/property를 쓰지 않는다.'을 무조건 준수한다.

3) 기존 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            InputValue inputValue = null;
            HoljjakScore score = new HoljjakScore();

            while((inputValue = new InputValue(in)).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                score.addResult(holjjakCheck(inputValue, randomNumber));
            }
            score.printScoreInfo();
            in.close();
        }

        /**
        * @title 홀짝 체크
        * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
        */
        public boolean holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
            int input = inputValue.getInputValue();

            if(input == randomNumber.getHoljjak()) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return true;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
            return false;
        }
    }

    /**
    * @title 사용자 입력 값 클래스
    * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
    */
    class InputValue{

        int inputValue;

        InputValue(Scanner in){
            inputValue = in.nextInt();
        }

        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(inputValue == 1 || inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        public int getInputValue() {
            return inputValue;
        }
    }

    /**
    * @title 랜덤 값 클래스
    * @desc 랜덤 값을 관리하는 Wrapper 클래스
    */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            randomNumber = random.nextInt(9)+1;
        }

        // 랜덤 값 조회
        public int getRandomNumber() {

            return randomNumber;
        }

        // 홀/짝 조회
        public int getHoljjak() {
            if(randomNumber % 2 == 1) {
                return 1;
            }
            return 2;
        }
    }

    /**
    * @title 홀짝 결과 클래스
    * @desc 홀짝 결과를 관리하는 일급 컬렉션
    *
    */
    class HoljjakScore{

        List<Boolean> scoreBoard;

        HoljjakScore(){
            scoreBoard = new ArrayList<Boolean>();
        }

        // 홀짝 결과 추가
        public void addResult(boolean result){
            scoreBoard.add(result);
        }

        // 홀짝 결과 출력
        public void printScoreInfo() {
            int correctCnt = Collections.frequency(scoreBoard, true);
            int totalCnt = scoreBoard.size();
            double correctPercent = (double)correctCnt/(double)totalCnt*100;

            if(totalCnt != 0) {
                System.out.println("정답률은 "+String.format("%.1f",correctPercent)+"%("+correctCnt+","+totalCnt+")입니다.");
            }
        }
    }

4) 리팩토링 코드

    class Holjjak{

        public void holjjak() {

            System.out.println("안신나는 홀짝 게임!");
            System.out.println("1 : 홀, 2 : 짝, 그외 : 종료");

            Scanner in = new Scanner(System.in);
            InputValue inputValue = null;
            HoljjakScore score = new HoljjakScore();

            while((inputValue = new InputValue(in)).inputCheck()) {
                RandomNumber randomNumber = new RandomNumber();
                score.addResult(holjjakCheck(inputValue, randomNumber));
            }
            score.printScoreInfo();
            in.close();
        }

        /**
        * @title 홀짝 체크
        * @desc 난수에 대한 홀짝을 추출하고, 입력값과 비교하여 최종 홀짝 여부를 판단한다 
        */
        public boolean holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
            boolean inputValueIsHol = inputValue.isHol();
            boolean randomNumberIsHol = randomNumber.isHol();

            randomNumber.printRandomNumber();

            if(inputValueIsHol == randomNumberIsHol) {
                System.out.println("맞췄습니다.");
                return true;
            }
            System.out.println("틀렸습니다.");
            return false;
        }
    }

    /**
    * @title 사용자 입력 값 클래스
    * @desc 사용자가 입력한 값을 관리하는 Wrapper 클래스
    */
    class InputValue{

        int inputValue;

        InputValue(Scanner in){
            inputValue = in.nextInt();
        }

        InputValue(int inputValue){
            this.inputValue = inputValue;
        }

        // 입력값 체크
        public boolean inputCheck() {

            if(inputValue == 1 || inputValue == 2) {
                return true;
            }
            System.out.println("종료되었습니다.");
            return false;
        }

        public boolean isHol() {
            if(inputValue == 1) {
                return true;
            }
            return false;
        }
    }

    /**
    * @title 랜덤 값 클래스
    * @desc 랜덤 값을 관리하는 Wrapper 클래스
    */
    class RandomNumber{

        int randomNumber;

        // 난수 생성 생성자
        RandomNumber(){

            Random random = new Random();
            randomNumber = random.nextInt(9)+1;
        }

        // 홀 여부 확인
        public boolean isHol() {
            if(randomNumber % 2 == 1) {
                return true;
            }
            return false;
        }

        //
        public void printRandomNumber() {
            System.out.println("개수 : "+randomNumber);
        }
    }

    /**
    * @title 홀짝 결과 클래스
    * @desc 홀짝 결과를 관리하는 일급 컬렉션
    *
    */
    class HoljjakScore{

        List<Boolean> scoreBoard;

        HoljjakScore(){
            scoreBoard = new ArrayList<Boolean>();
        }

        // 홀짝 결과 추가
        public void addResult(boolean result){
            scoreBoard.add(result);
        }

        // 홀짝 결과 출력
        public void printScoreInfo() {
            int correctCnt = Collections.frequency(scoreBoard, true);
            int totalCnt = scoreBoard.size();
            double correctPercent = (double)correctCnt/(double)totalCnt*100;

            if(totalCnt != 0) {
                System.out.println("정답률은 "+String.format("%.1f",correctPercent)+"%("+correctCnt+"/"+totalCnt+")입니다.");
            }
        }
    }

1. get 대신 isHol 메서드를 사용했다. 입력값, 랜덤 값에 대한 상태 값을 getter로 조회하여 다른 클래스에서 홀, 짝 여부를 판단하는 것보다 객체 스스로 판단하도록 하였다. 또한 메서드 명을 isHol로 하여 객체에 메시지를 전달하는 형태로 구현하였다. getter보다 훨씬 가독성이 좋아보인다.

3번 규칙을 적용할 때 isHol을 넣었다가 삭제한 부분이 있다. 그 때 당시 회고에 다음과 같이 기록하였다.

InputValue와 RandomNumber 클래스의 isHol 메서드를 제거하고 RandomNumber 클래스에 홀/짝에 대한 숫자 값을 조회하는 getHoljjak 메서드를 추가하였다. 두 곳에 홀 여부를 판단하는 메서드를 넣으니 홀 여부 판단에 대한 책임이 늘어났고, 홀짝 체크를 홀 여부로 판단할 필요가 없다고 느꼈기 때문이다. 이로써 조건절또한 훨씬 간단해졌다.

isHol을 두 클래스에 넣어 홀 여부 판단에 책임이 늘어났다고 했는데 잘못 생각한 것같다. InputValue의 isHol은 입력 값에 대한 홀 여부 판단(1 = 홀, 2 = 짝), RandomNumber의 isHol은 랜덤 값에 대한 홀 여부 판단(랜덤값 % 2로 판단)으로 두 기능은 아예 다른 기능이기 때문이다.

randomNumber를 가져오는 getRandomNumber대신 printRandomNumber로 수정하였다. randomNumber를 조회하는 이유는 오로지 출력이었기 때문이다.

before

    class InputValue{

        ...

        public int getInputValue() {
            return inputValue;
        }
    }
    class RandomNumber{

        ...

        // 홀/짝 조회
        public int getHoljjak() {
            if(randomNumber % 2 == 1) {
                return 1;
            }
            return 2;
        }
    }


    class Holjjak{

        ...

        public boolean holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
            int input = inputValue.getInputValue();

            if(input == randomNumber.getHoljjak()) {
                System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 맞췄습니다.");
                return true;
            }

            System.out.println("개수는 "+ randomNumber.getRandomNumber() +"! 틀렸습니다.");
            return false;
        }
    }

after

    class InputValue{

        ...

        public boolean isHol() {
            if(inputValue == 1) {
                return true;
            }
            return false;
        }
    }


    class RandomNumber{

        ...

        // 홀 여부 확인
        public boolean isHol() {
            if(randomNumber % 2 == 1) {
                return true;
            }
            return false;
        }

        // 랜덤값 출력
        public void printRandomNumber() {
            System.out.println("개수 : "+randomNumber);
        }
    }

    class Holjjak{

        ...

        public boolean holjjakCheck(InputValue inputValue, RandomNumber randomNumber) {
            boolean inputValueIsHol = inputValue.isHol();
            boolean randomNumberIsHol = randomNumber.isHol();

            randomNumber.printRandomNumber();

            if(inputValueIsHol == randomNumberIsHol) {
                System.out.println("맞췄습니다.");
                return true;
            }
            System.out.println("틀렸습니다.");
            return false;
        }

        ...
    }

5) 회고

이로써 객체지향 생활체조 9규칙을 모두 적용하였다. 처음 코드와 비교했을 때 코드 라인 수로만 보면 훨씬 길어졌지만 기능, 가독성, 유지보수, 재활용성 등 모든 면에서 훨씬 유연한 코드가 아닌가 싶다.

과정을 통해 배우고 느낀점이 많지만 내 생각에 한정된 리팩토링 코드이기에 잘못된 부분, 수정할 부분이 분명 존재할 것이다. 나는 배우고싶다. 부족한 점을 채우고 싶고 더 나아가 후배 개발자에게 정확한 지식을 알려주고 싶다. 이를 위해서는 개인 공부도 물론 중요하지만 동료 개발자들과 코드를 공유하고 의논하며 피드백을 받는것 또한 너무 중요하고 소중한 것이라 생각한다.

현재 재직중인 회사는 이런 분위기가 아니다. 입사 당시에는 이런 문화가 나에게 있어 다행이라고 생각했지만 지금 생각해보니 불행중 불행 겔겔...

올해 초 멘토링 프로그램 참여를 계획중인데 만약 등록된다면 적극적으로 임하여 많은 피드백을 받을수 있도록 노력할것이다.

반응형

+ Recent posts