Read Book/CleanCode

CleanCode 5장 형식 맞추기

I move forward every day. 2022. 9. 24. 11:18

출처: 인사이트

형식 맞추기


  • 형식을 맞추는 목적
  • 적절한 행 길이를 유지하라
  • 가로 형식 맞추기
  • 팀 규칙

누군가 내 코드를 봤을 때 전문가라는 인상을 심어줄 수 있다면 얼마나 좋을까. 더 나은 코드를 작성하기 위해, 다른 프로그래머가 봤을 때 더 나은 가독성을 위해서는 끊임없는 고민과 노력이 필요한 것 같다. 회사에서 일한다면 회사에서 정한 규칙에 따라서, 개인이라면 개인이 정한 규칙에 따라서 일관성 있는 코드를 작성해야 한다. 그렇다면 일관성 있는 규칙을 세우기 위해서 무엇을 중요시해야 하는지 알아보자.

 


형식을 맞추는 목적


우리는 한가지를 분명히 짚고 넘어가야 한다. 우리 프로그래머들에게 코드란 의사소통의 일환이다. 그렇기에 코드 형식은 아주 중요하다는 것이다. 코드 형식을 맞추는 것이 개발자들의 일차적인 의무이다. 어쩌면 기능이 동작하는 코드가 개발자의 일차적인 업무라 생각할지도 모르겠다. 그러나 제4장 형식 맞추기에서는 그 생각이 바뀌게 되는 계기를 제공한다.

 

예를 들어 우리가 IOT통합 관제, 제어 시스템 1차 버전을 만들었다 생각해보자. 다음 버전에서는 클라이언트의 요구사항들과 개선점을 반영하여 기능이 바뀌고 수정될 가능성이 아주 높다. 보통 우리는 다음 버전을 개발할 때 이전 버전에서 구현한 코드를 참고하여 다음 버전을 개발할 것이다. 오랜 시간이 지나 초기 버전에서 만든 코드의 흔적을 더 이상 찾아보기 힘들지라도 바뀌지 않는 것이 하나 있다. 바로 코드 형식이다. 개발자의 스타일과 규율은 사라지지 않는다. 그렇기에 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 앞으로 바뀔 코드의 품질에 계속해서 큰 영향을 끼칠 것이다. 그렇다면 우리는 원활한 소통을 하기 위한 코드 형식을 알아야 한다.

 


적절한 행 길이를 유지하라


코드의 세로 길이는 어느정도가 적당할까? 책에서는 자바 소스파일로 이루어진 유명 프로젝트 7개를 조사하였다. 수천 줄이 넘어가는 파일도 있지만 대부분의 프로젝트들은 파일당 500줄을 넘어가지 않으며 대다수가 200줄 미만의 코드로 작성되어있다. 우리가 이 조사 결과에서 알 수 있는 것은 대부분 200줄 미만인 파일로도 커다란 시스템을 구축할 수 있다는 것이다. 이미 다들 알고 있겠지만 일반적으로 큰 파일보다는 작은 파일이 이해하기가 쉽다. 반드시 지켜야 할 규칙은 아니지만 그렇게 하길 권장한다.

 

 

신문 기사처럼 작성하라

우리는 신문을 읽을때 위에서 아래로 읽는다. 내용을 몇 마디로 요약한 표제, 흥미를 끌 수 있는 첫 문단, 세세한 내용들이 고차원적인 순서부터 저차원 적인 순서로 작성된다. 소스 파일도 마찬가지다. 이름은 간단하면서도 설명이 가능하게 짓고, 그 아래로는 고차원적인 내용부터 저차원적인 순서로 세세한 사항들을 작성하는 것이 좋다.

 

 

개념은 빈 행으로 분리하라

우리가 보통 코드를 읽을때에는 위에서 아래로, 왼쪽에서 오른쪽으로 읽는다. 또한 일련의 행 묶음은 완결된 생각 하나를 표현한다. 그렇기에 생각 사이에는 빈 행을 넣어 분리해야 한다. 아래 코드를 살펴보자.

 

빈 행이 있는 코드

package com.fc_study.monsterGrowth.service;

import java.util.stream.Collectors;

import static com.fc_study.monsterGrowth.exception.MMakerErrorCode.NO_MONSTER;

@Service
public class MMakerService {

    private final MonsterRepository monsterRepository;
    private final DeadMonsterRepository deadMonsterRepository;

    public List<DetailMonsterDto> getAllDetailMonster() {
        return monsterRepository.findAll()
                .stream().map(DetailMonsterDto::fromEntity)
                .collect(Collectors.toList());
    }

    public CreateMonsterDto.Response createMonster(CreateMonsterDto.Request request) {
        validateCreateMonsterRequest(request);

        return CreateMonsterDto.Response.fromEntity(
                monsterRepository.save(createMonsterFromRequest(request)));
    }

    ......... 코드들
}

 

빈 행이 없는 코드

package com.fc_study.monsterGrowth.service;
import java.util.stream.Collectors;
import static com.fc_study.monsterGrowth.exception.MMakerErrorCode.NO_MONSTER;
@Service
public class MMakerService {
    private final MonsterRepository monsterRepository;
    private final DeadMonsterRepository deadMonsterRepository;
    public List<DetailMonsterDto> getAllDetailMonster() {
        return monsterRepository.findAll()
                .stream().map(DetailMonsterDto::fromEntity)
                .collect(Collectors.toList());
    }
    public CreateMonsterDto.Response createMonster(CreateMonsterDto.Request request) {
        validateCreateMonsterRequest(request);
        return CreateMonsterDto.Response.fromEntity(
                monsterRepository.save(createMonsterFromRequest(request)));
    }
    ......... 코드들
}

첫 번째 코드는 패키지의 선언부, import문, 각 함수들의 사이에 빈 행이 들어간다. 두 번째 코드는 빈 행들을 모두 제거했다. 괄호 덕에 조금 환기가 되는 것 같지만 첫 번째에 비해 가독성이 크게 떨어진다. 두 코드가 다른 것은 줄 바꿈 뿐이다. 별것 아닌 규칙이지만 이 빈행이 빠진다면 코드의 가독성이 현저하게 떨어지고 암호문처럼 보이게된다.

 

 

세로 밀집도

줄 바꿈이 개념을 분리해준다면 세로 밀집도는 연관성을 의미한다. 서로 밀접한 코드는 세로로 가까이 놓아야 한다. package는 package끼리, import문은 import문끼리,  변수는 변수끼리 모아두어야 코드가 '한눈'에 들어오는 효과를 볼 수 있다.

 

 

수직 거리

함수 연관 관계들이 서로 흩어져 있다면 연관 관계와 동작 방식을 이해하기 위해서 소스파일들을 뒤지고 다녀야 할 것이다. 또한 연관된 함수들의 조각들이 어디 있는지 기억하기도 만만치 않다. 그렇기에 서로 밀접한 개념들은 세로로 가까이 둬야 한다. 한 파일에 속하는 것이 좋다.

 

1. 변수 선언

변수는 사용하는 위치에 최대한 가까이 선언하자. 우리가 앞으로 설계해야 할 함수는 매우 짧아야 한다. 분명 다들 긴 함수를 본 적이 있고 만들어본 적이 있을 것이라 생각한다. 다음 코드를 통해 살펴보자.

private static String itemProcessing(){
    String resultItem = "";
    String item = "";
    int itemNum = 5;
    
    for (){
    	if(){
        .......
        ....
        }
    }
    .......
    try{
    	.....
    	.........
    }catch{
    	...
    	......
    }
    // resultItem이 선언되어야할 이상적인 위치
    for (){
    	if(){
           resultItem = ....;
        }
    }
    return resultItem;
}

resultItem은 가장 마지막에서 사용된다. 그러나 선언은 가장 맨 위에 되어있다. 다른 독자가 resultItem이 무슨 타입인지, defalut값은 무엇이었는지 알기 위해서는 코드를 읽다가 변수의 선언부를 찾아가야 한다. 주석으로 표기해 둔 곳처럼 최대한 사용하는 곳에 가까이 위치하도록 해주자.

 

2. 인스턴스 변수

방금 변수 선언에서는 변수는 사용하는 위치에 최대한 가까이 두자고 하였다. 반면 인스턴스 변수는 클래스의 가장 처음에 선언한다. 변수 간의 세로 거리 또한 두지 않는다.

public class Example {
    private String fruit;
    private String basket;
    private String ......;
    private String ....;
    
    private String getFruit(){
    	.....
    }
}

자바에서는 클래스의 선언부에 인스턴스 변수들을 모아 두고, C++에서는 가장 아래에다가 모아둔다고 한다. 어디에 모아두는가는 중요치 않다. 다만 변수 선언을 어디서 찾아야 할지 모두 알고 있어야 한다는 것이다.

 

3. 종속 함수

public class Example{

    public ResponseData exampleResponse(Request request){
        ResponseData response = new ResponseData();
        String examplePath = getExamplePath(request.exmaplePath);
        if(examplePath == null){
            response.setExamplePath(makeExmaplePath(request.menuId));     
        }else{
            response.setExamExamplePath(examplePath) 
        }
        return response;
    }
    
    private String getExamplePath(String path){
    	..................
        return examplePath;
    }
    
    private String makeExamplePath(String menuId){
    	.....................
        return examplePath;
    }

}

한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치하는 것이 좋다. 호출하는 함수가 가장 위에 위치하고 그 아래로 호출되는 함수들이 배치되야한다. 코드를 읽는 사람은 당연히 해당 함수 후에 해당 함수가 호출하는 함수가 정의되어있으리라 생각한다. 해당 규칙을 지킨다면 전체적인 모듈 가독성도 높아진다.

 

4. 개념적 유사성

개념적인 유사성 즉, 개념적인 친화도가 높을수록 코드를 가까이 배치하자. 한 함수가 다른 함수를 호출해 생기는 종속성이 한 예다. 변수와 그 변수를 사용하는 함수도 마찬가지다. 이외에도 개념적 유사성이 있는 예가 있는데 비슷한 동작을 수행하는 일군의 함수가 좋은 예다.

@RestControllerAdvice(annotations = RestController.class)
public class APIExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
        return handleExceptionInternal(e, ErrorCode.VALIDATION_ERROR, request);
    }

    @ExceptionHandler
    public ResponseEntity<Object> general(GeneralException e, WebRequest request) {
        return handleExceptionInternal(e, e.getErrorCode(), request);
    }

    @ExceptionHandler
    public ResponseEntity<Object> exception(Exception e, WebRequest request) {
        return handleExceptionInternal(e, ErrorCode.INTERNAL_ERROR, request);
    }

    .......................................
}

위 함수들은 개념적인 친화도가 매우 높다. 기본 기능이 유사하고 간단하다. 종속적인 관계가 없더라도 가까이 배치할 함수들이다.

 

5. 세로 순서

호출되는 함수는 호출하는 함수 아래에 배치한다. 그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다. 이건 위에서도 알게 모르게 계속 강조되었던 내용이다. 

 


 

가로 형식 맞추기


가로로 한 행은 얼마나 길어야 할까? 책에서 제공하는 통계에 따르면 대부분이 60자 미만으로 유지된다고 한다. 대부분의 프로그래머들은 긴 행보다 짧은 행을 좋아한다. 그렇다면 우리는 짧은 행을 만들기를 노력해야 한다. 사실 글쓴이는 어느 정도 기준을 정해뒀다. 아래 이미지를 살펴보자!

가로 행.img
내가 생각하는 적당한 가로행의 길이

개발할 때에 한쪽에 참고할 코드를 켜 두고 반대쪽에 코드를 작성할 때가 있다. 이때에 한쪽의 코드가 충분히 보일만한 크기가 적당한 가로 행의 길이라고 생각한다. 화면의 크기와 글씨 크기에 따라 좀 더 크게 볼 수 있겠지만 일정 기준을 팀원과 함께 정해놓고 사용한다면 될 것이라고 생각한다.

 

가로 공백과 밀집도

가로 공백으로는 밀접한 개념과 느슨한 개념을 표현할 수 있다.

private void Example(ExamplePage example){
        int exampleTotalCount = example.getTotalCount();
        exampleTotalCount += testNumber;
        exampleTestPage(exampleTotalCount, example.getPageNum());
    }

할당 연산자를 강조하기 위해 앞뒤로 공백을 넣어줬다. 할당 문은 왼쪽과 오른쪽에 요소가 분명히 나뉘는데 공백을 넣어줄 시 나뉜다는 사실을 더욱 강조해줄 수 있다. 반면, 함수는 이름과 이어지는 괄호 사이에 공백을 넣지 않았다. 서로 밀접한 개념임을 알려주기 위해서이다. 다만 인수는 쉼표 뒤에 공백을 사용하여 별개라는 사실을 강조해준다.

 

여기서 재미있는 것은 연산자들에 우선순위를 공백으로 표현해 줄 수도 있다는 것이다. 아래 코드를 살펴보자

int num = a*b - c*b*7;

사칙연산의 순위는 곱셈 == 나눗셈 이후 덧 샘 == 뺄셈이다. 공백을 이용해 사칙연산의 우선순위를 표현해주었다.

 

 

가로 정렬

가로 정렬은 변수들의 가로 행 거리를 일정간격으로 맞추는것이다. 글보다는 코드로 살펴보자.

private int      age;
private String   name;
private String   phoneNumber;
private char     gender;
private String[] groups;

가로정렬은 언뜻 보면 유용한 것 같지만 코드가 엉뚱한 부분을 강조하는 문제가 있다. 변수의 유형보다는 변수명이 먼저 눈에 뜨인다. 결함이 있을 시 엉뚱한 곳이 강조되어 문제를 바로 발견하지 못할 수 있다. 아래와 같은 코드가 더 보기도 좋다. 익숙하지 않아서 그런지는 몰라도 위와 같은 코드는 어색하다.

private int age;
private String name;
private String phoneNumber;
private char gender;
private String[] groups;

 

들여 쓰기

들여 쓰기는 정보의 적용범위를 나타낸다. 파일 전체에 적용되는 정보, 파일 내 개별 클래스에 적용되는 정보, 클래스 내 각 메서드에 적용되는 정보, 블록 내 블록에 재귀적으로 적용되는 정보들이 있다. 우리는 코드를 작성할 때에 범위로 이루어진 계층을 표현하기 위해 코드를 들여 쓴다. 만일 들여 쓰기가 없다면 코드를 읽지 못할 것이다. 코드 난독 화도 코드를 일자로 변경하는 것뿐인데 읽기가 싫어진다.

 

들여 쓰기를 무시한 코드

@ExceptionHandler
public ResponseEntity<Object> general(GeneralException e, WebRequest request) {return handleExceptionInternal(e, e.getErrorCode(), request);}
@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {return handleExceptionInternal(e, ErrorCode.INTERNAL_ERROR, request);}
@Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
    return handleExceptionInternal(ex, ErrorCode.valueOf(status), headers, status, request);}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorCode errorCode, WebRequest request) {return handleExceptionInternal(e, errorCode, HttpHeaders.EMPTY, errorCode.getHttpStatus(), request);}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorCode errorCode, HttpHeaders headers, HttpStatus status, WebRequest request) {return super.handleExceptionInternal(
            e, APIErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(e)), headers, status, request);}}

들여쓰기 한 코드

 @ExceptionHandler
    public ResponseEntity<Object> general(
    GeneralException e, WebRequest request
    ) {
        return handleExceptionInternal(e, e.getErrorCode(), request);
    }


    @ExceptionHandler
    public ResponseEntity<Object> exception(
    Exception e, WebRequest request
    ) {
        return handleExceptionInternal(e, ErrorCode.INTERNAL_ERROR, request);
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
    Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request
    ) {
        return handleExceptionInternal(
        ex, ErrorCode.valueOf(status), headers, status, request
        );
    }

    private ResponseEntity<Object> handleExceptionInternal(
    Exception e, ErrorCode errorCode, WebRequest request
    ) {
        return handleExceptionInternal(
        e, errorCode, HttpHeaders.EMPTY, errorCode.getHttpStatus(), request
        );
    }

    private ResponseEntity<Object> handleExceptionInternal(
    Exception e, ErrorCode errorCode, HttpHeaders headers, HttpStatus status, WebRequest request
    ) {
        return super.handleExceptionInternal(
                e,
                APIErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(e)),
                headers,
                status,
                request
        );
    }

위, 아래 두 개의 코드다 같은 동일한 코드이다. 차이는 들여 쓰기의 유무이다.

 

가끔씩 if문 또는 while문에 들여 쓰기를 무시하고싶은 때가 있다. 한줄로 처리할 수 있기 때문이다. 그러나 이미 우리는 들여쓰기를 규칙으로 사용하고있기에 들여쓰기를 통해 해당 함수의 범위를 확실하게 표현해주자. 글쓴이만 그런지는 모르겠지만 가끔 들여 쓰기가 무시된 if, while문을 보면 눈에 잘 안 들어와 별생각 없이 지나치게 될 때가 있다. 아래처럼 괄호가 없는 것도 아주 드물게 보인다.

if (isBooleanTestMethod()) test = true;
if (isBooleanTestMethod()){
	test = true;
}

 


 

팀 규칙


각자가 속한 팀마다 코드를 작성해 나가는 방법이 다를 것이다. 자신이 만약 팀에 속한다면 따라야 할 규칙은 팀 규칙이다. 모든 팀원들이 해당 팀의 규칙을 따라야 하고, 그래야만 소프트웨어가 일관적인 스타일을 보이게 된다. 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다. 스타일은 일관적이고 매끄러워야 한다. 일관적임으로 독자에게 신뢰감을 줄 수 있는 코드를 작성하자.

 


마무리


코드 스타일에 있어 팀 규칙이 가장 중요하다고 생각된다. 모두가 일관된 코드 스타일을 유지함으로써 코드를 좀 더 읽기 쉽게 해 준다. 상수나 변수 그리고 클래스 파일들이 당연히 있을 것이라고 여겨지는 위치에 항상 존재한다. 앞서 말했던 규칙들은 모두 좋은 형식을 맞출 수 있는 규칙들이지만 개인적으로는 팀 규칙이 가장 위에 있으리라고 생각된다. 로마에 가면 로마 법을 따르듯이 팀에 속한다면 팀의 규칙을 따르는 건 당연한 것이다. 팀의 규칙에 불만이 있을 수도 있다. 더 좋은 방법이 있을수도 있다. 그렇다면 팀원들과 적극적으로 소통해 자신의 의견을 나누고 의논해보자. 마무리가 길었지만 결론은 일관성 있고, 좋은 코드 스타일을 갖추도록 노력하자.