[TDD, Clean Code with JAVA] 자동차 경주 미션 3

Dayeon myeong·2021년 4월 14일
0

자동차 경주

본격 자동차 경주 미션을 시작했다. 코드를 작성할 때 매번 순차적으로 로직을 구현하게 되어 이를 객체지향적으로, 테스트 코드에 유연하게 작성하도록 많이 고민해야한다는 것을 느꼈다.

요구사항 적기

개발 전에 요구사항 목록을 적어줘야 한다. 얼마든지 개발하는동안 변경되기 때문에 계속 계속 업데이트하기.
이때 예외 사항도 같이 꾸준히 업데이트하면서 이에 따라 클래스 설계와 개발이 진행되어야한다..!

코딩 컨벤션

코딩컨벤션에 맞춰 구현을 하는 것이 생각보다 많이 어려운 것을 느꼈다. 놓치게 되는 부분들을 메모해놓고 항상 보면서 습관을 들이자

  • package 명 소문자 시작
  • 클래스명 대문자 시작
  • 메소드 소문자 시작
  • 문자열은 상수로 관리하기 : private static final
  • 변수명은 자료구조형을 직접 쓰기보다는 복수형으로 사용하기
  • for, while, if와 괄호 사이의 space 두기
  • indent(들여쓰기)가 2이면 안된다
  • 의미없는 주석 x
  • else 예약어 쓰지 않기
  • 인터페이스 명은 Impl을 붙이지않기
  • 해당 인터페이스,클래스가 무엇인지(what he is)에 기반해 이름짓기
  • return random ≥ value ? true : false 대신 return random ≥ value로 사용하기

intelliJ

코딩 컨벤션 일일이 맞추는 게 생각보다 힘들었는데, intelliJ의 포맷을 맞춰주는 기능이 있었다.
단축키 command + shift + option + L

추가적으로 생성자, equals And hashCode 생성 등등 : command + n

패키지 분리하기 : MVC 패턴

이번 미션에서는 MVC 패턴을 사용하여 패키지를 구분했다.
입출력을 담당하는 부분(view), 비즈니스로직 담당과 도메인 부분(model), 모델과 뷰를 연결하는 부분(controller)으로 나누었다.

각 패키지들의 역할이 분리될 수 있고
단일 책임의 원칙도 지키게 된다.
그리고 domain 영역에 대한 부분만 단위 테스트가 가능하기 때문에 사용하게 되었다.

getter를 사용하지 않기 : 객체 자체가 스스로 작업하게 하기

View 패키지에서는 데이터를 꺼내야하니 getter를 사용할 수 밖에 없지만,
도메인에서는 객체를 객체스럽게 사용하기 위해서 getter를 사용하지 말아야한다.

만약에 getter를 계속해서 쓰다보면 객체가 스스로 작업을 하지 않고 외부에서 작업이 일어나기 때문에 무분별한 사용을 하지 말자.

즉, 상태 데이터를 꺼내 로직을 처리하도록 하지 말고 객체 자체에 메시지를 보내 로직을 처리하자..!

public void move(Movable movable)
    {
        if (movable.moveOrNot()) {
            position++;
        }

    }

자동차를 움직이는 코드가 있다고할 때, 데이터를 꺼내 로직을 처리한다면 getter로 position 데이터를 꺼내 position을 증가시킬 수도 있다.

하지만, move라는 메서드로 Car 객체 자체에 move()라는 메시지를 보내면 데이터를 꺼내지 않고도 객체 자체가 스스로 작업을 하게 가능하다.

클래스 분리

이번 과제 전에 '객체지향 생활 체조'와 'SOLID 원칙'을 알게되었다. 그 중 과제를 하면서 많이 어렵고 와닿은 규칙이 있었는데

  • 모든 클래스는 각각 하나의 책임을 가져야한다.
    라는 것이다.

사실 car 클래스를 처음 작성할 때는 이 책임이 모호해져서 car의 움직임, 움직임 결정 까지 모두 car 클래스에서 담당하도록 했다.


public class Car {

    private int position;

    public void decideMovable() {
        RandomMovable randomMovableImple = new RandomMovable();
        int randomValue = randomMovableImple.makeRandomValue();
        if (randomMovableImple.MoveOrNot(randomValue)) {
            move();
        }
    }
    ...
 }

그런데, 코드리뷰를 통해 car가 움직일지 말지를 결정하는 것은 car의 역할이 아니란 것을 배웠고 그에 맞춰 리팩토링을 진행했다.

car는 움직인다는 move()만 필요하지 움직임을 결정하는 것은 외부에서 할 일이다.
(실제로도 자동차의 움직임을 결정하는 것은 운전자의 역할이다)

모든 car들을 멈춰야하는 상황의 경우 위와 같이 코드를 작성하면 decideMovable() 메서드 외에 자동차를 멈추기만하는 메서드인 stop()를 따로 작성해야 할 듯하다.

그러니 외부에서 움직임 여부를 관리한다면 자동차는 단지 그 움직임 여부에 따라 이동하기만 하면 된다.

그래서 위와 같은 코드를 아래와 같이 고쳤다.

//CarRacingInformation 클래스
 public void decideMovableByRandomValue(Car car) {
        randomMovable.makeRandomValue();
        car.move(randomMovable);
    }

//Car 클래스
  public void move(Movable movable)
    {
        if (movable.moveOrNot()) {
            position++;
        }

단지 car는 움직임 여부를 받아서 이동만 가능하게 된다.

테스트 범위

테스트 코드를 작성하는 데에 있어 어떤 것을 테스트해야하는지 기준이 너무 애매했다.

객체 생성까지 테스트 해야하는 건지, 메서드 하나하나 다 테스트해야하는건지..

리뷰어님에게 물어보니 기능 단위로 테스트 하는 것이 좋다고 한다.
이때 기능단위로 method 단위를 의미하는 것..!

만약 method 단위로 테스트 작성이 어렵다면 테스트 하고자 하는 method가 너무 많은 일을 하고 있을 가능성이 있다. 그럴 땐 method를 분리하자..!

단위 테스트가 가능하도록 리팩토링하기

car 클래스의 move()가 만약 아래와 같다면 테스트 코드 작성이 더욱 길어지고 힘들어진다.

public void move() {
	if (getRandomNo() >= 4) {
    	this.position++;
    }
}

이를 해결하기 위해선 테스트 가능한 코드와 테스트 하기 힘든 부분을 분리하자.
getRandomNo대신 number를 인자로 받아 테스트 가능한 부분만 메서드에 남기자

public void move(int number) {
	if (number >= 4) {
    	position++;
    }
}

이것도 좋지만 인터페이스를 통해 해결하는 방법도 있다.

public interface Movable {

    boolean moveOrNot();
}

//car 클래스
 public void move(Movable movable)
    {
        if (movable.moveOrNot()) {
            position++;
        }

    }

//테스트 코드
@Test
    @DisplayName("자동차 전진")
    public void moveCar() {
        Car car = new Car("one");
        car.move(() -> true);
        assertEquals(1,car.getPoisition());
    }

인터페이스를 사용하면 테스트 코드 작성시에 훨씬 가독성있는 코드가 된다.
익명 클래스나 람다로 바로 값을 오버라이드하기 때문에 좋다.

profile
부족함을 당당히 마주하는 용기

0개의 댓글