티스토리 뷰

나의 공부방

[클린코드] 좋은 코드는 어떤 코드일까? 좋은 코드에 대한 고찰

망나니개발자 2021. 5. 5. 17:29
반응형

1. 좋은 코드는 어떤 코드일까?


[ 1. 이야기하는 코드 ]

과거에 수능을 치르던 시절, 국어 영역 김동욱 선생님은 좋은 지문이라면 부족함이 있는 문장 뒤에 부연 설명이 있어야 한다는 얘기를 하셨습니다. 예를 들어 다음과 같은 문장을 우리가 읽었다고 합시다.

망나니 개발자는 오늘도 일을 하러 간다.

 

위의 문장은 상당히 함축적이기 때문에, 우리가 위의 문장을 읽었다면 당연히 다음과 같은 의문이 생길 것입니다.

어떤 일을 하러 가는데? 혹은 직업이 뭔데?

 

그리고 좋은 지문이라면 위의 문장에 이어 다음과 같은 이야기가 있을 것입니다.

망나니 개발자는 오늘도 일을 하러 간다. 그는 N사를 다니는 백엔드 개발자이다. 그는 Java와 Spring 프레임워크를 이용해 개발을 하고 있다.

 

저는 좋은 코드 역시 좋은 지문과 마찬가지라고 생각합니다. 좋은 코드는 독자에게 자연스럽게 이야기를 전달해야 하며, 독자는 이를 의문점 없이 받아들일 수 있어야 합니다. 예를 들어 다음과 같은 코드를 우리가 읽었다고 합시다.

public static void main(String[] args) {
    Initializer initializer = new Initializer();
    initializer.initialize();
        
    initializer.run();
}

위와 같은 코드를 보고 우리는 도대체 무엇을 초기화하는건지, 뭘 실행하는건지 아무것도 이해할 수 없을 것입니다. 왜냐하면 위의 코드는 우리에게 어떠한 것도 이야기해주지 않기 때문입니다.

 

위와 같은 코드를 조금만 수정하여도 우리는 대략적인 갈피를 잡을 수 있습니다.

public static void main(String[] args) {
    GameInitializer initializer = new GameInitializer();
    initializer.initialize();

    initializer.startGame();
}

 

위의 코드는 게임을 초기화하고, 실행하는 코드임을 말입니다.

 

이야기를 하는 또 다른 코드에 대한 예시로 다음과 같은 것이 있습니다.

public class GameInitializer {

    public void initialize() {
        initDate();
    }

    private void initDate() {
        LocalDateTime lastGameDate = getLastGameDate();
    }

    private void getLastGameDate() {
    }

    public void startGame() {

    }

}

 

예를 들어 initialize 함수 내부에서 날짜를 초기화하는 initDate 함수를 호출한다고 가정합시다. 이야기를 해주는 코드는 우리가 멀리갈 필요 없이 (바로 아래에서) initDate가 어떠한 함수인지 보여줄 것입니다.

저도 과거에 private 메소드는 아래에 모아두는 등 다양한 시도를 해보았지만, 결국 이러한 구조가 가장 좋은 선택지라고 생각하게 되었습니다.

우리가 작성한 코드는 우리만 보는것이 아니고, 같이 협업하는 개발자 또는 미래에 팀으로 들어올 개발자가 보게 될 것입니다. 그렇기에 우리는 우리에게 이야기를 해주는 코드를 작성해야 합니다. 이야기를 하는 코드를 작성하기 위한 방법에는 다음과 같은 것들이 있습니다.

  • 직관적인 변수 또는 클래스 이름 짓기
  • 사용자에게 직관적인 함수 이름 짓기
  • 코드가 길어지거나 직관적이지 못한 경우, 별도의 메소드로 분리하기
  • 기타 등등

 

[ 2. 추적하기 좋은 코드 ]

Spring에서 지원하는 핵심 프로그래밍 모델 중 하나로 AOP(Aspect Oriented Programming, 관점 지향 프로그래밍) 가 있습니다.

AOP는 애플리케이션에 공통적으로 나타나는 부가적인 기능들을 독립적으로 모듈화하는 프로그래밍 기법입니다. AOP를 이용하면 중복을 제거하여 깔끔한 코드를 유지할 수 있게 해줍니다.

예를 들어 우리가 모든 API에 대한 실행 시간을 측정해야 하는 상황이라고 가정합시다. 그러면 우리는 모든 코드에 다음과 같은 코드를 삽입해야 할 것입니다.

@GetMapping(value = "/list")
public ResponseEntity<UserListResponseDTO> findAll() {
    final StopWatch sw = new StopWatch(); 
    sw.start(); 
    
    // 실제 비지니스 로직
    final UserListResponseDTO userListResponseDTO = UserListResponseDTO.builder()
            .userList(userService.findAll()).build();

    sw.stop(); 
    final long executionTime = sw.getTotalTimeMillis();

    return ResponseEntity.ok(userListResponseDTO);
}

 

위와 같은 코드는 다음과 같은 문제점이 있습니다.

  • 중복되는 코드
  • 서로 다른 성격을 갖는 두 로직의 결합
  • 낮은 확장성 등등

그렇기 때문에 이러한 상황에서는 AOP를 사용하는 것이 적합할 것 입니다.

하지만 중복 로직을 제거하기 위해 항상 AOP를 적용하는 것이 올바르냐라고 하면 반드시 그렇지는 않을 것입니다. 그러한 대표적인 이유는 AOP를 이용하면 로직들이 추상화되기 때문입니다.

@GetMapping(value = "/list")
public ResponseEntity<UserListResponseDTO> findAll() {
    final UserListResponseDTO userListResponseDTO = UserListResponseDTO.builder()
            .userList(userService.findAll()).build();

    return ResponseEntity.ok(userListResponseDTO);
}

새로 들어오게된 개발자가 위와 같은 코드를 보고, 다른 코드가 추상화되어 있거나 실행 시간이 측정되고 있음을 파악하는 것은 거의 불가능에 가까울 것입니다.

즉, 로직들이 추상화되어 있다는 것은 우리가 해당 코드를 추적하기 어렵다는 것을 동반합니다. 회사의 개발자들은 결국 변화가 생기기 마련인데, 만약 우리가 많은 로직들을 추상화시켜두었다면 어떨까요? 그리고 추상화된 로직들이 핵심 비지니스 로직을 포함한다면 어떨까요? 해당 프로젝트는 상당히 유지보수하기 어려운 코드가 될 것이고, 요구사항에 변경이 생기면 대처가 상당히 어려울 것입니다. 그렇기 때문에 추적하기 어려운 코드는 좋지 못한 코드라는 생각이 듭니다.

결국 중복 로직의 제거와 추상화 사이의 Trade-Off를 잘 고려해야 하며, 이에 대한 대안으로 Composition 등을 고려하는 것도 좋은 선택지가 될 수 있을 것 입니다.

이와 유사한 성질의 클래스로 유틸성 클래스가 있습니다. 메소드를 static으로 선언하여 객체의 생성없이 손쉽게 이용할 수 있는 유틸성 클래스에는 치명적인 단점이 있습니다. 그것은 바로 우리가 해당 클래스의 존재를 추적하기가 어렵다는 것입니다. 다른 사람들에게 유틸성 클래스의 존재를 알리거나, 그 사람이 유틸성 클래스 코드를 살펴보지 않는 한 그 코드는 나만 아는 코드가 될 것입니다.

결국 좋은 코드는 변경이 쉽고 유지보수를 용이하게 하기 위함인데, 개발자가 해당 코드를 따라가기 어렵다면 문제가 있는 상황이라는 생각이 듭니다.

 

 

[ 3. 기본을 지키는 코드 ]

사람에게도 기본이 있듯이 코드에도 기본이 있을 것입니다. 그리고 기본을 잘 지키는 사람이 좋은 사람이듯 저는 이러한 기본을 잘 지키는 코드가 좋은 코드라고 생각합니다.

그렇다면 좋은 코드의 기본은 무엇인가? 라고 얘기하면 다음과 같은 것들이 떠오릅니다. (깊게 생각하지 않고, 짧은 시간 머릿속에 바로 떠오르는 것들을 적어 보았습니다.)

  • 직관적인 패키지 구조
  • 파라미터의 개수가 최소인 함수
  • 들여쓰기 뎁스가 최소인 함수
  • 변경 가능성이 최소화 된 변수와 객체(불변성이면 최고)
  • 기타 등등

우선 위와 같은 생각들을 가지게 된 이유에 대해 설명해보도록 하겠습니다.

 

1. 직관적인 패키지 구조

직관적인 패키지 구조는 해당 프로젝트를 파악하는데 상당히 중요하다고 생각합니다. 신발은 신발장에, 옷은 옷장에 있듯이 어떤 코드는 자기의 위치에 있어야 한다고 생각합니다. 물건이 있는 위치가 뒤죽박죽이면 우리가 찾기 어려운 것과 마찬가지로, 코드도 잘못된 위치에 있다면 파악이 어려울 것입니다. 반면에 원하는 코드가 적재적소의 위치에 있다면 불필요하게 코드를 찾는 시간을 줄이는 등의 효과를 누릴 수 있습니다.

 

2. 파라미터의 개수가 최소인 함수

파라미터의 개수가 일정 개수 이상으로 많아지면, 일일이 그 파라미터들을 파악하기 힘들어집니다. 즉, 코드를 읽기 어려워 진다는 것입니다. 그렇기 때문에, 이런 경우라면 DTO로 감싸서 코드의 가독성을 높이는 것이 좋다는 생각입니다.

 

3. 들여쓰기 뎁스가 최소인 함수

들여쓰기 뎁스는 하나의 함수에 if나 for while 등에 의해 띄워진 칸을 의미합니다. 하나의 함수에 if 문 안에 if문이 있다면 이것은 depth가 2임을 의미하는데, 이것은 일반적으로 1가지 역할을 하고있지 않을 가능성이 99%입니다. 그렇기 때문에 들여쓰기 뎁스가 최소일수록 1가지 역할에 충실할 가능성이 높으므로 위와 같은 생각이 들었습니다.

 

4. 변경 가능성이 최소화 된 변수와 객체

변경 가능성이 최소화된 변수나 객체는 그 자체로 의미가 큽니다. 물리적으로 불필요한 시간을 줄여줄 수 있고, 코드의 유지보수성도 높여줄 수 있습니다. 실제로 이펙티브 자바에서는 가변적이여야 하는 매우 타당한 이유가 있지 않는 한 반드시 불변으로 만들어야 한다고 설명하고 있습니다. 수 많은 Setter들로 불필요하게 변경가능성을 열어두는 것은 그다지 합리적이지 못하다고 생각합니다.

 

 

조금 더 생각해보면 더 많은 좋은 조건들이 있을 것이고, 또 사람마다 견해가 다를 수 있습니다. 누군가는 SOLID를 철저히 지키는 코드가 좋은 코드라고 얘기할 수도 있고, 테스트코드를 작성하기 좋은 코드가 좋은 코드라고 생각할 수 있는 등 의견은 다양하다고 생각합니다. 그렇기 때문에 본인 만의 기준 또는 관점으로 조건들을 세우고, 그 조건들을 만족하도록 코드를 작성하면 좋을 것 같습니다. 중요한 것은 타당한 이유를 갖는 본인의 견해라고 생각합니다.

(나중에 조금 더 좋은 코드에 대해 기술적인 측면으로 접근하면서 깊이있는 이야기를 하는 포스팅을 작성하면 좋을 것 같습니다. 적당한 때가 되면 작성해보도록 하겠습니다ㅎㅎ)

 

 

 

결국 위의 내용들을 종합하면 저는 읽기 쉽고 따라가기 좋은 코드가 좋은 코드라는 생각이 듭니다. 참 당연한 이야기를 한 것 같지만, 이러한 당연한 부분도 지키는 못하는 코드들이 너무 많이 있는 것 같습니다.

물론 2~3년 뒤에 제가 이 글을 보았을 때에는 추가적인 견해가 생겼거나, 위의 글에 반하는 견해가 생겼을 수도 있을 것입니다. 그 때에는 다시 한번 좋은 코드에 대해 정의를 해보고자 합니다.

추가로 개발을 하다가 정말 좋은 내용인데 빼먹은 내용이 있다면 꾸준히 업데이트 하도록 하겠습니다. 혹시 다른 분들께서도 자신만의 좋은 견해나 관점이 있다면 언제든 댓글 남겨주세요! 꼼꼼히 생각하고 고민해보겠습니다:)

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
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
글 보관함