관리 메뉴

공부 기록장 💻

우아한테크코스 프리코스를 시작하기에 앞서 (과제 미션과 진행 방식, 클린 코드 원칙: 객체지향 생활체조 원칙 적용) 본문

# Tech Studies/우아한테크코스 프리코스

우아한테크코스 프리코스를 시작하기에 앞서 (과제 미션과 진행 방식, 클린 코드 원칙: 객체지향 생활체조 원칙 적용)

dream_for 2022. 10. 28. 17:51

Java 백엔드 온보딩 미션 (https://github.com/woowacourse-precourse/java-onboarding) 참고

 

진행 방식


- 미션은 다음과 같이 3가지로 구성되어 있다.

  1. 기능 요구 사항
  2. 프로그래밍 요구 사항
  3. 과제 진행 요구 사항

- 위 3가지의 요구 사항을 만족하기 위해 노력해야 하며, 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행하라.
- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현하라.

미션 제출 방법

- 미션 구현을 완료한 후 Github을 통해 제출해야 한다. (프리코스 과제 제출 문서를 참고)
- Pull Request 완료 후, 지원 플랫폼에 과제를 최종 제출해야 한다.

과제 제출 전 체크 리스트

- 기능 구현을 모두 정상적으로 완료하고, 요구 사항에 명시된 출력값 형식을 지키도록
- 기능 구현 완료 후 테스트 실행헀을 때 모든 테스트가 성공했는지 확인하도록
- 테스트 실패 시 0점 처리!

테스트 실행 가이드
- Java 버전 11 확인
- Windows의 경우 gradlew.bat cleat test 명령을 실행할 때 모든 테스트가 통과하는지 확인해야 함


1. 기능 요구사항

- 문제 1 ~ 문제 7

2. 프로그래밍 요구 사항

- JDK 11 버전에서 실행 가능해야 함
- build.gradle 변경할 수 없고, 외부 라이브러리 사용 X
- 프로그램 종료 시 System.exit() 호출 X
- 프로그램 구현 완료 후 ApplicationTest의 모든 테스트가 성공해야 함
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름 수정하거나 이동하지 않도록

3. 과제 진행 요구 사항

- java-onboarding 저장소를 fork & clone 하여 시작
- 프리코스 과제 제출 문서 참고 후 진행 및 제출


우아한 테크코스 클린 코드 원칙


이전 기수에서 다뤘던 우아한테크코스 문서 저장소에 포함되어 있는 클린코드 원칙을 살펴보자. (https://github.com/woowacourse/woowacourse-docs)

 

"좋은 설계의 바탕에 있는 핵심 개념은 쉽게 알 수 있다. 예를 들어 보통 중요하다고 알고 있는 7가지 코드 품질 항목, 즉 응집력, 느슨한 결합, 무중복, 캡슐화, 테스트 가능성, 가독성, 그리고 초점이 있다. 하지만 그런 개념을 실제로 펼치기란 어렵다. 캡슐화가 데이터, 구현, 타입, 설계, 또는 생성의 은닉을 가리킨다고 이해하는 것과 캡슐화를 잘 구현하는 코드를 설계하는 것은 별개의 문제다. 그래서 좋은 객체지향 설계의 원칙을 자기 것으로 하고 실생활에서 쓰도록 도와주는 훈련을 소개하려 한다." 

소프트웍스 앤솔러지 - 객체지향 생활 체조 내용 中

 



1. 자바 코드 컨벤션을 지키면서 프로그래밍했는가?

- 참고 : https://google.github.io/styleguide/javaguide.html , https://myeonguni.tistory.com/1596
- IntelliJ, Eclipse에서 서식을 지정하도록

 


2. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 허용했는가?

- 하나의 단락(block)이 한 가지의 일을 하는가? 들여쓰기가 한 번 이상 적용된 단락, 즉 중첩된 제어 구조가 있다면, 이 단락은 한 가지 이상의 일을 하고 있음을 증명하는 셈이 되어버린다. 따라서 메서드 당 들여쓰기 한 번 이라는 규칙은 메서드가 가지는 구문 중 한 단락에서는 한 가지의 일만 수행하게 만듦으로써 메서드의 내부의 구조를 평이하게 만드는 효과를 만들어 낸다.

 

- 다음 예제를 통해 중첩 for문을 통해 행렬의 행과 열을 추가하는 메서드를 행 추가하는 메서드, 열 추가하는 메서드로 구분하여 한 가지의 메서드가 한 가지의 일만 하도록 코드를 리팩토링하는 과정의 이해를 도와보자.

 

public class Table {

    ...
    
    public String createView() {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) { // 중첩 indent 발생
                buffer.append(data[i][j]);  // 중첩 index 발생
            }
            buffer.append("\n");
        }
        return buffer.toString();
    }
}

// 리팩토링 후 
public class Table {

    ...

    public String createView() {
        StringBuffer buffer = new StringBuffer();
        addRows(buffer);
        return buffer.toString();
    }
    
    private void addRows(StringBuffer buffer) {
        for (int row = 0; row < 10; row++) {
            addColumns(buffer, row);
        }
    }
    
    private void addColumns(StringBuffer buffer, int row) {
        for (int column = 0; column < 10; column++) {
            buffer.append(data[row][column]);
        }
        buffer.append("\n");
    }
}

 

 

- 들여쓰기: 4개의 빈 칸(space)를 들여 쓰기 단위로 사용한다. 탭은 8개의 빈 칸으로 설정하는 것이 좋다.

- 한 줄의 길이: 한 줄에 80자 이상 쓰는 것은 대부분의 터미널과 툴에서 다룰 수 없기 때문에 피해야 한다. 

- 줄 나누기: 하나의 식이 한 줄에 들어가지 않을 때는, 다음의 일반적인 원칙들을 따라서 두 줄로 나눈다.

  •  콤마 후에 두 줄로 나눈다.
  • 연산자 앞에서 두 줄로 나눈다.
  • 레벨이 낮은 원칙보다는 레벨이 높은 원칙에 따라 두 줄로 나눈다.
  • 앞줄과 같은 레벨의 식이 시작되는 새로운 줄은 앞줄과 들여쓰기를 일치시킨다.
  • 위의 원칙들이 코드를 더 복잡하게 하거나 오른쪽 끝을 넘어간다면, 8개의 빈 칸을 대신 사용해 들여쓴다.

 

 

 


3. else 예약어를 쓰지 않았는가?

- else 예약어(keyword)를 쓰지 않도록 한다.

- if/else, switchcase와 같은 분기 구문은 안 좋은 코드를 양산하기 쉽다. "else 예약어 금지" 규칙은 단순히 if/else만 사용하지 않는 것이 아니라 switch/case 구문을 포함한 분기 구문을 사용하지 않게 만들어 코드를 복잡하지 않고 간단명료하게 만들어준다.

- 아래와 같이 간단한 분기문이라면, 조기 반환(early return)이나 보호절(guarnd clause)를 이용해 간단하게 else를 사용하지 않는 방법도 있지만, 조기 반환문을 너무 많이 사용하는 경우 간결함을 저해할 수도 있다.

 

public static String print() {
    if (status == PREPARED) {
        doSomething();
    } else {
        // other code
    }
}


// 리팩토링 후
public static String print() {
    if (status == PREPARED) {
        doSomething();
    }

    // other code
}


4. 모든 원시값과 문자열을 포장(Wrap)했는가?

"int 값 하나 자체는 그냥 아무 의미 없는 스칼라 값일 뿐이다"

- 일반적으로 프로그래밍에서 쓰이는 원시값들은 값(리터럴)의 정의만 가질 뿐 별 다른 의미를 지니지 못하며, 값이 값 이상의 의미를 지니도록 하기 위해서 원시적인 값을 포장(Wrap)을 통해 이름을 가질 수 있도록 하자.

 

- int 타입의 원시값을 단순히 number가 숫자라는 것 이상의, 명확한 의미를 전달할 수 있다. 클래스를 사용해 원시값을 감싸서 다음과 같이 표현하여 리팩토링할 수 있다.

int number = 1;

// 리팩토링
public class LottoNumber {
  int value;
}

 


5. 콜렉션에 대해 일급 콜렉션을 적용했는가?

- 일급 콜렉션 (First Class Collection) 을 사용하라.

- 콜렉션을 가진 클래스는 콜렉션 외에는 다른 멤버 변수를 가져서는 안 된다. 어떤 데이터 Set을 가지고 있는데, 조작이 필요하다면 그 데이터에만 집중된 클래스를 만들어야 한다.

- 모든 원시값을 포장하라는 것과 비슷하게, 콜렉션을 클래스로 포장하라는 의미이다. 이를 통해 콜렉션과 관련된 코드의 중복을 막을 수 있고, 강제로 데이터를 캡슐화한다는 점에서 더 객체지향적인 코드를 작성할 수 있다.

 

- RacingGame 클래스 내부에 Car 클래스의 List 콜렉션의 객체를 선언하는 코드를 리팩토링 하여 List<Car>을 Cars 클래스로 감싸는 예제이다.

이를 통해 데이터를 감춤과 동시에 중요한 데이터를 전담으로 관리하는 클래스를 따로 만들어 인터페이스를 단순화할 수 있다. 

public class RacingGame {
  private final List<Car> cars;
  //...
    
  public RacingGame(String carInputs) {
    cars = initCars(carInputs.trim());
  }
  // ...
}

// 리팩토링
public class RacingGame {
  private final Cars cars;
  // ...
    public RacingGame(String carInputs) {
      cars = initCars(carInputs);
    }
  // ...
}

Public class Cars {
  private final List<Car> cars;
  
  public Cars(List<Car> cars) {
    this.cars = cars;
  }
  // ...
}

 


6. 3개 이상의 인스턴스 변수를 가진 클래스를 구현하지 않았는가?

- 가능하면 인스턴스 변수의 수를 줄이기 위해 노력한다. 

- 새로운 인스턴스 변수를 기존 클래스에 하나 더 추가하게 되면, 그 클래스의 응집도는 즉시 떨어진다. 

 

- 아래의 예시처럼, Customer Class가 First/Last Name, CustomerId를 모두 가지는 대신, 문자열과 정수값을 포함한 하위 클래스들의 계층 구조를 가지게 됨으로써 오직 Name과 CustomerId, 두 클래스만 관리하도록 하고 있다.

 

 


7. getter/setter 없이 구현했는가?

- 핵심 로직을 구현하는 도매인 객체에 getter/setter을 쓰지 않고 구현했는가? (단 DTO는 허용)

"말은 하되, 묻지는 말라. (Tell, don't ask)" 

- 객체의 상태를 가져오는 접근자(accessor) 을 사용하는 것은 괜찮지만, 객체 바깥에서 그 결과값을 이용해 객체에 대한 결정을 내려서는 안 된다. 한 객체의 상태에 대한 결정은 어떤 것이든, 그 객체 안에서만 이루어져야 한다. 

 

- 아래의 첫번째 예시에서는, getsCore() 메서드가 어떠한 결정을 내리는 용도로 사용되고 있다. setScore과 getScore을 같이 사용함으로써 game 객체의 score을 설정하여, Game 인스턴스에 책임을 부여하게 된 것이다.

- getter/setter 를 제거하여 리팩토링을 한 결과는, addScore을 통해 game에 score을 수정하라고 말하고 있다. (클래스를 무엇을 할지 말해야지, 물어봐서는 안 된다는 것!)

// Game
private int score;

public void setScore(int score) {
    this.score = score;
}

public int getScore() {
    return score;
}
game.setScore(game.getScore() + ENEMY_DESTROYED_SCORE);


// 리팩토링

// Game
public void addScore(int delta) {
    score += delta;
}

game.addScore(ENEMY_DESTROYED_SCORE);

 


8. 메소드의 인자 수를 제한했는가?

- 4개 이상의 인자는 허용하지 않는다.
- 3개도 가능하면 줄이기 위해 노력해 본다.



9. 코드 한 줄에 점(.)을 하나만 허용했는가?

- 디미터(DemeteR)의 법칙("친구하고만 대화하라")를 지켰는가? (Don;t talk to stranger -> 낯선 사람과 대화하지마라, 친구 하고만 말을 하라 라는 의미)

- 코드 한 라인에, "." 하나를 사용하는 규칙을 적용하여 코드의 가독성을 높여보자. 즉, A->B->C의 형태로 A가 B를 통해 C를 부르는 형태, 강한 결합도(Strong Coupling)를 가지는 코드를 리팩토링하자.

- 예를 들어 location.current.representation.substring(0,1)과 같이 여러 개의 점이 등장하면 리팩토링 해보도록

 

public class Board {
    ...

    Post post;
}

public class Post {
    ...

    String message;
}

public class BoardViewer {
    ...

    String boardPrintPreviewAll() {
        StringBuffer stringBuffer = new StringBuffer();

        for (Board board : boards) {
           stringBuffer.append(board.post.message);
        }

        return stringBuffer.toString();
    }
}

- 위의 예제의 경우, BoardViewer에서 board의 post와 message를 추가하는 경우 stringBuffer.append(board.post.message)의 코드로 작성이 되어있다. 이는 Board를 통해 Post에 접근하고, message를 가져와 사용하는 경우이다. 즉 message를 출력하기 위해 Post가 message를 가지고 있다는 사실을 알아야 하며, 직접 Post의 Message를 가져와야만 사용 가능한 경우이다.

 

- 아래와 같이 리팩토링 할 수 있다. BoardViewer는 Board만 알고 해야 할 일을 말하면 될 뿐, 이전처럼 Post를 직접적으로 가져오지 않게 된다.

 

public class Board {
    ...

    Post post;

    void addTo(StringBuffer stringBuffer) {
        post.addTo(stringBuffer);
    }
}

public class Post {
    ...

    public String message;

    void addTo(StringBuffer stringBuffer) {
        stringBuffer.append(message);
    }
}

public class BoardViewer {
    ...

    String boardPrintPreviewAll() {
        StringBuffer stringBuffer = new StringBuffer();

        for (Board board : boards) {
           board.addTo(stringBuffer);
        }

        return stringBuffer.toString();
    }
}

 

- 즉, 자신이 알고 있는 객체하고만 이야기하고, 제 3자인 내부의 객체와는 소통하지 않음으로써 결합도를 약하게 만들어 종속성을 최소화시키는 것을 말한다.

 

 


10. 메소드가 한 가지 일만 담당하도록 구현했는가?

 


11. 클래스를 작게 유지하기 위해 노력했는가?

 

 


주의할 점들

 

주석

- javadoc 툴을 사용하면 문서화 주석을 포함하는 HTML 파일을 자동으로 만들 수 있다. 소스 코드가 없는 개발자들도 읽고 이해할 수 있도록, 실제 구현된 코드와는 상관이 없는 코드의 명세 사항(specification)을 포함한다.

- 주석은 코드에 대한 개요와 코드 자체만 가지고는 알 수 없는, 중요하거나 코드만으로는 명확하지 않은 프로그램 설계에 대한 추가적인 설명을 포함하는 것은 적절하지만, 코드상에 이미 표현되어 있는 중복 정보는 피해야 한다. 프로그램을 읽는 것과 이해하는 것에 관계된 정보만을 포함해야 한다. (때로는 코드에 대한 주석이 많이 필요하다는 것은 코드의 품질이 좋지 않다는 것을 의미하며, 주석을 추가해야 한다고 느낄 때 코드를 좀 더 명확하게 다시 작성하는 것을 고려해 보는 것이 좋다.)

 

선언

- 한 줄에 하나의 선언문을 쓰도록 하자. 같은 줄에 서로 다른 타입을 선언해서는 안 된다. (int 변수, 배열)

- 초기화 : 지역 변수의 경우 선언 시 초기화하는 것이 좋다. 다른 계산에 의해 결정되는 경우라면, 초기화하지 않아도 괜찮다.

- 배치 : 선언은 블록의 시작에 위치해야 한다. 변수를 처음 사용할 때까지 변수의 선언을 미루지 말자. ( { 블록의 시작)  

 

return 문

- 값을 반환하는 return문은 특별한 방법으로 더 확실한 return 값을 표현하는 경우를 제외하고는 괄호를 사용핮하지 않는 것이 좋다.

return;
  
return myDisk.size();
  
return (size ? size : defaultSize);

 

if, if-else, if else-if else문

- if문은 항상 중괄호를 사용한다.

if (condition) {
    statements;
}
  
if (condition) {
    statements;
} else {
    statements;
}
  
if (condition) {
    statements;
} else if (condition) {
    statements;
} else {
    statements;
}

 

try-catch문

- try 블록이 성공적으로 완료되든지, 아니면 중간에 발생하든지에 상관없이 실행되어야 하는 부분을 추가하기 위해 finally 부분을 사용할 수 있다.

try {
    statements;
} catch (ExceptionClass e) {
    statements;
} finally {
    statements;
}

 

공백

- 괄호와 함께 나타나는 키워드는 공백으로 나누어야 한다.

- 메서드 이름과 메서드를 여는 괄호 사이에는 공백이 사용되어는 안 된다. 공백은 인자 리스트에서 콤마 이후에 나타나야 한다.

while (true) {
    ...
}

- 모든 이항 연산자는 연산수들과는 공백으로 분리되어져야 한다.

a += c + d;
a = (a + b) / (c * d);
 
while (d++ = s++) {
    n++;
}
printSize("size is " + foo + "\n");

- 변수의 타입을 변환하는 캐스트의 경우 공백으로 구분해야 한다.

myMethod((byte) aNum, (Object) x);
myMethod((int) (cp + 5), ((int) (i + 3)) + 1);

 

명명 규칙

- 메소드: 메소드의 이름은 동사여야 하며, 복합 단어일 경우 첫 단어는 소문자로 그 이후의 단어의 첫 문자는 대문자를 사용하는 (camelCase) 를 사용해야 한다. (run(), runFast(), getBackground())

- 변수

  • 변수 이름이 언더바_나 달러 표시 문자로 가급적 시작되지 않도록 하자.
  • 이름은 짧지만 의미 있어야 한다. 이름은 그 변수의 사용 의도를 알아낼 수 있도록 의미적이어야 한다.
  • 한 문자로만 이루어진 변수 이름은 암시적으로만 사용하고 버릴 변수일 경우를 제외하고는 피해야 한다.
  • 임시 변수의 이름은 integer인 경우 i,j,k,m,n을 사용하고 character인 경우 c,d,e를 사용한다.

상수: 모든 문자를 대문자로 쓰고 각각의 단어는 언더바로 분리해야 한다. (static final int MIN_WIDTH = 4;)

 

 

좋은 프로그래밍 습관

1. 인스턴스 변수와 클래스 변수를 외부에 노출하지 말고, 접근을 제공하자

- 인스턴스 변수, 클래스 변수를 합당한 이유 없이 public으로 선언하지 말자.

- 인스턴스 변수가 public으로 선언되는 것이 적절한 경우는, 클래스 자체가 어떠한 동작(behavior)을 가지지 않는 데이터 구조일 경우이다. 

 

2. 클래스 변수와 클레스 메서드는 클래스 이름을 사용하여 호출하자.

- 클래스(static) 변수 또는 클래스 메서드를 호출하기 위해 객체를 사용하는 것은 피하자. 클래스 이름을 사용하라.

 

3. 숫자는 바로 사용하지 않고 선언하여 변수 이름으로 접근하자

- 숫자 상수는 카운트 값으로 for 루프에 나타나는 -1, 0, 1을 제외하고는 숫자 자체를 코드에 사용하지 말자.

 

4. 변수에 값을 할당 할때 주의하자.

- 하나의 문에서 같은 값을 여러 개의 변수들에 할당하지 말자 (가독성 저하)

fooBar.fChar = barFoo.lchar = 'c'; // 이렇게 사용하지 말자

- 비교 연산자(==)와 혼동되기 시운 곳에 할당 연산자(=)를 사용하지 말자

// 이렇게 사용하지 말자 (자바가 허용하지 않음)
if (c++ = d++) {
    ...
}

// 다음과 같이 써야 한다
if ((c++ = d++) != 0) {
    ...
}

- 실행 시 성능 향상을 위해 할당문 안에 또다른 할당문을 사용하지 말자.

// 이렇게 사용하지 말자
d = (a = b + c) + r;

// 다음과 같이 써야 한다
a = b + c;        
d = a + r;

- 괄호: 연산자 우선순위 문제를 피하기 위해서 복합 연산자를 포함하는 경우 괄호를 자유롭게 사용하자.

if (a == b && c == d)     // 이렇게 사용하지 말자
if ((a == b) && (c == d)) // 이렇게 사용하자

- 반환값 : 프로그램의 구조와 목적이 일치해야 한다.

// 이렇게 사용하지 말자
if (booleanExpression) {
    return true;
} else {
    return false;
}

// 다음과 같이 써야 한다
return booleanExpression;


// 이렇게 사용하지 말자
if (condition) {
    return x;
}
return y;

// 다음과 같이 써야 한다
return (condition ? x : y);

 

 

 

 


 

[참고자료]

https://7942yongdae.tistory.com/8

https://myeonguni.tistory.com/1596

https://devwooks.tistory.com/59

 

 

 

728x90
반응형
Comments