SpringBoot Application을 80 port로 사용하고 싶은데, 리눅스 환경에서 80, 443과 같은 포트는 루트 권한으로 사용이 가능하다.
SpringBoot application의 내장 톰켓이 해당 포트를 사용할 수 있도록 sudo 권한을 사용해 서비스를 올리는 것 보다 proxy 서버를 올리는게 더욱 용이하므로 Nginx를 사용한 내용을 정리한다.

 

Nginx 란?

Apache의 1만명 규모 클라이언트의 동시 접속을 다루는 기술적인 이슈를 해결하기 위해 만든 Event-driven 구조의 오픈소스 서버 프로그램이다.
일반적인 HTTP 웹서버의 역할 외에도 proxy, reverse proxy를 제공한다.

 

설치 및 설정

Centos6

설치

$  yum install nginx

 

라우팅 설정

yum으로 nginx를 설치하게 되면 /etc/nginx 경로에 설치된다.
nginx 디렉토리 중 conf.d디렉토리 하위의 default.conf 파일을 변경하여 SpringBoot Application을 80 port로 포팅 한다.

$ vi /etc/nginx/conf.d/default.conf
# 아래와 같이 변경
# The default server
#

server {
#    listen       80 default_server; 
#    listen       [::]:80 default_server;
    listen       80;
    server_name  _;
    root         /usr/share/nginx/html;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    location / {
#                SpringBoot application을 매핑
        proxy_pass http://localhost:9100;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }

}
server 블럭

도메인 단위의 1차 라우팅에 대한 설정을 담당한다..

  • listen : 서버 블록 정의 중 해당 서버에서 라우팅 할 특정 port를 정의하는 필드
    • default_server : 여러개의 server 블록을 작성할 때 default_server는 프로토콜 별로 단 하나의 server 블록에만 존재해야 합니다. 이 설정은 별도로 지정하지 않은 도메인으로 들어오는 다른 모든 요청에 대해서 해당 server 블록이 처리함을 의미한다.
  • server_name : 어떤 도메인을 라우팅 할지에 대한 필드

 

location 블럭

URI 리퀘스트의 path prefix의 매칭을 확인하여 적절한 어플리케이션으로 매핑한다.

  • proxy_pass : 실질적으로 요청 할 서비스를 명시하는 필드

 

Nginx 실행

아래의 명령어로 nginx 명령어 만으로 실행이 완료되고 별다른 알람 없이 실행이 완료되면 ps 명령어로 한번 더 확인 해 준다.

$ sudo nginx
$ ps -ef | grep nginx
root     20270     1  0 16:36 ?        00:00:00 nginx: master process nginx
nginx    20271 20270  0 16:36 ?        00:00:01 nginx: worker process
nginx    20272 20270  0 16:36 ?        00:00:01 nginx: worker process
nginx    20273 20270  0 16:36 ?        00:00:01 nginx: worker process
반응형

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

[LINUX] JVM에서 thread dump 가져오기  (0) 2020.05.19
[Linux] 기본 명령어 정리(옵션) + 기타  (1) 2018.03.27

도메인

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


도메인 모델

특정 도메인을 개념적으로 표현한 것을 도메인 모델이라고 한다.
도메인을 이해하기 위해서는 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 하는데, 이런 면에서 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기에 적합하다.
도메인 모델을 모델링 하는 방법은 객체 모델 외에도 상태 다이어그램이나 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

쓰레드 덤프를 추출해보면 좋을 상황

  • 서비스 OOM 장애 발생 시
  • 기대보다 웹 어플리케이션이 느리게 동작할 때
  • 사용자 수가 많지 않은데 CPU사용량이 높을때
  • 간헐적으로 응답이 느릴때
  • 서비스 실행시간이 길어질수록 응답시간이나 cpu 사용량이 늘어날 때
  • 등등..

쓰레드 덤프는 획득하는 순간의 쓰레드 상태만 알 수 있기 때문에 쓰레드 상태 변화를 확인하기 위해서는 일정 시간 주기로 여러번의 쓰레드 덤프를 추출하여 확인하는 것이 좋다.

 

쓰레드 덤프를 추출하는 방법

원하는 시점에 커널에서 추출하는 방법과 Java 어플리케이션 실행 시 OOM이 발생하면 쓰레드 덤프를 남길 수 있도록 JVM 옵션을 설정하는 방법이 있다.

  • 원하는 시점에 커널을 이용해 추출하는 방법
OS 단에서
    리눅스 : kill -3 [PID]
    
java단에서 
    jps -v 로 java 프로세스 확인
    - jstack [PID] (jstack으로 반응이 없는 경우 –F 옵션)
    - jcmd [PID] Thread.print (jdk7 이후 등장)
        jcmd [PID] help 로 명령어 확인 가능, 직관적임
  • OOM 발생 시 원하는 경로에 Thread dump 저장하는 Java Application 실행 옵션
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${path/to/save/thread/dump/file}

 

쓰레드 덤프 읽기

스레드 덤프를 그대로 읽기는 어렵다.
스레드 덤프를 파일로 저장하여 시각화 시켜주는 사이트를 사용하는 것이 더욱 용이
e.g.) JVM Thread dump analyzer - Universal Java Thread dump analysis

반응형

목차

  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

+ Recent posts