우아한테크코스 4주 후기

5tr1ker·2023년 11월 16일
0
post-thumbnail

개요

이번에 우아한 테크 코스 6기에 지원하면서 프리 코스를 4주동안 진행했었는데, 그 시간동안 배운 것과 느낀점, 그리고 더 나은 코드를 작성하기 위해 받았던 피드백을 기록 남기고 지식을 공유하고자 해당 글을 작성하게 되었습니다.

프리 코스 4주 동안..

프리 코스는 우테코에서 교육 기간동안 함께 나아갈 수 있는 사람을 선별하기 위해 진행되기에 실력이 없는 저로써 쥐어 짜서라도 요구 사항에 만족하는 프로그램을 만들고자 노력했습니다.. 물론 프리 코스를 진행하면서 실력이 많이 향상된것을 느끼면서 동시에 공통 피드백을 통해 다른 사람이 많이 실수하는 부분에 대해 피드백을 주는 게 많이 신기했습니다. ( 모든 코드를 다 보는구나.. )
또한 학습을 어떻게 하는지 과정을 물어보셔서 성장 가능성이 충분한지 매 주 확인을 합니다!

느낀점 및 배운점

우아한 테크 코스를 진행하면서 다양한 사람들과 함께 코드리뷰를 하면서 부족했던 부분을 채워나가거나 다른 사람과 지식을 공유할 수 있는 좋은 시간을 갖게 되었습니다. 이를 통해 클린 코드에 대해 다시 생각해볼 수 있는 유익한 시간이 되었고, 나의 잘못된 지식은 빠르게 고칠 수 있었습니다.
따라서 코드 리뷰나 커뮤니케이션이 얼마나 중요한 지 깨달을 수 있었고 앞으로 지속적인 코드리뷰를 통해 내가 몰랐거나, 잘못된 지식을 바로 고치기 위해 끊임없이 활동을 해야 한다고 느꼈습니다.

더 나은 코드를 위해 생각해보아야 할 것들!

README.md를 상세히 작성한다

ReadMe는 해당 프로젝트가 어떤 프로젝트이며 어떠한 기능을 갖고있는지 소개하는 문서이므로, 마크다운 문법 학습하여 상세히 작성할 수 있게 해야합니다.

기능 목록을 재검토 및 업데이트한다.

기능 목록에서는 상세한 기능을 적지 않습니다. 수정될 가능성이 있기때문이며, 예외로 발생할 수 있는 기능을 함께 작성해야합니다. 이때 기능 목록은 추가될 수 있기 때문에 처음에 기능 목록을 정의하지 않고 기능을 구현하면서 추가해 나갑니다.

공백 라인을 의미 있게 사용한다

공백 라인을 사용하면 코드가 더 깔끔해보입니다. 단일 책임으로 코드를 작성했다 하더라도 클래스나 메서드 내에서 코드마다 역할이 있는데 공백을 이용해 구분하면 보기 쉬워집니다.

공백도 코딩 컨벤션이다

if, for, while문 사이의 공백도 코딩 컨벤션입니다.

이름을 통해 의도를 드러낸다

불용어(Info, Data, a, an, the) 이나 연속된 숫자 ( a1,a2,a3 ) 을 붙이는 것은 부적절 합니다. 다른 개발자와 소통을 하기 위해선 좋은 이름을 붙이는 것이 중요하며 이름을 붙이는 것에 많은 시간을 투자하는 것이 좋습니다. 이름을 통해 해당 변수나 함수의 의도를 나타내면 좋습니다.

축약하지 않는다

만약 의도를 들어내기에 긴 이름이 필요하다면 괜찮습니다. 불필요한 축약은 더욱 헷갈리게 만드는 코드가 됩니다.

누구나 실은 클래스, 메서드, 또는 변수의 이름을 줄이려는 유혹에 곧잘 빠지곤 한다. 그런 유혹을 뿌리쳐라. 축약은 혼란을 야기하며, 더 큰 문제를 숨기는 경향이 있다. 클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제하자. 클래스 이름이 Order라면 shipOrder라고 메서드 이름을 지을 필요가 없다. 짧게 ship()이라고 하면 클라이언트에서는 order.ship()라고 호출하며, 간결한 호출의 표현이 된다.

  • 객체 지향 생활 체조 원칙 5: 줄여쓰지 않는다 (축약 금지)

변수 이름에 자료형은 사용하지 않는다

만약 다음과 같은 변수가 있을 때 다음과 같은 이름을 사용하지않습니다.

String carNameList = Console.readLine();
String[] arrayString = carNameList.split(",");

차라리 carNames 를 사용하는 것이 더 좋습니다!

값을 하드 코딩하지 않는다

문자열, 숫자 등의 값을 하드 코딩하지 않습니다. 상수(static final)를 만들고 이름을 부여해 이 변수의 역할이 무엇인지 의도를 드러냅니다. 구글에서 "java 상수"와 같은 키워드로 검색해 상수 구현 방법을 학습하고 적용해 봅시다.

구현 순서도 코딩 컨벤션이다

클래스는 상수, 멤버 변수, 생성자, 메서드 순으로 작성합니다.

class A {
    상수(static final) 또는 클래스 변수

    인스턴스 변수

    생성자

    메서드
}

한 함수가 한 가지 기능만 담당하게 한다

만일 함수다 여러 개의 책임을 갖게 된다면 코드가 중복되고 더러워질 뿐만 아니라 메서드의 이름도 짓기 어려워집니다. 따라서 한 가지의 기능만 담당하게 분리한다면 더욱 좋은 코드를 작성할 수 있습니다.

public List<String> userInput() {
    System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
    String userInput = Console.readLine().trim();
    String[] splittedName = userInput.split(",");
    for (int index = 0; index < splittedName.length; index++) {
        if (splittedName.length < 1 || splittedName.length > 5) {
            throw new IllegalArgumentException("[ERROR] 자동차 이름은 1자 이상 5자 이하만 가능합니다.");
        }
    }
    return Arrays.asList(splittedName);
}

이 때 함수가 한 가지 기능을 하는지 확인하는 기준을 세운다

만약 코드가 중복되고 길어진다면 코드를 분리해야 하는지 고민합니다. 또한 코드의 길이 ( 세로 줄 ) 이 15줄이 넘어가지 않게 구현하며 분리하는 것이 좋습니다.

의미 없는 주석을 달지 않는다

주석은 말 그대로 코드에 // 로 시작하여 코드를 설명하는데 메서드나 클래스에 의도는 명명 규칙으로만 충분합니다. 의미없는 주석은 코드를 이해하는데 더 많은 시간이 걸립니다.

Java에서 제공하는 API를 적극 활용한다

구현하기 전에 자바에서 제공하는 API가 존재하는지 확인합니다. 자바에서 제공하는 API가 존재하지 않으면 그때 구현을 합니다!

배열 대신 Java Collection을 사용한다

자바 Collection은 다양한 메서드를 제공합니다. 따라서 다양한 데이터를 조작할 때 stream 이나 contains 를 이용하면 더욱 깔끔한 코드를 작성할 수 있습니다.

발생할 수 있는 예외 상황에 대해 고민한다

정상적인 로직이 수행되는 것 보다 잘못된 값으로 인해 발생할 수 있는 모든 예외를 생각해서 코드를 작성해야합니다! 예를 들어 날짜를 입력하는 코드에는 다음과 같은 예외 상황이 발생할 수 있습니다.

  • 1 미만 혹은 31 초과의 수
  • 문자열이 입력될 때
  • 공백 혹은 강제 종료 시

비즈니스 로직과 UI 로직을 분리한다

비즈니스 로직과 UI 로직이 같은 클래스 내부에 존재해서는 안됩니다. 예시로 다음과 같은 코드가 존재합니다.

public class Restaurants {
    private List<Integer> orderNumber;

    // 해당 숫자가 포함되는지 확인하는 비즈니스 로직
    public boolean contains(int number) {
        ...
    }

    // UI 로직
    private void print() {
        ...
    }
}

만약 현재 객체 상태를 출력하기 위해선 toString 메서드를 오버라이딩합니다.

연관성이 있는 상수는 static final 대신 enum을 활용한다

클래스 내부에 static final 로 두는 것 보단 enum 을 활용해 따로 상수를 관리하는 것이 좋습니다.

final 키워드를 사용해 값의 변경을 막는다

최근에 사용되는 언어들의 값들은 불변입니다. Java 에서는 final 키워드를 이용해 불변 객체로 선언을 하여 값이 변경될 수 있는 가능성을 배제합니다.
왜 불변 값으로 해야하는 이유에 대해선 해당 블로그 에서 참고하실 수 있습니다.

객체의 상태 접근을 제한한다

객체의 상태 접근은 private 키워드를 이용해서 상태 접근을 제한할 수 있습니다. 만약 접근이 필요하다면 getter 를 이용해 값을 반환합니다.

객체는 객체스럽게 사용한다

객체는 단순히 값을 갖고 값을 반환하는 방식으로 사용해서는 안됩니다.
해당 값에 대해 [값을 포함하고 있거나] , [해당 값을 가공하거나] , [다른 값을 받아 가지고 있는 값과 비교하여 결과를 내놓거나] 로 해당 객체가 일을 하게 구현해야합니다.

public class Restaurants {
    private List<Integer> orderNumber;

    // 값 설정.
    public void setOrderNumber(int number) {
        ...
    }
    
    // 값 반환.
    public List getOrderNumber() {
        ...
    }

}

위의 클래스 처럼 단순히 값을 설정하고 반환하려는 로직이 필요하면 굳이 클래스를 쓸 필요가 없습니다.

클래스 내 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다.

클래스 내 필드를 줄이기 위해 무지성으로 줄이는 것은 오히려 악 효과를 줍니다. 불필요한 것은 줄이고 중복되는 필드를 줄이기 위해 노력해야합니다..

public class Restaurants {
    private List<Integer> orderNumber; // 주문 번호
    private int orderCount; // 총 주문 갯수
    private int totalOrderPrice; // 총 주문 가격

}

예시로 위와 같은 클래스가 존재한다고 하면 총 주문 횟수와 가격은 굳이 따로 변수로 두지않고 필요에 따라 값을 가공 후 반환하는 것이 좋습니다. 그 이유는 중복되는 값이 여러개 있을 경우 버그가 발생할 수 있습니다. 따라서 위의 코드는 밑의 코드로 변경할 수 있습니다.

public class Restaurants {
    private List<Integer> orderNumber; // 주문 번호
   
   	public int getOrderCount() {
    	return orderNumber.size();
    }
    
    public int getTotalOrderPrice() {
    	return ...
    }

}

성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다

TDD ( 테스트 주도 개발 방식 ) 은 실패 코드 작성 -> 성공 코드 작성 -> 리팩토링을 과정으로 테스트 코드를 작성해야합니다.
프로그램에서 발생하는 버그는 대부분 경계값으로 발생하기 때문에 이를 꼼꼼히 확인해야 합니다.

테스트 코드도 코드다 코드를 깔끔히 작성해라

테스트 코드도 코드이기에 리팩토링을 해야합니다. 따라서 중복되는 테스트 코드는 다음과 같이 하나의 테스트 코드로 리팩토링할 수 있습니다.

@DisplayName("천원 미만의 금액에 대한 예외 처리")
@ValueSource(strings = {"999", "0", "-123"})
@ParameterizedTest
void underLottoPrice(Integer input) {
    assertThatThrownBy(() -> new Money(input))
            .isInstanceOf(IllegalArgumentException.class);
}

테스트를 위한 코드는 구현 코드에서 분리되어야 한다

테스트를 작성하기 위해 편의 메서드를 작성하거나 메서드를 생성하지 않습니다. 다시 말해 테스트를 통과하기 위해 구현 코드를 수정하거나, 테스트를 위한 코드를 구현 코드에 작성하지 않아야 합니다.

  • 테스트를 위해 접근 제어자를 바꾸는 경우
  • 테스트 코드에서만 사용되는 메서드

처음부터 큰 단위의 테스트를 만들지 않는다

테스트는 본인이 작성한 코드에 대해 빠르게 피드백을 받기 위함입니다. 큰 단위의 테스트를 먼저 작성한다면 코드에 대한 피드백이 늦게 도출됩니다. 따라서 작은 단위의 테스트를 작성하고 다음 큰 단위의 테스트 코드를 작성합니다.

큰 단위의 테스트

  • 자동차경주를 시작해서 사용자가 이름, 진행 횟수를 입력하면, 게임을 진행한 후 그 결과를 알려준다.

작은 단위의 테스트

  • 무작위 값이 4 이상이면 자동차가 전진한다.
  • 무작위 값이 3 이하이면 자동차가 전진하지 않는다.

단위 테스트하기 어려운 코드를 단위 테스트하기

만약 테스트하기 어려운 코드라면 코드를 분리해봅니다.

public class Lotto {
    private List<Integer> numbers;

    public Lotto() {
        this.numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6);
    }
}

위와같이 테스트 코드를 작성하려고 할떄 Randoms 함수때문에 테스트하기가 어렵습니다. 만약 Randoms 함수를 분리하면 다음과 같은 코드로 분리가 되므로 테스트에 용이해집니다.

public class Lotto {
    private List<Integer> numbers;

    public Lotto(List<Integer> numbers) {
        this.numbers = numbers;
    }
}

private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다

가독성 이유로 분리한 private 함수는 public 검증이 가능합니다. 또한 private 함수는 public 함수에서 사용되고 있기 때문에 자연스레 테스트 범위에 포함될 수 있습니다.
하지만 가독성 이상으로 많은 책임을 갖고 있다면 단일 책임에 맞게 함수를 분리해야할 고민을 해야합니다.

profile
https://github.com/5tr1ker

0개의 댓글