Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

 

오래된 주석은 거짓 정보를 퍼트린다.

거짓된 주석은 없는 것보다 나쁘다.

 

주석은 순수하게 선하지 않다. 필요악이다.

코드 자체가 표현력이 풍부하다면, 필요하지 않다.

 

따라서 주석을 단다는 것은 자신의 표현력이 부족하는 것이다.

 

좋은 주석이라도 시간이 지나면서, 관리되지 않아 나쁜 주석이 된다. 현실적으로 주석은 관리 되지 않는다.

 

코드만이 거짓을 말하지 않는다. 최대한 주석이 없는 방향으로 개발해야 한다.

 

주석은 나쁜 코드를 보완하지 못한다

주석은 코드 품질이 나쁠 단다. 따라서 주석이 보이면, 코드를 정리해야 한다

 

코드로 의도를 표현하라!

  //어느것이 더 좋은지 비교
  void example() {
    //직원에게 복지 혜택을 받을 자격이 있는지 검사
    if((employee.flags & HOURLY_FLAG) && employee.age > 65) {
      /* ~~~~ */
    }
    if(employee.isEligibleForFullBenefits()) {
      /* ~~~~ */
    }

 

좋은 주석

필수 정보는 주석을 밖에 없다.

 

법적인 주석

코드 상단에 카피라이트에 해당된다.

 

정보를 제공하는 주석

코드로 표현이 도저히 힘든 정규식 같은게 좋은 예다.

    //kk:mm:ss EEE, MMM dd, yyyy 형식
    Pattern timeMatcher = Pattern.compile(
      "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

마저도 시간과 날짜를 변환해주는 클래스를 별도로 만들어 옮기면, 주석이 필요하지 않다.

 

의도를 설명하는 주석

  public int compareTo(Object o) {
    if(o instanceof WikiPagePath) {
      WikiPagePath p = (WikiPagePath) o;
      String compressedName = StringUtil.join(names, "");
      String compressedArgumentName = StringUtil.join(p.names, "");
      return compressedName.compareTo(compressedArgumentName);
    }
    return 1; //오른쪽 유형이므로 정렬 순위가 더 높다.
  }
  void testConcurrentAddWidgets() throws Exception {
    WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});
    String text = "'''bold text'''";
    ParentWiget parent = new BoldWidget(new MockWidgetRoot(), "'''bold test'''");
    AtomicBoolean failFalg = new AtomicBoolean();
    failFalg.set(false);
    //스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
    for (int i = 0; i < 25000; i++) {
      WidgetBuilderThread widgetBuilderThread =
          new WidgetBuilderThread(widgetBuilder, text, parent, failFalg);
      Thread thread = new Thread(widgetBuilderThread);
      thread.start();
    }
    assertEquals(false, failFalg.get());
  }

 

의미를 명료하게 밝히는 주석

  void testCompareTo() throws Exception {
    WikiPagePath a = PathParser.parse("PageA");
    WikiPagePath ab = PathParser.parse("PageA.PageB");
    WikiPagePath b = PathParser.parse("PageB");
    WikiPagePath aa = PathParser.parse("PageA.PageA");
    WikiPagePath bb = PathParser.parse("PageB.PageB");
    WikiPagePath ba = PathParser.parse("PageB.PageA");
    //assertTrue 가 라이브러리를 사용한 것이라고 가정
    assertTrue(a.compareTo(a) == 0); // a == a
    assertTrue(a.compareTo(b) != 0); // a != b
    assertTrue(a.compareTo(b) == -1);// a < b
  }

모호한 인수나 반환값을 의미를 좋게 표현하려고 바꾸고 싶지만,

라이브러리를 사용하는 경우 변경이 불가하다. 경우는 주석이 유용하다.

 

물론 주석을 완전히 신뢰할 없다는 문제가 남아있다.

처럼 주석은 다른 방법으로 표현할 방법이 없는 고민 달아야 한다.

 

결과를 경고하는 주석

특정 테스트 케이스가 수행 시간이 오래걸린다면, 이를 위한 경고는 괜찮다.

요즘은 @Ignore 어노테이션 같은 것으로 안에 사유를 적어 주석보다 명시적으로 사용한다.

 

또는 해당 코드가 스레드 세이프하지 않을 경고를 남기기도 한다

 

TODO 주석

요즘 IDE TODO 인식 해서 보여주는 기능이 있다.

 

보통은 현재 구현이 어려운 코드를 TODO 주석으로 남길 것이다.

따라서 이를 주기적으로 체크해 줄여가야 한다.

 

중요성을 강조하는 주석

얼핏 봤을 대수롭지 않게 넘길 만한 코드에 중요한 사실을 경고할

    //trim() 중요하다. 시작에 공백이 들어가 있으면, 다른 문자열로 인식된다.
    String listItemContent = match.group(3).trim();
    new ListItemWidget(this, listItemContent, this.level + 1);
    return buildList(text.substring(match.end()));

 

공개 API에서 Javadocs

Javadocs 만들기로 했으면, 정말 만들어야 한다.

만든 공개 API Javadocs 훌륭하다.

 

 

나쁜 주석

대부분의 주석이 해당된다.

 

주절거리는 주석

무언가를 설명하려고 주석을 생각이면, 최대한 의미있게 자세히 서술해야 한다.

설명이 미흡하면, 결국 주석의 의미는 희미해지고, 코드를 뒤지며 로직을 확인해야 한다.

 

같은 이야기를 중복하는 주석

변수나 함수 이름만으로도 충분한데, 의미 없이 중복된 내용을 주석으로 작성한 것을 말한다.

  /*
   * this.closed true 일 때 반환되는 메서드
   * 타임아웃에 도달하면 예외를 던진다.
   */
  synchronized void waitForClose(final long timeoutMillis) throws Exception {
    if(!closed) {
      wait(timeoutMillis);
      if(!closed) {
        throw new Exception("MockResponseSender Could not be closed");
      }
    }
  }
  /* 로그를 찍기 위한 로거 */
  Logger logger;

 

오해할 여지가 있는 주석

 

의무적으로 다는 주석

자바독 예시

  /**
   * @param title CD 제목
   * @param author CD 저자
   * @param tracks CD 트랙수
   * @param durationInMinutes CD 분 길이
   */
  public void addCd(String title, String author, int tracks, int durationInMinutes) {
  }

오히려 가독성만 저해한다.

 

이력을 기록하는 주석

Git 같은 형상관리 프로그램이 이력을 저장하고 있다.

이제 필요없다.

 

있으나 마나 주석

이런 주석을 자주 접하면, 개발자는 주석을 무시하게 된다.

그러면 정말 중요한 주석인 경우 읽게 된다.

 

위치는 표시하는 주석

// 여기 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

이런 주석이다. 시적으로 혼자만 가끔 사용할 있어도 커밋 시점엔 제거돼야 한다.

 

닫는 괄호에 다는 주석

예전에 IDE 없던 시절에는 함수가 길어지면, 찾기 힘들 었다

현재는 IDE 닫은 괄호를 눈에 띄게 표시해주며, 특히 여는 괄호에서 닫는 괄호로 이동 기능도 제공한다.

 

 

Vscode Ctrl + Shift + P 괄호 이동 단축키다.

 

공로를 돌리거나 저자를 표시하는 주석

형상 관리 프로그램이 관리한다.

 

주석으로 처리한 코드

언젠가 필요할 까봐 주석으로 남기는 코드가 있다.

역시 형상 관리 프로그램으로 과거 코드를 언제든지 접근할 있으므로 제거한다.

 

HTML주석

Javadocs 작성할 주석에 HTML 태그를 사용한다. 이렇게 되면 javadocs 만들지 않는 이상 주석만 보고 파악하기 힘들다.

 

현재 IDE 팝업 같은 형식으로 보기 편하게 지원해서 의미는 없는 듯하다.

 

전역 정보

주석을 단다면 주변 코드 정보만 달아야 한다. 전역 시스템 정보를 달면 안된다.

조금이라도 코드에 변경이 생기면, 전역 주석을 항상 최신화 있는가

 

 

너무 많은 정보

예를 들어 JWT 라이브러리를 사용하는데 라이브러리 주석에 JWT 동작방식이 있다고 생각하면 이해가 쉽다.

 

모호한 관계

코드와 이를 설명하는 주석의 관계가 명확하지 않는 경우를 말한다.

 

비공개 코드에서 Javadocs

공개 API에선 의미가 있지만, 비공개 코드에선 의미가 없다. 오히려 코드를 읽기 힘들게 한다.

 

예제

리팩터링

package chap04.ex07;
/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다.
 * 사용된 알고리즘은 에라스토테네스의 체다.
 *
 * 에라스토테네스 소개  [의미없는 주석]
 * ~~~~~~~~~~~~~~~~~~
 *
 * 알고리즘 설명
 * ~~~~~~~~~~~~~~~~~~
 *
 * @author 누군가
 * @version 2023.09.01
 */
public class GeneratePrimes {
  /** [의미없는 주석]
   * @param maxValue소수를 찾아낼 최대 값
   */
  //지나지게 거대한 메서드
  public static int[] generatePrimes(int maxValue) {
    if (maxValue > 2) { //유일하게 유효한 경우 [의미없는 주석]
      //선언
      int s = maxValue + 1;
      boolean[] f = new boolean[s];
      int i;
      //배열을 참으로 초기화 [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      for(i = 0; i < s; i++) {
        f[i] = true;
      }
      //소수가 아닌 알려진 숫자를 제거 [의미없는 주석]
      f[0] = f[1] = false;
      // [의미없는 주석] [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      int j;
      for (i = 2; i < Math.sqrt(s) + 1; i++) {
        if (f[i]) {
          for (j = 2 * i; j < s; j += i) {
            f[j] = false; //배수는 소수가 아니다.
          }
        }
      }
      //소수 개수는? [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      int count = 0;
      for (i = 0; i < s; i++) {
        if (f[i]) {
          count++; // 카운트 증가
        }
      }
     
      int[] primes = new int[count];
      // 소수를 결과 배열로 이동한다. [의미없는 주석] 메서드로 뺄 것(메서드 이름을 명시적으로)
      for (i = 0, j = 0; i < s; i++) {
        if (f[i]) {
          primes[j++] = i;
        }
      }
      return primes;
    } else { // maxValue < 2 [의미없는 주석]
      return new int[0]; //입력이 잘못되면 비어 있는 배열을 반환하다. [의미없는 주석]
    }
  }
}

 

리팩터링

package chap04.ex07;
/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 구한다.
 * 알고리즘은 에라스토테네스의 체다.
 * 2에서 시작하는 정수 배열을 대상으로 작업한다.
 * 처음으로 남아 있는 정수를 찾아 배수를 모두 제거한다.
 * 배열에 더 이상 배수가 없을 때까지 반복한다.
 */
public class PrimeGenerator {
  private static boolean[] crossedOut;
  private static int[] result;
  public static int[] generatePrimes(int maxValue) {
    if (maxValue < 2) {
      return new int[0];
    } else {
      uncrossIntegersUpTo(maxValue);
      crossOutMultiples();
      putUncrossedIntegersIntoResult();
      return result;
    }
  }


  private static void uncrossIntegersUpTo(int maxValue) {
    crossedOut = new boolean[maxValue + 1];
    for (int i = 2; i < crossedOut.length; i++) {
      crossedOut[i] = false;
    }
  }
  private static void crossOutMultiples() {
    int limit = determineIterationLimit();
    for (int i = 2; i <= limit; i++) {
      if (notCrossed(i)) {
        crossOutMultiplesOf(i);
      }
    }
  }
  private static int determineIterationLimit() {
    /*
     * 배열에 있는 모든 배수는 배열의 제곱근보다 작은 소수의 인수다.
     * 따라서 이 제곱근보다 더 큰 숫자의 배수는 제거할 필요가 없다.
     */
    double iterationLimit = Math.sqrt(crossedOut.length);
    return (int) iterationLimit;
  }
  private static void crossOutMultiplesOf(int i) {
    for (int multiple = 2 * i;
        multiple < crossedOut.length;
        multiple += i) {
      crossedOut[multiple] = true;
    }
  }
  private static boolean notCrossed(int i) {
    return crossedOut[i] == false;
  }
  private static void putUncrossedIntegersIntoResult() {
    result = new int[numberOfUncrossedIntegers()];
    for(int j = 0, i = 2; i < crossedOut.length; i++) {
      if (notCrossed(i)) {
        result[j++] = i;
      }
    }
  }
  private static int numberOfUncrossedIntegers() {
    int count = 0;
    for (int i =2; i < crossedOut.length; i++) {
      if (notCrossed(i)) {
        count++;
      }
    }
    return count;
  }
}

 

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

6장 객체와 자료 구조  (1) 2023.12.18
5장 형식 맞추기  (0) 2023.12.11
3장 함수  (1) 2023.11.27
2장 의미 있는 이름  (1) 2023.11.20
1장 깨끗한 코드  (0) 2023.11.13

 

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

 

프로그램의 가장 기본 단위가 함수다. (자바 기준 메서드)

 

원본 코드

public class HtmlUtil {
    public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
        WikiPage wikiPage = pageData.getWikiPage();
        StringBuffer buffer = new StringBuffer();
        if (pageData.hasAttribute("Test")) {
            if(includeSuiteSetup) {
                WikiPage suiteSetup =
                    PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
                if(suiteSetup != null) {
                    WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("!include -setup .")
                        .append(pagePathName)
                        .append("\n");
                }
            }
        }
        WikiPage setUp = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
        if(setUp != null) {
            WikiPagePath setUpPath = wikiPage.getPageCrawler().getFullPath(setUp);
            String setupPathName = PathParser.render(setUpPath);
            buffer.append("!include -setup .")
                .append(setupPathName)
                .append("\n");
        }
        buffer.append(pageData.getContent());
        if(pageData.hasAttribute("Test")) {
            WikiPage teardown =
                PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
            if(teardown != null) {
                WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
                String tearDownPathName = PathParser.render(tearDownPath);
                buffer.append("\n")
                    .append("!include -teardown .")
                    .append(tearDownPathName)
                    .append("\n");
            }
            if(includeSuiteSetup) {
                WikiPage suiteTearDown =
                    PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
                if(suiteTearDown != null) {
                    WikiPagePath pagePath = wikiPage.getPageCrawler().getFullPath(teardown);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("\n")
                        .append("!include -teardown .")
                        .append(pagePathName)
                        .append("\n");
                }
            }
        }
        pageData.setContent(buffer.toString());
        return pageData.getHtml();
    }
}

 

리팩터링 코드

    public static String renderPageWithSetupsAndTeardowns(
            PageData pageData,
            boolean includeSuiteSetup) throws Exception {
               
        boolean isTestPage = pageData.hasAttribute("Test");
        if (isTestPage) {
            WikiPage wikiPage = pageData.getWikiPage();
            StringBuffer newPageContent = new StringBuffer();
            includeSetupPages(includeSuiteSetup, wikiPage, newPageContent);
            newPageContent.append(pageData.getContent());
            includeTeardownPages(includeSuiteSetup, wikiPage, newPageContent);
            pageData.setContent(newPageContent.toString());
        }
        return pageData.getHtml();
    }

 

 

작게 만들어라!

함수를 만드는 규칙은 하나도 작게, 둘도 작게다.

명확한 근거는 없지만, 저자 경험에서 나온 규칙이다.

 

작은 함수 기준

가로 0 ~ 150

세로 2 ~ 4

들여쓰기 수준 1 ~ 2

 

    public static String renderPageWithSetupsAndTeardowns(
            PageData pageData,
            boolean includeSuiteSetup) throws Exception {
        if (isTestPage(pageData)) {
            includeSetupAndTeardownPages(pageData, includeSuiteSetup);
        }
        return pageData.getHtml();
    }

 

블록과 들여쓰기

제어문과 반복문 속에 들어가는 블록은 원래 줄이어야 한다는 뜻이다.

내부 코드를 함수로 추출하여 이름을 짓는다면, 코드를 이해하기 쉬워ㅣ진다.

 

가지만 해라!

함수는 가지를 해야 한다. 가지를 해야 한다. 가지만을 해야 한다.

 

함수는 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 함수는 가지 작업만 한다.

함수를 만드는 이유는 개념을 다음 추상화 수준에서 여러 단계로 나누기 위해서다.

 

기존 함수에서 다른 의미를 가지는 함수를 추출할 있다면 함수는 여러 작업을 하는 것이다.

 

함수 섹션

함수에서 섹션이 나눠진다면, 함수는 여러가지 일을 한다는 뜻이다.

가지만 하는 함수는 섹션을 나누기 힘들다.

 

 

함수 추상화 수준은 하나로!

함수가 가지만 잘하려면 모든 문장의 추상화 수준이 동일해야 한다.

 

함수 내에 추상화 수준이 섞이면, 읽는 사람이 헷갈린다.

특히 근본 개념과 세부사항이 섞여있다면, 깨진 창문 처럼 함수는 사람들이 세부사항을 추가하기 시작한다.

 

위에서 아래로 코드 읽기: 내려가기 규칙

좋은 코드는 좋은 책을 읽는 것과 같아야 한다.

높은 추상화 수준 코드에서 점점 낮아지는 추상화 수준으로 코드는 구성돼야 한다.

 

Switch (if-else 포함)

스위치문은 근본적으로 N 가지 일을 처리한다. 그리고 구조적으로 작게 만들기 어렵다.

 

개발 제어문은 사용할 밖에 없다.

 

스위치문은 다형성을 이용해 분리할 있다.

 

예시

  public Money calculatePay(Employee e) throws InvalidEmployeeType{
    switch (e.type) {
      case COMMISSIONED:
        return calculateCommissionedPay(e);
      case HOURLY:
        return calculateHourlyPay(e);
      case SALARIED:
        return calculateSalariedPay(e);
      default:
        throw new InvalidEmployeeType(e.type);
    }
  }

문제점,

함수가 길다.

신규 유형을 추가하기 힘들다.

SRP 위반한다. 코드를 변경할 이유가 하나가 아니다

OCP 위반한다. 신규 유형을 추가 마다 기존 코드가 영향을 받는다.

 

가장 문제

똑같은 유해한 구조가 무한정 반복될 수 있다.

  /*
  * 페이지 48, 가장 큰 문제 예시
  * 똑같은 유해한 구조가 무한정 반복될 수 있다.
  */
  void isPayDay(Employee e, Date date) throws InvalidEmployeeType {
    switch (e.type) {
      case COMMISSIONED:
        /* 뭔가 하는 코드 */
        return ;
      case HOURLY:
        /* 뭔가 하는 코드 */
        return ;
      case SALARIED:
        /* 뭔가 하는 코드 */
        return ;
      default:
        throw new InvalidEmployeeType(e.type);
    }
  }
  void deliverPay(Employee e, Money money) throws InvalidEmployeeType{
    switch (e.type) {
      case COMMISSIONED:
        /* 뭔가 하는 코드 */
        return ;
      case HOURLY:
        /* 뭔가 하는 코드 */
        return ;
      case SALARIED:
        /* 뭔가 하는 코드 */
        return ;
      default:
        throw new InvalidEmployeeType(e.type);
    }
  }

 

개선

public interface EmployeeFactory {
  Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
  @Override
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (r.type) {
      case COMMISSIONED:
        return new CommissionedEmployee(r);
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmployee(r);
      default:
        throw new InvalidEmployeeType(r.type);
    }
  }
}
public abstract class Employee {
  EmployeeRecord r;
  Employee(EmployeeRecord r) {
    this.r = r;
  }
  public abstract boolean isPayDay() ;
  public abstract Money calculatePay() ;
  public abstract void deliverPay(Money pay) ;
}

추상 팩터리로 객체를 생성하는 switch 문을 외부로 부터 숨긴다.

팩터리 구현체는 switch 문을 구현해 Employee 파생 클래스의 인스턴스를 생성한다.

 

호출자는 Employee라는 인터페이스를 통해 구현부는 모른체 호출하게 된다.

언어 차원에서 다형성으로 인해 실제 파생 클래스 메서드가 호출된다.

 

코드 안에서 다형성을 이용한 switch 문은 번만 사용한다.

 

서술적인 이름을 사용하라!

단편적인 이름은, 함수가 무엇을 하는지 알기 힘들다.

renderHtml()  함수는 무엇을 하는지 정확히 없다.

 

서술적인 이름을 지어 함수가 하는 일을 표현해야 한다.

 

함수 이름이 너무 길거나, 짓기 어렵다면, 함수가 가지 일을 하는지 확인해야 한다.

가지만 하는 함수는 이름을 짓기 쉽다.

 

요즘 IDE 이름 변경이 매우 쉬우니, 좋은 이름이 떠올랐으면 즉시 바꾸자

 

함수 인수

가장 좋은 인수는 0

다음 1

다음 2 항이다. 2 부터는 인수 순서에 의존성이 생긴다.

3 부터는 피하는 좋고,

4 항은 특별한 이유 없이는 사용하면 안된다. (특별한 이유 예시 _ 배열 복사 같은 것은 인수가 많을 밖에 없다)

 

많은 인수는 테스트 코드 작성도 어렵게 한다. 경우의 수가 늘어나기 때문

되도록 인수는 0 ~ 1 유지해야 한다.

 

많이 쓰는 단항 형식

같이 유념하고 개념, 명령과 질의 분리

 

질의와 찾기가 많다

질의

Boolean    isExists("123")

찾기

User         findById("123")

 

자주 사용하지 않지만, 이벤트 형식도 있다.

이벤트 함수는 입력 인수만 있다. 출력 인수는 없다. (명령, command)

함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.

passwordAttemptFailedNtimes(int attempts)

 

이벤트 함수는 시스템 상태를 바꾸기에 이름에서 이벤트 함수임을 명확히 해야 한다.

 

경우는 가급적 단항 함수를 피해야 한다.

 

좋은 예시

Void includeSetupPageInto(StringBuffer pageText)

 

StringBuffer includeSetupPageInto(StringBuffer pageText)

항등 함수처럼 자기 자신을 리턴하는 났다.

 

플래그 인수

함수에 Boolean 값을 넣는 것은 좋다.

함수는 여러 가지 일을 하는 함수라는 것을 드러내는 꼴이다.

 

이항 함수, 삼항 함수

이함 함수부터는 인수 순서를 신경써야 한다.

, 고려 사항이 기하급수적으로 증가한다.

 

인수 객체

인수가 2 ~ 3 이상 필요 독자적인 클래스 변수를 고려한다.

  //이것 보다.
  Circle makeCircle(double x, double y, double radius){
    return new Circle(x, y, radius);
  }
  //이것이 더 명료하다. 연관있는 인수를 하나로 묶었기 때문이다.
  Circle makeCircle(Point center, double radius){
    return new Circle(center, radius);
  }

단순한 눈속임처럼 보이지만, 아니다. 서로 연관있는 인수를 하나로 묶었다는 것에서 가치가 있다.

 

인수 목록

가변 인수가 필요한 경우가 있다.

경우는 사실상 이항 함수로 봐야한다.

 

동사와 키워드

함수 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.

 

함수 동사, 인수 명사 이렇게 쌍을 이뤄야 한다.

 

writeField(String name)

경우 누가봐도 이름을 필드에 쓴다는 것을 있다.

 

인수의 순서를 함수 이름에 적시하는 것은 좋은 방법이다.

assertExpectedEqualsActual(expected, actual)

 

부수 효과를 일으키지 마라!

부수 효과는 함수에서 가지를 하겠다고 해놓고 몰래(부수효과) 다른 짓도 하는 행위다.

 

public class UserValidator {
  private Crytograher crytograher;
  public boolean checkPassword(String username, String password) {
    User user = UserGateway.findByName(username);
   
    if(user != User.NULL) {
      String codePhrase = user.getPhraseEncodeByPassword();
      String phrase = crytograher.decrpt(codePhrase, password);
      if("Valid Password".equals(phrase)) {
        Session.initialize();
        return true;
      }
    }
    return false;
  }
}

패스워드가 일치하면 true, 아니면 false 리턴하는 함수다.

 

예시에서 부수효과는 Session.initialize(); 이다

 

함수 이름만 봐선 로그인 성공 세션을 초기화 한다는 것을 없다.

, 사용자가 의도치 않게 인증 세션을 지울 가능성이 있다.

 

또한 시간적 결합이 존재한다.

checkPassword() 함수는 세션을 초기화해도 되는 상황(시간적 결합)에서만 호출 가능하다.

세션을 지울 수도 있기 때문이다.

 

경우 함수 이름에서 이를 명시해 줘야 한다.

checkPasswordAndInitializeSession

 

참고로 함수는 가지 일을 한다

 

명령과 조회를 분리하라!

함수는 뭔가를 수행하거나 뭔가에 답하는 가지 행동만 해야한다.

같이 하면 안된다.

 

  public boolean set(String attrbute, String value) {
    //저장에 성공하면 true를 실패하면 false를 반환한다고 가정
    return false;
  }
  public void doSomething() {
    /* 이상한 코드 형식 */
    if(set("하나", "")) {
    }
  }

명령과 질의를 같이 수행하면 위와 같은 괴상한 코드가 나올 있다.

 

추가로 함수는 이름이 불명확하다.

set() 무슨 동작을 하는지 예상이 안된다.

  //명령
  public void set(String attribute, String value) {
  }
  //질의
  public boolean attributeExists(String string) {
    return false;
  }
  public void doSomething() {
    if(attributeExists("하나")) {
      set("하나", "");
    }
  }

명령과 질의를 분리하고, 이름을 명확히해 주석 같은 정보가 없어도 하는지 있다.

 

오류 코드보다 예외를 사용하라!

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리에 위반한다.

 

오류 코드는 중첩 코드를 야기한다.

반환되는 오류코드를 처리하기 위해 제어문이 필요하기 때문이다.

 

  String something(Page page) {
    if(deletePage(page) == E_OK) {
      if(registry.deleteReference(page.name) == E_OK) {
        if(configKeys.deleteKey(page.name.makeKey()) == E_OK){
          logger.info("page deleted");
        } else {
          logger.info("configKey not deleted");
        }
      } else {
        logger.info("deleteReference from registry failed");
      }
    } else {
      logger.info("delete failed");
      return E_ERROR;
    }
    return E_OK;
  }

오류 코드 대신 예외 사용

  void something(Page page) {
    try {
      deletePage(page);
      registry.deleteReference(page.name);
      configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
      logger.info(e.getMessage());
    }
  }

 

try/catch 블록 뽑기

Try/catch 블록은 구조가 추하다.

정상 처리 코드와 오류 처리 코드를 뒤섞기 때문이다.

여기서 뒤섞인 다는 것은 하나의 함수는 하나만 잘해야 하는데,

오류 처리 코드와 정상 처리 코드가 함께 있다는 것을 의미하는 같다.

 

별도 함수를 뽑아 문제를 제거하는 편이 옳다.

  void delete(Page page) {
    try {
      deletePageAndAllReferences(page);
    } catch (Exception e) {
      logError(e);
    }
  }
  private void logError(Exception e) {
    logger.info(e.getMessage());
  }
  private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
  }

 

이렇게 오류 처리 코드와 정상 동작 코드를 분리하면 이해하고 수정하기 쉬워 진다.

 

오류 처리도 가지 작업이다.

오류 처리도 가지 작업이기 때문에 이전 코드에서 별도 함수로 추출해서 정상 코드와 분리한 것이다.

 

Error.java 의존성 자석

에러 코드를 반환한다는 것은 어딘 가에 오류 코드를 반환하기 위한 코드를 정의한 다는 뜻이다.

public enum Error {
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WATTING_FOR_EVENT;
}
interface OtherError {
  String OK = "OK";
  String INVALID = "INVALID";
  String NO_SUCH = "NO_SUCH";
  String LOCKED = "LOCKED";
  String OUT_OF_RESOURCES = "OUT_OF_RESOURCES";
  String WATTING_FOR_EVENT = "WATTING_FOR_EVENT";
}

 

위와 같은 클래스가 의존성 자석이다.

 

열거형 Error 경우, 새로운 오류를 정의해야 하는 경우 Error 사용하는 모든 클래스를 컴파일 해야 한다.

 

문제로 Error 열거형은 변경이 쉽지 않다.

재컴파일을 피하려고 기존 에러 코드를 재사용하게 된다.

 

오류 코드 대신 예외를 사용하면, 새로운 예외는 Exception 클래스에서 파생되어 기존 코드는 전혀 재컴파일이 필요 없다.

 

반복하지 마라!(Don’t Repeat Yourself : DRY 원칙)

중복의 문제는 변경에 취약하다.

중복이 10 라면 하나의 변경에도 10 수정 해야 한다.

10 번의 수정 과정에서 개발자가 실수를 하지 않는 다는 보장도 없다.

 

현대 소프트웨어에서 가장 문제되는 코드는 중복이다.

그래서 이를 극복하기 위한 다양한 방법들이 존재한다.

 

데이터 베이스도 이런 중복 문제로 정규화를 수행한다.

AOP 중복 제거를 위함이다.

 

구조적 프로그래밍

에츠허르 데이크스트라 구조적 프로그래밍 원칙에 따르면, 모든 함수는 입구와 출구가 하나만 존재해야 한다.

단일입구/단일출구 원칙

 

Break, continue, goto(절대) 사용하면 안된다.

 

규율은 함수가 때만 이익을 제공한다.

작은 함수에선 효과가 없다.

 

작은 함수에선 여러 출구가 오히려 함수 표현에 도움될 때도 있다.

 

함수를 어떻게 짜죠?

글쓰기라 생각하면 된다.

일단 생각한 바를 기록한 다듬고, 다듬는다.

 

이를 함수에 대입하면,

일단 동작을 하는 로직을 만든다. 이땐 들여쓰기가 아무리 많아도 괜찮다.

다만, 초안 작성에도 테스트 코드는 만들면서 진행해야 한다.

 

함수를 쪼개거나 이름을 짓는 , 이후 리팩터링을 한다.

 

결론

대가 프로그래머는 시스템을 구현할 프로그램이 아닌 풀어갈 이야기로 여긴다.

프로그래밍 언어라는 수단을 사용해 표현려기 강한 언어를 만들어 이야기를 풀어간다.

함수가 이를 표현하는 언어에 속한다.

 

좋은 함수는 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수다.

좋은 함수의 궁극적 목표는 시스템 이야기를 풀어가는 것이다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

5장 형식 맞추기  (0) 2023.12.11
4장 주석  (1) 2023.12.04
2장 의미 있는 이름  (1) 2023.11.20
1장 깨끗한 코드  (0) 2023.11.13
다시 시작  (0) 2023.11.10

 

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

코드

https://github.com/rkwhr0010/clean_code/tree/main/src

변경 사항은 git history 참고

 

의도를 분명히 밝혀라

이름을 짓는데 시간을 투자해야 한다. 좋은 이름으로 절약하는 시간이 크기 때문이다.

 

이름 붙일 대상은 전부다. 패키지, 클래스, 변수

 

주석이 필요한 것은 의도가 명확하지 않은 것이다.

class Ex001 {
    void 의도를분명히밝혀라(){
        /**
         * 주석이 필요하면 의도가 명확하지 않은 것
         */
        Date d; // 도착시간
       
        /**
         * 의도는 명확히
         */
        Date arrivalTime;
    }
}

 

class Ex002 {
    List<int[]> theList = new ArrayList<>();
    public List<int[]> getThem(){
        List<int[]> list1 = new ArrayList<>();
        // theList 내용물이 뭐지?
        theList.stream()
            .filter(intArr -> intArr[0] == 4) // intArr[0] == 4 는 뭐지?
            .forEach(intArr -> list1.add(intArr));
        //반환되는 리스트는 어떻게 사용하지?
        return list1;
    }
}

코드 로직은 쉬우나, 무엇을 하는 코드인지 판단이 안된다.

 

class Ex002_2 {
    //지뢰찾기 게임이라는 것을 알아냈다.
    private static final int FLAGGED = 4;
    private static final int STATUS_VALUE = 0;
    List<int[]> gameBoard = new ArrayList<>();
    public List<int[]> getFlaggedCells(){
        List<int[]> flaggedCells = new ArrayList<>();
        gameBoard.stream()
            .filter(cell -> cell[STATUS_VALUE] == FLAGGED)
            .forEach(flaggedCells::add);
           
        return flaggedCells;
    }
}

 

코드 로직은 변경된게 하나 없이, 좋은 이름만으로 의도가 명확해 졌다.

 

class Ex002_3 {
    List<Cell> gameBoard = new ArrayList<>();
    public List<Cell> getFlaggedCells(){
        List<Cell> flaggedCells = new ArrayList<>();
        gameBoard.stream()
            .filter(Cell::isFlagged)
            .forEach(flaggedCells::add);
           
        return flaggedCells;
    }
    class Cell {
        private int[] status;
        private static final int FLAGGED = 4;
        private static final int STATUS_VALUE = 0;
        public boolean isFlagged() {
            return FLAGGED == status[STATUS_VALUE];
        }
    }
}

나아가 int[] 배열 대신, 별로 클래스를 만들어 로직을 캡슐화 했다.

 

그릇된 정보를 피하라

그릇된 정보는 코드 의미를 흐린다.

축약어 같은 경우 보는 사람 관점에서 서로 다르게 해석될 여지가 있다.

hp 같은 경우 게임을 좋아하는 사람은 health point 노트북을 좋아하는 사람은 hp 회사를 떠올린다.

 

자료구조와 연관된 단어는 신중히 사용한다.

Account 여러 개를 그룹화하는 컨테이너를 만든다고 가정하면 AccountList 같은 이름은 List 구현한 처럼 보이기에 사용하면 안된다.

실제로 컨테이너가 List라도 List이름을 붙이는 것을 지양해야 한다.



class Ex003 {
    class Account {}
    class AccountList{} // 안 좋은 이름 마치 List를 구현한 것 처럼 느껴진다.
    //GOOD
    class AccountGroup{}
    class Accouts{}
}

 

되도록 비슷한 이름을 사용하는 것을 피해야 한다.

인지적으로 찾기 너무 힘들다.

클래스 이름이 만약 앞부분 제외하고, 같아면 크게 상관 없겠지만,

이름 길이도 비슷한데, 중간 부분 철저만 조금 다르면, 다른 점을 찾기가 힘들다.

 



class Ex003_2 {
    void exam(){
        //아무 의미없는 임시 변수명 지양
        int a = 0;
        int b = 1;
        if(a == b) {
            a = 10;
        }
    }
    void exam2(){
        //아무 의미없는 임시 변수명에, 비슷한 철자까지 더 하면 더 끔찍하다.
        int i = 0;
        int l = 1;
        int I = 2;
    }
    void exam3(){
        //보통 기업들 코딩 컨벤션을 보면, 임시 변수는 아래와 같이
        //반복문에 같은 곳에만 허용한다.
        for(int i = 0; i < 10; i++){
        }
    }
}

 

 

의미 있게 구분하라

 



class Ex004 {
    //나쁜 이름
    public static void copyChars(char[] a1, char[] a2){
        for(int i = 0; i < a1.length; i++){
            a2[i] = a1[i];
        }
    }
}
class Ex004_2 {
    //의미가 분명한 좋은 이름
    public static void copyChars(char[] source, char[] destination){
        for(int i = 0; i < source.length; i++){
            destination[i] = source[i];
        }
    }
}

a1, a2 같은 불용어를 쓰면, 어느 배열에서 어느 배열로 복사하는 로직을 봐야지 있다.

, 정보를 제공하지 않는 의미없는 불용어 때문에 메서드 내부 구조를 봐야한다.

 

불용어

문자 의미를 파악하는 기여하는 거의 없는 문자를 불용어라고 한다.

주로 접두나 접미에 많이 위치한다.

My, Info, Data, a, The 같은 것이 불용어다.

 

과거 좋은 IDE 없던 시절엔 유의미한 경우가 있었지만, 지금은 의미가 없다.

예를 들어, strName  String 타입 name 이라는 뜻이지만,

현재는 IDE 명확히 타입을 알려주기에 str 의미가 없는 불용어다.

 

만약, 계좌에 대한 정보를 담은 클래스를 찾아야하는

Account AccontInfo 클래스를 발견했다.

어떤 클래스를 봐야하는 알수가 없다.

 

남이 봤을 명확히 구분되는 이름을 사용해야 한다.

 

발음하기 쉬운 이름을 사용하라

인간의 두뇌는 단어라는 철자 묶음을 처리한다.

대부분의 단어는 발음하기도 쉽다.

따라서 발음하기 쉬운 이름은 단어를 사용하는 것이다.

 

이는 지나친 축약어에서 발견된다.

//발음하기 쉬운 이름을 사용하라
class Ex005 {
    //안 좋은 예
    private Date modymdhms; //y년, m월, d일, h시, m분, s초;
    //좋은 예
    private Date modificationTimestamp;
}

 

검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 상수는 찾기가 힘들다.

예시 ) 0, 1 , a, i

 

이런 이유로 이름이 검색이 쉬운 것은 당연하다.

 

이름의 길이는 자신이 속한 스코프 범위에 비례해야 한다.

 

예를 들어, 반복문 i 같은 변수는 괜찮다. 그런데 멀리 외부 범위 여기 저기서 읽어오는 변수이름이 i 라면 정말 찾기 힘들다.

class Ex006 {
    void bad() {
        //예시 용 변수
        int s = 0;
        int[] t = null;
        for (int j = 0; j < 34; j++ ) {
            s += (t[j] * 4 ) / 5;
        }
    }
}

s 그나마 누산용 변수인 것을 있다.

34까지 반복하는지 파악이 안된다.

t[] 배열은 무슨 배열이고, 계산식도 어떤 의미인지 파악이 안된다.

 

class Ex006_2 {
    void good() {
        //예시 용 변수
        int[] taskEstimate = null;
        int realDaysPerIdealDay = 4;
        final int WORK_DAYS_PER_WEEK = 5;
        int sum = 0;
        for (int j = 0; j < WORK_DAYS_PER_WEEK; j++ ) {
            int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
            int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
            sum += realTaskWeeks;
        }
    }
}

의미 있는 이름을 지으면, 메서드가 길어진다.

그래도 무엇을 하는지 있으며, 검색하기 쉽다.

 

인코딩을 피하라

이름에 불필요한 정보를 더하지 말아야한다.

 

헝가리식 표기법

프로그래밍에서 변수 함수 인자 이름 앞에 데이터 타입을 명시하는 표기법

String strName;

 

헝가리식 표기법이 대표적

 

과거에는 자원 문제로 이름 길이가 제한되거나, 컴파일러 기능이 미약해 타입 감지를

해줬다.

 

현재는 상황이 다르다. 필요없다.

 

자신의 기억력을 자랑하지 마라

i, s ,r 같이 한글자로 지어놓고, 기억해놨으니 괜찮다는 식은 안좋다.

아무리 자신의 기억력이 좋아도, 한참 보면 무슨 역할 변수인지 없다.

 

클래스 이름

명사나 명사구가 적합하다. 동사는 사용하지 않는다.

JSONParser, Product

불용어가 들어가게 조심한다.

ProductInfo, ProductData

 

메서드 이름

동사나 동사구가 적합하다.

 

 

기발한 이름은 피하라

코드를 개발한 자신만 있다. 심지어 나중에 자신이 봐도 기억 못할 있다.

무조건 명확한 이름이 좋다.

 

개념에 단어를 사용하라

메서드들을 예시로 들면

찾는 것은 find, 저장은 save, 삭제는 delete 것을 있다.

찾기라는 개념을 fetch, retrieve, search 혼용해서 사용하면 안된다.

말장난을 하지 마라



class Ex007 {
    //다른 곳도, add 어휘 메서드는 무언 가를 더한 값을 리턴한다.
    int add(int left, int right) {
        return left + right;
    }
    //무언가 컨테이너에 값을 더하는 메서드가 필요해서
    //구현하고 이름을 add로 지으면, 기존 add 어휘가 가지는 의미가 두 개가 된다.
    List<Object> list;
    void add(Object obj){
        list.add(obj);
    }
    //add 어휘에 일관성을 지키기 위해 insert라는 이름으로 바꾼다.
    void insert(Object obj){
        list.add(obj);
    }
}

 

해법 영역에서 가져온 이름을 사용하라

디자인 패턴이나, 알고리즘 같은 용어를 사용하는 것이 좋다.

적절한 해법 영역에 용어가 없다면, 문제 영역에서 이름을 가져온다.

 

코드 성격이 문제 영역인지, 해법 영역인지 어느 쪽에 가까운 판단 이름을 지어야 한다.

 

의미 있는 맥락을 추가하라

변수 하나의 이름이 스스로 의미있는 맥락을 가지는 경우는 거의 없다.



class Ex008_2 {
    //addr 라는 접두를 추가해 맥략을 분명히 했다.
    void eaxm() {
        String addrFirstName, addrLastName, addrStreet
            , addRhouseNumber, addrCity, addrState, addrZipcode;
        /*
         * 무언가 하는 코드...
         */
    }
}
class Ex008_3 {
    void eaxm() {
        Address address = new Address();
        /*
         * 무언가 하는 코드...
         */
    }
    //더 좋은 방법은 야에 새로운 클래스로 같은 맥락 변수를 묶는 것이다.
    class Address {
        String firstName, lastName, street, houseNumber, city, state, zipcode;
    }
}

불필요한 맥락을 없애라

미래 백화점 어플리케이션을 만든다고 가정하고,

어플리케이션에 클래스를 만드는데 굳이 불필요하게

Future Department Store 약어로 FDS 접두로 클래스마다 붙이는 것은 의미가 없다.

 

마치면서

우리는 클래스, 메서드 등에 붙은 모든 이름을 외우지 못한다.

좋은 이름을 짓는 것은 매우 중요하다.

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

4장 주석  (1) 2023.12.04
3장 함수  (1) 2023.11.27
1장 깨끗한 코드  (0) 2023.11.13
다시 시작  (0) 2023.11.10
마무리  (0) 2022.11.15

 

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

https://github.com/rkwhr0010/clean_code/tree/main/src

 

 

코드란 고객의 요구사항을 표현하는 언어다.

 

나쁜코드

당장의 일정 때문에 빠르게 "구동만 되는" 코드를 만들다보면, 시간이 지남에 따라 결국 코드 때문에 유지보수가 힘들어져 생산성 저하로 이어진다.

 

결과적으로 클린코드는 유지보수 용이성이 상승하여, 생산성 증대를 가져온다.

 

르블랑의 법칙

나중은 결국 오지 않는다. 지금해야 한다.

 

태도

코드 하나를 수정하려고 했는데, 다른 코드까지 수정해야 하는 경험을 해본 있을 것이다.

 

이는 개발자의 핵심이 크다.

 

요구사항이 자주 변경되거나, 일정이 촉박하다고 변명하면 안된다.

의사 선생님에게 간단한 외과 진료를 받는데, 빨리 치료받게 손씻지 말라고, 요구해서 의사가 손을 씻는 것을 본적 있는가?

 

나쁜 코드의 위험을 이해하지 못하는 관리자 말을 그대로 따는 것은 프로 답지 못한 행동이다.

 

난제

나쁜 코드는 업무 속도를 늦추게 되어있다.

하지만, 당장 눈앞에 시간을 맞추려고, 나쁜 코드를 양산하는 유혹에 빠진다.

 

이게 나쁜 것임을 알고 있지만, 앞의 일정을 지키려고, 나쁜 코드를 양산한다.

 

결과적으로 보자, 종국엔 나쁜 코드 때문에 어차피 기한을 맞추지 못할 날이 것이다.

결국, 빨리 가는 유일한 방법은 클린 코드다.

 

깨끗한 코드

클린 코드와 나쁜 코드를 구분할 아는 것과 직접 클린 코드를 작성하는 것은 다르다.

우리 목표는 클린 코드를 작성하는 방법을 채득하는 것이다.

 

비야네 스트롭스트룹
"나는 우아하고 효율적인 코드를 좋아한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다. 깨끗한 코드는 가지를 제대로 한다."

 

깨끗한 코드는 "보기에 즐거운" 코드다.

나쁜 코드는 코드를 고치면서 나쁜 코드를 만들게 한다.

 

깨진 창문 효과

깨진 창문은 관리되지 않는 느낌을 준다. 누구도 깨진다고 신경쓰지 않는다.

나중엔 자신도 창문을 깬다.

, 일단 창문이 깨지면 쇠퇴하게 된다.

 

깨끗한 코드는 세세한 사항까지 처리하는 코드다.(명명법, 메모리 누수 )

 

깨끗한 코드는 가지를 잘한다. 나쁜 코드는 여러 일을 하려다 목적과 의도가 모호해 진다.

 

그래디 부치
"깨끗한 코드는 단순하고 직접적이다. 깨끗한 코드는 문장처럼 읽힌다. 깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다."

 

깨끗한 코드는 소설과 같이 읽혀야 한다.

기승전결이 명확한 것처럼 클린 코드를 읽을 코드의 목적과 개발자가 의도한 바를 있어야 한다.

 

데이브 토마스
"깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 깨끗한 코드에는 의미 있는 이름이 붙는다. 특정 목적을 달성하는 방법은 여러 가지가 아니라 하나만 제공한다. 의존성은 최소이며 의존성을 명확히 정의한다. API 명확하며 최소로 줄였다. 언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 없기에 코드는 문학적으로 표현해야 마땅하다."

 

읽기 쉬운 코드와 고치기 쉬운 코드는 별개다.

 

테스트 케이스가 없는 코드는 아무리 우하한 코드라도 깨끗한 코드가 아니다.

 

코드 보다 작은 코드가 좋다.

 

마이클 페더스
"깨끗한 코드의 특징은 많지만 중에서도 모두를 아우르는 특징이 하나 있다. 깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다. 고치려고 살펴봐도 딱히 곳이 없다. 작성자가 이미 모든 사항을 고려했으므로, 고칠 궁리는 하다보면 언제나 제자리로 돌아온다. 그리고는 누군가 남겨준 코드, 누군가 주의 깊게 짜놓은 작품에 감사를 느낀다."

 

깨끗한 코드는 누군가 시간을 들여 깔끔하고 단정하게 정리한 코드다.

 

제프리스(요약)
중요도 좋은코드
  • 모든 테스트를 통과한다.
  • 중복이 없다.
  • 시스템 모든 설계 아이디어를 표현한다.
  • 클래스, 메서드, 함수 등을 최대한 줄인다.
표현력이 좋은 코드
  • 이름이 중요하다. 요즘 IDE 이름 변경이 매우 쉬워 좋은 이름으로 변경하기 좋다.
  • 책임이 많은 객체나 메서드를 찾아 여러 개로 나눠야 한다.
 
표현력이 좋은 코드
  • 프로그램에서 유사한 요소 집합이 보이면, 추상화한다.
    다시말해, 추상 클래스나 인터페이스로 실제 구현을 감싼다.
    이렇게 되면, 호출하는 쪽은 인터페이스를 바라보기 때문에 변경에 강한 코드가 된다

작게 추상화, 중복 제거, 기능만

 

 

워드 커닝햄
"코드를 읽으면서 짐작했던 기능을 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다. 코드가 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다."

 

깨끗한 코드는 독해를 필요가 없어야 한다. 명백하고 단순하기 때문이다.

 

접근법

의심하지 말고, 빠져서 익혀볼 , 그렇다고 다른 방법론이 거짓이라는 것이 아니다.

충분히 학습 다른 방법론도 익히면서, 견문을 넓일

 

우리는 저자다

실제로 개발을 시작하면, 코드를 읽는 시간이 코드를 짜는 시간보다 훨씬 많다.

따라서 읽기 쉬운 코드를 짜는 것은 중요하다. 그래야 코드를 빠르게 있다.

 

보이스카우트 규칙
"캠프장은 처음 왔을 때보다 깨끗하게 해놓고 떠나라."

체크인 때보다 체크아웃 보다 깨끗한 코드를 만들면 된다.

노력을 기울일 필요 없다.

중요한 것은 지속성이다.

 

 

원칙

SOLID

  • SRP (Single Responsibility Principle)
    클래스는 변경할 , 가지 이유만 존재해야 한다.
  • OCP(Open Closed Principle)
    클래스는 확장엔 열리고, 변경에 닫혀 있어야 한다.
  • LSP(Liskov Substitution Principle)
    상속받은 클래스는 기초 클래스로 대체할 있어야 한다.
  • DIP(Dependency Inversion Principle)
    추상화에만 의존해야 한다. 구체화에 의존하면 안된다.
  • ISP(Interface Segregation Principle)
    클라이언트가 필요한 만큼 인터페이스로 기능을 최소한으로 분리해 제공해야 한다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

3장 함수  (1) 2023.11.27
2장 의미 있는 이름  (1) 2023.11.20
다시 시작  (0) 2023.11.10
마무리  (0) 2022.11.15
13장 동시성 - 2  (1) 2022.11.12

이 책은 처음 읽었을 당시는 머리에 남는 것은 좋은 이름 밖에 없었는데

지금 개발을 시작한지 2년이 약간 안되는 시점에 다시 읽어보니 훨씬 더 좋은 책이라는 것을 깨닫고 있다.

 

처음 읽었을 당시 개발을 접한지 1년도 안된 시점이라 잘 이해도 안갔다.

현재는 비교적 수월하게 읽히도 글귀에 숨겨진 의미도 보인다. 당시엔 객체지향 원칙, 디자인 패턴 등을 전혀 몰라서 이 책의 정수를 내것으로 취하지 못했다.

 

이렇게 좋은 책을 다시 읽지 않으면 나만 손해인 것 같아 다시 정리를 시작

 

https://ebook-product.kyobobook.co.kr/dig/epd/ebook/E000003160816

 

Clean Code(클린 코드) | 로버트 C. 마틴 | 인사이트- 교보ebook

애자일 소프트웨어 장인 정신, 나쁜 코드도 돌아는 간다. 하지만 코드가 깨끗하지 못하면 개발 조직은 기어간다. 매년 지저분한 코드로 수많은 시간과 상당한 자원이 낭비된다. 그래야 할 이유

ebook-product.kyobobook.co.kr

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

2장 의미 있는 이름  (1) 2023.11.20
1장 깨끗한 코드  (0) 2023.11.13
마무리  (0) 2022.11.15
13장 동시성 - 2  (1) 2022.11.12
13장 동시성 - 1  (0) 2022.11.08

14장 점진적인 개선, 15 JUnit들여다보기,16 SerialDate 리팩터링은 이전 내용을 기반으로한 예제 위주 챕터이다. 

따라서 책으로 직접봐야한다.

 

17장 냄새와 휴리스틱의 소주제를 나열하는 것으로 책 요약을 마무리하겠다.

주석

C1 : 부적절한 정보

C2 : 쓸모 없는 주석

C3 : 중복된 주석

C4 : 성의 없는 주석

C5 : 주석 처리된 코드

환경

E1 : 여러 단계로 빌드해야 한다

E2 : 여러 단계로 테스트해야 한다

함수

F1 : 너무 많은 인수

F2 : 출력 인수

F3 : 플래그 인수

F4 : 죽은 함수

일반

G1 : 한 소스 파일에 여러 언어를 사용한다.

G2 : 당연한 동작을 구현하지 않는다.

G3 : 경계를 올바로 처리하지 않는다.

G4 : 안전 절차 무시

G5 : 중복

G6 : 추상화 수준이 올바르지 못하다

G7 : 기초 클래스가 파생 클래스에 의존한다

G8 : 과도한 정보

G9 : 죽은 코드

G10 : 수직 분리

G11 : 일관성 부족

G12 : 잡동사니

G13 : 인위적 결합

G14 : 기능 욕심

G15 : 선택자 인수

G16 : 모호한 의도

G17 : 잘못 지운 책임

G18 : 부적절한 static 함수

G19 : 서술적 변수

G20 : 이름과 기능이 일치하는 함수

G21 : 알고리즘을 이해하라

G22 : 논리적 의존성은 물리적으로 드러내라

G23 : If/Else 혹은 Switch/Case 문보다 다형성을 사용하라

G24 : 표준 표기법을 따르라

G25 : 매직 숫자는 명명된 상수로 교체하라

G26 : 정확하라

G27 : 관례보다 구조를 사용하라

G28 : 조건을 캡슐화하라

G29 : 부정 조건은 피하라

G30 : 함수는 한 가지만 해야 한다

G31 : 숨겨진 시간적인 결합

G32 : 일관성을 유지하라

G33 : 경계 조건을 캡슐화하라

G34 : 함수는 추상화 수준을 한 단계만 내려가야 한다

G35 : 설정 정보는 최상위 단계에 둬라

G36 : 추이적 탐색을 피하라

자바

J1 : 긴 import 목록을 피하고 와일드카드를 사용하라

J2 : 상수는 상속하지 않는다

J3 : 상수 대 Enum

이름

N1 : 서술적인 이름을 사용하라

N2 : 적절한 추상화 수준에서 이름을 선택하라

N3 : 가능하다면 표준 명명법을 사용하라

N4 : 명확한 이름

N5 : 긴 범위는 긴 이름을 사용하라

N6 : 인코딩을 피하라

N7 : 이름으로 부수 효과를 설명하라

테스트

T1 : 불충분한 테스트

T2 : 커버리지 도구를 사용하라!

T3 : 사소한 테스트를 건너뛰지 마라

T4 : 무시한 테스트는 모호함을 뜻한다

T5 : 경계 조건을 테스트하라

T6 : 버그 주변은 철저히 테스트하라

T7 : 실패 패턴을 살펴라

T8 : 테스트 커버리지 패턴을 살펴라

T9 : 테스트는 빨라야 한다.

 

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

1장 깨끗한 코드  (0) 2023.11.13
다시 시작  (0) 2023.11.10
13장 동시성 - 2  (1) 2022.11.12
13장 동시성 - 1  (0) 2022.11.08
12장 창발성  (0) 2022.11.05

동기화하는 메서드 사이에 존재하는 의존성을 이해하라

동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다.

자바는 개별 메서드를 보호하는 synchronized라는 개념을 지원한다. 하지만 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 확인해야 한다.

 

권장사항: 공유 객체 하나에 메서드 하나만 사용하기

 

  • 공유 객체 하나에 여러 메서드가 필요한 상황은 다음 세 가지 방법을 고려

클라이언트에서 잠금 - 클라이언트에서 번째 메서드 호출하기 전에 서버를 잠근다. 마지막 메서드를 호출할 때까지 잠금을 유지한다.

서버에서 잠금 - 서버에다 "서버를 잠그고 모든 메서드를 호출한 잠금을 해제하는" 메서드를 구현한다. 클라이언트는 메서드를 호출한다.

연결(Adapted)서버 - 잠금을 수행하는 중간 단계를 생성한다. '서버에서 잠금' 방식과 유사하지만 원래 서버는 변경하지 않는다.

동기화하는 부분을 작게 만들어라

synchronized 키워드를 사용하면 락을 설정한다. 같은 락으로 감싼 모든 코드 영역은 번에 스레드만 실행이 가능하다. 락은 스레드를 지연시키고 부하를 가중시킨다. 따라서 남발하면 안된다.

반면, 임계 영역은 반드시 보호해야반드시 한다. 코드를 때 임계 영역 수를 최대한 줄여야 한다.

거대한 임계영역 하나로 구현하라는 말이 아니다. 작고 적게.

올바른 종료 코드는 구현하기 어렵다

영구적으로 돌아가는 시스템 구현 방법과 잠시 돌다 깔끔하게 종료하는 시스템 구현하는 방법은 다르다

깔끔하게 종료하는 코드는 구현이 어렵다. 가장 흔한 이유는 데드락이다. 스레드가 절대 오지 않을 시그널을 기다린다.

부모 스레드가 자식 스레드 여러 만든 모두가 끝나기를 기다렸다 자원을 해제하고 종료한다고 하면, 자식 스레드 하나가 데드락에 걸렸다면? 부모 스레드는 영원히 기다린다.

 

이번에는 유사한 시스템이 사용자에게서 종료하라는 지시를 받았다고 가정. 부모 스레드는 모든 자식 스레드에게 작업을 멈추고 종료하라는 시그널을 전달한다. 그런데 자식 스레드 개가 생상자/소비자 관계라면? 생산자 자식 스레드는 금세 종료하지만 소비자 자식 스레드는 생상자 스레드에서 오는 메시지를 기다린다면? 생산자에서 메시지를 기다리는 소비자 스레드는 차단 상태에 있으므로 종료하라는 시그널을시그널을 못 받는다. 소비자 스레드는 생산자 스레드를 영원히 기다리고, 소비자 스레드 부모 스레드는 영원히 기다린다.

 

권장사항: 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라

 

스레드 코드 테스트하기

현실적으로 불가능. 테스트가 정확성을 보장하지 않는다. 그럼에도 충분한 테스트는 위험을 낮춘다.

 

권장사항: 문제를 노출하는 테스트 케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안된다.

 

고려할 사항이 아주 많다는 뜻이다. 아래에 가지 구체적인 지침을 제시한다.

  • 말이 되는 실패는 잠정적인 스레드 문제로 취급하라
  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.
  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 있도록 스레드 코드를 구현하라
  • 다중 스레드를 쓰는 코드 부분을 상황에 맞춰 조정할 있게 작성하라
  • 프로세서 수보다 많은 스레드를 돌려보라
  • 다른 플랫폼에서 돌려보라
  • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해 보라

말이 되는 실패는 잠정적인 스레드 문제로 취급하라

다중 스레드 코드는 때때로 말이 안 되는 오류를 일으키기 때문이다.

저자들을 포함해서 대다수 개발자는 스레드가 다른 코드와 교류하는 방식을 직관적으로 이해하지 못한다. 이유는 스레드 코드에 잠입한 버그는 수천~수백만 번에 번씩 드러나, 도저히 실패를 재현하기 어렵기 때문이다. 다만, 이런 문제를 일회성 문제로 취급하면취급하면 안 된다. 그냥 일회성 문제는 존재하지 않는 개념이라 생각하라. 계속 일회성 문제를 무시하면 잘못된 코 드위에 코드가 계속 쌓인다.

 

권장사항: 시스템 실패를 '일회성'이라 치부하지 마라.

다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자

당연한 소리지만 다시 한번 강조한다. 스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인한다. 일반적인 방법으로, 스레드가 호출하는 POJO 만든다. POJO 스레드를 모른다. 따라서 스레드 환경 밖에서 테스트가 가능하다.

 

, 순차 코드도 검증 안된 상태에서 다중 스레드를 동시에 테스트하면 가뜩이나 어려운 어려워진다.

 

권장사항: 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라. 먼저 스레드 환경 밖에서 코드를 올바로 돌려라.

 

다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 있게 작성하라

적절한 스레드 개수 파악은 상당한 시행착오가 필요. 그래서 처음부터 다양한 설정으로 프로그램의 성능 측정 방법을 강구해야 한다. 스레드 개수를 조율하기 쉽게 코드를 구현해 프로그램이 돌아가는 도중에 스레드 개수를 변경할 방법도 고려한다. 나아가 프로그램 처리율과 효율에 따라 스스로 스레드 개수를 조율하는 코드도 고민한다

프로세서 수보다 많은 스레드를 돌려보라

시스템이 스레드를 스와핑 할 때도 문제가 발생한다. 스와핑을 일으키려면 프로세서 수보다 많은 스레드를 돌린다. 스와핑이 잦을수록 임계 영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다

 

 

다른 플랫폼에서 돌려보라

단지 운영체제마다 스레드를 처리하는 정책이 달라 결과가 달라질 있다. 다중 스레드 코드는 플랫폼에 따라 다르게 돌아간다. 따라서 코드가 돌아갈 가능성이 있는 플랫폼 전부에서 테스트를 수행해야 한다.

 

권장사항 : 처음부터 그리고 자주 모든 목표 플랫폼에서 코드를 돌려라

코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해 보라

스레드 코드는 오류를 찾기 쉽지 않다. 간단한 테스트로는 버그가 드러나지 않는다. 버그가 달만에 나타날 수도 있다.

스레드 버그가 산발적이고 우발적이고 재현이 어려운 이유는 코드가 실행되는 수천 가지 경로 중에 아주 소수만 실패하기 때문이다.

드물게 발생하는 오류를 자주 일으킬 방법은 없을까? 보조 코드를 추가해 코드가 실행되는 순서를 바꿔준다.

예를 들어 Object.wait(), Object.sleep(), Object.yield(), Object.priority() 등과 같은 메서드를 추가해 코드를 다양한 순서로 실행한다.

메서드는 스레드가 실행되는 순서에 영향을 미친다. 따라서 버그가 드러날 가능성도 높아진다.

  • 코드에 보조 코드를 추가하는 방법은 두 가지

직접 구현하기

자동화

직접 구현하기

코드에다 직접 wait(), sleep(), yield(), priority() 함수를 추가

특별히 까다로운 코드를 테스트할 적합

yield() 삽입하면 코드가 실행되는 경로가 바뀐다. 그래서 버그를 발견할 확률이 증가한다. , yield()때문이 아니라 원래 있던 버그가 발견된 것이다.

 

방법의 문제점

  • 보조 코드 삽입할 적정 위치 선정
  • 어떤 함수를 어디서 호출해야 적당한지
  • 배포 환경에 보조 코드를 그대로 남겨두면 프로그램 성능 저하
  • 무작위적, 버그 발견이 확률임

 

스레드를 전혀 모르는 POJO 스레드를 제어하는 클래스로 프로그램을 분할하면 보조 코드를 추가할 위치 찾기가 쉬워진다.

자동화

AOF(Aspect-Oriented Framework), CGLIB, ASM 등과 같은 도구를 사용

ThreadJigglePoint.jiggle() 호출은 무작위로 sleep이나 yield 호출한다. 때론 아무 동작도 하지 않는다

ThreadJigglePoint클래스를 가지로 구현하면 편리하다

하나 jiggle() 메서드를 비워두고비워두고 배포 환경에서 사용

무작위로 nop(무동작), sleep, yield 등을 테스트 환경에서 수행

테스트 환경에서 두 번째 방법으로 수천번 통과했다면 나름 만큼 했다고 말해도 된다. 복잡한 방법이 있지만, 어렵다면 방법이 합리적인 대안이다

 

IBM 개발한 ConTest라는 도구가 있다. 위와 같은 방식의 테스트를 지원하지만 복잡하다

 

코드를 흔드는 이유는 스레드를 매번 다른 순서로 실행하기 위해서다. 좋은 테스트 케이스와 흔들기 기법은 오류가 드러날 확률을 크게 높여준다.

 

권장사항: 흔들기 기법을 사용해 오류를 찾아내라.

결론

다중 스레드 코드는 올바로 구현하기 어렵다. 간단한 코드가 여러 스레드와 공유 자료를 추가하면서 악몽으로 변한다

 

먼저, SRP를준수한다. POJO 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다. 스레드 코드를 테스트할 때는 전적으로 스레드만 테스트한다, , 스레드 코드는 최대한 집약되고 작아야 한다

 

동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다.

여러 스레드가 공유 자료를 조작하거나 자원 풀을 공유할 동시성 오류가 발생한다.

루프 반복을 끝내거나 프로그램을 깔끔하게 종료하는 경계 조건의 경우가 까다로우므로 특히 주의한다.

 

사용하는 라이브러리와 기본 알고리즘을 이해한다. 특정 라이브러리 기능이 기본 알고리즘과 유사한 어떤 문제를 어떻게 해결하는지 파악한다.

 

보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법을 이해한다.

잠글 필요가 없는 코드는 잠그지 않는다.

잠긴 영역에서 다른 잠긴 영역을 호출하지 않는다.

그러려면 공유하는 정보와 공유하지 않는 정보를 제대로 이해해야 한다.

공유하는 객체 수와 범위를 최대한 줄인다.

클라이언트에게 공유 상태를 관리하는 책임을 떠넘기지 않는다.

필요하다면 객체 설계를 변경해 클라이언트에게 편의를 제공한다.

 

어떻게든 문제는 생긴다. 초반에 들어가지 않는 문제는 일회성으로 치부하지 말자 무시하지 말자

대게 일회성 문제는 시스템 부하 상태나 뜬금없이 발생한다. 그러므로 스레드 코드는 플랫폼에서 많은 설정으로 반복적으로 테스트해야 한다

테스트 용이성은 TDD 3 규칙을 따른 자연히 얻어진다.

테스트 용이성은 또한 넓은 설정 범위에서 코드를 수행하기 위해 필요한 기능을 제공하는 플러그인 수준을 의미한다.

 

시간을 들여 보조 코드를 추가하면 오류가 드러날 가능성이 크게 높아진다.

직접 구현도 좋고 가지 자동화 기술을 사용해도 괜찮다.

초반부터 보조 코드를 고려한다. 스레드 코드는 출시 전까지 최대한 오랫동안 돌려봐야 한다.

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

다시 시작  (0) 2023.11.10
마무리  (0) 2022.11.15
13장 동시성 - 1  (0) 2022.11.08
12장 창발성  (0) 2022.11.05
11장 시스템  (0) 2022.11.01

동시성과 깔끔한 코드는 양립하기 아주 어렵다.

깨끗한 동시성은 하나를 할당할 정도로 복잡한 주제다.

 

동시성이 필요한 이유?

동시성은 결합을 없애는 전략이다. 무엇과 언제를 분리하는 전략이다. 쓰레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다. 호출 스택을 살펴보면 곧바로 알수 있다.

 

무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다. 구조적 관점에서 거대한 루프 하나가 아니라 작은 협력 프로그램 여럿으로 보인다. 따라서 시스템을 이해하기 쉽고 문제를 분리하기도 쉽다.

 

예를 들어, 어플리케이션 표준인 서블릿 모델을 보면 서블릿은 혹은 EJB컨테이너라는 우산 아래서 동작한다. 컨테이너는 동시성을 부분적으로 관리한다. 요청이 들어올 때마다 서버는 비동기식으로 서블릿을 실행한다. 서블릿 프로그래머는 들어오는 모든 요청을 관리할 필요가 없다. 원칙적으로 서블릿 스레드는 다른 서블릿 스레드와 무관하게 자신만의 세상에서 돌아간다.

실제로 컨테이너가 제공하는 결합분리(decoupling)전략은 완벽과 거리가 아주 멀다. 따라서 서블릿 프로그래머는 동시성을 정확히 구현하도록 각별한 주의와 노력을 기울여야 한다. 그럼에도 서블릿 모델이 제공하는 구조적 이점은 아주 크다.

 

구조적 개선만을 위해 동시성을 채택하는 아니다. 응답 속도와 작업 처리량 이점이 존재한다. 예를 들어 CPU 보통 I/O단계에서 병목이 발생한다. 충분한CPU 속도를 I/O는 절대 따라올 수가 없다. 이럴 다중 쓰레드는 I/O 병목을 다소다소 완화 있다.

 

미신과 오해

  • 동시성은 항상 성능을 높여준다

동시성은 때로 성능을 높여준다.

대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.

  • 동시성을 구현대로 설계는 변하지 않는다

단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다. 일반적으로 무언과 언제를 분리하면 시스템 구조가 크게 달라진다

  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.

실제로는 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 있는지를 알아야만 한다.

 

반대로 동시성과 관련된 타당한 생각 가지

 

동시성은 다소 부하를 유발한다. 성능 측면에서 부하가 걸리며, 코드도 더 짜야한다

동시성은 복잡하다. 간단한 문제라도 동시성은 복잡하다

일반적으로 동시성 버그는 재현하기 어렵다. 그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다

동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

동시성 방어 원칙

  • 단일 책임 원칙(Single Responsibility Principle)

SRP 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙

동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다.

, 동시성 관련 코드는 다른 코드와 분리해야 한다는 뜻이다.

  • 고려사항

동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.

동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.

잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.

따름 정리 : 자료 범위를 제한하라

객체 하나를 공유한 동일 필드를 수정하면 스레드 간섭이 발생한다.

해결 방법으로 객체를 사용하는 코드 내내 임계 영역 임계 영역(critical section) synchronized 키워드로 보호한다.

다만 이런 임계 영역의 수를 줄이는 기술이 중요하다. 수가 많을수록 다음 가능성이 커진다.

보호할 임계 영역을 빼먹는다.

모든 임계영역을 올바로 보호했는지 확인하느라 노력과 수고를 반복한다

버그 찾기가 더욱 어려워진다.

 

따름 정리: 자료 사본을 사용하라

공유하지 않는 방법으로 객체를 복사해 읽 전용으로 사용하는 방법이다.

데이터의 변경이 없는 읽기는 동시성에서 자유롭다.

 

따름 정리: 스레드는 가능한 독립적으로 구현하라

다른 스레드와 자료를 공유하지 않는다. 스레드는 클라이언트 요청 하나를 처리한다. 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.

예를 들어, HttpServlet클래스에서 파생한 클래스는 모든 정보를 doGet doPost매개변수로 받는다. 그래서 서블릿은 마치 자신이 독자적인 시스템에서 동작하는 요청을 처리한다. 서블릿 코드가 로컬 변수만 사용한다면 서블릿이 동기화 문제를 일으킬 가능성은 전무하다. 물론 DB 연결과 같은 자원을 공유하는 상황은 처한다.

 

권장사항: 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라

라이브러리를 이해하라

  • 자바 5는 동시성 측면에서 이전 버전보다 많이 나아졌다.

스레드 환경에 안전한 컬렉션을 사용한다. 자바 5부터 제공한다.

서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.

가능하다면 스레드가 차단(blocking) 되지 않는 방법을 사용한다.

일부 클래스 라이브러리는 스레드에 안전하지 못한다.

 

추가로 자바 8부터 지원하는 Stream API는 동시성 구현이 쉽고 안전하다.

스레드 환경에 안전한 컬렉션

java.util.concurrent 패키지가 제공하는 클래스는 다중 스레드 환경에서 사용해도 안전하며, 성능도 좋다.

실제로 ConcurrentHashMap 거의 모든 상황에서 HashMap보다 빠르다. 동시 읽기/쓰기를 지원하며, 다중 스레드 환경에서 문제가 생시는 자주 사용하는 복합 연산을 다중 스레드 상에서 안전하게 만든 메서드로 제공한다.

좀 더 복잡한 동시성설계를 지원하고자 자바 5에는 다른 클래스도 추가되었다.

 

권장사항 : 언어가 제공하는 클래스를 검토하라.

자바에서는 java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks 익혀라

실행 모델을 이해하라

한정된 자원(Bound Resource) 다중 스레드 환경에서 사용하는 지원으로, 크기나 숫자가 제한적이다. 데이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등이 예다.
상호 배제(Mutual Exclusion) 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
기아(Strarvation) 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다. 예를 들어, 항상 짧은 스레드에게 우선순위를 준다면, 짧은 스레드가 지속적으로 이어질 경우, 긴 스레드가 기아 상태에 빠진다.
데드락(Deadlock) 여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다.
라이브락(Livelock) 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명(resonance)으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.

생산자-소비자

하나 이상 생산자 스레드가 정보를 생성해 버퍼나 대기열(queue) 넣는다

하나 이상 소비자 스레드가 대기열에서 정보를 가져와 사용한다.

생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원이다.  생산자 스레드는 대기열에 공간이 있어야 정보를 채운다. 즉, 빈 공간이공간이 생길 때까지 기다린다.

소비자 스레드는 대기열에 정보가 있어야 가져온다. , 정보가 채워질 때까지 기다린다.

대기열을 올바로 사용하고자 생산자 스레드와 소비자 스레드는 서로에게 시그널을 보낸다.

생산자 스레드는 대기열에 정보를 채운 다음 소비자 스레드에게 "대기열에 정보가 있다" 시그널을 보낸다.

소비자 스레드는 대기열에서 정보를 읽어 들인 후 "대기열에 공간이 있다" 시그널을 보낸다.

 

읽기-쓰기

읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 스레드가 공유 자원을 이따금 갱신한다고 하자.

처리율이 문제의 핵심이다. 처리율을 강조하면 기아 현상이 생기거나 오래된 정보가 쌓인다.

갱신을 허용하면 처리율에 영향을 미친다.

쓰기 스레드가 버퍼를 갱신하는 동안 읽기 스레드가 버퍼를 읽으면 안 되고

읽기 스레드가 버퍼를 읽는 동안 쓰기 스레드가 버퍼를 갱신하면 안 된다.

대개 쓰기 스레드가 버퍼를 오랫동안 점유하는 바람에 여러 읽기 스레드가 버퍼를 기다리느라 처리율이 떨어진다.

 

읽기 스레드의 요구와 쓰기 스레드의 요구를 적절히 만족시켜 처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.

간단한 전략은 읽기 스레드가 없을 때까지 갱신을 원하는 쓰기 스레드가 버퍼를 기다리는 방법

읽기 스레드가 계속 이어지면 쓰기 스레드가 기아 상태가 된다.

쓰기 스레드에게 우선권을 상태에서 쓰기 스레드가 계속 이어진다면 처리율이 떨어진다.

양쪽 균형을 잡으면서 동시 갱신 문제를 피하는 해법이 필요하다

 

생각하는 철학자들

둥근 식탁에 철학자  무리가 둘러앉았다.

철학자들 사이 마다 포크가   있다.

식탁 가운데에 거대한 스파게티  접시가 있다

철학자들은 배고프면 양손에 포크를 집어 스파게티를 먹는다.

따라서 왼쪽, 오른쪽 철학자는  철학자가 포크를 내려놓을 때까지 기다려야 한다.

 

여기서 철학자를 쓰레드, 포크를 자원으로 치환하면 된다.

주의해서 설계하지 않으면 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다.

 

대부분의 쓰레드 문제는  세가지 생산자-소비자, 읽기-쓰기, 생각하는 철학자들 범주 속한다.  알고리즘을 공부하고 해법을 직접 구현해보자. 나중에 문제  해결이 쉬워진다.

 

 

 

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

마무리  (0) 2022.11.15
13장 동시성 - 2  (1) 2022.11.12
12장 창발성  (0) 2022.11.05
11장 시스템  (0) 2022.11.01
10장 클래스  (0) 2022.10.26

창발적 설계로 깔끔한 코드를 구현하자

착실히 따르면 우수한 설계가 나오는 간단한 규칙 가지.

코드 구조와 설계를 파악하기 쉬워지게 해 준다.

그래서 SRP(Single Responsibility Principle, 단일 책임원칙)나 DIP(Dependency Inversion Principle, 의존관계 역전 원칙)을 적용하기적용하기 쉬워진다.

 

켄트 백이제 시한단순한 설계 규칙 가지 (중요도 )

  • 모든 테스트를 실행한다
  • 중복을 없앤다
  • 프로그래머 의도를 표현한다
  • 클래스와 메서드 수를 가능한 최소로 줄인다

단순한 설계 규칙 1: 모든 테스트를 실행하라

모든 테스트 케이스를 항상 통과하는 시스템은 테스트가 가능한 시스템이어야 한다.

테스트가 불가능한 시스템은 검증도 불가능하므로 절대 출시하면 안 된다

 

테스트가 가능한 시스템을 만들려고 애쓰면 설계 품질이 더불어 높아진다.

크기가 작고 목적 하나만 수행하는 클래스가 나온다.

SRP 준수하는 클래스는 테스트가 훨씬 쉽다.

 

결합도가 높으면 테스트 케이스를 작성하기 어렵다.

테스트 케이스를 많이 작성할수록 개발자는 DIP 같은 원칙을 적용하고, DI, 인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮춘다.

 

결과적으로 "테스트 케이스를 만들고 계속 돌려라"라는 규칙을 따르면 시스템은 낮은 결합도와 높은 응집력을응집력을 목표로 하는 객체지향 방법론에 가까워진다.가까워진다.

, 테스트 케이스를 작성하면 설계 품질이 높아진다.

단순한 설계 규칙 2~4: 리펙터링

테스트 케이스를 모두 작성했다면 이제 코드와 클래스를 정리한다.

구체적으로는 코드를 점진적으로 리팩터링 해나간다.

코드를 정리하면서 시스템이 깨질까 걱정할 필요가 없다. 테스트 케이스가 있으니까!

 

리팩터링 단계에서는 소프트웨어 설계 품질을 높이는 기법이라면 무엇이든 적용해도 괜찮다.

응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 나은 이름을 선택하는 다양한 기법을 동원한다.

또한 단계는 단순할 설계 규칙 나머지 3개를 적용해 중복을 제거하고, 프로그래머 의도를 표현하고, 클래스와 메서드 수를 최소로 줄이는 단계이기도 하다.

 

중복을 없애라

중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻한다.

중복은 여러 가지 형태로 표출된다.

똑같은 코드는 당연히 중복이고, 구현 중복도 중복의 형태다.

//구현 중복 예시
int size(){구현...}
boolean isEmpty(){구현...}
--------------------------------
//구현 중복 제거 예시
boolean isEmpty(){
	return size() == 0 ;
}

깔끔한 시스템을 만들려면 줄이라도 중복을 제거하겠다는 의지가 필요하다.

작은 중복을 제거하겠다는 것을 무시하면 안 된다.

작은 중복 제거를 할 줄 알아야 큰 중복을 제거할 수 있다.

 

"소규모 재사용" 시스템 복잡도를 극적으로 줄여준다

소규모 재사용을 제대로 익혀야 대규모 재사용이 가능하다.

 

TEMPLATE METHOD 패턴은 고차원 중복을 제거할 목적으로 자주 사용하는 기법이다

표현하라

우리 대다수는 엉망인 코드를 접한 경험이 있으리라.

자신이 이해하는 코드는 짜기 쉽다. 코드를 짜는 동안에는 문제에 빠져 코드를 구석구석 이해하니까

하지만 나중에 코드를 유지 보수할 사람이 코드를 짜는 사람만큼이나 문제를 깊이 이해할 가능성은 희박하다.

 

소프트웨어 프로젝트 비용 대다수는 장기적인 유지보수에 들어간다. 따라서 유지보수 개발자가 시스템을 제대로 파악해야 버그가 스며들지 않는다.

시스템이 점차 복잡해지면 유지보수 개발자가 코드 분석에 보내는 시간은 점점 늘어나 오해할 가능성도 커진다. 그러므로 코드는 개발자의 의도를 분명히 표현해야 한다. 개발자가 코드를 명백하게 수록 다른 사람이 코드를 이해하기 쉽다. 이는 유지보수 비용 절감으로 나타난다.

 

우선, 좋은 이름을 선택하라.

기능과 딴판인 이름을 지으면 안 된다.

 

둘째, 클래스 크기를 가능한 줄여라

이름 짓기도 구현도 이해도 쉬워진다

 

셋째, 표준 명칭을 사용하라.

예를 들어, 패턴을 적용했다면 클래스 이름 뒤에 패턴 이름을 붙인다.

의사소통과 표현력이 강화된다

 

넷째, 단위 테스트 케이스를 꼼꼼히 작성하라

테스트 케이스는 예제로 보여주는 문서와도 같다.

다시 말해 만든 테스트 케이스를 읽어보면 클래스 기능을 이해할 있게 된다

 

마지막으로 가장 중요한 방법은 노력이다.

흔히 코드만 돌린 다음 문제로 직행하는 개발자가 많다.

나중에 읽을 사람을 고려해 조금이라도 노력하지 않는 개발자가 많다.

하지만 나중에 코드를 읽을 사람은 바로 자신일 가능성이 높다는 사실을 명심하자

 

함수와 클래스에 조금 시간을 투자하자.

나은 이름을 선택하고,

함수를 작은 함수 여럿으로 나누고,

자신의 작품에 조금만 주의를 기울이자.

주의는 대단한 재능이다.

 

클래스와 메서드 수를 최소로 줄여라

중복 제거, 의도 표현, SRP 준수를 극단적으로극단적으로 지키다 보면 득 보 다실이실이 많아진다.

클래스와 메서드 크기를 줄이자고 조그마한 클래스와 메서드를 수 없이 만드는 사례도 없지 않다.

그래서 규칙은 함수나 클래스 수를 "가능한" 줄이라 것이다.

 

때로는 무의미하고 독단적인 정책 탓에 클래스 수와 메서드 수가 늘어나기도 한다.

클래스마다 무조건 인터페이스를 생성하라고 요구하는 구현 표준이 좋은 예다.

자료 클래스와 동작 클래스는 무조건 분리해야 한다고 주장하는 개발자도 좋은 예다.

가능한 독단적인 견해는 멀리하고 실용적인 방식을 택한다.

 

목표는 함수와 클래스 크기를 작게 유지하면서 동시에 시스템 크기도 작게 유지하는 있다.

하지만! 규칙은 간단한 설계 규칙 우선순위가 가장 낮다.

다시 말해, 클래스와 함수 수를 줄이는 작업도 중요하지만, 테스트 케이스를 만들고 중복을 제거하고 의도를 표현하는 작업이 중요하다는 뜻이다.

 

 

결론

경험을 대신할 단순한 개발 기법은 없다. 하지만 책에서 소개한 기법은 저자들의 수십 경험이 녹아있다. 규칙을 따르다 보면 나중에 오랜 경험이 쌓인 기법을 활용할 있을 것이다.

 

 

 

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

13장 동시성 - 2  (1) 2022.11.12
13장 동시성 - 1  (0) 2022.11.08
11장 시스템  (0) 2022.11.01
10장 클래스  (0) 2022.10.26
9장 단위 테스트  (0) 2022.10.21

복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.

도시를 세운다면?

여러분이 도시를 세운다면? 온갖 세세한 사항을 혼자서 직접 관리할 있을까?

불가능하다.

이미 세워진 도시라도 사람의 힘으론 무리다.

도시들이 돌아가는 이유는 수도 관리팀, 전력관리팀, … 분야를 관리하는 팀이 있기 때문이다. 도시의 그림을 그리는 사람도 있으며, 작은 사항에 집중하는 사람도 있다

도시가 돌아가는 다른 이유!!

적절한 추상화와 모듈화 때문이다. 그래서 그림을 이해하지 못할지라도 개인과 개인이 관리하는 구성요소는 효율적으로 돌아간다.

 

소프트웨어 팀도 도시처럼 구성한다.

깨끗한 코드를 구현하면 낮은 추상화 수준에서 관심사를 분리하기 쉬워진다.

장에서는 높은 추상화 수준, 시스템 수준에서도 깨끗함을 유지하는 방법을 살펴본다.

 

시스템 제작과 시스템 사용을 분리하라

제작(construction) 사용(use) 아주 다르다

호텔을 짓기 위해 작업복과 안전모를 사람들이 열심히 일을 한다. 내부 인테리어까지 예쁘게 꾸미고 나면 이제 호텔에 있는 사람들은 잘차려진 복장을 입은 사람들이다.

 

소프트웨어 시스템은 애플리케이션 객체를 제작하고 의존성을 서로 연결하는 준비 과정과 준비 과정 이후에 이어지는 런타임 로직을 분리해야 한다.

 

시작 단계는 모든 애플리케이션이 풀어야 관심사다 관심사 분리는 우리 분야에서 가장 오래되고 가장 중요한 설계 기법 하나다.

불행히도 대다수 애플리케이션은 시작 단계라는 관심사를 분리하지 않는다.

Main 분리

시스템 생성과 시스템 사용을 분리하는 가지 방법 소개

생성과 관련한 코드는 모두 main이나 main 호출하는 모듈로 옮긴다.

나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.

 

main함수에서 시스템에 필요한 객체를 생성한 이를 애플리케이션에 넘긴다. 애플리케이션은 그저 객체를 사용할 뿐이다.

애플리케이션은 main이나 객체가 생성되는 과정을 전혀 모른다. 그저 적절히 생성됐다고 가정한다.

 

의존성 주입

사용과 제작을 분리하는 강력한 메커니즘 하나가 의존성 주입이다. (Dependency Injection)

의존성 주입은 제어의 역전(Inversion of Control) 기법 의존성 관리에 적용한 메커니즘이다.

제어 역전에서는 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다.

새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임 원칙을 지키게 된다.

의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임을 지지 않는다.

대신에 이런 책임을 다른 '전담' 메커니즘에 넘겨야만 한다.

그렇게 함스로써 제어를 역전한다.

초기 설정은 시스템 전체에서 필요하므로 대개 '책임질' 메커니즘으로 main 루틴이나 특수 컨테이너 사용한다.

 

DI컨테이너는 대개 요청이 들어올 때마다 필요한 객체의 인스턴스를 만든 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다.

실제로 생성되는 객체 유형은 설정 파일에서 지정하거나 특수 생성 모듈에서 코드로 명시한다

 

스프링 프레임워크는 가장 널리 알려진 자바 DI 컨테이너를 제공한다.

객체 사이 의존성은 XML 파일에 정의한다.

그리고 자바 코드에서는 이름으로 특정한 객체를 요청한다.

그러나 초기화 지연으로 얻는 장점은 포기해야하는 걸까?

기법은 DI를사용하더라도 때론 여전히 유용하다.

대다수 DI 컨테이너는 필요할 때까지는 객체를 생성하지 않고, 대부분은 계산 지연이나 비슷한 최적화에 있도록 팩토리를 호출하거나 프록시를 생성하는 방법을 제공한다.

, 계산 지연 기법이나 이와 유사한 최적화 기법에서 이런 메커니즘을 사용할 있다.

 

확장(Open-Closed Principle 원칙을 기억하자)

군락-> 마을-> 도시이렇게 점차 규모 확장되며 그게 맞게 도로 같은 인프라도 확장해야하지만 꽉막힌 도로를 보면서 처음부터 도로를 6차선으로 뚫지 않았나라고 생각한적 있는가?

반대로 생각해보자. 마을 규모에서 반드시 도시로 성장할 것이라 생각하고 처음부터 6차선 도로를 뚫을 것인가?

 

'처음부터 올바르게' 시스템을 만들 있다는 믿음은 미신이다.

당장 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다.

내일은 새로운 스토리에 맞춰 시스템을 조정하고 확장하면 된다.

이것을 반복적이고 점진적인 애자일 방식의 핵심이다.

테스트 주도 개발(Test-driven Development, TDD),리팩터링, 깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만든다

 

하지만 시스템 수준에서는 어떨까? 시스템 아키텍처는 사전 계획이 필요하지 않을까?

단순한 아키텍처를 복잡한 아키텍처로 조금씩 키울 없다는 현실은 정확하다.

 

소프트웨어 시스템은 물리적인 시스템과 다르다.

관심사를 적절히 분리해 관리한다면 소프트웨어 아키텍처는 점진적으로 발전할 있다.

 

소프트웨어 시스템은 '수명이 짧다' 본질로 인해 아키텍처의 점진적인 발전이 가능하다.

먼저, 관심사를 적절히 분리하지 못하는 아키텍처 예를 소개한다.

 

EJB1, EJB2 아키텍처는 관심사를 적절히 분리를 하지 못했다. 따라서 유기적인 성장이 어려웠다.

불필요한 장벽이 생긴 탓이다.

 

횡단(cross-cutting) 관심사

EJB2 아키텍처는 일부 영역에서 관심사를 거의 완벽하게 분리한다.

, 원하는 트랜잭션, 보안, 일부 연속적인 동작은 소스코드가 아니라 '배치 기술자'에서 정의한다.

 

영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다.

모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다.

예를 들어, 특정 DBMS 독자적인 파일을 사용하고, 테이블과 열은 같은 명명 관례를 따르며, 트랜잭션 의미가 일관적이면 더욱 바람직하다.

원론적으로는 모듈화 되고 캡슐화된 방식으로 영속성 방식을 구상 있다. 영속성 방식을 구현한 코드가 온갖 객체로 흩어진다. 여기서 횡단 관심사라는 용어가 나온다. 영속성 프레임워크 또한 모듈화 할 수 있다. 도메인 논리도 독자적으로 모듈화할 수 있다. 문제는 영역이 세밀한 단위로 겹친다는 점이다.

 

EJB아키텍처가 영속성, 보안, 트랜잭션을 처리하는 방식은 관점 지향 프로그래밍(Aspect-Oriented Programming, AOP) 예견했다고 본다. AOP 횡단 관심사에 대처해 모듈성을 확보하는 일반적인 방법론이다.

 

AOP에서 관점(aspect)이라는 모듈 구성 개념은  "특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다"라고 명시한다. 명시는 간결한 선언이나 프로그래밍 메커니즘으로 수행한다.

영속성을 예로 들면, 프로그래머는 영속적으로 저장할 객체와 속성을 선언한 영속성 책임을 영속성 프레임워크에 위임한다. AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다

 

 

자바에서 사용하는 관점 혹은 관점과 유사한 메커니즘 개를 살펴보자

자바 프락시

자바 프락시는 단순한 상황에 적합하다.

개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예다.

하지만 JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다.

클래스 프락시를 사용하려면 CGLIB, ASM Javassist 등과 같은 바이트 코드 처리 라이브러리가 필요하다. 스프링은 CGLIB 기본 탑재

import java.lang.reflect.*;

public class Test {
	public static void main(String[] args) {
		Runnable instance = 
				(Runnable) Proxy.newProxyInstance(
						Runnable.class.getClassLoader()
						, new Class[] {Runnable.class} 
				        , new PersonHandler(new Person()) );
		instance.run("빠르게");
		instance.eat("맛있게");
	}
}
class PersonHandler implements InvocationHandler{
	Runnable runnable;
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		String methodName = method.getName();
		
		if(methodName.equals("run")) {
			runnable.run((String) args[0]);
		}else if (methodName.equals("eat")) {
			runnable.eat((String) args[0]);
		}
		return null;
	}
	public PersonHandler(Runnable runnable) {
		this.runnable = runnable;
	}
}

interface Runnable {
	void run(String str);
	void eat(String str);
}

class Person implements Runnable{
	@Override
	public void run(String str) {
		System.out.println(str + "달린다.");
	}
	@Override
	public void eat(String str) {
		System.out.println(str + "먹는다.");
		
	}
}

프락시에는 InvocationHandler를 넘겨줘야 한다.한다.

단순한 예제지만 코드가 상당히 많으며 제법 복잡하다.

바이트 조작 라이브러리를 사용하더라도 만만찮게 어렵다.

코드 양과 크기는 프락시의 두 가지 단점이다.

다시 말해, 프락시를 사용하면사용하면 깨끗한 코드를 작성하기 어렵다

또한 프락시는 AOP 해법에 필요한 시스템 단위로 실행 지점을 명시하는 메커니즘도 제공하지 않는다.

순수 자바 AOP 프레임워크

복잡한 프록시 코드는 대부분 판박이라 다행스럽게도 도구로 자동화 있다

순수 자바 관점을 구현하는 스프링 AOP, JBoss AOP 등과 같은 여러 자바 프레임워크는 내부적으로 프락시를 사용한다. 스프링은 비즈니스 논리를 POJO 구현한다

POJO  순수하게 도메인에 초점 맞춘다. POJO 엔터프라이즈 프레임워크에 의존하지 않는다. 그리고 단순하다. 따라서 테스트와 유지보수가 쉽다.

 

프로그래머는 설정 파일이나 API 사용해 필수적인 애플리케이션 기반 구조를 구현한다.

영속성, 트랜잭션, 보안, 캐시, 장애조치 등과 같은 횡단 괌 심사도 포함된다

많은 경우 실제로는 스프링이나 JBoss라이브러리의 관점을 명시한다.

이때 프레임워크는 사용자가 모르게 프락시나 바이트코드 라이브러리를 사용해 이를 구현한다.

이런 선언들이 요청에 따라 주요 객체를 생성하고 서로 연결하는 DI 컨테이너의 구체적인 동작을 제어한다.

 

AspectJ관점

마지막으로, 관심사를 관점으로 분리하는 가장 강력한 도구는 AspectJ언어다

AspectJ 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다.

 

앞전에 스프링 AOP JBoss AOP 제공하는 순수 자바 방식은 관점이 필요한 상황 80-90% 충분하다. AspectJ 관점을 분리하는 강력하고 풍부한 도구 집합을 제공하지만, 언어 문법과 사용법을 익여야 된다는 단점 있다.

 

최근 나온 AspectJ 애너테이션폼은폼은 이런 부담을 어느 정도 완화한다

애너테이션폼은순수한 자바 코드에 자바 5 애너테이션을사용해 관점을 정의한다.

추가로 스프링 프레임워크는 AspectJ 미숙한 개발자들을 위해 애너테이션 기반 관점을 쉽게 다용하도록 다양한 기능을 제공한다.

 

AspectJ 대한 상세한 설명은 범위를 벗어난다. 자세한 내용은 AspectJ, Colyer, Spring 참조한다.

테스트 주도 시스템 아키택처 구축

관점으로 혹은 유사한 개념으로 관심사를 분리하는 방식은 위력이 막강하다.

애플리케이션 도메인 논리를 POJO 작성할 있다면, 코드 수준에서 아키텍처 관심사를 분리할 있다면 진정한 테스트 주도 아키텍처 구축이 가능해진다.

그때그때 새로운 기술을 채택해 단순한 아키텍처를 복잡한 아키텍처로 키워갈 수도 있다.(처음부터 거대한 시스템을 설계할 필요가 없다 아니 안 해도 된다.)

BDUF(Big Design Up Front) 추구할 필요가 없다. 실제로 BDUF 해롭기까지 하다.

처음에 쏟아부은 노력을 버리지 않으려는 심리적 저항으로 인해, 그리고 처음에 쏟아부은 노력을 버리지 않으려는 심리적 저항으로 인해, 그리고 처음 선택한 아키텍처가 향후 사고방식에 미치는 영향으로 인해, 변경을 쉽사리 수용하지 못하는 탓이다.

 

건축가는 BDUF 방식을 취한다. 물리적 구조는 일단 짓기 시작하면 극적인 변경이 불가능한 탓이다.

소프트웨어 역시 나름대로 형체가 있지만, 소프트웨어 구조가 관점을 효과적으로 분리한다면, 극적인 변화가 강제적으로 가능하다.

 

다시 말해, 아주 단순하면서도 멋지게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 결과물을 재빨리 출시한 , 기반 구조를 추가하며 조금씩 확장해 나가도 괜찮다 말이다.

세계 최대 사이트들은 고도의 자료 캐싱, 보안, 가상화 등을 이용해 아주 높은 가용성과 성능을 효율적이고도 유연하게 달성했다.

설계가 최대한 분리되어 추상화 수준과 범위에서 코드가 적당히 단순하기 때문이다.

 

그렇다고 아무 방향 없이 프로젝트에 뛰어들어도 좋다는 소리는 아니다.

프로젝트를 시작할 때는 일반적인 범위, 목표, 일정은 물론이고 결과로 내놓을 시스템의 일반적인 구조도 생각해야 한다. 하지만 변하는 환경에 대처해 진로를 변경할 능력도 반드시 유지해야 한다.

 

초창기 EJB 아키텍처는 기술을 너무 많이 넣느라 관심사를 제대로 분리하지 못했던 유명한 API 하나다.

설계가 아주 멋진 API 조차도 정말 필요하지 않으면 과유불급이다.

좋은 API 걸리적거리지 않아야 한다.

그래야 팀이 창의적인 노력을 사용자 스토리에 집중한다.

그리하지 않으면 아키텍처에 발이 묶여 고객에게 최적의 가치를 효율적으로 제공하지 못한다.

 

요약

최선의 시스템 구조는 각기 POJO 객체로 구현되는 모듈화 된 관심사 영역(도메인)으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다. 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 있다.

 

 

 

의사 결정을 최적화하라

모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다.

아주 시스템에서는 사람이 모든 결정을 내리기 어렵다.

가장 적합한 사람에게 책임을 맡기면 가장 좋다.

우리는 때때로 가능한 마지막 순간까지 결정을 미루는 방법이 최선이라는 사실을 까먹곤 한다.

게으르거나 무책임해서가 아니다.

최대한 정보를 모아 최선의 결정을 내리기 위해서다.

성급한 결정은 불충분한 지식으로 내린 결정이다.

너무 일찍 결정하면 고객 피드백을 모으고, 프로젝트를 고민하고, 구현 방안을 탐험할 기회가 사라진다.

 

관심사를 모듈로 분리한 POJO시스템은 기민함을 제공한다. 이런 기민함 덕택에 최신 정보에 기반해 최선의 시점에 최적의 결정을 내리기가 쉬워진다. 또한 결정의 복잡성도 줄어든다.

 

 

 

 

명백한 가치가 있을 표준을 현명하게 사용하라

EJB2 단지 표준이라는 이유만으로 많은 팀이 사용했다.

가볍고 간단한 설계로 충분했을 프로젝트에서도 EJB2 채택했다.

나는 업계에서 여러 형태로 아주 과장되게 포장된 표준에 집착하는 바람에 고객 가치가 뒷전으로 밀려난 사례를 많이 봤다.

 

표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다. 하지만 때로는 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못한다. 어떤 표준은 원래 표준을 제정한 목적을 잊어버리기도 한다.

 

 

 

 

시스템은 도메인 특화 언어가 필요하다.

대다수 도메인과 마찬가지로, 건축 분야 역시 필수적인 정보를 명료하고 정확하게 전달하는 어휘, 관용구, 패턴이 풍부하다.

소프트웨어 분야에서도 최근 들어 DSL(Domain-Specific Language) 새롭게 조명받기 시작했다.

DSL(Domain-Specific Language) 간단한 스크립트 언어나 표준 언어로 구현한 API 가리킨다.

DSL 코드는 도메인 전문가가 작성한 구조적인 산문처럼 읽힌다.

좋은 DSL 도메인 개념과 개념을 구현한 코드 사이에 존재하는 의사소통 간극을 줄여준다.

도메인 전문가가 사용하는 언어로 도메인 논리를 구현하면 도메인을 잘못 구현할 가능성이 줄어든다.

 

효과적으로 사용한다면 DSL 추상화 수준을 코드 관용구나 디자인 패턴 이상으로 끌어올린다. 그래서 개발자가 적절한 추상화 수준에서 코드 의도를 표현할 있다.

 

도메인 특화 언어를 사용하면 고차원 정책에서 저 차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO 표현할 있다.

 

 

 

결론

시스템 역시 깨끗해야 한다.

깨끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다.

도메인 논리가 흐려지면 제품 품질이 떨어진다.

버그가 숨어들기 쉬워지고, 스토리를 구현하기 어려워지는 탓이다.

기민성이 떨어지면 생산성이 낮아져 TDD 제공하는 장점이 사라진다.

 

모든 추상화 단계에서 의도는 명확히 표현해야 한다.

그러려면 POJO 작성하고 관점 혹은 관점과 유사한 메커니즘을 사용해 구현 관심사를 분리해야 한다.

 

시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌아가는 가장 단순한 수단을 사용해야 한다는 사실을 명심하자.

 

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

13장 동시성 - 1  (0) 2022.11.08
12장 창발성  (0) 2022.11.05
10장 클래스  (0) 2022.10.26
9장 단위 테스트  (0) 2022.10.21
8장 경계  (0) 2022.10.18

+ Recent posts