도메인

소프트웨어로 해결하고자 하는 문제영역을 도메인이라고 한다.
도메인은 하위 도메인으로 이루어 지기도 하는데, 예를 들어 주문이라는 도메인에는 혜택, 결제, 배송, 정산이라는 하위 도메인이 존재한다. 그러나 모든 도메인이 하위 도메인이 존재하는 것은 아니므로, 상황에 따라 하위 도메인 구성을 판단하면 된다.


도메인 모델

특정 도메인을 개념적으로 표현한 것을 도메인 모델이라고 한다.
도메인을 이해하기 위해서는 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 하는데, 이런 면에서 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기에 적합하다.
도메인 모델을 모델링 하는 방법은 객체 모델 외에도 상태 다이어그램이나 UML 표기기법 외에도 그래프(관계가 중요한 도메인일 시), 수학 공식(계산 규칙이 중요한 도메인일 시)을 이용해서 도메인을 모델링할 수 있다.
도메인 모델은 표현 방식이 중요한 것이 아니라 도메인을 잘 이해할 수 있도록 하는 개념 모델이다.
개념 모델은 바로 코드를 작성하기에 적합하지 않으므로 개념 모델을 최대한 따르는 구현 모델이 추가적으로 필요하다.


도메인 모델 패턴

일반적인 어플리케이션 아키텍처는 네 개의 계층으로 나뉜다.

  • 표현 - 사용자의 요청을 처리, 응답
  • 응용 - 직덥 도메인 로직을 가지지 않고 도메인 계층을 조합해서 사용자 요청에 대한 기능을 실행
  • 도메인 - 시스템이 제공할 도메인의 규칙을 구현
  • 인프라스트럭처 - DB와 같은 외부 시스템과 연동

도메인 모델 도출

개발자들은 기획서, 유즈케이스, 사용자 스토리 등을 통해 요구사항의 도메인을 이해한 후에 이를 통해 도메인 모델을 만들고 개발을 시작해야 한다.
도메인 모델링의 기본 작업?

  • 모델을 구성하는 핵심요소, 규칙, 기능을 찾기

엔티티와 밸류

도메인 모델은 두가지로 구분할 수 있음

Entity

  • 식별자를 가진다는 것이 큰 특징
  • 식별자는 바뀌지 않고 고유함
    • 엔티티를 구현한 클래스는 식별자를 이용해 equals(), hashCode()메서드를 구현할 수 있음

Entity 식별자 생성

Entity 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.

  • 특정 규칙에 따라 생성
  • UUID(Universally Unique Identifier) 사용
    • 다수의 개발 언어는 UUID 생성기를 제공
  • 값을 직접 입력
    • e.g.) 회원 아이디 / 이메일 : 중복 케이스 발생 가능성이 있으므로 중복 입력을 막는 로직이 필요
  • 일련번호 사용(시퀀스 / DB auto increment)

Value type

Value type은 개념적으로 완전한 하나(!??!?!!!)를 표현할 떄 사용

Bad :

public class ShippingInfo {
  // 받는 사람
  private String receiverName;
  private String receiverPhoneNumber;

  // 주소
  private String shippingAddress1; 
  private String shippingAddress2;
  private String shippingZipcode;

  ...
}

Good : 받는 사람, 주소 자체가 하나의 도메인 개념이므로 Value type을 사용함으로써 개념적으로 온전한 하나의 도메인을 표현

public class Receiver {
  private String name;
  private String phoneNumber;

  ...
}

public class Address {
  private String address1; 
  private String address2;
  private String zipcode;

  ...
}

public class ShippingInfo {
  private Receiver receiver;
  private Address address;

  ...
}

Value type의 장점

  • 해당 데이터(값/변수)의 도메인(개념/의미)을 명확하게 명시할 수 있음
  • 해당 Value type을 위한 기능 추가 가능

Value type 사용시 특징

  • 데이터 변경 시 해당 인스턴스의 값을 변경하는 것이 아닌 변경 할 값을 가지는 새로운 인스턴스를 생성하는 방식을 선호 -> 불변 객체로 사용하라!
    • 의도치 않은 곳에서 데이터가 변경되는 것을 방지하기 위함
  • Value 객체 비교 시, 모든 속성이 같은지 비교해야 함

엔티티 식별자와 밸류 타입

일반적으로 식별자는 String 타입이지만, 식별자가 도메인 특성을 가지는 경우 식별자를 위한 Value type을 사용해서 의미가 잘 드러나도록 한다.

도메인 모델에 set 메서드 넣지 않기

  • get/set 메서드로 인해(특히 set!) 값이 어디서든 바뀔 수 있기 때문에 도메인 핵심 개념이나 의도가 코드에서 사라지게 됨
  • 기본 생성자에 set 메서드로 값을 전달하다보면 객체 내부에 누락되는 데이터가 존재할 수 있음
    • 도메인 객체는 생성 시점에 필요한 것을 전달!

도메인 용어

도메인에서 사용하는 용어는 코드에 그대로 사용해야 가독성도 좋아지고 버그도 줄어들게 된다.

반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 10장. 클래스  (0) 2020.05.31
[CleanCode] 9장. 단위 테스트  (0) 2020.05.18
[CleanCode] 8장. 경계  (0) 2020.05.18
[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15

클래스 체계

클래스 정의 표준 자바 관례에 따른 클래스 구성 순서

  • public satic 상수
  • private static 변수
  • private instance 변수
  • public 함수
  • private 함수 - 자신을 호출하는 public 함수 직후에 위치

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않아야 함
테스트를 위해 protected 선언/패키지 전체 공개, but!!!! 캡슐화를 풀어주는 결정은 최후의 수단


클래스는 작아야 한다!

클래스를 만들때의 규칙은 무조건 작게!
얼마나 작아야 할까?
함수와는 다르게 맡은 책임의 수가 기준

클래스의 책임?
네이밍이 나타낸다. 구현 과정에서 간결한 이름이 떠오르지 않거나 클래스 이름이 모호하다는 것은 책임이 많다는 반증

  • 단일 책임의 원칙

    Single Responsibility Principle
    클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다

  • Bad : 버전에 대한 책임과 Component에 대한 책임을 모두 가지고 있음*

     public class SuperDashboard extends JFrame implements MetaDateUser {
       public Component getLastFocusedComponent()
       public void setLastFocused(Component lastFocused)
       public int getMajorVersionNumber()
       public int getMinorVersionNumber()
       public int getBuildNumber()
     }

    Good : SuperDashboard 클래스는 Component에 대한 책임만을 갖고 별도의 Version에 대한 책임을 가지는 클래스를 분리

       public class Version {
         public int getMajorVersionNumber()
         public int getMinorVersionNumber()
         public int getBuildNumber()
       }
    
       public class SuperDashboard extends JFrame implements MetaDateUser {
         public Component getLastFocusedComponent()
         public void setLastFocused(Component lastFocused)
       }
  • 응집도

    클래스는 인스턴스 변수 수가 적어야 함
    메서드가 클래스 변수를 많이 사용 할수록 클래스는 응집도가 더 높아짐
    응집도가 높다? 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미이기도 함
    몇몇 메서드만이 사용하는 인스턴스 변수가 많아지게 되면 클래스를 쪼개야 한다는 신호

  • 응집도를 유지하면 작은 클래스 여럿이 나온다

    큰 함수의 일부를 작은 함수로 나눌 수 있을 때 나눌 부분에서 사용되는 변수가 큰 함수 내의 다른 곳에서 사용되지 않으면 클래스로 분리가 가능하다.
    클래스로 분리하였을 때 응집도가 떨어진다면 다시 클래스를 쪼개야 한다.


변경하기 쉬운 클래스

깨끗한 시스템? 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춤
새 기능을 추가할 때 시스템을 확장할 뿐 기존 코드를 변경하지 않도록 클래스를 설계하는 것이 매우 중요!!!

Bad :아래의 클래스는 새로운 SQL문을 지원할 때(책임1), 기존 SQL문을 수정할 때(책임2) 수정이 필요하므로 SRP 위반

public class Sql {
    public Sql(String table, Column[] columns)
    public String create()
    public String insert(Object[] fields)
    public String selectAll()
    public String findByKey(String keyColumn, String keyValue)
    public String select(Column column, String pattern)
    public String select(Criteria criteria)
    public String preparedInsert()
  // 일부 메서드에서만 사용되는 비공개 메서드는 코드 개선의 잠재적인 여지를 시사
    private String columnList(Column[] columns)
    private String valuesList(Object[] fields, final Column[] columns) private String selectWithCriteria(String criteria)
    private String placeholderList(Column[] columns)
}

Good :
공개 인터페이스를 전부 SQL 클래스에서 파생하는 클래스로 분리
비공개 메서드는 해당 메서드를 사용하는 클래스로 이동
공통된 인터페이스는 따로 클래스로 분리
아래와 같이 분리 후에는 새로운 SQL문 추가 시, 기존 SQL문 수정 시에 기존의 클래스를 건드릴 이유가 없어짐

abstract public class Sql {
    public Sql(String table, Column[] columns) 
    abstract public String generate();
}

public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns) 
    @Override public String generate()
}

public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns) 
    @Override public String generate()
}

public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns, Object[] fields) 
    @Override public String generate()
    private String valuesList(Object[] fields, final Column[] columns)
}

public class SelectWithCriteriaSql extends Sql { 
    public SelectWithCriteriaSql(
    String table, Column[] columns, Criteria criteria) 
    @Override public String generate()
}

public class SelectWithMatchSql extends Sql { 
    public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern) 
    @Override public String generate()
}

public class FindByKeySql extends Sql public FindByKeySql(
    String table, Column[] columns, String keyColumn, String keyValue) 
    @Override public String generate()
}

public class PreparedInsertSql extends Sql {
    public PreparedInsertSql(String table, Column[] columns) 
    @Override public String generate() {
    private String placeholderList(Column[] columns)
}

public class Where {
    public Where(String criteria) public String generate()
}

public class ColumnList {
    public ColumnList(Column[] columns) public String generate()
}

결과적으로 OCP(Open-Closed-Priciple)도 지킬 수 있게됨
OCP란? 확장에 개방적이로 수정에 폐쇄적이어야 한다는 객체 지향 설계의 핵심 원칙 중 하나

  • 변경으로부터 격리
    인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리
    매번 달라지는 API로 테스트 코드를 짜기 힘듦 -> API를 구현하는 기능을 인터페이스로 분리하라!
    테스트 시 해당 인터페이스를 상속받아 API를 흉내내는 테스트용 클래스를 만들어 테스트가 가능해짐
    e.g.,

    public interface stockExchange {
      Money currentPrice(String symbol);  // API의 결과값을 뱉어주는 메서드
    }
    
    public Portfolio {
      private StockExchange exchange;
      public Portfolio(StockExchange exchange) {
        this.exchange = exchange;
      }
      // ...
    }
    
    public class PortfolioTest {
      private FixedStockExchangeStub exchange;
      private Portfolio portfolio;
    
      @Before
      protected void setUp() throws Exception {
        exchange = new FixedStockExchangeStub();
        exchange.fix("MSFT", 100);
        portfolio = new Portfolio(exchange);
      }
    
      @Test
      public void GivenFiveMSFTTotalShouldBe500() throws Exception {
        portfolio.add(5, "MSFT");
        Assert.assertEquals(500, portfolio.value());
      }
    }

    결합도가 낮아짐!
    -> 각 시스템 요소(클래스)가 다른 요소로부터 그리고 변경으로부터 잘 격리되어 있다는 의미
    결합도가 낮아지면 자연스럽게 DIP(Dependency-Inversion-Priciple)을 따르는 클래스가 구현 됨
    DIP란? 클래스가 상세한 구현이 아니라 추상화에 의존해 다른 클래스의 변경에 영향받지 않도록 한다는 원칙

반응형

'Dev > Books' 카테고리의 다른 글

[DDD Start!] 1장. 도메인 모델 시작  (0) 2020.06.06
[CleanCode] 9장. 단위 테스트  (0) 2020.05.18
[CleanCode] 8장. 경계  (0) 2020.05.18
[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15

목차

  1. TDD 법칙 세 가지
  2. 깨끗한 테스트 코드 유지하기
  3. 깨끗한 테스트 코드
  4. 테스트 당 assert 하나
  5. F.I.R.S.T

TDD 법칙 세 가지

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

깨끗한 테스트 코드 유지하기

깨끗하지 않은 테스트 코드는 유지 보수에 많은 비용이 필요하게 되고, 테스트 코드가 없으면 프로덕션 코드의 결함율이 높아지게 된다.
테스트 코드는 유연성, 유지보수성, 재사용성을 제공해야 한다.


깨끗한 테스트 코드

깨끗한 테스트 코드는 명료성, 단순성, 풍부한 표현을 바탕으로 한 가독성이 필수이다.


테스트 당 assert 하나

assert 하나만을 사용하기 위해 아래와 같은 방법을 사용 할 수 있다.

  • 테스트를 쪼개서 assert를 분리
  • Template Method 패턴을 사용해 중복을 제거
  • @Before 함수에 given/when 부분을 분리

테스트 함수 하나는 개념 하나만 테스트 해야 한다!


F.I.R.S.T

  • Fast
  • Independent
  • Repeatable
    네트워크가 없는 환경에서도 실행 가능해야 함
  • Self-Validating
    테스트는 성공 혹은 실패로만 결과가 나와야 한다. 별도의 log 등을 통한 확인 작업은 절대 금물
  • Timely
    테스트 코드 작성 -> 실제 코드 작성


참고

BUILD-OPERATE-CHECK 패턴

Given-When-Then 패턴과도 비슷

  • Build (Given)
    Input 데이터를 생성
  • Operate (When)
    Build 단계에서 생성한 데이터로 실제 코드 실행
  • Check (Then)
    Operate 단계의 결과값을 확인

e.g.

public class BowlingTest {
  @Test
  public void FrameTest() {
    // Build (Given)
    Boll testBoll = Boll.getInstance(10);
    Frame frame = Frame.getInstance(Boll);
    // Operate (When)
       Boolean isStrike = frame.isStrike();
    // Check (Then)
    assertThat(isStrike).isTrue();
  }
}

TEMPLATE METHOD 패턴

어떠한 기능의 일부분을 서브 클래스로 캡슐화해 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계의 수행 내역을 바꾸는 패턴
e.g.

Original :

public class SumoDeadLift {
  final void start() {
    stand();
    getBarbell();
    doOneRep();
  }

  private void stand() {  // 각 DeadLift 별로 수행 내역이 다른 부분
      System.out.println("다리를 넓게 벌리고 선다.");
  }

  private void getBarbell() {  // 모든 DeadLift의 공통된 알고리즘
    System.out.println("바벨을 들어올린다.");
  }

  private void doOneRep() {
    System.out.println("바벨을 다시 바닥으로 내려놓는다.");
  }
}

public class RumanianDeadLift {
  final void start() {
    stand();
    getBarbell();
    doOneRep();
  }

  private void stand() {
      System.out.println("다리를 골반 넓이 만큼 벌리고 선다.");
  }

  private void getBarbell() {
    System.out.println("바벨을 들어올린다.");
  }

  private void doOneRep() {
    System.out.println("정강이 중간까지만 내려간다");
  }
}

With Template Method Pattern: 모든 DeadLift의 수행 내역이 같은 부분은 추상 클래스에 정의한다.

public abstract class DeadLift {
  final void start() {
    stand();
    getBarbell();
    doOneRep();
  }

  abstract void stand();

  private void getBarbell() {  // 모든 DeadLift의 공통된 알고리즘
    System.out.println("바벨을 들어올린다.");
  }

  abstract void doOneRep();
}

public class SumoDeadLift extends DeadLift {
  @Override
  private void stand() {  // 각 DeadLift 별로 수행 내역이 다른 부분
      System.out.println("다리를 넓게 벌리고 선다.");
  }

  @Override
  private void doOneRep() {
    System.out.println("바벨을 다시 바닥으로 내려놓는다.");
  }
}

public class RumanianDeadLift extends DeadLift {
  @Override  
  private void stand() {
      System.out.println("다리를 골반 넓이 만큼 벌리고 선다.");
  }

  @Override
  private void doOneRep() {
    System.out.println("정강이 중간까지만 내려간다");
  }
}
반응형

'Dev > Books' 카테고리의 다른 글

[DDD Start!] 1장. 도메인 모델 시작  (0) 2020.06.06
[CleanCode] 10장. 클래스  (0) 2020.05.31
[CleanCode] 8장. 경계  (0) 2020.05.18
[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15

목차

  1. 외부 코드 사용하기
  2. 경계 살피고 익히기
  3. log4j 익히기
  4. 학습 테스트는 공짜 이상이다
  5. 아직 존재하지 않는 코드를 사용하기
  6. 깨끗한 경계

패키지를 사거나, 오픈소스를 사용하는 등 외부의 코드와 팀의 코드를 함께 사용할 때 소프트웨어 경계를 깔끔하게 처리하여 통합하는 방법에 대하여 이야기 한다.


외부 코드 사용하기

  • 인터페이스 제공자
    더 많은 환경에 돌아가게 하기 위해 적용성을 최대한 넓히려 함
  • 인터페이스 사용자
    자신의 요구에 집중하는 인터페이스를 바람

경계 인터페이스가 외부에 공개되지 않도록 한다.
캡슐화 등을 통해 API의 인수나 메서드의 반환 값 등으로 경계 인터페이스를 직접 노출하지 않도록 한다. 이 방법을 통해 경계 인터페이스가 변경 되었을 때 관련된 코드를 모두 고치지 않아도 되고, 불필요한 인터페이스를 노출시키지 않는 장점도 얻을 수 있다.

Bad :

Map sensors = new HashMap();
Sensor s = (Sensors)sensors.get(sensorId);

Good :

public class Sensors {
  private Map sensors = new HashMap();

  public Sensor getById(String id) {
    return (Sensor) sensors.get(id);
  }
}

경계 살피고 익히기

사용하려는 방식대로 외부 API를 호출하는 학습 테스트 작성을 통해 API를 익힌다.


log4j 익히기

문서를 모두 다 읽어서 라이브러리를 익히는 것이 아닌, 학습 테스트를 작성하며 발생하는 에러들을 해결해 나가며 학습한다.
이 과정에서 얻은 지식을 단위 테스트로 정리하고 독자적인 클래스로 분리하면 나머지 클래스는 해당 라이브러리의 경계 인터페이스를 몰라도 된다.

학습 테스트는 공짜 이상이다

학습 테스트는 해당 API를 학습하는 것 외에도 장점을 가진다.
학습 테스트를 위해 생성한 테스트 코드로 새 버전이 출시되었을 때 해당 버전이 우리 코드에 호환되는지를 확인할 수 있다.


아직 존재하지 않는 코드를 사용하기

아직 존재하지 않아 모르는 기능을 사용해야 할 때는 인터페이스로 구현한다.
인터페이스로 구현하면?

  • 인터페이스를 전적으로 통제 할 수 있다
  • 코드 가독성이 높아지고 의도가 분명해진다
  • 테스트 코드 작성에 용이하다

깨끗한 경계

외부 인터페이스를 사용할 때 경계에 위치하는 코드는 깔끔히 분리하고 사용할 의도에 알맞는 테스트 케이스를 작성하여 향후 변경 비용이 커지지 않도록 주의해야 한다.

  • 새로운 클래스로 경계를 감싼다
  • ADAPTER 패턴을 활용하자


참고하면 좋을 문서

일급 컬렉션

  • https://jojoldu.tistory.com/412 - 컬렉션도 외부 인터페이스이다. 일급 컬렉션은 외부 인터페이스 사용하기 단락의 의미대로 컬렉션을 사용하는 방법이다.

Adapter Pattern

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 인터페이스로 변환하는 패턴
호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해준다.

img

Head First Design Pattern의 예시

public interface Duck {
  public void quack();
  public void flyHigh();
}

public interface Turkey {
  public void gobble();
  public void fly();
}

public class WildTurkey implements Turkey {
  ...
}

public class TurkeyAdapter implement Duck {
  Turkey turkey;

  public TurkeyAdapter(Turkey turkey) {
    this.turkey = turkey;
  }

  public void quack() {
    turkey.gobble;
  }

  public void flyHigh() {
    turkey.fly();
  }
}

public class DuckDrive {
  public static void main(String[] args) {
    WildTurkey wildTurkey = new WildTurkey();
    Duck turkeyAdapter = new TurkeyAdapter(wildTurkey);

    turkeyAdapter.quack();
  }
}
반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 10장. 클래스  (0) 2020.05.31
[CleanCode] 9장. 단위 테스트  (0) 2020.05.18
[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15
[CleanCode] 5장. 형식 맞추기  (0) 2020.05.12

목차

  1. 오류 코드보다 예외를 사용하라
  2. Try-Catch-Finally 문부터 작성하라
  3. 미확인 예외를 사용하라
  4. 예외에 의미를 제공하라
  5. 호출자를 고려해 예외 클래스를 정의하라
  6. 정상 흐름을 정의하라
  7. null을 반환하지 마라
  8. null을 전달하지 마라

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

오류 코드를 받아 처리 로직을 추가하는 것보다 오류가 발생하면 예외를 던지는게 좋음
Bad : 함수를 호출한 즉시 오류를 확인하지 않으면 문제가 발생할 확률이 높음

public class DeviceController {
  ...
  public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    if (handle != DeviceHandle.INVALID) {
      retrieveDeviceRecord(handle);
      if (record.getStatus() != DEVICE_SUSPEND) {
        pauseDevice(handle);
        ...
      } else {
        logger.log("Device suspend.");
      }
    } else {
      logger.log("Invalid handle for: ");
    }
  }
}

Good :

public class DeviceController {
  ...
  public void sendShutDown() {
    try {
      tryToShutDown();
    } carch (DeviceShutDownError e) {
      logger.log(e);
    }
  }

  private void tryToShutDown() throws DeviceShutDownError {
    DeviceHandle handle = getHandle(DEV1);
    DeviceHandle record = retrieveDeviceRecord(handle);

    pauseDevice(handle);
    ...
  }

  private DeviceHandle getHandle(DeviceID id) {
    ...
       throw new DeviceShutDownError("Invalid handle for: ");
    ...
  }
}

Try-Catch-Finally 문부터 작성하라

try-catch-finally 문으로 시작하면 try 블록에서 무슨 일이 생기는지 호출자가 기대하는 상태를 정의하기 쉬워진다.


미확인 예외를 사용하라

확인된 예외

OCP(Open Closed Principle)를 위반하고 캡슐화가 꺠짐
예외를 던지는 메서드가 catch 블록이 있는 메서드가 아닌 더 하위에 있다면 그 사이의 메서드에서 모두 해당 예외를 선언부에 추가하거나 catch 블록에서 처리해야 함

public void get() {
  try {

  } catch (InvalidGetException e) {
    logger.log(e);
  }
}

public void getById() throws InvalidGetException {
  call();
}

public void call() throws InvalidGetException {
  throw new InvalidGetException();
}

예외에 의미를 제공하라

호출 스택만으로 사용자가 의도를 파악하기 어려우므로 오류 메세지에 정보를 담아 예외와 함께 던져야 한다.


호출자를 고려해 예외 클래스를 정의하라

프로그래머는 오류를 정의할 때 오류를 잡아내는 방법을 고려해야 한다.
외부 API의 다양한 예외를 직접 노출하지 않고 감싸기 기법을 통해 새로운 클래스를 만들어 캡슐화
Bad : 다른 종류의 예외를 처리 로직은 모두 같으므로 의미가 없음

ACMEPort port = new ACMEPort(12);

try {
  port.open();
} catch (DeviceResponseException e) {
  reportPortError(e);
  logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
  reportPortError(e);
  logger.log("Unlock exception", e);
} catch (GMXError e) {
  reportPortError(e);
  logger.log("Device response exception", e);
} finally {
  ...
}

Good :

public class LocalPort {
  private ACMEPort innerPort;

  public LocalPort(int portNumber) {
    innerPort = new ACMEPort(portNumber);
  }

  public void open() {
    try {
      innerPort.open();
    } catch (DeviceResponseException e) {
      throw new PortDeviceFailure(e);
    } catch (ATM1212UnlockedException e) {
      throw new PortDeviceFailure(e);
    } catch (GMXError e) {
      throw new PortDeviceFailure(e);
    }
  }
  ...
}

정상 흐름을 정의하라

외부 API를 감싸 독자적인 예외를 던져 중단한 뒤 호출하는 코드에서 처리를 정의해 중단된 계산을 처리하는 것은 대게 적합하지만 중단이 적합하지 않은 경우도 있다. 이러한 경우 특수 사례 패턴(SPECIAL CASE PATTERN)을 적용해 개선한다.
특수 사례 패턴이란?
반환할 값이 없을 때 예외를 던지는 것이 아니라 기본값을 반환

Bad :

try {
  MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
  m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
  m_total += getMealPerDien();
}

Good :

public int testMethod() {
  ...
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getId());
    m_total += expenses.getTotal();
  ...
}

public class PerDiemMealExpenses implements MealExpenses {
  private static final int MEAL_EXPENSES_DEFAULT = 3000;
  public int getTotal() {
        ...
    // 반환 값이 없을 경우 기본값으로 일일 기본 식비를 반환한다.
    return MEAL_EXPENSES_DEFAULT;
  }
}

null을 반환하지 마라

메서드가 null을 반환하면 해당 메서드를 사용하는 클라이언트는 null 체크 코드를 추가할 수 밖에 없다. 이 과정에서 null 체크가 누락되면 버그로 이어지고, null 체크 코드는 아름답지 못하다. 특수 사례 객체를 통해 이러한 문제를 해결할 수 있다.
Bad :

List<Employee> employees = getEmployees();
if (eployees != null) {
  for (Employee e : employees) {
    totalPay += e.getPay();
  }
}

Good :

List<Employee> employees = getEmployees();
for (Employee e : employees) {
  totalPay += e.getPay();
}

public List<Employee> getEmployees() {
  if (.. 직원이 없다면 ..) {
    return Collections.emptyList();
  }
}

null을 전환하지 마라

메서드의 argument로 null을 전달하게 되면 메서드는 내부에서 null 체크 또는 assert를 이용하여 처리하는 로직을 추가할 수 밖에 없다. 이러한 로직을 통해 exception을 던지는 메서드가 되면 해당 메서드를 사용하는 쪽에서도 exception 처리를 추가해야 한다.
호출자가 넘기는 null을 처리하는 방법은 없다. 정책적으로 null을 넘기지 못하게 하는것이 가장 합리적이다.

반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 9장. 단위 테스트  (0) 2020.05.18
[CleanCode] 8장. 경계  (0) 2020.05.18
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15
[CleanCode] 5장. 형식 맞추기  (0) 2020.05.12
[CleanCode] 4장. 주석  (0) 2020.05.12

목차

  1. 자료 추상화
  2. 자료/객체 비대칭
  3. 디미터 법칙
  4. 자료 전달 객체

자료 추상화

변수를 함수를 통해 계층을 추가한다고 해서 구현이 저절로 감춰지지는 않는다.
추상 인터페이스를 제공해 사용자가 구현을 모르는 채 자료의 핵심을 조작할 수 있어야 클래스라고 할 수 있다.
Bad :

// 해당 클래스에 getX, getY 메서드를 추가한다 해도 사용자는 x, y를 반환할 뿐일 것이라고 추측이 가능하다  
// 이것은 전혀 추상화되지 않은 상태이다
public class Point {
  public double x;
  public double y;
}

Good :

// x, y가 실제 직교 좌표계인지, 극좌표계인지 알 수 없다  
// 내부에서 변수가 바뀌던, 로직이 바뀌던 클라이언트는 상관이 없다
public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}

자료/객체 비대칭

객체 vs 자료 구조

  • 객체
    추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개
  • 자료 구조
    자료를 그대로 공개
    별도의 함수는 제공하지 않음

객체 지향 코드 vs 절차 지향 코드

  • 객체 지향 코드
    기존 함수를 변경하지 않으면서 새 클래스를 추가하기 용이
    새로운 함수를 추가하기가 어려움
  • 절차 지향 코드
    기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 용이
    새로운 자료구조를 추가하기 어려움

필요에 따라 자료구조 + 절차 지향 코드가 객체 지향 코드보다 나을 수 있다. 필요에 따라 사용하자.


디미터 법칙

모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙
한 객체의 메서드가 반환하는 객체의 메서드를 호출하면 안됨

  • 기차 충돌
    Bad : 기차 충돌 상황

    final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    
    String outFile = outputdir + "/" + className.replace('.', '/') + ".class";
    FileOutputStream fout = new FileOutputStream();
    BufferedOutputStream bos = new BufferedOutputStream(fout);

    Better :

    Options opts = ctxt.getOptions();
    File scratchDir = opts.getScratchDir();
    final String outputDir = scratchDir.getAbsolutePath();

    기차 충돌 상황보다는 낫지만 각 메서드가 반환하는 값이 객체인지 자료 구조인지에 따라 디미터 법칙 위반여부가 결정됨
    객체라면 내부 구조를 getter를 통해 노출시키는 것이므로 디미터 법칙을 위반하는 것
    getter가 객체인지 자료 구조인지 알 수 없게 혼동을 주므로 아래와 같이 명확하게 한다.

    Good :

    final String outputDir = ctxt.options.scratchDir.absolutePath;
  • 잡종 구조
    공개 변수가 있으면서 공개 조희/설정 함수가 있고 기능을 수행하는 함수도 있는 구조

  • 구조체 감추기
    기차 충돌 예시에서 ctxt, opts, scratchDir이 진짜 객체라면?
    내부의 객체를 가져다 쓰는 것이 아닌 메세지를 보내 필요한 정보를 반환 받도록 해야 한다.

    // outputDir의 목적이 무엇인지를 확인하여 수정한다.
    BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);

자료 전달 객체

Data Transfer Object(DTO) - 공개 변수만 있고 함수가 없는 클래스
데이터베이스와 통신하거나 소켓에서 받은 메세지의 구문을 분석할 때 유용

  • bean 구조
    private 변수와 생성자, gettter만 존재
  • 활성 레코드
    DTO의 일반적인 형태에 save, find와 같은 탐색 함수도 제공
    데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과
    비즈니스 규칙 메서드를 직접 추가하지 않도록 조심해야 함
반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 8장. 경계  (0) 2020.05.18
[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 5장. 형식 맞추기  (0) 2020.05.12
[CleanCode] 4장. 주석  (0) 2020.05.12
[CleanCode] 3장. 함수  (0) 2020.05.06

목차

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

프로그래머는 규칙에 맞게 형식을 깔끔하게 맞춰 코드를 짜야 한다.
팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다.


형식을 맞추는 목적

맨 처음 잡아놓은 코드의 스타일과 가독성은 유지보수 용이성과 확정성에 계속 영향을 미친다.


적절한 행 길이를 유지하라

  • 신문 기사처럼 작성하라
    소스 파일은 고차원 개념/알고리즘 -> 저차원 함수와 세부 내역 순으로 아래로 내려갈수록 의도를 세세하게 표현
  • 개념은 빈 행으로 분리하라
  • 세로 밀집도
    서로 밀접한 코드 행은 세로로 가까이 놓여야 함
  • 수직 거리
    변수 선언 - 사용하는 위치에 최대한 가까이 선언
    인스턴스 변수 - 클래스의 맨 처음에 선언, 변수 간에 세로로 거리를 두지 않음
    종속 함수 - 한 함수에서 호출하는 다른 함수는 세로로 가까이 배치 (함수 -> 호출되는 함수 순서)
    개념적 유사성 - 친화도가 높을수록 코드를 가까이 배치 (한 함수가 다른 함수를 호출하는 직접적인 종속성, 변수와 그 변수를 사용하는 함수, 명명법이 똑같거나 기능이 같은 함수)
  • 세로 순서
    함수 호출 종속성은 아래 방향으로 유지, 자연스럽게 소스 코드 모듈이 고차원 -> 저차원으로 기술

가로 형식 맞추기

Max 120자

  • 가로 공백과 밀집도
    공백을 통해 개념을 분리 (e.g., 할당 연산자, parameter 분리를 위한 ", ")
    연산자 우선순위 구분을 위한 공백 추가
  • 가로 정렬
    정렬이 필요할 정도로 목록이 길다면 클래스를 쪼개는 방법을 고려
  • 들여쓰기
    scope로 이뤄진 계층을 표현하기 위해 사용
    1줄의 if/while/함수도 꼭 {} 괄호를 사용하며 들여쓰기를 사용
  • 가짜 범위
    빈 while/for문은 다음 행에 세미콜론을 들여써서 작성

팀 규칙

팀에 속해 있다면?
각자의 선호하는 규칙 < 팀 규칙

반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15
[CleanCode] 4장. 주석  (0) 2020.05.12
[CleanCode] 3장. 함수  (0) 2020.05.06
[CleanCode] 2장. 의미있는 이름  (0) 2020.05.02

목차

  1. 주석은 나쁜 코드를 보완하지 못한다
  2. 코드로 의도를 표현하라
  3. 좋은 주석
  4. 나쁜 주석

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

모듈이 지저분하다면 주석을 다는것이 아니라 코드를 정리해야 한다.

 

코드로 의도를 표현하라

Bad :

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다
if ((employee.flags && HOURLY_FLAG) &&
   (employee.age > 65))

Good :

if (employee.isEligibleForFullBenefits())

 

좋은 주석

  • 법적인 주석
  • 정보를 제공하는 주석
    코드를 개선하면 없앨 수 있는 주석이다.
  • 의도를 설명하는 주석
    소스 코드의 알고리즘을 결정하게 된 의도를 설명하는 주석
  • 의미를 명료하게 밝히는 주석
    인수나 반환값이 변경하지 못하는 코드일 경우 의미를 명료하게 밝히는 주석 (이 또한 오류가 존재하더라도 확인이 어려워지니 고민하고 정확히 달도록 해야 한다.)
  • 결과를 경고하는 주석
  • TODO 주석
  • 중요성을 강조하는 주석
  • 공개 API에서 Javadocs

 

나쁜 주석

  • 주절거리는 주석
  • 같은 이야기를 중복하는 주석
  • 오해할 여지가 있는 주석
  • 의무적으로 다는 주석
  • 이력을 기록하는 주석
  • 있으나 마나 한 주석
  • 무서운 잡음
  • 함수나 변수로 표현할 수 있다면 주석을 달지 마라
  • 위치를 표시하는 주석
  • 닫는 괄호에 다는 주석
  • 공로를 돌리거나 저자를 표시하는 주석
  • 주석으로 처리한 코드
  • HTML 주석
  • 전역정보
  • 너무 많은 정보
  • 모호한 관계
    주석이 코드의 부족한 설명 외에 도메인 지식이 있어 추가적인 설명이 필요한 경우는 최악
  • 함수 헤더
  • 비공개 코드에서 Javadocs
반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15
[CleanCode] 5장. 형식 맞추기  (0) 2020.05.12
[CleanCode] 3장. 함수  (0) 2020.05.06
[CleanCode] 2장. 의미있는 이름  (0) 2020.05.02

목차

  1. 작게 만들어라!
  2. 한 가지만 해라!
  3. 함수 당 추상화 수준은 하나로!
  4. Switch 문
  5. 서술적인 이름을 사용하라
  6. 함수 인수
  7. 부수 효과를 일으키지 마라!
  8. 명령과 조회를 분리하라!
  9. 오류 코드보다 예외를 사용하라!
  10. 반복하지 마라!
  11. 구조적 프로그래밍
  12. 함수를 어떻게 짜죠?
  13. 결론

작게 만들어라!

작게의 기준은 무엇일까?

  • 블록과 들여쓰기
    if/else/switch 문 등에 들어가는 블록은 한 줄이며 indent는 2단을 넘어서면 안된다.
    적절한 메서드명을 가지는 메서드를 호출하는 방식을 통해 이를 충족시킨다.

한 가지만 해라!

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

함수는 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
한 함수에서 섹션을 나눌 수 없다면 함수가 한 가지 작업을 한다고 할 수 있다.

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

함수 내에 추상화 수준이 섞이면 특정 표현이 근본 개념인지 세부사항인지 구분하기 어려워진다.

  • 위에서 아래로 코드 읽기: 내려가기 규칙
    한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 오면 함수 추상화 수준이 한번에 한 단계씩 낮아진다. 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.

Switch 문

Bad :

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);
  }
}

Switch문의 문제점

  1. 함수가 길어진다
  2. '한 가지' 작업만 수행하지 않는다.
  3. Single Response Principle를 위반한다.
  4. Open Closed Principle을 위반한다.

Switch문은 다형적 객체를 생성하는 코드안에서만 사용하도록 해야 한다.

서술적인 이름을 사용하라

일관적이고 서술적인 이름을 사용하여 함수 이름만으로 해당 함수가 하는 일을 파악할 수 있도록 해야 한다.

함수 인수

테스트 코드 관점에서도 인자가 많을수록 테스트를 위한 유효한 인자를 만들어야 하는 문제가 생긴다.
가장 이상적인 인수 개수는 0개, 최대 3개. 하지만 적을수록 좋다. 줄이자 줄이자!

  • 많이 쓰는 단항 형식
    • 인수에 질문을 던지는 경우
    • 인수를 뭔가로 변환해 결과를 반환하는 경우
    • 이벤트 (출력은 없는 경우, 다소 드물다) - 이름과 문맥을 통해 이벤트 함수임을 명확히 드러내야 한다.
  • 플래그 인수
    플래그 인수 값에 따라 메서드가 다른 기능을 하게 된다는 의미이므로 플래그 인수는 지양해야 한다.
  • 이항 함수
    무시해도 되는 인자가 있는 순간 버그 확률이 늘어난다.
    이항 함수가 적절한 경우는 인수 2개가 한 값을 표현하는 두 요소인 경우, 자연적인 순서가 있는 경우이다.
    같은 클래스인 2개의 인자를 받는 경우 순서를 헷갈릴 수 있으므로 지양해야 한다.
  • 삼항 함수
    순서, 주춤, 무시로 야기되는 문제는 두 배 이상으로 늘어난다.
    인자의 의미가 명확한 경우가 아니면 지양한다.
  • 인수 객체
    결국 변수에 이름을 붙여야 하므로 개념을 표현하게 되는 것이다.
    e.g.,
    Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius);
  • 인수 목록
    가변 인수 전부가 동등하게 취급될 경우 List 형 인수 하나로 취급할 수 있다.
    e.g., public String format(String format, Object... args)
  • 동사와 키워드
    함수의 의도나 인수의 순서와 의도를 함수 이름에 담아야 한다.
    함수 이름에 인자의 키워드를 추가하여 인자의 의미와 순서를 보장 해 준다.

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

함수는 꼭 한 가지 기능만을 한다.
부수 효과로 발생 가능한 문제?

  • Temporal Coupling
  • Order Dependency

명령과 조회를 분리하라!

함수의 역할

  • 뭔가를 수행한다.
  • 뭔가에 답한다.

Bad :

// attribute인 속성을 찾아 값을 value로 설정 후 성공시 true, 실패시 false
public boolean set(String attribute, String value);

if (set("username", "unclebob")) ...

Good :

if (attributeExist("username")) {
  setAttribute("username", "unclebob");
}

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

오류코드의 문제점

  • 호출자는 오류 코드를 바로 처리해야 한다.
  • 의존성 자석 : 오류 코드의 수정/추가/삭제가 일어날 경우 처리 코드도 수정해야 한다

Try/Catch 블록 뽑아내기
오류처리도 한 가지 작업임을 인지하고 오류 처리 함수를 분리하도록 한다.

Bad : try/catch 블록은 정상 동작과 오류 처리 동작을 섞으며 코드 구조에 혼란을 일으킨다.

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

Good :

public void delete(Page page) {
  try {
    deletePageAndAllReferences(page);
  } catch (Exception e) {
    logError(e);
  }
}

// 정상 동작만을 처리한다.
private void deletePageAndAllReferences(Page page) throws Exception {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
  logger.log(e.getMessage());
}

반복하지 마라!

반복되는 코드가 다른 코드와 섞이면 모양이 달라지면서 중복이 잘 드러나지 않게 되어 수정이 일어날 때 오류가 발생할 확률이 높아진다.

중복을 없애거나 제어 할 목적의 원칙과 기법들

  • 관계형 데이터베이스에 정규 형식을 만듦
  • 객체지향 프로그래밍 - 코드를 부모 클래스로 몰아 중복을 없앰
  • 구조적 프로그래밍
  • AOP(Aspect Oriented Programming) - Spring의 핵심 개념 중 하나로 부가기능의 모듈화 (하단 링크들 참고)
  • COP(Component Oriented Programming) - 기존 시스템이나 소프트웨어를 구성하는 컴포넌트를 조립해서 하나의 새로운 응용프로그램을 만드는 소프트웨어 개발 방법론

구조적 프로그래밍

모든 함수와 함수 내 모든 블록에 entry와 exit이 하나만 존재해야 한다. 즉, return 문이 하나여야 한다. 루프 안에서 break, continue를 사용해서는 안 되며 goto는 절대로 안된다.

함수가 클 때만 이익을 제공하는 기법이다.
함수가 작을때는 return, break, continue를 여러차례 사용해도 괜찮지만 goto는 작은 함수에서도 피해야 한다.

함수를 어떻게 짜죠?

TDD
테스트 코드 -> 중복, 네이밍, 들여쓰기 등 상관없이 우선 기능을 개발 -> 리팩토링 (무조건 단위테스트는 계속 통과)
위 과정을 반복한다.

결론

위의 규칙들을 잘 따라 시스템을 잘 풀어가야한다!


참고하면 좋을 문서

AOP

반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15
[CleanCode] 5장. 형식 맞추기  (0) 2020.05.12
[CleanCode] 4장. 주석  (0) 2020.05.12
[CleanCode] 2장. 의미있는 이름  (0) 2020.05.02

의미있는 이름

목차


의도를 분명히 밝혀라

이름을 지을 때 아래의 질문들을 고려해야 한다.

  • 변수(혹은 함수나 클래스)의 존재 이유는?
  • 수행 기능은?
  • 사용 방법은?

Bad

public List<int[]> getThem() {
  List<int[]> list1 = new ArrayList<int[]>();
  for (int[] x : theList)
    if (x[0] == 4)
        list1.add(x);
  return list1;
}

복잡한 코드는 아니지만 해당 메서드가 무엇을(them) 가져오는 것인지, List에 담기는 int[] 배열은 무슨 데이터인지, 4는 무슨 의미를 가지는지를 알 수 없다. 이 말은 곧 각 변수들의 정보를 모르는 경우 코드의 맥락을 완전히 이해 할 수 없다는 뜻이다.
각 변수들의 정보를 모르더라도 이해할 수 있도록 아래와 같이 수정할 수 있다.

Good

public List<Cell> getFlaggedCells() {
  List<Cell> flaggedCells = new ArrayList<Cell>();
  for (Cell cell : gameBoard)
    if (cell.isFlagged())
      flaggedCells.add(cell);
  return flaggedCells;
}

 

그릇된 정보를 피하라

  • 약어를 지양하자
  • 여러 계정을 그룹으로 묶을 때 List와 같은 특정 컨테이너의 사용을 지양하라 (e.g., accountList -> Accounts)
  • 네이밍은 서로 비슷한 이름을 사용하지 말자
  • 유사한 개념은 유사한 표기법을 사용하자, 일관성이 떨어지는 표기법은 그릇된 정보이다.
  • 다른 글자와 혼동되는 단일 알파벳을 피하자 (e.g., 소문자 L <-> 1, 대문자 I)

 

의미 있게 구분하라

한 scope에서 동일 이름을 사용할 수 없으니 컴파일러를 통과하기 위한 무의미한 철자 변경의 오류를 범하지 않도록 해야 한다. 무의미한 철자 변경의 대표적인 두가지 예시는 아래와 같다.

  • 연속된 숫자를 덧붙이기
    Bad :

    public static void copyChars(char a1[], char a2[])

    Good :

    public static void copyChars(char souce[], char destination[])
  • 불용어(noise word) 추가
    불용어란? 없어도 의미 전달에 영향이 없는 단어

    • 접두어 사용을 조심하자
      a, an, the와 같은 접두어는 의미가 분명히 다를 때만 사용하도록 한다. (e.g., 모든 지역변수는 a 접두사 사용, 모든 함수 인수는 the 접두사 사용)
    • 불용어를 사용하여 중복을 발생시키지 말자
      product라는 클래스가 존재 할 때 ProductInfo, ProductData와 같은 개념이 구분되지 않는 클래스를 생성하여 중복을 발생시키지 않도록 한다. (e.g., Money & MoneyAccaount, CustomerInfo & Customer, AccountData & Account)

 

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

발음하기 어려운 단어는 커뮤니케이션에도 영향을 끼친다. 이는 실제 존재하는 단어를 사용하는 것만으로도 해결이 된다.

Bad :

아 래의 변수는 generate date, year, month, day, hour, minute, second 의 줄임말이다.

Date genymdhms

Good :

Date generationTimetamp

 

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

  • 상수
    상수값에 버그가 있을 경우 검색으로 찾아낼 수 없다. 그러나 상수값의 의미를 나타내는 변수로 정의하여 사용하면 검색에 용의하다.

  • 변수
    알파벳 하나를 변수로 사용하면 검색이 어렵다.

Bad :

int s = 0;
for (int i = 0; i < 30; i++) 
  s += i * 4;

Good :

int maxIndex = 30;
const int MULTIPLICATION_CONDITION = 4;
int sum = 0;
for (int i = 0; i < maxIndex; i++) // 하나의 메서드에서만 쓰이며, 다시 쓰이지 않는다면 한 문자 변수도 나쁘지 않다
  sum += i * MULTIPLICATION_CONDITION;

 

인코딩을 피하라

  • 헝가리식 표기법을 지양하라
    과거 프로그래밍 언어는 변수 및 함수의 인자 이름 앞에 데이터 타입을 명시하는 헝가리식 표기법을 사용하였으나 현대의 프로그래밍 언어는 많은 컴파일러가 타입을 기억하고 강제하므로 네이밍 시 타입을 직접 명시하는 것을 피하는 것이 좋다. 오히려 아래와 같은 문제가 발생 할 수 있다.

    PhoneNumber phoneString; // 이러한 경우 타입이 바뀌게 되었을 때 변수명도 바꿔주어야 하는 문제가 생긴다.
  • 멤버 변수 접두어
    멤버 변수임을 명시하기 위해 "m_" 접두어를 붙이는 것을 지양하고 클래승와 함수는 접두어가 필요없을 정도로 작게 구현해야 한다.

  • 인코딩이 필요한 경우?
    Abstract Factory를 구현하는 경우, 인터페이스 클래스 이름보다는 구현 클래스의 이름을 인코딩 하는 것이 좋다.
    Bad :

    public interface IShapeFactory;  // 인터페이스 클래스
    public class ShapeFactory;  // 구현 클래스

    Good :

    public interface ShapeFactory;  // 인터페이스 클래스
    public class ShapeFactoryImp;  // 구현 클래스
    public class CShapeFactory;  // 구현 클래스

 

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

  • 하나의 문자만 사용하는 것을 지양하라
    • 루프에서 반복 횟수 변수는 전통적으로 한 글자만을 사용
      보통 i, j, k를 사용 (소문자 l은 대문자 I와 헷갈리므로 절대 안됨!)
    • 이외에는 대부분 적절하지 않다
  • 나만 이해할 수 있는 변수명이 아닌 모두가 이해할 수 있는 명료한 네이밍을 지향하자

 

클래스 이름

  • 동사가 들어가지 않는 명사/명사구가 적합하다.
  • 불용어를 지양하자 (e.g., Manager, Processor, Data, Info)

Good :

public class Customer
public class WikiPage
public class AddressParser

 

메서드 이름

  • 동사/동사구가 적합하다.

  • 접근자(Accessor), 변경자(Mutator), 조건자(Predicate)는 javabean 표준에 따라 get, set, is를 붙인다.
    e.g.,

    String name = employee.getName();
    customer.setName("mike");
    if (paycheck.isPosted());
  • 생성자의 중복정의 시 정적 팩토리 메서드를 사용한다. (이때, 생성자 사용을 제한하려면 해당 생성자를 private으로 사용한다.)

    • 해당 내용의 상세한 특징은 Effective Java 3/E 기준 Item1을 참고한다.

    Bad : 

    Complex fulcrumPoint = new Complex(23.0);

    Good : 

    Complex fulcrumPoint = Complex.FromRealNumber(23.0);

 

기발한 이름은 피하라

  • 특정 문화, 배경지식이 있어야 이해할 수 있는 이름이 아닌 명료한 이름을 선택하여 의도를 분명하고 솔직하게 표현하라

 

한 개념에 한 단어를 사용하라

  • 똑같은 기능을 하는 메서드는 모든 클래스에서 하나의 이름으로 통일한다.
    (e.g., 각 클래스마다 fetch, retrieve, get으로 다르게 구현하지 않도록 한다.)
  • 주석이 필요하지 않도록 메서드 이름은 독자적이고, 일관적이어야 한다.
  • 동일 layer의 클래스는 하나의 이름으로 통일한다.
    (e.g., 하나의 프로젝트에서 DeviceManager, ProtocolController와 같이 같은 의미지만 다른 단어의 혼용을 피한다.)

 

말장난을 하지마라

  • 한 개념에 한 단어를 사용하라 규칙을 따르되, 일관성을 고려한다.
  • 같은 맥락일때만 같은 이름을 사용하여 코드 내용을 확인하지 않아도 기능이 보장되도록 해야 한다.

 

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

  • 모든 이름에 도메인 영역의 단어를 사용하지 않는다. 기술 개념에는 기술 이름이 가장 적합한 선택이다. (e.g., JobQueue, AccountVisitor)

 

문제 영역에서 가져온 이름을 사용하라

  • 적절한 기술 이름이 없는 경우 또는 도메인 영역과 관련이 깊은 코드에 도메인 영역의 이름을 사용한다.
  • 우수한 프로그래머라면, 해법(기술) 영역과 문제(도메인) 영역을 구분할 줄 알고 각 영역에 알맞은 네이밍을 정해야 한다.

 

의미 있는 맥락을 추가하라

  • 네이밍은 독자적으로 문맥을 가지기 보단 클래스, 메서드, name space의 위치에 따라 맥락을 부여한다.
  • 접두어는 최후의 수단으로 붙인다.
  • 특정 문맥을 가지는 클래스를 생성하면 컴파일러에게도 문맥이 분명해진다.

Bad : 

  • 메서드의 멤버 변수로 사용될 때 어디에 어떻게 이용될지는 길고 복잡한 메서드를 다 읽어야만 이해할 수 있다.
  • 조건문으로 분기되어 변수의 값을 세팅해주지만 해당 값의 의미는 알 수 없다.
private void printGuessStatistics(char candidate, int count) {
  String number;
  String verb;
  String pluralModifier;
  if (count == 0) {
    number = "no";
    verb = "are";
    pluralModifier = "s";
  }
  else if (count == 1) {
    number = "1";
    verb = "is";
    pluralModifier = "";
  } else {
    number = Integer.toString(count);
    verb = "are";
    pluralModifier = "s";
  }
  String guessMessage = String.format("There %s %s %s%s", verb, number, candidate, pluralModifier);
  print(guessMessage);
}

Good : 

  • 클래스의 멤버 변수가 됨으로써 통계 추측 메세지에 사용되는 값임을 클래스의 이름만으로 알 수 있다.
  • 메서드로 분리함으로서 값을 세팅할 때 어떠한 의미를 가지는 값을 세팅하는지 명확해진다.
public class GuessStatisticsMessage {
  private String number;
  private String verb;
  private String pluralModifier;

  public String maek(char candidate, int count) {
    createPluralDependentMessageParts(count);
    return String.format(
      "There %s %s %s%s",
      verb, number, candidate, pluralModifier);
  }

  private void createPluralDependentMessageParts(int count) {
    if (count == 0) {
      thereAreNoLetters();
    } else if (count == 1) {
      thereIsOneLetter();
    } else {
      thereAreManyLetters(count);
    }
  }

  private void thereAreManyLetters() {
    number = Integer.toString(count);
    verb = "are";
    pluralModifier = "s";
  }

  private void thereIsOneLetter() {
    number = "1";
    verb = "is";
    pluralModifier = "";
  }

  private void thereAreNoLetters() {
    number = "no";
    verb = "are";
    pluralModifier = "s";
  }
}

 

불필요한 맥락을 없애라

  • 모든 클래스를 아우르는 맥락을 굳이 접두어로 사용하지 않는다.

    • IDE에서 해당 접두어를 입력하는 순간 수많은 클래스가 열거되어 정말 필요한 클래스를 찾기 어려워진다.
    • 굳이 접두어를 붙임으로서 이름이 불필요하게 너무 길어진다. 긴 이름이 짧은 이름보다 좋지만 의미가 분명한 경우에 한해서이다.

    Bad : 

    package com.gsd;
    
    public class GsdAccountAddress

    Good : 

    package com.gsd;
    
    public class AccountAddress
  • 클래스의 이름으로 좋은 것과 인스턴스의 이름으로 좋은 것은 다르다.

 

마치면서

좋은 이름을 선택한다는 것은 결국 가독성을 높이기 위한 것


참고하면 좋을 문서

반응형

'Dev > Books' 카테고리의 다른 글

[CleanCode] 7장. 예외 처리  (0) 2020.05.16
[CleanCode] 6장. 객체와 자료 구조  (0) 2020.05.15
[CleanCode] 5장. 형식 맞추기  (0) 2020.05.12
[CleanCode] 4장. 주석  (0) 2020.05.12
[CleanCode] 3장. 함수  (0) 2020.05.06

+ Recent posts