반응형

1. 과제

 테스트 코드를 작성하라.

2. 배운점

2.1. 테스트 코드

토이프로젝트에 TDD를 적용해볼까 하고 테스트코드에 대해 알아본 적이 있었다. 무작정 예제코드를 통해 공부를 시작했는데 개념과 방법론을 이해하지 못하니 어렵고 복잡하게만 느껴져 건너 뛴적이 있다.

 이번 주자에 테스트 코드를 작성하면서 개념과 방법론에 대해 다시 궁금해졌다. 사용하는 개념들을 정리하고 멘토님의 조언을 통해 TDD의 필요성을 조금은 이해하게 된것같다. 그중에서도 특히 이해가지 않던 Mock Object. 가짜객체에 대한 개념과 사용 이유를 확실히 이해하게 되었다. 이 내용은 현재 정리중이며 어느정도 정립이 되었을 때 블로그에 올리도록 하겠다.

 

2.1. given.willthrow에 대한 문제

 예외를 설정하는 stubbing 메서드인 given.willthrow를 호출했을 때 실제로 해당 예외가 발생하는 이슈가 있었다.

 원인은 동일한 상황에 대한 stubbing 케이스에 대해 given.willthrow 구문이 두번 실행될 경우 발생했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    ...
    
        @Nested
        @DisplayName("Task 상세조회 테스트")
        class TaskDetailTest{
            
            @BeforeEach
            void setUp(){
                ...
                
                   given(taskService.getTask(INVALID_ID)).willThrow(new TaskNotFoundException(INVALID_ID));
          }
 
            @Test
            @DisplayName("유효 ID 상세조회")
            void detailWithValidId() throws Exception {
                ...
            }
 
            @Test
            @DisplayName("유효하지 않는 ID 상세조회")
            void detailWithInvalidId() throws Exception {
 
                mockMvc.perform(get("/tasks/"+INVALID_ID))
                        .andExpect(status().isNotFound())
                        .andExpect(content().string("{\"message\":\"Task not found\"}"));
            }
        }
cs

TaskDetailTest에 대한 Junit 테스트를 실행할 경우 detailWithValidId, detailWithInvalidId 메서드에 대한 테스트를 각각 진행하게 되는데 @BeforEach 어노테이션으로 인해 한 메서드의 테스트가 시작할때마다 setUp() 메서드를 호출하게 됐고, 두번째 테스트 시 호출되는 setUp() 의 given절에서 TaskNotFoundException 이 발생하게 됐다.

다양한 테스트를 통해 실제 예외가 발생한 구간을 알아내었다.

 

1. setUp() 메서드 호출

2. given() 메서드를 통해 taskService.getTask에 대한 stubbing 객체 생성

3. willThrow를 통해 stubbing에 대한 가짜 예외 생성

4. detailWithValidId 테스트 코드 실행

5. setUp() 메서드 호출

6. given() 메서드를 통해 taskService.getTask에 대한 stubbing 객체 시 TaskNotFoundException  발생!!

 

즉, taskService.getTask에 대한 stubbing이 정해진 상태에서 다시 given() 을 통해 해당 taskService.getTask()에 대한 stubbing을 생성하려 하나 이미 TaskNotFoundException을 갖고 있기에 해당 값이 반환되고 있었다.

reset 메서드를 이용하여 taskService를 리셋시키거나, given.willthrow 구문이 한번만 실행될 수 있도록 setUp에서 detailWithInvalidId 로 이동시켜주면 된다.

 

3. 느낀점

테스트 코드를 처음 작성하며 느낀건,

첫번째, 테스트 코드는 객체 지향적이고 클린한 코드를 만들수 있도록 노력하게 해준다(?)

 단위 테스트는 객체, 메서드 단위이다. 결합도가 높거나 코드가 복잡할 경우 테스트의 범위도 커지기 때문에 다양하고 정밀한 테스트가 어려워진다. 때문에 테스트를 위해 객체지향적이고 클린한 코드를 지향하게 되는 느낌을 받았다.

 

두번째, 현업에서 TDD를 지양하는 이유

 작성한 모든 로직에 대한 테스트 코드를 작성하니 실제 로직을 작성하는 시간과 테스트 코드를 작성하는 시간이 비슷하게 들었다. 실제 서비스를 개발하는 현업에서 TDD가 포함된다면 그 공수는 배로 들지않을까.. 라는 생각과 함께 TDD를 지양하는 이유에 대해 확실히 느끼게 되었다.

 

세번째, 테스트를 코드로

 현업에서 어떠한 테스트를 한다라는 건 실제로 API를 호출해 보는 것이었다. 테스트는 로직이 개발되거나 수정됐을 때 진행했기에 코드가 수정이 될 때마다 실제 API를 호출했다. 하지만 이를 코드로 구현하니 테스트에 걸리는 시간을 많이 줄일 수 있고, 현업에서 TDD를 지양하는 이유인 '시간'이 이 상쇄될 수 있지 않을까라는 생각이 들었다. 테스트를 코드로 한다는 건 딱 들었을땐 당연한 소리같지만 실제 현업에서는 당연하지만은 않았기 때문이다. 테스트를 코드로 구현한다는 것에 다시금 많은 생각이 들었던 주차였다.

 

반응형
반응형

1. 과제

 객체지향적인 REST API를 스프링 부트로 구현하라.

 

2. 배운점

2.1. HTTP Status 를 개념있게 사용하자

 서버 오류가 발생했을 때는 500, 리소스가 없을때는 404, 성공했을 때는 200, 요청 값이 비정상적일때는 400. 개발을 하면서 내가 접했던, 그리고 아는 HTTP Status는 딱 이정도였다. 이번 과제를 진행하며 처음으로 201, 204 등의 코드를 접해보았으나 의미와 개념을 파악하지 않고 '메서드에 따라서 상태코드가 달라질 수 있구나' 라고만 생각하고 넘겼다.

알고보니 201은 요청성공 및 리소스 생성일때, 즉 Create한 요청을 보냈을 때의 상태코드이고, 204는 요청 성공했으나 응답값이 없을 때, Delete한 요청을 보냈을 때 사용 가능한 상태코드이다. 404도 클라이언트 자원(html, css, img 등)이 없을 때의 상태코드로 알고있었으나 클라이언트 자원 뿐 아니라 서버 리소스가 없을 때도 404 상태코드로 표현이 가능했다.

이를테면 id에 매핑된 정보를 수정요청할 때 서버에서 유효한 id 값인지 확인하게되는데 이 값이 없을 경우 상태코드를 404로 리턴해도 된다.

 100번 대는 정보응답, 200번 대는 성공, 300번 대는 리다이렉트 400번 대는 클라이언트 에러, 500번 대는 서버 에러로 백의자리 숫자에 따라 상태코드가 구분된다는 것도 처음 알게 되었다.

아래는 HTTP 상태코드에 대한 공식문서이다.

https://developer.mozilla.org/ko/docs/Web/HTTP/Status

 

2.2. 스프링 부트야 고맙다.

 1주차에서는 스프링 없이 Java로만 코드를 짜다보니 코드가 길어지고 시간도 많이들었다. 이번 주차에 스프링 프레임워크를 사용하니 훨씬 쉽고 깔끔한 코드를 구현할 수 있었다. 나쁘게만 보였던 스프링 부트가 조금은 착해보인다.

 

2.3. 공식문서 보는 습관을 들이자.

 람다식을 사용하니 멘토님께서 공식문서 주소를 주시며 한번 봐보라고 하셨다. 하지만 필자는 공식문서의 영어가 보이는 순간 자신감을 잃는다. 한글로 번역을 때리면 코드들도 번역이 되기에 블로그를 기웃기웃 거렸다.

 하지만 생각과 달리 공식문서에서 제공되는 예제 코드들은 정말 간단하고 이해하기 쉽게 되어있었다. 설명들은 번역기로 돌려가며 이해했고, 코드들은 쭉 코딩해보니 맥락이 이해되면서 예제코드도 이해되기 시작했다. 그리고 무엇보다 간지가 좀 나는것 같다. 공식문서를 읽는 개발자... 아무쪼록 공식문서를 보는 습관을 들여보겠다!

3. 느낀점

이번 과제는 1주차보다 빨리 끝났다. 이유는 스프링 문법을 사용했기 때문이다. 생각없이 사용했던 어노테이션과 스프링 문법들에 대해 생각의 여지를 갖게 해준 시간이었던 것 같다.

 HTTP Status 코드를 새롭게 이해하게 됐고, 공식문서를 보는 게 얼마나 중요한 것인지도 알게되었다. 뭔가 다음주차부터는 큰 고통받을 것 같아 두려운 마음이 있지만 멘토님이 있으니 걱정안한다. -ㅅ-

반응형
반응형

1. 과제

 객체지향적인 REST API를 구현하라

 

2. 배운점

2.1. '객체에게 묻기보단 시켜야 한다.'

 이번 주차에 가장 나에게 와닿았던 피드백이다. 객체가 갖고있는 정보에 대해서 해야할 일이 있을 때 단순히 그 정보를 받아온 후에 다른 객체에서 뭔가를 처리하는게 아니라 그 처리마저도 객체에게 시켜야한다는 의미였다.

처음엔 이 의미가 이해가 가지 않아 현재 내 코드를 '후임에게 업무를 맡기는 상황'에 적용해보았다.

 

 일반적으로 후임에게 업무를 맡길때  '~에 대한 A, B, C 업무를 모두 처리해주시고, 저한테 보고해주세요' 라고 시킨다. 그런데 내 코드는 '~에 대한 업무를 처리해주시되, C 업무는 저한테 주시면 제가 알아서 처리할게요' 였다. (ㅜㅠ)

객체의 멤버필드에 대한 비지니스 로직이 필요한 부분이 있었는데, 나는 해당 멤버필드를 get메서드로 가져온 후 다른 객체에서 로직을 처리하고 있었기 때문이다.

 

 만약 이 상황에서 업무 내용이 변경되거나 차질이 생긴다면 어떨까?

 전자의 경우 업무에 대한 모든 책임을 후임이 맡게된다. 후임은 작업을 하고 나는 보고받으면 된다.

 후자의 경우 A, B 업무에 대한 책임은 후임이, C 업무에 대한 책임은 내가 맡게 된다. 맡은 업무에 대해서 작업은 책임을 맡은 사람이 하며, A, B, C 모두 관련성 있는 업무이기 때문에 나와 후임 모두 사이드 이팩트를 확인하는 일을 추가적으로 해야한다. 결국 업무에 대한 응집도는 분산되어 낮아지고, 결합도는 나와 후임으로 증가하는 느낌이 들었다.

 

 '객체에게 묻기보단 시켜야 한다.' 라는 피드백이 객체지향적인 설계의 가장 기본을 지키세요 라는 의미가 아니었을까 라는 생각과 함께 자바 개발자로써의 부끄러움와 지금이라도 알게되어 다행이라는 안도감이 몰려왔다.

 

2.2. 관습을 지키자.

 코드를 짜면서 사실 언어의 관습에 대해 생각해본적이 없다. 언어별 코드 정렬 방식, 변수 네이밍 및 케이스 등.

그런데 관습을 지키지 않는다면 관습을 지키는 개발자들에게 혼란을 야기할 수 있다는 걸 알았다.

 

2.3. 변수명에 대한 고민

 변수명은 개발자가 이해하기 쉬운 이름으로 설정하자. 불필요한 축약이나 본인만 알 수 있는 네이밍을 피해야 하고, 상황에 따라 실제 사용하는 기술 명칭을 넣는 것도 방법이다.

 

배운점을 3개정도 썼지만 사실 이것말고도 아주 많다. 1번은 너무 강렬하게 다가와 꼭 정리를 하고 싶어 길게 썼다.

 

3. 느낀점

 처음 과제를 접했을 때 너무 간단하게 느껴져 하루 이틀이면 끝날것이라 생각했다.

 하지만 코드를 fork, commit, pr하는 과정부터 버벅거렸고, 코드 리뷰를 받다보니 어느새 일주일이 지나있었다. 멘토님께서 개선이 필요한 부분을 찾아주셨으나, 계속적으로 피드백이 온걸 보면 거의 지뢰찾기 게임을 하시지 않았나 싶다. 너무 감사했지만 한편으로는 조금 죄송스러웠다.

 이제 1주차인데 나의 잘못된 코딩 습관들을 고쳐야 하고, 언어에 대해 배워야할 것들이 아주 많다는 것을 느꼈다. 언어의 특성에 대해 너무 생각없이 개발해 온 지난 세월에 대해 안타까움도 느껴졌다. 이번 피드백을 머릿속에 새기고 내일부터 있을 2주차에 적용할 수 있도록 고민하며 개발할 것이다. 2주차도 열심히!

 

 

 

 

반응형
반응형

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규칙을 모두 적용하였다. 처음 코드와 비교했을 때 코드 라인 수로만 보면 훨씬 길어졌지만 기능, 가독성, 유지보수, 재활용성 등 모든 면에서 훨씬 유연한 코드가 아닌가 싶다.

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

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

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

반응형
반응형

1. 개요

 코딩테스트를 하면 자주 나왔던 Iterator. 이게 무엇인지, 또 왜 사용하는지 알아보았다.


2. Iterator란?

 Iterator란 자바의 컬렉션(Collection)에 저장되어 있는 요소들을 순회하는 인터페이스이다.


3. Collection?

 Collection이란 자바에서 제공하는 자료구조들의 인터페이스로 List, ArrayList, Stack, Quque, LinkedList 등이 이를 상속받고있다. 즉, 이러한 컬렉션 인터페이스를 상속받는 클래스들에 대해 Iterator 인터페이스 사용이 가능하다.

Collection 구조 / 출처 : 위키백과


4. 사용 이유

 컬렉션 프레임워크에 대해 공통으로 사용이 가능하고 사용법이 간단하기 때문이다.

 저 위 그림에 나와있는 클래스, 인터페이스에서 모두 사용이 가능하다.

 

 Iterator를 사용하려면 정의 방법과 메서드 3개만 알면 된다.

 

 정의방법은 Iterator<T> iterator = Collection.iterator(); 이고,

 메서드는 다음 요소가 있는지 판단하는 hasNext(), 다음 요소를 가져오는 next(),  가져온 요소를 삭제하는 remove()가 끝이다. 아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IteratorTest {
 
    public static void main(String[] args) {
        
        List<Integer> list = new ArrayList<Integer>();
 
        for(int i = 0;i <= 100; i++) {
            list.add(i);
        }
        
        Iterator<Integer> iter = list.iterator();
        
        while(iter.hasNext()) {
            int data = iter.next();
            System.out.print(data);
        }
        
    }
    
}
cs

5~9번 라인에서 Collection 인터페이스를 상속받는 ArrayList 객체를 생성하고 0부터 100 값을 add 한다.

11번 라인에서 Iterator<T> iterator = Collection.iterator(); 형식에 맞게 Iterator<Integer> iter = list.iterator(); 를 사용하여 Iterator를 참조한다.

13번 라인에서 hasNext() 메서드를 사용하여 다음 요소가 있는지 확인한다. (있으면 true, 없으면 false를 반환)

14번 라인에서 next() 메서드를 사용해 다음 요소의 값을 조회한다.


5. Iterator과 반복문

 Iterator를 통한 순회는 반복문을 통한 순회와는 메모리적으로 중요한 차이가 있다.

LinkedList를 통해 예를 들어보겠다.

 

1
2
3
4
5
6
7
8
9
10
11
    public void linkedListTest() {
        LinkedList<Integer> list = new LinkedList<Integer>();
        
        for(int i = 0;i <= 100; i++) {
            list.add(i);
        }
        
        for(int i = 0; i<= 100; i++) {
            list.get(i);
        }
    }
cs

 

Linked List의 메모리구조

add 메서드를 이용해 데이터 입력이 다 끝나면 위 그림과 같은 구조가 된다.

그리고 get(0)부터 get(100)까지를 수행하게 되는데 이는 0부터 100까지 총 101번의 요소를 조회하는게 아니다.

get(int index) 메서드는 시작 주소부터 index 만큼 요소들을 밟아가며 조회하는 메서드이기 때문이다.

만약 5번째 값을 조회한다면 처음 시작주소부터 시작하여 다음주소를 타고... 타고.. 를 총 5번 반복해야한다.

get 메서드가 실행되며 i 값이 증가할 때마다 메모리적으로 조회해야 하는 요소는 1번, 2번, 3번, 4번... 101번까지 증가하는 것이다. 총 5151번을 조회해야 한다.

 

이에반해 Iterator는 1부터 101번째까지의 요소에 대해 내부적으로 객체로 생성한 후 순차적으로 조회한다.

처음 주소로 돌아갈 필요가 없기때문에 next 메서드를 통해 조회 시 요소의 개수인 101번만 조회를 하게된다.

 

그렇다면 드는 생각. 속도면에서 훨씬 빠르지않을까?

 

훨씬 빠를것이라고 생각했으나... Iterator를 구현하기 위해 객체를 생성하는 부분에서 시간이 더 걸린다고 한다.

물론 그 차이는 크지 않지만...

 

결론.

 Iterator는 컬렉션 프레임워크에 대한 인터페이스이고, 사용법이 쉽다.

 하지만 반복문보다 속도면에서 조금 느리다는 평이 있다.

반응형
반응형

1. 개요

 - List에 대해 알고, 배열과 연결리스트의 개념과 차이를 이해한다.

 

2. 리스트(List)란?

 - 순서가 의미를 갖는 데이터들의 집합이다.

 - 삽입, 삭제, 검색 등의 기본적인 연산이 가능한 자료구조이다.

 - 리스트를 구현하는 대표적인 두가지 방법은 배열과 연결리스트이다.

 

즉, 리스트란 첫번째에는 A가, 두번째에는 B가 세번째에는 C가 들어있는 것처럼 순서마다 특정 데이터를 갖고 있고, 이 순서를 통해 삽입, 수정, 삭제, 검색을 할 수 있는 자료구조를 말한다.

 

3. 배열이란?

 - 같은 종류의 데이터들이 메모리상에 순차적으로 저장되어 있는 자료구조이다.

 - 같은 종류의 데이터들이기때문에 각각 데이터들의 크기가 같고, 메모리상에 순차적으로 저장되어 있기 때문에 주소 값 계산이 쉽고, 랜덤 액세스가 가능하다.

 - 조회할때는 빠르나 중간 데이터를 삭제, 추가 시에는 시간이 많이 걸린다.

 - 배열의 구조는 다음과 같다.

배열의 구조

 - int 형은 정수형 자료형으로 4Byte이다. 주소를 보면 4byte 간격이란 것을 알 수 있다. 내가 배열의 5번째 값을 조회하고 싶다면 처음 주소 값에서 자료형 크기 * 5 해준 값(주소값)을 구한 후 바로 접근하면 된다. 즉, 배열에서 특정 데이터를 조회하려면 자료형과 인덱스를 통해 주소 값 계산 후 바로 접근한다. (참고로 이 방식이 랜덤 액세스 방식이다.) 대신 중간에 데이터를 추가하거나 삭제 한다면 빈 자리를 매꾸기 위해 아주 많은 데이터들이 움직여야한다. 예를들면, 3번째 인덱스에 위치한 4000 값을 삭제한다면, 5000이 그 그 자리로 이동해야하고, 5000 자리로는 6000이 이동해야하고... 이게 끝까지 반복되어야한다. 만약 100000개 크기의 배열에서 1번째 값을 삭제한다면 100000-1번 움직여야한다.

 

4. 연결리스트

 - 같은 종류의 데이터들이 메모리상에 비순차적으로 저장되어 있는 자료구조이다.

 - 크기의 제한이 없다.

 - 다른 데이터의 이동 없이 중간에 삽입, 삭제가 가능하다. 

 - 랜덤 엑세스가 불가능하다.

 - 연결리스트의 구조는 다음과 같다.

 - 연결리스트의 값에는 총 2가지 정보가 들어간다. 하나는 메모리 주소에 해당하는 데이터. 하나는 다음 주소 값이다. 그리고 이 주소 값을 거칠때마다 인덱스가 증가한다. 예를들어 이 연결리스트의 2번째 인덱스에 해당하는 값을 조회하려면 시작주소인 00ffff00에 저장된 다음 주소인 00ffff08이 된다. 만약 100000개 크기의 연결리스트에서 100000번째 값을 조회하려면 주소 계산이 되지 않기 때문에 무작정 메모리 시작주소부터 다음 주소를 100000번 까야한다. 즉 조회에 시간이 오래걸린다. 대신, 어떤 값을 삭제하거나 추가할때에는 메모리를 찾은 후 다음주소만 바꿔주면 되기 때문에 배열에 비해 시간이 적게 걸린다. 예를들어 3번째와 4번째 사이에 3500을 추가하고싶다면 먼저 3번째 주소를 찾은 후(00ffff18) 메모리 빈 공간(00ffff10)에 3500값을 만들고 3500의 다음 주소를 3번째 인덱스의 다음 주소로 저장한다. 그리고 3번째 인덱스의 다음 주소에는 3500값의 주소로 저장한다.

 

3500 추가

 

5. 마치며

 가려운 등이 긁힌 느낌이다.. java에서 왜 배열 길이를 선언하고 후에 못늘리는지,(메모리에 순차적으로 들어가기 때문에 나중에 길이를 늘리게 되면 현재 다른 데이터를 위해 할당된 메모리 주소를 침범할 수 있기 때문) 굳이 길이 제한이 없는 ArrayList같은 연결리스트를 놔두고 왜 배열을 쓰는지 (랜덤 액세스로 조회 시간이 엄청 짧음), 그리고 이 둘의 차이, 메모리 구조 등을 확실히 이해할 수 있는 좋은 공부였다. 너무좋다...ㅎㅎ

반응형
반응형

1. 개요

Generic 프로그래밍, Generic 프로그래밍을 사용하는 이유를 예제를 통해 알아보자.


2. Generic 프로그래밍이란?

 제네릭 프로그래밍이란 하나의 데이터가 특정 데이터 타입에만 종속되지 않고 여러 데이터 타입을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그램 방식이다. - 위키백과

 

 무슨 말인지 이해가 잘 안간다. 일단 Generic이 무슨 의미를 갖는지 확인해보자.

 

 Generic의 사전적 의미는 '포괄적인, 총칭, 일반적인' 이다.

 

 이를 Generic 프로그래밍의 정의와 혼합하여 생각해보니 '데이터를 포괄적으로 사용할수 있도록 하는 프로그래밍, 어떤 데이터 타입도 가질 수 있도록 일반화시키는 프로그래밍' 으로 이해를 해보았다.

 

 그렇다면 데이터를 포괄적으로 사용한다는 것이 프로그래밍에서 어떤 의미를 가질까?

 어떤 데이터가 A가 될수도, B가 될수도, C가 될수도 있다라고 생각이 되는데... 예제를 통해 이해해보자.


3. 예제

Box.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Box<T> {
 
    private T t;
    
    public void set(T t) {
        this.t = t;
    }
    
    public T get() {
        return t;
    }
}
cs

이 Box 클래스는 제네릭한 클래스이다. T란 녀석이 중간 중간 껴있는 것을 확인할 수 있는데,

T는 내가 생성한 클래스도, 타입도 아니다. Generic 한 클래스를 만들기 위해 사용하는 제네릭 변수이다.

 

Box 객체 생성시 제네릭 변수 T로 들어온 데이터 형을 아래와 같이 변환(?) 시켜준다.

main 메서드의 예제를 보면 이해할 수 있을 것이다.

Generic Class

 

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
public class Main {
 
    public static void main(String[] args) {
        Box<Integer> box = new Box<Integer>();
        box.set(10);
        
        Integer i = box.get();
        System.out.println(i.intValue());
    }
}
 
cs

다음과 같이 Box<Integer> box = new Box<Integer>() 로 생성하면, 앞서 이미지의 T 값이 Integer가 되는 것이다.

그럼 Box 클래스는 Integer 타입의 데이터를 관리할 수 있는 객체로 활용되게 된다.

 

Box<String> box = new Box<String>() 으로 생성하면 Box 클래스는 String 타입의 데이터를 관리하고, Human 으로 생성하면 Human 타입의 데이터를 관리하게 된다. 이제 Box라는 클래스가 특정 데이터 타입에 종속되지 않는 Generic한 클래스가 된 것이다.

 

그래도 이해가 잘 안된다면 ArrayList를 생각해봐도 좋을 것 같다.

ArrayList 생성 시 new ArrayList<String>, new ArrayList<Integer>, new ArrayList<Human> 형태로 작성하게 되는데, 입력한 데이터 타입에 따라 add할 수 있는 데이터 타입이 정해지게 된다. Generic과 비슷한 형태와 성질을 띄는 것 같지 않는가?

 

덧붙이면 예제에는 제네릭 변수를 T로 사용했는데 굳이 T가 아니어도 된다. K, V, A 등의 값으로 사용해도 된다.

 

Pair.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Pair<K,V> {
 
    private K key;
    private V value;
    
    public void set(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }
}
cs

Pair 클래스의 제네릭 변수를 K, V로 하여 클래스를 작성하였다.

그리고 main 메서드에서 K에는 String 타입을, V에는 Integer 타입으로 Pair 객체를 생성하였다.

 

1
2
3
4
5
6
7
8
9
10
public class Main {
 
    public static void main(String[] args) {
 
        Pair<String, Integer> pair = new Pair<String, Integer>();
        pair.set("test"9);
        System.out.println(pair.getKey()); // test
        System.out.println(pair.getValue()); // 9
    }
}
cs

 

그런데 생각해보니 Object로 대체해도 Generic한 클래스가 되지 않을까?

Box 클래스의 T를 빼버리고, Object 타입으로 넣으면 Object 타입은 모든 클래스의 슈퍼클래스이기 때문에 정형화시킬 수 있지 않는가? 그런데 왜 굳이 Object를 쓰지 않고 Generic을 사용할까?

 


4. Generic 사용 이유

Box.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Box {
 
    private Object t;
    
    public void set(Object t) {
        this.t = t;
    }
    
    public Object get() {
        return t;
    }
}
cs

제네릭 변수를 빼고 Object로 치환하였다.

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
 
    public static void main(String[] args) {
 
        Box box = new Box();
        box.set(10);
        
        //Integer i = box.get(); // 컴파일 에러
        Integer i = (Integer) box.get();
        System.out.println(i.intValue());
    }
}
cs

그리고 main 메서드에서 Box 객체를 생성하였더니 box.get() 부분에서 컴파일 에러가 발생하였다.

Integer i = box.get() 은 슈퍼클래스인 Object를 서브 클래스인 Integer가 참조하려는 형태이기 때문에 에러가 발생한다.

그래서 강제로 캐스팅하는 코드를 추가해주고 있다.

 

반면에 Generic 클래스를 사용하면 캐스팅하는 코드가 없다. Object를 사용하는 것보다 효율적이다.

 

추가적으로 Generic을 사용하면 컴파일 시점에 잡을 수 없었던 타입 에러를 검출 할 수 있다. 

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
 
    public static void main(String[] args) {
 
        Box box = new Box();
        box.set(10);
        
        //Integer i = box.get(); // 컴파일 에러
        String i = (String) box.get();
        System.out.println(i);
    }
}
cs

다음과 같이 9행을 String으로 수정하였다.

box.get 이라는 메서드는 Object 형태의 데이터를 return 하기 때문에 컴파일 시점에서는 Integer로 받아도, String으로 받아도 상관없다. 그래서 컴파일 에러가 발생하지 않는다. 하지만 런타임 시 10이라는 정수 타입의 변수라 들어가게 되고, 9번 라인에서 Integer를 String 타입으로 변환하려 하니 castException이 발생하게 된다.

castException

Generic은 캐스팅 할 필요가 없기 때문에 이러한 문제가 발생하지도 않는다.

 

이처럼 Generic은 Object를 사용하는 방법보다 효율적임을 알 수 있다.

 


5. 마치며

오늘은 Generic에 대한 개념을 잡자는 목적으로 포스팅을 하였는데, 목적을 달성했다는 느낌이 든다.

기부니가좋다.

 

반응형
반응형

1. 개요

 추상클래스와 인터페이스에 대해 알아보자.


2. 추상클래스란?

 - 추상(abstract) 클래스란 추상 메서드를 포함한 클래스이다.

 - 추상(abstract) 메서드란 선언만 있고, 구현이 없는 메서드이다.

 - 추상 메서드가 하나라도 선언되어있으면 객체를 만들 수 없기때문에 서브클래스를 만드는 용도로 사용된다.

 - 추상 메서드와 추상 클래스는 abstract 키워드로 표시한다.

 - 추상 클래스를 상속받는 클래스는 추상메서드를 구현해야한다.


3. 추상 클래스 사용 이유?

 객체도 못만들고, 번거롭게 abstract 키워드 추가하고, 구현하지 않는 추상메서드를 굳이 선언... 왜 쓸까?

 라는 의문이 들기 마련이다. 말보다는 직접 코드로 경험하면 이해할 수 있을 것이다. 다음 상황을 생각해보자.


4. 상황

 찰흙을 빚어 도형을 만들고, 만든 도형을 전시하는 프로그램을 구현한다. 이에 대해 요구사항을 부여하였다.

 1) 삼각형, 사각형, 원 모양의 도형을 만들 수 있다.

 2) 내가 만든 도형에 대해 관리 및 조회할 수 있다.

 3) 도형 전시를 위해 해당 도형의 넓이를 알아야 한다.

 

 직접 코딩을 하기 전에 생각해보자. 

 1번을 처리하기 위해서는 삼각형, 사각형, 원에 대한 클래스가 필요하다.

 2번을 처리하기 위해서는 내가 만든 도형을 배열이나 리스트로 관리해야한다. 삼각형 배열에는 삼각형 객체를, 사각형 배열에는 사각형 객체를, 원 배열에는 원 객체를 넣어 관리해도되지만, 3개의 배열을 각각 선언해야 하니 효율적으로 느껴지진 않는다. 그래서 도형이라는 클래스를 만들고 삼각형, 사각형, 원 클래스가 이를 상속하도록 구현한다. 이렇게 되면 도형이라는 자료형 하나로 모든 클래스를 관리할 수 있다.

 3번을 처리하기 위해서는 각 클래스마다 자신의 넓이를 구하는 메서드가 필요하다.

 이제 직접 예제를 통해 이를 구현해보겠다.


5. 예제

  

5.1. 클래스 생성

 - Triangle, Square, Circle, Figure(도형), Exhibition(전시), Main 클래스를 생성한다.

 - Triangle, Square, Circle은 Figure 클래스를 상속받는다.

 - 삼각형은 밑변, 높이. 사각형은 가로, 세로. 원은 반지름 값을 나타내는 멤버변수를 정의하고, 생성자를 생성한다.

 

5.2. 1번 요건 처리

 - 세 클래스를 다음과 같이 구현한다.

 - 클래스를 구현했으니 각 도형에 대한 객체를 만들 수 있게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Triangle extends Figure{
 
    private int base;
    private int height;
    
    public Triangle( int baseint height) {
        this.base = base;
        this.height = height;
    }
}
 
 
/////////////////////////////////////
 
public class Square extends Figure{
 
    private int horizontal;
    private int vertical;
    
    public Square( int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
}
 
/////////////////////////////////////
 
public class Circle extends Figure{
 
    private int redius;
    
    public Circle(int redius) {
        this.redius = redius;
    }
}
cs

 

5.3. 2번 요건 처리

 - 조회에 사용할 toString 메서드를 각 클래스에 추가한다.

 - 도형들을 관리할 Exhibition 클래스를 정의한다.

 

Trigangle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Triangle extends Figure{
 
    private int base;
    private int height;
    
    public Triangle( int base, int height) {
        this.base = base;
        this.height = height;
    }
    
    public String toString() {
        return "class : Triangle , base : "+base + ", height : "+height; 
    }
}
cs

 

Square.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Square extends Figure{
 
    private int horizontal;
    private int vertical;
    
    public Square( int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
    
    public String toString() {
        return "class : Square , horizontal : "+horizontal + ", vertical : "+vertical; 
    }
}
cs

 

Circle.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Circle extends Figure{
 
    private int redius;
    
    public Circle(int redius) {
        this.redius = redius;
    }
    
    public String toString() {
        return "class : Circle , redius : "+redius; 
    }
}
cs

 

Exhibition.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Exhibition {
 
    private List<Figure> list;
    
    public void setList( List<Figure> list ) {
        this.list = list;
    }
    
    public void showList() {
        for(Figure figure : list) {
            System.out.println(figure.toString());
        }
    }
}
cs

 

이제 Main 클래스에 main 메서드를 생성하여 1,2 번 요건이 만족되도록 정의 후 실행시켜보자.

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
 
    public static void main(String[] args) {
        //삼각형, 사각형, 원 객체 생성
        Triangle triangle = new Triangle(4,3);
        Square square = new Square(5,5);
        Circle circle = new Circle(10);
                
        // 관리에 사용될 리스트 생성 및 추가
        List<Figure> list = new ArrayList<Figure>();
                
        list.add(triangle);
        list.add(square);
        list.add(circle);
                
        Exhibition exhibition = new Exhibition();
        exhibition.setList(list);
        exhibition.showList();
                
    }
}
cs

5~7번 라인에서 삼각형, 사각형 원 객체를 만든다.

10번 라인에서 도형을 관리할 list를 생성한다.

12~14번 라인에서 생성한 도형들을 list에 넣는다.

16~17번 라인에서 전시 객체를 생성하고 그 안에 만든 도형들을 set 한다.

18번 라인에서 도형들을 조회한다.

 

출력 결과를 확인해보면 다음과 같이 조회된다.

showList()

 

이제 마지막 요건인 도형의 넓이를 구하면 끝이다. 그리고 여기서 추상 클래스의 사용 이유에 대해 조금은 와닿을 수 있을 것이다.

 

5.4 3번 요건 처리

 - 각 도형마다 구하는 넓이 공식이 다르기때문에 삼각형, 사각형, 원 클래스에 넓이를 구하는 메서드를 각각 구현한다.

 - Exhibition 클래스에서는 구현한 메서드를 출력하는 메서드를 생성한다.

 

Triangle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Triangle extends Figure{
 
    private int base;
    private int height;
    
    public Triangle( int base, int height) {
        this.base = base;
        this.height = height;
    }
    
    public String toString() {
        return "class : Triangle , base : "+base + ", height : "+height; 
    }
    
    public double getArea() {
        return base * height * 0.5;
    }
}
cs

 

Square.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Square extends Figure{
 
    private int horizontal;
    private int vertical;
    
    public Square( int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
    
    public String toString() {
        return "class : Square , horizontal : "+horizontal + ", vertical : "+vertical; 
    }
    
    public double getArea() {
        return horizontal * vertical;
    }
}
cs

 

Circle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Circle extends Figure{
 
    private int redius;
    
    public Circle(int redius) {
        this.redius = redius;
    }
    
    public String toString() {
        return "class : Circle , redius : "+redius; 
    }
    
    public double getArea() {
        //파이 값은 임의로 3.14로 가정한다.
        return redius * redius * 3.14;
    }
}
cs

 

Exhibition.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Exhibition {
 
    private List<Figure> list;
    
    public void setList( List<Figure> list ) {
        this.list = list;
    }
    
    public void showList() {
        for(Figure figure : list) {
            System.out.println(figure.toString());
        }
    }
    
    public void getFigureArea() {
        for(Figure figure : list) {
            //컴파일 발생
            System.out.println(figure.getClass().getSimpleName()+ " : "+ figure.getArea());
        }
    }
}
cs

 

 이로써 넓이를 구하는 것까지 구현했다고 생각했으나, Exhibition.java 코드의 18번째 라인에서 에러가 발생하게 된다.

발생 원인은 figure 객체에 getArea() 메서드를 찾지 못해서 발생한다.

하지만 분명 figure는 삼각형, 사각형, 원 클래스 중 한 객체를 참조하고 있고, getArea 메서드가 정의되어 있는데 왜 찾지 못하는 걸까? 바로 다이나믹 바인딩의 처리가 런타임시점에서 진행되기 때문이다.

 실제 코드를 돌리면 16번째의 figure 객체는 삼각형, 사각형, 원 객체 중 하나겠지만, 컴파일러 입장에서는 컴파일 시점에 이를 확인할 방법이 없기 때문에 에러가 뜨는것이다.

 

그럼 해결방안으로 Figure 클래스에 getArea 메서드를 생성하는 방법이 있다.

그렇게 되면 컴파일 시점에는 에러가 발생한 지점에서 getArea 메서드를 Figure 클래스의 메서드로 인식하여 에러를 떨구지 않을 것이고, 런타임 시에는 다이나믹 바인딩으로 인해 참조 객체의 getArea 메서드를 호출할 것이다.

 

하지만 Figure 클래스의 getArea가 전혀 사용되지 않는 상황인데 저렇게 메서드를 정의해두면, 불필요한 코드일 뿐더러, 타 개발자가 이를 봤을 때 당연히 이 메서드와 클래스가 어디서 사용되고 있을 것이라고 생각할 수 있다. 하지만 코드를 보면 실제로 Figure 클래스는 참조객체로써 사용되지 않고있다. 뭔가 혼란스럽지 않는가? 이러한 혼란 해소할 수 있는 것이 바로 추상 메서드, 추상 클래스이다.

 

 추상 메서드를 사용했기 때문에 Figure 클래스는 getArea 메서드를 선언만 해놓으면 된다. 그럼 타 개발자가 이 코드를 봤을 때 이 클래스가 객체로 생성되어 어디선가 사용되고 있지 않을까에 대해 생각할 필요가 없다. 개요에서 설명했듯이 추상클래스는 서브클래스를 위한 클래스이고, 객체로 생성이 불가능하기 때문이다. 또한 자식 클래스에서 정의해서 쓰고 있다라는 사실을 명확하게 알 수 있다.

 

Figure.java

1
2
3
4
5
public abstract class Figure {
 
    public abstract double getArea();
}
 
cs

 

실행화면

 

실행 결과를 통해 모든 요건을 만족하는것을 확인할 수 있다.


 

 

설명이 좀 중구난방 한것같지만 그래도 쥐어짜면서 글을 써내려가니 머릿속에서 확실히 조금 더 정리된 느낌이고, 이전에 공부했던 정형화나 다이나믹 바인딩도 복습할 수 있었다.

반응형
반응형

1. 개요

 상속 관계인 두 클래스의 메서드를 오버라이딩 한 후, 다형성의 원리를 적용하여 자료형과 참조 객체를 다르게 설정하였때 어떤 결과가 나올지 알아보았다.

 

 말이 조금 어렵게 느껴지니 예로 설명하면 다음과 같다.


 Parent라는 부모 클래스와 이를 상속받는 Child 자식 클래스가 있다.

 Parent 클래스에는 hi라는 메서드가 있기때문에 Child에서도 호출이 가능하나, 메서드를 재정의하고 싶어 메서드 오버라이딩을 하였다.

 객체 생성 시점에 Child child = new Child(); 형태로 생성 후 child.hi() 메서드를 호출해도 되지만, 다형성의 원리를 사용하여 Parent child = new Child(); 형태로 생성하였다.

 만약, 이 상황에서 child.hi() 메서드를 호출하면 Parent 클래스와 Child 클래스의 hi() 중 어떤 메서드가 실행될까?


 

 Child 객체를 참조하고있으니 Child 객체의 메서드가 호출될 것 같지만 자료형을 Parent이니 Parent의 메서드가 호출될 것 같기도 하다. 다형성에 대해 알아보고 이 고민을 해소해보자.


2. 다형성(Ploymorphism)이란?

 슈퍼클래스 타입의 변수가 서브클래스 타입의 객체를 참조할 수 있다는 성질이다.

 Child 클래스가 Parent 클래스를 상속받고 있다면 슈퍼 클래스 타입의 변수인 Parent가 Child 클래스를 참조할 수 있다는 의미인데, 예제를 보면 이해가 빠를 것이다. 다형성이란 말이 어렵지 사실 되게 익숙한 개념이다.

 

 그럼 예제를 통해 상속관계, 메서드 오버라이딩, 다형성이 적용된 객체를 생성해보도록 하겠다.


3. 예제

3.1. Parent.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Parent {
    
    protected String name;
    protected int age;
    
    public Parent(String name, int age) {
        this.name = name;
        this.age = age;
    }
    protected void hi() {
        System.out.println("안녕하십니까 올해로 "+age+"살인 "+name+"입니다.");
    }
}
cs

 

3.2. Child.java

1
2
3
4
5
6
7
8
9
10
11
public class Child extends Parent{
 
    public Child(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void hi() {
        System.out.println("안녕? 내 이름은 "+name+"이고, "+age+"살이야! 만나서반가워~");
    }
}
cs

Parent 클래스를 상속받고 있으며 부모 클래스의 hi() 메서드를 오버라이드하여 재정의하였다.

 

3.3. Main.java

1
2
3
4
5
6
7
public class Main {
 
    public static void main(String[] args) {
        Parent child = new Child("심드류 아들",10);
        child.hi();
    }
}
cs

main 메서드에서 다음과 같이 자료형이 Parent인 child 변수에 Child 객체를 생성하여 초기화해주었다.

슈퍼클래스 타입(:Parent)인 변수(:child)가 서브클래스의 객체(new Child)를 참조하고 있다. 바로 이게 다형성이다.

그럼 이 상태에서 hi() 메서드를 실행하면 어떤 결과가 나올까?


4. 결과

Child 객체의 hi 메서드가 실행됨

 결과는 바로! Child 객체의 hi 메서드가 실행된다. 즉 자료형이 아닌, 참조 객체의 메서드가 실행되는 것이다.

 다형성의 원리를 사용하여 객체를 생성하고, 오버라이드된 메서드를 호출했을 때에는 슈퍼클래스의 메서드가 아닌, 서브클래스의 메서드가 호출된다. 즉, 참조 객체가 무엇이냐에 따라 호출하는 메서드가 동적으로 바뀔 수 있다는 뜻이다.

이러한 내용을 동적 바인딩(dynamic binding) 이라고 한다.


5. 정리

 상속관계에서 서브 클래스의 메서드를 오버라이딩 하고 다형성의 원리에 입각하여 객체 생성 후 메서드를 호출하면, 해당 메서드는 동적 바인딩된다.

 

반응형
반응형

1. 개요

 자주 쓰는 상속과 생성자의 개념에 대해 간단히 정리하고, 상속관계에서 발생할 수 있는 문제에 대해 알아보자.


2. 상속이란?

 상속이란 부모님에게 재산을 물려받듯이 부모 클래스의 멤버필드와 메서드를 자식클래스가 물려받는 것이다.

 상속을 통해 불필요한 중복을 제거할 수 있다.


3. 생성자란?

 객체에 대한 초기화 메서드이다. 멤버 변수를 초기화하거나 자원을 할당할 수 있다.

 클래스 내 생성자가 정의되어 있지 않을 경우 자바가 자동으로 no-parameter 생성자를 생성한다.

 

 여기까지가 많은 사람들이 알고 있는 생성자에 대한 간단한 정의이다. 하지만 다음과같은 내용도 있다.

더보기

 자식클래스의 생성자는 부모클래스의 생성자를 먼저 호출한다.

 명시적으로 호출하지 않을 경우 부모클래스의 no-parameter 생성자가 자동적으로 호출된다.

별게 아닌 것 같지만, 이 내용을 알지 못했을 때 발생할 수 있는 문제 상황이 있다. 한번 알아보자.


4. 상속, 생성자 예제

 상속 관계로 부모 클래스는 술(Alcohol), 자식 클래스는 럼(Rum)으로 하겠다. (럼: 해적들이 마시던 술)

 

4.1. Alcohol.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Alcohol {
 
    protected double frequency;
    protected double price;
    protected String flavor;
    
    public Alcohol(double frequency, double price, String flavor) {
        this.frequency = frequency;
        this.price = price;
        this.flavor = flavor;
    }
    
    public String whatFlavor() {
        return "flavor is "+flavor;
    }
}
cs

모든 술에는 도수, 가격, 맛이 존재한다. 그에 맞게 frequency, price, flavor 멤버필드를 선언해주었고, 모든 멤버필드의 값을 초기화해주는 생성자도 생성하였다. 추가적으로 whatFlavor라는 간단한 메서드도 정의하였다.

 

4.2. Rum.java

1
2
3
4
5
6
7
8
9
10
11
public class Rum extends Alcohol{
 
    private String rumType;
 
    public Rum(String rumType, double frequency, double price, String flavor) {
        this.rumType = rumType;
        this.frequency = frequency;
        this.price = price;
        this.flavor = flavor;
    }
}
cs

Alcohol 클래스를 상속받는 Rum 클래스를 정의하였다.

Alcohol 클래스를 상속받았기때문에 이에 대한 멤버필드와 메서드를 Rum 클래스에서 할당받게 된다. 즉, Rum 클래스의 멤버필드는 rumType, frequency, price, flavor가 된다. 마찬가지로 Rum 클래스의 생성자도 정의하였다.

...

...

...

혹시 이 예제에서 문제점을 발견했는가? 발견했다면 당신은 멋진사람...

 

4.3. 컴파일 에러

 이 예제는 언뜻보면 이상이 없어보이나 다음과 같이 컴파일 에러가 발생하는 코드이다.

컴파일 에러

 에러 내용은 다음과 같다. 

더보기

 Implicit super constructor Alcohol() is undefined.

 암시 적 슈퍼 생성자 Alcohol ()이 정의되지 않았습니다.

 바로 이게 앞서 언급했던 "자식클래스의 생성자는 부모클래스의 생성자를 먼저 호출한다." , "명시적으로 호출하지 않을 경우 부모클래스의 no-parameter 생성자가 자동적으로 호출된다." 라는 생성자의 정의에 의해 발생한 에러이다.

 

 자식클래스의 생성자는 호출 전에 부모클래스의 생성자를 호출해야하는데, 현재 Rum 생성자를 보면 알수있듯이 부모 클래스의 생성자를 호출하는 메서드인 super(...)가 없다. 즉, 명시적으로 호출하지 않았기 때문에 자바가 Rum 생성자 내에서 부모 클래스의 no-parameter 생성자를 자동적으로 호출하려했으나 현재 부모클래스에는 no-parameter 생성자가 없기 때문에 위 에러가 발생한 것이다.

 

그렇다면 해결은 어떻게 할까?


5. 해결

이를 해결하기 위해서는 두가지 방법이 있다.

첫번째, 부모클래스에 no-parameter 생성자를 생성한다.

두번째, 자식클래스의 생성자에 부모클래스의 생성자를 명시적으로 호출한다.

첫번째 방법은 에러 제거만을 목적으로 불필요한 코드를 추가하기 때문에 추천하지 않는 방법이다. 자식 클래스 생성자 내에서 부모 클래스의 생성자를 명시적으로 호출하는 두번째 방법을 사용하자.

 

5.1. Rum.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Rum extends Alcohol{
 
    private String rumType;
 
    public Rum(String rumType, double frequency, double price, String flavor) {
        super(frequency, price, flavor);
        this.rumType = rumType;
    }
    
    public static void main(String[] args) {
        Rum rum = new Rum("DarkRum"40.069000"bitter");
        System.out.println(rum.whatFlavor());
    }
}
cs

Rum 생성자 안에서 super() 메서드를 통해 부모 클래스의 생성자를 명시적으로 호출해주고, 부모클래스에 없는 rumType은 this를 통해 초기화하였다.

 

main 메서드에서 생성자를 통해 Rum 객체를 생성하여 테스트하면 정상적으로 작동됨을 확인할 수 있다.

 

 

오늘 자료구조를 공부하며 알게 된 새로운 내용을 정리할 수 있어 기쁘다.

반응형

+ Recent posts