728x90
반응형
728x90
반응형
반응형

포인트

  • 1,000,000 TPS
  • 높은 안정성
  • 트랜젝션
  • A -> B로 이체한다고 가정

 

인메모리 샤딩

계정-잔액(map) 저장

레디스 노드 한대로 100만 TPS는 무리

클러스터를 구성하고 계정을 모든 노드에 균등하게 분산시켜야(파티셔닝, 샤딩)

  • 분배하기 위해 키의 해시 값 mod 개수를 계산하여 분산

모든 레디스 노드의 파티션 수 및 주소는 주키퍼를 사용(높은 가용성 보장)

  • 이체 요청 시 각 클라이언트의 샤딩 정보를 물어봐서 클라이언트 정보를 담은 레디스 노드를 파악

데이터 내구성이 없다는 단점 존재..

분산 트랜젝션

레디스 사용 시 원자적 트랜젝션은 어떻게? RDB로 교체?

두 번째 이체(B로 입금) 시 서비스가 죽는다면?

1. 저수준: 데이터베이스 자체에 의존; 2PC

  • 실행 주체는 디비며 어플리케이션이 중간 결과를 알 수 없음
  • 모든 데이터베이스가 X/Open XA만족해야(JtaTransaction; mysql 지원)
  • 단점:
    • 두 단계가 한 트랜젝션
    • 락이 오랫동안 유지, 성능이 안 좋음
    • 조정자가 단일 장애 지점(SPOF)

 

2. try-confirm/cancel

  • -1 -> +1 해야
  • 실행 주체는 어플리케이션이며 독립적 로컬 트랜젝션의 중간 결과를 알 수 있음
  • 보상 트랜젝션이 별도로 구현되어 있고
  • 1단계 2단계가 각각 다른 트랜젝션으로 구성됨; 여러 개의 독립적인 로컬 트랜젝션으로 구성
  • 특정 데이터 베이스에 구애받지 않고 어플리케이션 단계에서 관리하고 처리
  • 실행 도중 coordinator 다운되는 것을 대비하여 각 단계 상태정보를 테이블에 저장(분산 트랜젝션 ID의 각 단계별 상태)
  • 취소를 대비해 실행 순서가 중요한데(+1을 하고 취소 시 -1을 해야 하는데 네트워크 이슈로 -1이 먼저 요청되는 경우)
    • 취소가 먼저 도착하면 디비에 마킹하고 다음에 실행 명령이 오면 이전에 취소 명령이 있는지 확인(그림 12.12)
  • 병렬가능

3. saga

  1. 모든 연산은 순서대로 정렬된다. 각 연산은 자기 디비에 독립적인 트랜젝션으로 실행된다.
  2. 연산은 첫 번째부터 마지막까지 순서대로 실행된다. 한 연산이 완료되면 다음 연산이 실행된다.
  3. 연산이 실패하면 전체 프로세스는 실패한 연산부터 맨 처음 연산까지 역순으로 보상 트랜젝션을 통해 롤백된다. 따라서 n개 연산을 실행하는 분산 트랜젝션은 보상트랜잭션 n개까지 총 2n개의 연산을 준비해야 한다.
  • choreography: 이벤트를 구독하여 작업 수행; 비동기
  • orchestration: 하나의 조정자가 모든 서비스가 올바른 순서로 작업하도록 조율

분산 트랜젝션의 경우 문제의 원인을 역추적하고 모든 계정에서 발생하는 연산을 감사(audit)할 수 없음

 

이벤트 소싱

  • command: 의도가 명확한 요청; 순서가 중요 FIFO 큐(카프카)에 들어감
    • A - $1 - C ->
  • event: 검증된 사실로 실행이 끝난 상태; 과거에 실제로 있었던 일
    • A -$1; C+$1 두 이벤트로 분리
  • state: 이벤트가 적용될 때 변경되는 내용(여기서는 잔액)
  • state machine: 이벤트 소싱 프로세스를 구동; 명령의 유효성을 검사하고 이벤트를 생성, 이벤트를 적용하여 상태를 갱신

시간 단축? (카프카 대신에)

1. 로컬 디스크

이벤트 소싱에 사용한 카프카(원격 저장소) 대신 로컬 디스크에 파일로 저장하여 네트워크 전송시간 줄일 수

순차적 읽기 쓰기가 이미 최적화되어 있어 빠름

잔액 정보를 RDB 말고 로컬 디스크에 저장: SQLite, RocksDB

  • RocksDB: LSM(log structured merge tree) 사용하여 쓰기 작업 최적화

2. mmap

최근 명령과 이벤트를 메모리에 캐시 할 수도.. 메모리에 캐시 하면 로컬 디스크에서 다시 로드하지 않아도

mmap는 디스크 파일을 메모리 배열에 대응하여 메모리처럼 접근할 수 있게 하여 실행 속도를 높일 수 있음

 

재현성 reproducibility

이벤트를 처음부터 재생하면 과거 잔액 상태 재구성 가능

이벤트 리스트는 불변이고 상태 기계 로직은 결정론적이므로 이벤트 이력을 재생하여 만든 상태는 언제나 동일

계정 잔액 정확성 재검하거나, 코드 수정 후에도 시스템 로직이 올바른지 replay로 확인 가능

 

명령 질의 책임 분리 CQRS

계정 잔액을 공개하는 대신 모든 이벤트를 외부에 보내서 외부 주체가 직접 상태를 재구축 가능(읽기에 유리하게)

읽기 전용 상태 기계는 여러 개 있을 수 있는데, 이벤트 큐에서 다양한 상태 표현을 도출할 수 있다(단순 뷰, 정산 등).

eventual consistency

 

스냅샷: 과거 특정 시점의 상태

모든 것이 파일 기반일 때 재현 프로세스의 속도를 높이는 방법?

이벤트 소싱은 항상 처음부터 다시 읽어서 상태를 파악하는데, 그 대신 주기적으로 상태 파일을 저장하여 시간을 절약 가능

그 시점부터 이벤트 처리 시작, 보통 0시

보통 하둡에 저장

모든 것을 로컬 디스크로(데이터를 한 곳에 두기엔)..  SPOF 위험..

 

높은 신뢰성을 보장할 유일한 데이터는 이벤트

높은 안정성을 제공하려면 이벤트 목록을 여러 노드에 복제해야 하는데 데이터 손실 없고 순서를 유지해야 한다.

합의 기반 복제(consensus based replication)

래프트 알고리즘 사용

  • 래프트 알고리즘(Raft Algorithm)은 분산 시스템에서 합의를 이루기 위한 분산 합의 알고리즘으로, 특히 리더 선출과 로그 복제를 단순하고 이해하기 쉽게 설계한 것이 특징
  • 일관성: Raft는 각 노드가 로그를 일관되게 유지하도록 보장하며, 리더 노드가 로그를 추가하거나 업데이트할 때, 이를 팔로워 노드들에게 전달한다. 모든 노드가 동일한 상태를 유지하도록 보장함으로써 데이터의 일관성을 유지
  • 고가용성: Raft는 단일 노드 실패 또는 리더 실패와 같은 장애를 처리할 수 있도록 설계. 클러스터의 과반수 이상이 살아 있으면, 시스템은 안정적이고 일관성 있게 동작함

 

  • Follower: 리더의 지시를 따르고, 리더의 Heartbeat를 수신하여 상태를 유지
  • Candidate: 리더가 되기 위해 투표를 요청하고, 과반수의 투표를 얻으면 리더가 됨
  • Leader: 클러스터를 관리하고, 클라이언트 요청을 처리하며, 로그 항목을 복제

 

  1. 리더 선출:
    • Follower는 Leader로부터 Heartbeat 메시지를 정기적으로 수신
    • 일정 시간 동안 Heartbeat가 없으면, Follower는 Candidate가 되어 새 리더 선출을 시도
    • 다수의 노드로부터 투표를 받아야 리더로 선출
  2. 로그 복제:
    • Leader는 클라이언트의 요청을 로그에 추가하고 이를 Follower에 복제
    • 과반수의 Follower가 로그를 수락하고 이를 확인하면 Leader는 해당 로그 항목을 커밋
  3. 일관성 유지:
    • Leader는 모든 노드가 동일한 로그 상태를 유지하도록 보장
    • 새로운 리더가 선출되면, 로그 일관성을 확보하기 위해 추가 작업을 수행

리더 장애 처리:

  1. 리더 장애 발생:
    • 리더가 장애를 겪어 더 이상 Heartbeat를 보내지 못하면, Follower는 일정 시간 동안 Heartbeat를 받지 못한 상태가 됨
    • Follower는 Election Timeout이 지나면 Candidate 상태로 전환
  2. 리더 선출 과정:
    • Candidate가 되면, 다른 노드에 투표를 요청하고 선거를 시작
    • 과반수의 투표를 얻으면 Candidate는 새로운 Leader가 됨
    • 새로운 Leader는 기존 로그의 일관성을 확인하고 필요한 경우 Follower에게 누락된 로그를 전송하여 동기화
  3. 장애 복구:
    • 장애가 발생한 리더가 복구되면, Follower 상태로 전환
    • 리더 선출 과정에서 새로운 리더가 선출되었기 때문에 복구된 노드는 더 이상 리더가 아님

팔로워 장애 처리:

  1. 팔로워 장애 발생:
    • Follower가 장애를 겪으면, 로그 복제 및 Heartbeat 수신이 중단
    • Leader는 계속해서 다른 Follower들과 로그를 복제하고 클러스터를 관리
  2. 팔로워 복구:
    • 장애에서 복구된 Follower는 Leader로부터 현재 로그 상태를 동기화
    • Leader는 AppendEntries 메시지를 통해 복구된 Follower에 누락된 로그를 보냄
    • 복구된 Follower는 로그를 복제하고, 현재 상태를 동기화한 후 정상 운영을 재개

장애 상황에 따른 동작:

  • 다수 노드 장애: 클러스터는 과반수의 노드가 살아있으면 정상 동작을 유지합니다. 과반수 이상이 실패하면 클러스터는 동작을 멈추고 장애 복구가 실시
  • 네트워크 파티션: 네트워크가 분할되면, 두 개 이상의 그룹으로 나뉨. 각 그룹은 독립적으로 리더를 선출할 수 있지만, 과반수를 차지하는 그룹만이 유효한 리더를 가질 수 있음. 네트워크가 복구되면, 하나의 리더만 유지되도록 통합

장애 허용을 위한 메커니즘:

  • Election Timeout: 리더의 장애를 감지하기 위한 타이머. 일정 시간 동안 Heartbeat가 없으면 선거가 시작
  • Majority Agreement: 리더가 되려면 과반수의 노드로부터 투표를 받아야. 이는 장애가 발생하더라도 시스템이 계속 운영될 수 있도록 보장
  • Log Consistency: 리더는 모든 팔로워가 일관된 로그 상태를 유지하도록 보장하며, 새로운 리더가 선출될 때 로그 일관성을 유지

 

CQRS에서 읽을 때(폴 vs 푸시)

풀 방식: 클라가 서버에게 request를 보낼 때 읽기 디비에서 가져옴

역방향 프록시: 캐시같이 디비에서 직접 가져가지 않고 만들어진 데이터를 가져가게 둔 중간 저장소

  • 클라이언트와 서버 간의 중간 계층으로, 클라이언트 요청을 백엔드로 전달하고, 백엔드의 응답을 클라이언트로 반환;
  • 이벤트 수신 후 역방향 프락시에 푸시하도록

프로세스 흐름:

  1. 이벤트 수신:
    • 읽기 전용 상태 기계는 외부 시스템으로부터 이벤트를 수신. 이 이벤트는 상태 업데이트를 요구하는 데이터일 수 있음.
  2. 상태 업데이트 및 푸시:
    • 상태 기계는 이벤트를 처리하고 내부 상태를 업데이트
    • 업데이트된 상태는 즉시 역방향 프록시로 푸시. 푸시된 데이터는 클라이언트가 요청하기 전에 프록시에 전달되어 준비.
  3. 클라이언트 요청 처리:
    • 클라이언트가 상태 데이터를 요청하면, 역방향 프록시는 백엔드 서버에 요청을 전달하는 대신, 이미 준비된 최신 상태를 클라이언트에 반환

비동기 이벤트 소싱 프레임워크를 동기식 프레임워크로 제공하기 위해 역방향 프록시(Reverse Proxy)를 추가하는 것은 클라이언트와 서버 간의 통신 방식의 차이를 조율하고, 비동기 시스템의 응답성을 개선하기 위한 전략

1. 비동기 이벤트 소싱 프레임워크의 특성:

  • 이벤트 소싱(Event Sourcing): 시스템 상태를 이벤트의 시퀀스로 기록하고, 현재 상태를 이벤트를 재생하여 복구하는 방식
  • 비동기 처리: 이벤트는 비동기적으로 생성되고 처리되며, 상태는 eventual consistency(최종 일관성)를 가짐. 이는 즉각적인 응답이 보장되지 않고, 처리 완료까지 시간이 소요될 수 있음

2. 동기식 프레임워크 제공의 필요성:

  • 즉각적인 응답 필요: 동기식 시스템은 클라이언트가 요청을 보내면, 즉시 결과를 반환받기를 기대. 비동기 시스템의 특성상 바로 응답을 제공하기 어려운 상황에서, 동기적 동작을 요구하는 클라이언트와의 간극을 줄일 필요가 있음
  • 클라이언트 요구: 많은 클라이언트는 동기적으로 동작하는 전통적인 API 사용에 익숙

3. 역방향 프록시의 역할: 역방향 프록시를 추가함으로써 비동기 시스템을 동기적으로 제공 가능

3.1. 응답 캐싱 및 버퍼링:

  • 이벤트 결과 캐싱: 프록시는 비동기 이벤트가 처리된 결과를 캐싱하여 클라이언트의 요청에 대해 즉시 응답. 이벤트가 아직 처리되지 않았으면, 프록시가 응답을 보류하거나 기본 응답을 반환

3.2. 동기화된 응답 시뮬레이션:

  • 상태 확인 및 응답 대기: 클라이언트의 요청이 들어오면 프록시는 이벤트 소싱 시스템에 상태를 확인하고, 동기적 방식으로 응답을 보류하다가 결과가 준비되면 반환. 이는 동기 호출로 클라이언트에 투명하게 처리되며, 실제로는 백엔드에서 비동기적으로 처리.

3.3. 비동기 이벤트의 프리로드 및 상태 추적:

  • 사전 이벤트 처리: 프록시는 예상되는 이벤트나 데이터를 미리 가져와(이벤트를 받아서) 클라이언트 요청이 들어왔을 때 빠르게 제공. 이를 통해 동기적 행동처럼 느껴지게 함.

4. 의미와 장점:

  • 사용자 경험 개선: 클라이언트는 비동기적 시스템의 지연 시간이나 일관성 문제를 느끼지 않고, 동기적으로 즉각적인 응답을 받음
  • 시스템 간 통합: 비동기 시스템을 사용하면서도 동기적 API가 필요한 클라이언트와 통합할 수 있어, 다양한 환경에서의 호환성이 증가
  • 복잡성 분리: 비동기 처리의 복잡성을 역방향 프록시에서 관리하고, 클라이언트와의 인터페이스를 단순하게 유지가능

5. 고려 사항:

  • 응답 시간 증가: 프록시에서 동기적 응답을 시뮬레이션하는 과정에서 처리 지연이 발생할 수
  • 상태 일관성 관리: 프록시가 비동기 이벤트 결과를 반환할 때, 상태 일관성을 관리하는 로직이 필요
  • 추가 인프라 비용: 역방향 프록시를 운영하는 데 추가적인 인프라와 관리 비용이 발생할 수

6. 근데 이 역할을 할 때 꼭 프록시를 써야하는가? 그냥 다른 중간 서버를 두면 되는거 아냐?

역방향 프록시를 사용함으로써 보안을 강화하거나 로드 밸런싱을 강화할 수 있음. 중간 서버는 유연성은 높지만 속도나 유지보수 등 필요..

 

분산 이벤트 소싱

TC/C 또는 사가 조정자가 단계별 상태 테이블에 각각의 작업 상태를 다 저장하여 트랜젝션 상태를 추적하는 게 포인트

 

  • 사가 또는 tc/c 적용
  • 유저가 서로 다른 위치의 디비에 있다고 가정
  • raft 알고리즘 적용
  • 역방향 프록시 적용

 

728x90
반응형
반응형

포인트

  • QPS: 43,000
  • max QPS = 215,000

 

지연시간

평균 지연 시간은 낮아야 하고 전반적인 지연 시간 분포는 안정적이어야 함.

  • 지연시간 = 중요 경로상의 컴포넌트 실행 시간의 합

지연시간을 줄이기 위해서는

  • 네트워크 및 디스크 사용량 경감
    • 중요 경로에는 꼭 필요한 구성 요소만 둔다. 로깅도 뺀다.
    • 모든 것을 동일 서버에 배치하여 네트워크를 통하는 구간을 없앤다. 같은 서버 내 컴포넌트 간 통신은 이벤트 저장소인 mmap를 통한다.
  • 각 작업 실행 시간 경감
    • 꼭 필요한 로직만
  • 지연 시간 변동에 영향을 주는 요소 조사 필요
    • gc...

 

속도

주문 추가/취소/체결 속도: O(1)의 시간 복잡도를 만족하도록

  • 추가: 리스트의 맨 뒤에 추가
  • 체결: 리스트의 맨 앞에 삭제
  • 취소: 리스트에서 주문을 찾아 삭제 

주문 리스트는 DoubleLinkedList여야 하고 Map을 사용하여 주문을 빠르게 찾아야 한다.

 

영속성

시장 데이터는 실시간 분석을 위해 메모리 상주 칼럼형 디비(KDB)에 두고 시장이 마감된 후 데이터를 이력 유지 디비에 저장한다.

 

  • KDB+는 Kx Systems에서 개발한 고성능 시계열 데이터베이스로, 대규모 데이터 분석에 최적화되어 있다. 주로 금융 업계에서 빠른 데이터 처리와 분석을 위해 사용된다.
  • 초고속 데이터 처리 능력
  • 시계열 데이터를 효율적으로 저장 및 분석
    • 시간에 따라 변화하는 데이터를 저장하고 관리하는 데 최적화된 데이터베이스
    • 시계열 데이터는 각 데이터 포인트가 타임스탬프와 함께 기록되는 데이터
  • 금융 거래 데이터, 센서 데이터 등 대규모 데이터를 실시간으로 처리 가능

 

 

어플리케이션 루프

  • while문을 통해 실행할 작업을 계속 폴링하는 방식
  • 지연시간 단축을 위해 목적 달성에 가장 중요한 작업만 이 순환문 안에서 처리
  • 각 구성 요소의 실행 시간을 줄여 전체적인 실행 시간을 예측 가능하도록 보장
  • 효율성 극대화를 위해 어플리케이션 루프는 단일 스레드로 구현하며 CPU에 고정
    1. context switch가 없어지고
    2. 상태를 업데이트하는 스레드가 하나뿐이라 락을 사용할 필요 없고 잠금을 위한 경합도 없다.
    3. 단 코딩이 복잡해짐
      • 각 작업이 스레드를 너무 오래 점유하지 않도록 각 작업 시간을 신중하게 분석해야 함

 

mmap

mmap은 메모리 맵핑 (Memory Mapping)을 의미하며, 운영체제에서 제공하는 기능으로, 파일을 프로세스의 메모리에 맵핑하여 파일의 내용을 메모리 주소 공간에서 직접 읽고 쓸 수 있도록 한다. 이를 통해 파일 입출력(I/O) 속도를 향상시키고, 프로세스 간의 메모리 공유를 효율적으로 처리할 수 있다.

/dev/shm 메모리 기반 파일 시스템으로 여기에 위치한 파일에 mmap를 수행하면 공유 메모리에 접근해도 디스크io는 발생하지 않는다.

 

이벤트 소싱

현재 상태를 저장하는 대신 상태를 변경하는 모든 이벤트의 변경 불가능한(immutable) 로그를 저장

이벤트를 순서대로 재생하면 주문 상태를 복구 가능(이벤트 순서가 중요)

지연시간에 대한 엄격한 요구사항으로 카프카 사용 불가, mmap 이벤트 저장소를 메세지 버스로 사용(카프카 펍섭 구조와 비슷)

  • 주문이 들어오면 publish되고 주문 데이터가 필요한 각 컴포넌트가 subscribe 한다.

이벤트는 시퀀스, 이벤트 유형, SBE(simple binary encoding; 빠르고 간결한 인코딩을 위해) 인코딩이 적용된 메시지 본문으로 구성되어 있다.

게이트웨이는 이벤트를 링 버퍼에 기록하고 시퀀서가 링 버퍼에서 데이터를 가져온다(pull). 시퀀서가 이벤트 저장소(mmap)에 기록한다(pub).

  • 링 버퍼(Ring Buffer), 또는 원형 버퍼(Circular Buffer)는 고정된 크기의 버퍼로, 마지막 위치에 도달하면 다시 처음 위치로 돌아가서 덮어쓰는 방식으로 동작하는 자료구조. 링 버퍼는 주로 고정 크기의 메모리 할당을 유지하면서 데이터를 효율적으로 관리하고, FIFO(First In, First Out) 방식으로 데이터를 처리하는 데 사용됨. 데이터를 넣고 꺼내기만 하고 생성이나 삭제하는 연산은 필요 없다. 락도 사용하지 않는다.

 

고가용성

서비스 다운 시 즉시 복구

  1. 거래소 아키텍처의 단일 장애 지점을 식별해야 한다.
  2. 장애 감지 및 백업 인스턴스로 장애 조치 결정이 빨라야

서버 재시작 후 이벤트 저장소 데이터를 사용해 모든 상태를 복구한다.

주 서버의 문제를 자동 감지해야 하는데, 위에서 단일 서버로 설계했기 때문에 클러스터로 구성해야 하며 주서버의 이벤트 저장소는 모든 부 서버로 복제해야 한다.

이때 reliable UDP를 사용하면 모든 부 서버에 이벤트 메시지를 효과적으로 broadcast 할 수 있다.

  • 모든 수신자가 동시에 시장 데이터를 받도록 보장
  • 멀티캐스트: 하나의 출처에서 다양한 하위 네트워크상의 호스트로 보냄, 그룹에 가입한 수신자들만 데이터를 수신. 브로드캐스트와 유니캐스트의 중간

 

부 서버도 죽으면? fault tolerant

DRM마냥 여러 지역의 데이터 센터에 복제 필요..

  1. 주 서버가 다운되면 언제, 어떻게 부서버로 자동 전환하는 결정을 내리나
    • request가 이상하면? 소스 자체가 문제라면? => 운영 노하우를 쌓을 동안 수동으로 장애 복구
  2. 부 서버 가운데 새로운 리더는 어떻게 선출
    • 검증된 리더 선출 알고리즘(주키퍼, raft..)
  3. 복구 시간 목표(Recovery Time Objective)는 얼마
    1. 어플리케이션이 다운되어도 사업에 심각한 피해가 없는 최댓값
  4. 어떤 기능을 복구(Recovery Point Objective) 해야 하나
    1. 손실 허용 범위, 거의 0에 가깝게

 

보안

  • 공개 서비스와 데이터를 비공개 서비스에서 분리하여 디도스 공격이 가장 중요한 부분에 영향을 미치지 않도록. 동일한 데이터를 제공해야 하는 경우 읽기 전용 사본을 여러 개 만들어 문제를 격리
  • 자주 업데이트되지 않는 데이터는 캐싱
  • 디도스 공격에 대비해 아래와 같이 쿼리 파람에 제한을 둔다. 이렇게 바꾸면 캐싱도 유리하다.
    • before: /data?from=123&to=456
    • after: /data/recent
  • 블랙리스트/화이트리스트 작성
  • 처리율 제한 기능 활용
728x90
반응형
반응형

포인트

  • 하루 100만건 = 초당 10건의 트랜젝션(TPS)
  • 10TPS는 별 문제 없는 양, 트렌젝션의 정확한 처리가 중요
  • 각 거래별 멱등성을 위한 트렌젝션 아이디 필요(UUID)
  • 금액은 double이 아닌 string으로 직렬화/역직렬화에 사용되는 숫자 정밀도가 다를 수 있기 때문(반올림 이슈)

디비

  • 성능(nosql)보다는 안정성, ACID를 위한 관계형 데이터 베이스 선호

데이터 저장 시 고려

  • 결제 -> 지갑; 결제 -> 원장 테이블에 저장 시 상태값을 계속 변경하여 관리..
  • 지갑/원장 테이블의 상태가 모두 바뀌어야 결제 테이블 상태 업데이트..

시스템 구성 요소가 비동기적으로 통신하는 경우 정확성 보장?

  • 관련 상태값이 특정 시간동안 부적절한 상태로 남는지 확인하는 배치 돌려서 개발자에게 알람
  • 조정: 관련 서비스 간의 상태를 주기적으로 비교하여 일치하는지 확인(마지막 방어선)
    • 약간 파일 대사 같은 느낌.. 원장과 외부 업체와 데이터 비교

조정 시 불일치가 발생하면

  1. 어떤 유형의 문제인지 파악하여 해결 절차를 자동화
  2. 어떤 유형의 문제인지는 알지만 자동화 할 수 없으면(작업 비용이 너무 높으면) 수동으로
  3. 분류가 불가(판단이 안됨) 수동으로 처리하면서 계속 이슈 트래킹필요

동기 통신

  • 성능 저하
  • 장애 격리 곤란
  • 높은 결합도/낮은 확장성(트래픽 증가 대응 힘듦)

비동기 통신: queue

  • 단일 수신자: 큐 하나를 한 수신자가 처리. 병렬 처리를 위해 스레드 여러개가 동시에 처리하게 함. 큐에서 구독 후 바로 삭제됨
    • 보통 rabbitMQ사용
  • 다중 수신자: 큐 하나를 여러 수신자가 처리. 동일한 메세지를 각기 다른 호흡으로 읽어감. 읽어도 삭제되지 않음. 하나의 메세지를 여러 서비스에 연결할 때
    • 보통 카프카 사용
  • 실패 시 재시도 큐(일시적 오류) / dead letter queue로 이동(모든 재시도 실패)

정확히 한 번 전달? exactly once

  • 최소 한 번 실행 at least once: (지수적 백오프 사용하여) 재시도; 시스템 부하와 컴퓨팅 자원 낭비
  • 최대 한 번 실행 at most once: 멱등성 보장; 고유 키를 멱등 키로 잡아야
    • 광클 시 키 값을 확인하여 이미 처리되었는지 확인하고 되었으면 재처리 하지 않고 이전 상태와 동일한 결과 반환
    • 결제가 되었지만 타임아웃 나서 결과가 시스템에 전달되지 못함 ->  다시 결제 시도 시 이중 결제로 판단하여 종전 결과 반환

 

분산 환경에서 데이터 불일치가 발생할 수 있는데 일반적으로 멱등성조정 프로세스를 활용한다.

디비 불일치는 master-replica 동기화 

 

728x90
반응형
반응형

7. 경계조건

테스트 작성 시 경계 조건을 생각하는데 도움이 될 방법 CORRECT

 

7.1 Conformance 준수

특정 양식을 준수해야할 경우, N * M 가지의 경우의 수 고려

  • 이메일
  • 전화번호
  • 문서 등
헤더 데이터 트레일러
O O O
O O X
O X O
X O O
O X X
X O X
X X O
X X X
  • 처음 입력될 때 검증하면 매번 검증할 필요 없음
    • controller단이나 mapping될 때

 

7.2 ORDERING 순서

  • sorting
  • asc, desc 로 나열 시 맨 처음 값이 제일 큰지/작은지/최신순인지 등 확인 필요

 

7.3 RANGE 범위

경계 범위, 바깥 값 검증

  • int 인데 음수로 들어올 경우?
  • MAX + 1 이 들어올 경우?

primitive obsession(기본형의 과도한 사용)을 피하고 object로 만들어야 한다.

그리고 그 내부에서 검증로직과 경계값에 대한 조건을 가지고 있는게 좋다.

  • 예시: Bearing.java /  Rectangle.java : object안에서 범위에 대한 제약을 다룬다(넘는 경우 예외 포함).
  • RectangleTest.java : 공통 검증 로직을 @After 에서

cf. primitive obsession이란

  • 관련된 데이터를 묶지 못하고 흩어놓아 사용하는 것
  • 이 경우 각각의 데이터에 대한 정보를 외부에 공개하게 됨
  • 함수를 만들때도 각각의 데이터를 파라미터로 넘겨주어야 하기에 파라미터의 갯수가 늘어남
  • 이 때 관련된 데이터를 하나의 구조체(객체)로 묶어 사용해야한다.

https://medium.com/the-sixt-india-blog/primitive-obsession-code-smell-that-hurt-people-the-most-5cbdd70496e9

 

Primitive Obsession — code smell that hurt people the most

Most of the time, developers are aware of common code imperfections and most of the time know how to deal with them like long method…

medium.com

 

7.3.1 커스텀 매처 생성하는 법

hamcrest assertThat

public static <T> void assertThat(T actual, Matcher<? super T> matcher)

 TypeSafeMatcher를 상속받아서 매처 인스턴스를 반환하는 정적 팩토리 매서드(static @Factory method)를 제공하는 방식으로 커스텀 매쳐를 만들 수 있음(ConstrainsSidesTo.java, RectangleTest.java)

 

7.3.2 불변 함수를 내장하여 범위 테스트

sparseArray라는 자료구조를 직접 구현

  • 내부 배열에 저장된 값을 검증하는 테스트 코드를 작성할 때
  • 불필요하게 내부 구현 사항을 노출하기보다
  • 검증 함수를 만들어서 활용하는게 낫다(테스트 코드에서도 쓰고, 필요시 로직에서도 쓰고; checkInvariants())
    • 이 때 코드의 어느 부분에 문제가 있는지 더 쉽게 파악 가능

 

cf. sparseArray

0이 데이터보다 훨씬 많이 있는 array. 0을 실제 저장하지 않고 데이터가 있는 값만을 특별한 형식으로 저장하여 메모리를 효율적으로 사용하려는 방법을 취함.

A sparse array is an array of data in which many elements have a value of zero. This is in contrast to a dense array, where most of the elements have non-zero values or are “full” of numbers. A sparse array may be treated differently than a dense array in digital data handling.

https://developer88.tistory.com/entry/SparseArray-%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94

https://www.geeksforgeeks.org/what-is-meant-by-sparse-array/

 

What is meant by Sparse Array? - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

경계 대상이 인덱스일 경우 아래 사항 고려

  • 시작과 마지막 인덱스가 같으면 안됨
  • 시작이 마지막보다 크면 안됨
  • 인덱스는 음수가 아니어야 함
  • 인덱스가 허용된 값보다 크면 안됨
  • 갯수가 기대값과 같아야 함

 

7.4 Reference 참조

  • 외부 의존성이 있는지
    • 특정 상태에 있는 객체를 의존하는지
  • pre-condition(사전조건) / post-condition(사후조건; 코드가 참을 유지해야하는 조건; assertion; 메서드 반환 )이 있는지 확인
    • 가정이 맞을 때
    • 가정에 맞지 않을 때
      • side effect 확인

 

7.5 Existence 존재

  • null, 0, empty
  • 기대하는 파일이 없을 때
  • 네트워크 다운

 

7.6 Cardinality 기수 / 갯ㅅ

  • 0, 1, N(다수; 경계조건)
    • N은 비즈니스 요구사항에 따라 바뀔 수
  • 0 -> 1 -> N 에 관한 테스트코드

 

7.7 Time 시간

  • 상대적 시간(시간 순서)
    • 테스트 호출 순서 무관
    • 언제든지 반복가능한 테스트
  • 절대적 시간(측정된 시간)
    • 너무 오래걸리지 않는 테스트
    • timeout / 무한대기 고려
  • 동시성/동기화

 

728x90
반응형
반응형

5. 좋은 테스트의 조건 FIRST

5.1 FIRST

https://santim0ren0.medium.com/clean-code-unit-test-3e4b9ee63cb3

5.2 First: 빠르다

  • 테스트를 빠르게 유지해야 함
  • 하루에 서너번 실행하기도 버겁다면 잘못된 것
  • 지속적이고 종합적인, 빠른 피드백을 주지못하면 테스트의 가치가 떨어짐
  • 테스트가 느리다면, 디비와 같은 외부 의존성을 줄여라
    • 외부 의존성은 테스트를 느리게 할 뿐만 아니라 디비에서 값이 변하면 테스트 결과도 변할 것이기에 테스트 자가 불안해짐
    • 책의 예시
      • before: given절에서 테스트를 위한 데이터를 실 디비에서 가져옴
      • after:
        • 1. 디비 조회 결과를 받아서 함수에 주입해주는 방식으로(method argument) 테스트하고자하는 함수를 분리
        • 2. 테스트 코드에선 (디비 결과라고 가정된) 해시맵/리스트를 만들어 테스트
        • 3. 테스트가 빨라졌고 느린 것에 의존하는 코드도 최소화되어 더 나아졌다고 할 수 있음

 

5.3 Independent/Isolated: 고립시킨다

  • 단위테스트: 검증하려는 작은 양의 코드에 집중
  • 최소의 의존성
    • 테스트를 깨뜨리지 않는/ 느리게 하지 않는 / 가용성 / 접근성
  • 다른 단위 테스트에 의존하지 않도록
  • 순서나 시간에 관계없이 실행되어야
  • SRP: 테스트가 하나 이상의 이유로 깨진다면 테스트를 분할하라

 

5.4 Repeatable: 반복 가능해야한다

  • 실행할 때마다 같은 결과 보장
  • 직접 통제할 수 없는 외부 환경에 있는 항목들과 격리
  • 시간을 다뤄야 한다면? 
    • java8.time.Clock 객체를 주입받아(생성자나 setter로 생성시 주입) / 책의 예시

 

5.5 Self validating: 스스로 검증 가능하다

  • 테스트 결과를 수동으로(로그를 눈으로 읽는다거나)하는 방식은 시간이 많이 들고 리스크가 있음
    • -> 자동화 해야
  • CI(지속적 통합;저장소 감지하여 빌드와 테스트)/CD(지속적 배포;)

 

5.6 Timely: 적시에 하다

  • 단위 테스트를 언제 작성하는가?
    • 단위 테스트는 좋은 습관.
    • 절대 미루지말라. 한번 미루면 다시 테스트를 작성하기는 힘들어진다.
  • 팀 내 규칙을 세우라
    • ex. 테스트되지 않은 코드는 반영하지 않는다, CI 환경에 확인할 수 있도록 한다..
  • 옛날 코드에 대한 테스트는 시간 낭비가 될 수도
    • 코드에 큰 결함이 없고 당장 변경 예정이 없다면 그 노력을 신규 코드에

 

728x90
반응형
반응형

4. 테스트 조직

4.1 테스트 코드를 작성할 때 AAA

Arrange: 테스트 코드 실행 전 준비; 객체 생성

Act:테스트 코드 실행

Assert: 실행한 코드가 기대한 대로 동작하는지 확인

After: 때에 따라 필요; 테스트 실행 시 어떤 자원을 할당했다면 clean up 필

=> 각 구분을 구별하기 위해 빈 줄을 삽입

 

  1. Given/When/Then
    • Given
      • 테스트를 위해 주어진 상태
      • 테스트 대상에게 주어진 조건
      • 테스트가 동작하기 위해 주어진 환경
    • When
      • 테스트 대상에게 가해진 어떠한 상태
      • 테스트 대상에게 주어진 어떠한 조건
      • 테스트 대상의 상태를 변경시키기 위한 환경
    • Then
      • 앞선 과정의 결과

 

4.2 단위 테스트를 작성할 때는 먼저 전체적인 시각에서 시작해야

개별 메서트를 테스트하긴 하는 것이 아니라

  • 입금
  • 출금
  • 잔액확인

클래스의 종합적인 동작을 테스트 하는 관점에서 테스트를 작성해야함

  • 입금 -> 잔액 확인 -> 출금 -> 잔액 확인 등의 흐름으로 클래스 전체를 테스트

 

4.3 테스트와 프로덕션 코드의 관계

테스트 코드와 프로덕션 코드는 분리되어야

1. 테스트를 프로덕션 코드와 같은 디렉터리/패키지에 넣기

-> x; 테스트코드를 따로 분리하기 어려움

2. 테스트를 별도 디렉터리로 분리하지만 프로덕션 코드와 같은 패키지에 넣기

-> 주로 채택; test의 디렉터리 구조가 src 디렉터리를 반영; 테스트 클래스는 패키지 수준의 접근 권한을 가짐

3. 테스트를 별도의 디렉터리와 유사한 패키지에 유지

-> 테스트 코드를 프로덕션 코드의 패키지와 다르게하면 public 인터페이스만 활용하여 테스트코드 작성하게 되미. 많은 개발자가 의도적으로 설계할 때 이 정책을 채택

4.3.2 내부 데이터 노출 vs 내부 동작 노출

  • public method가 아닌 함수(비공개 코드; private method)를 호출하는 테스트는 구현 세부 사항과 결속이 심해져 테스트가 깨질 수 있음.
  • 내부의 세부 사항을 테스트하는 것은 저품질로 이어짐
    • ex. 코드의 작은 변화가 수많은 테스트를 깨면(과도하게 내부 구현 사항을 알고 있기에) -> 개발자는 당황 -> 테스트가 더 많이 깨질수록 개발자는 리팩토링이 꺼려짐 -> 코드의 퇴화
  • 테스트를 위해 내부 데이터를 노출하는 것은 테스트와 프로덕션 코드 사이에 과도한 결합을 초래
    • 테스트를 위해 getter 생성
  • private method를 테스트하고 싶은 충동이 든다면.. 설계에 문제가 있는 것(클래스의 SRP위배)
    • -> 의미있는 private method를 추출하여 별도의 클래스로 분리

 

4.4 집중적인 단일 목적 테스트의 가치

테스트는 주요 동작을 단위로 적당하게 나눠야

  • 테스트가 실패할 경우 실패한 테스트 이름이 표시되기에 어느 동작에 문제가 있는지 파악 가능
  • 실패한 테스트를 해독하는 시간을 줄일 수 있음
  • 테스트가 실패할 경우 테스트가 중지되기 때문에 모든 케이스가 실행됨을 보장가능

 

4.5 문서로서의 테스트

4.5.1 일관성 있는 이름으로 테스트를 문서화

  • 어떤 맥락에서 일련의 행동을 호출했을 어떤 결과가 나오는지 명시
  • 단어 일곱개를 넘어가면.. 너무 긴 이름
  • given-when-then 의 양식을 쓸수도
    • givenSomethingWhenDoingThisBehaviorThenSomeResultOccurs
    • 근데 너무 길다.. -> given절 생략
    • doingSomeOperationGeneratesSomeResult
  • 다른 사람에게 의미있게 만들기!

4.5.2 테스트를 의미있게 만들기

  • 테스트가 무슨 일을 하는지 바로 파악할 수 있도록 이름을 개선
  • 지역 변수 이름 개선
  • 의미있는 상수 도입
  • 햄크래스트 단언
  • 커다란 테스트를 나눠 집중적인 테스트 만들기
  • 공통 초기화/정리가 필요한 경우 @Before/@After 사용
  • 테스트 공통화

4.6 @Before/@After

  • @Before는 중복된 초기화가 있을 경우 공통화를 위해 사
    • @Before은 매번 테스트 메서드 실행에 앞서 실행됨
    • 초기화가 늘어갈 경우 @Before 메스드를 여러개로 분할
      • 이 경우 실행 순서를 보장하지 않음
      • 일정한 순서가 필요하다면 단일 @Before 메서드로 결합하여야 함
  • @After는 각 테스트를 한 후에 실행
    • 테스트가 실패하더라고 실행됨
    • 테스트에 발생하는 부산물을 정리

4.6.1 BeforeClass & AfterClass

@BeforeClass 모든 테스트 실행하기 전 한번만

@AfterClass 모든 테스트 실행 후 한번만

 

4.7 테스트를 의미있게 유지; 항상 통과하도록

4.7.1 단위 테스트는 빨라야 

  • 외부 자원에 접근 시 목객체 활용하여 빠르게
  • 필요하다고 생각하는 테스트만 실행
    • BUT 주기적으로 전체 테스트를 돌려 문제를 바로 찾을 수 있도록(묵히지 않도록)
    • 이게 힘들다면 변경되는 클래스 단위/함수 단위의 테스트라도 실행해서 테스트가 통과되도록 유지해야
    • -> 견딜 수 잇는 만큼 많은 테스트 실행하

4.7.2 테스트 제외

테스트 실패 시 주석처리말고 @Ignore 

테스트 돌리면 몇 개의 테스트가 skip 되었는지 알려

728x90
반응형
반응형

컴포넌트

  • 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위이며, 자바의 경우 jar 파일이 컴포넌트임
  • 컴포넌트는 다양한 형태로 만들어질 수 있음
    • 여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성할 수 있음
    • 여러 컴포넌트를 묶어서 war 파일과 같은 단일 아카이브로 만들 수 있음
    • 컴포넌트 각각을 jar과 같이 동적으로 로드 할 수 있는 플러그인이나 실행 가능한 파일로 만들어 독립적으로 배포할 수 있음
  • 컴포넌트가 최종적으로 어떤 형태로 배포되든, 잘 설계된 컴포넌트라면 반드시 독립적으로 개발 가능해야 함
  • 참고. 앞으로 내용을 따라갈 때 거시적인 관점에서 하나의 큰 시스템을 생각하며 흐름을 따라가야 함.

컴포넌트 응집도

  • 어떤 클래스를 어느 컴포넌트에 포함시켜야 할지는 중요한 결정이므로, 제대로 된 소프트웨어 엔지니어링 원칙이 필요함

 

REP(reuse/release equivalance principle): 재사용/릴리스 등가 원칙

  • 정의: 재사용 단위는 릴리스 단위와 같다.
    • 이는 당연한데, 컴포넌트가 릴리스 절차를 통해 관리되지 않거나, 릴리스 번호가 없다면 재사용하고 싶지도, 할 수도 없음
    • 릴리스 번호가 없다면 컴포넌트들이 호환되는지 보증할 수 없음
    • 개발자는 새로운 버전이 언제 출시되고 무엇이 변했는지 알아야 함(새로운 버전으로의 통합 여부 및 시기 결정)
  • REP를 소프트웨어 설계와 아키텍처 관점에서 보면 다음과 같음
    • 단일 컴포넌트는 응집성이 높은 클래스와 모듈들로 구성되어야 함
    • 이를 다르게 보면 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 한다는 것
    • 하나의 컴포넌트로 묶인 클래스와 모듈은 버전이 같고, 동일한 릴리스로 관리되고, 동일한 릴리스 문서에 포함되어야 함
  • 이 원칙 만으로는 클래스와 모듈을 단일 컴포넌트로 묶는 방법을 제대로 설명하지 못하지만(약점), 이 원칙의 약점은 다음 두 원칙(CCP와 CRP)으로 보완할 수 있음

 

CCP(common closure principle): 공통 폐쇄 원칙

  • 정의: 동일한 이유로 동일한 시점에 변경되는 클래스는 같은 컴포넌트로 묶고, 다른 시점에 다른 이유로 변경되는 클래스는 분리하라
  • 대다수의 애플리케이션에서 유지보수성은 재사용성보다 훨씬 중요하며, 변경은 단일 컴포넌트에서 발생해야 함(독립적인 배포)
    • 수정이 필요한 경우 모든 컴포넌트를 조금씩 수정하기 보다는 하나의 컴포넌트만 수정하도록 하는게 낫다.
  • OCP(개방 폐쇄 원칙)는 class level이고, CCP는 컴포넌트 level.
    • OCP: 공통적인 변경에 대해 클래스가 닫혀 있도록 설계.
  • SRP(단일 책임 원칙) class level이고, CCP는 컴포넌트 level : 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안됨
    • SRP: 서로 다른 이유로 변경되는 메소드를 서로 다른 클래스로 분리하라
    • CCP: 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라.

 

CRP(common reuse principle): 공통 재사용 원칙

  • 정의: 컴포넌트 사용자들을 필요로 하지 않는 것에 의존하게 강요하지 말라.
  • CRP도 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움이 됨
  • 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함
    • CRP에서는 재사용한 클래스들의 연결고리가 동일한 컴포넌트에 포함되어 있어야
  • 컴포넌트를 의존하겠다고 결정한다는 것은 생각보다 많은 유지보수가 필요할 것을 암시하며 CRP에 의거하면 동일 컴포넌트로 묶어서는 안되는 것을 의의함
    • 컴포넌트의 단 하나의 클래스만을 사용한다고 해서 의존성이 약해지는게 아님
      • 의존성은 이분법적인 개념(Y/N) 이지 %가 아님
    • 이로 인해 사용되는 컴포넌트가 변경될 때마다 같이 변경(재배포 등)해야 할 가능성이 높음
    • 그러므로 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 함
  • CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말함
  • ISP(인터페이스 분리 원칙)는 class level, CRP는 컴포넌트 level : 필요하지 않은 것에 의존하지 말라
    • ISP: 사용하지 않는 메소드가 있는 클래스에 의존하지 말라
    • CRP: 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라

 

컴포넌트 응집도에 대한 균형 다이어그램

  • 세 원칙은 서로 상충되는데, REP와 CCP는 포함 원칙(컴포넌트를 크게 만듦)이며, CRP는 배제 원칙(컴포넌트를 작게 만)임
  • 뛰어난 아키텍트는 3가지 원칙들이 균형을 이루는 방법을 찾아야 함.

응집도에 관한 세 원칙이 어떻게 상호작용하는가

 

  • 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야 할 비용을 나타냄
    • CCP를 포기하면, 사소한 변경이 생겼을 때 너무 많은 컴포넌트에 영향이 미침
    • CRP를 포기하면 불필요한 릴리스가 너무 빈번해짐
    • REP를 포기하면 재사용이 어려워짐
  • 일반적으로 삼각형의 오른쪽에서 시작해서 왼쪽으로 이동해 감
    • 프로젝트 처음부터 재사용성이 필요하지는 않다가 성숙해지다보면 점점 재사용성(REP)이 중요해짐
  • 프로젝트의 컴포넌트 구조는 시간과 성숙도에 따라 변한다.

컴포넌트 결합

ADP(Acyclic dependency principle): 의존성 비순환 원칙

  • 정의: 컴포넌트 의존성 그래프에 순환이 있어서는 안된다.
  • 많은 개발자가 동일한 소스 파일을 수정하는 환경에서 코드가 동작하지 않게 될 수 있으며, 2가지 해결방법이 발전되어 옴
    • 주단위 빌드
    • 순환 의존성 제거하기

 

주 단위 빌드

  • 4일은 서로를 신경쓰지 않고, 금요일이 되면 코드를 통합하여 시스템을 빌드함
  • 프로젝트가 커지면서 통합에 드는 시간이 계속해서 늘어나게 됨
  • 결국 빌드 일정을 늘려야 하고, 통합과 테스트는 수행하기 점점 어려워지며, 빠른 피드백이 주는 장점을 잃게됨

 

순환 의존성 제거하기

  • 이 문제의 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것
  • 이를 통해 컴포넌트는 개별 관리자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 됨
  • 개발자가 해당 컴포넌트가 동작하도록 만든 후, 릴리스하여 다른 개발자가 사용할 수 있도록 만듬
  • 이는 단순하며 합리적이라 널리 사용되지만 컴포넌트 사이의 의존성 구조를 반드시 관리해야 함
  • 의존성 구조에 순환이 있어서는 안되며, 컴포넌트 간의 의존성은 비순환 방향 그래프(DAG, Directed Acyclic Graph)여야 함

oneway

  • 어느 컴포넌트에서 시작하더라도 의존성 관계를 따라 최초의 컴포넌트로 돌아갈 수 없음
  • Presenters를 담당하는 팀에서 새로운 릴리스를 만들면 이 릴리스에 영향받는 팀을 쉽게 찾을 수 있음
  • Main은 새로 릴리스되더라도 시스템에서 영향받는 컴포넌트가 없음
  • 시스템 전체를 릴리스한다면 릴리스 절차는 상향식으로 진행됨(Entities부터 시작해 Main은 마지막에 처리)

이처럼 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있음

 

순환이 컴포넌트 의존성 그래프에 미치는 영향

circular

  • 요구사항으로 Entities에 포함된 클래스 하나가 Authorizer의 클래스 하나를 사용할 수 밖에 없다면 순환 의존성이 발생함
  • Database 컴포넌트 개발자는 컴포넌트를 릴리스 하려면 Entities, Authorizer, Interactors 모두와 호환되어야 함
  • 세 컴포넌트는 하나의 거대 컴포넌트가 되며, 개발자 서로가 얽매여 모두 항상 정확하게 동일한 릴리스를 사용해야 함
  • Entities를 테스트하려면 Authorizer와 Interactors도 빌드하고 통합해야 하면서 어려워짐
  • 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가함
  • 컴포넌트를 어떤 순서로 빌드해야 올바른지 파악하기 힘들어지며, 올바른 순서라는 것 자체가 없을 수 있음

 

순환 끊기

  • 컴포넌트 사이의 순환을 끊고, DAG로 복구하는 것은 언제든 가능하며, 의존성 역전 원칙 또는 새로운 컴포넌트 생성으로 가능함
  • 의존성 역전 원칙(DIP)
    • User가 필요로 하는 메소드를 제공하는 인터페이스(permissions in Entity)를 제공함
    • 그리고 이 인터페이스는 Entities에, 구현체는 Authorizer에 위치시킴

DIP

 

  • 새로운 컴포넌트 생성
    • Entities와 Authorizer가 의존하는 새로운 컴포넌트를 만듬
    • 그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킴

 

흐뜨러짐(Jitters)

  • 두 번째 해결책(새로운 컴포넌트 생성)이 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 사실
  • 실제로 애플리케이션이 성장하면서 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장함
  • 따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 하며, 어떤 식으로든 끊어내야 함

 

하향식(top-down) 설계

  • 프로젝트 초기에는 컴포넌트 구조를 설계할 수 없음. 즉, 컴포넌트 구조는 하향식(top-down)으로 설계될 수 없음
    • 컴포넌트 의존성 다이어그램은 애플리케이션 기능과는 거의 관련이 없고, 빌드 가능성과 유지보수성의 지도와 같음
    • 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 시스템이 성장하고 변경될 때 함께 진화함
  • 하지만 모듈들이 점차 쌓이기 시작하면 의존성 관리에 대한 요구가 점차 늘어남
    • 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원함
    • 함께 변경되는 클래스는 같은 위치에 배치시킴: 단일 책임 원칙(SRP), 공통 폐쇄 원칙(CRP)
    • 의존성 구조와 관련된 최우선 관심사는 변동성의 격리(자주 변경되는 컴포넌트로 부터 다른 컴포넌트를 보호함)
  • 애플리케이션이 계속 성장하면서 재사용 가능한 요소를 만드는 일에 관심을 기울이기 시작함: 공통 재사용 원칙(CRP)
    • 결국 순환이 발생하면 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장함: 의존성 비순한 원칙(ADP)

"아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도하면 큰 실패를 맛볼 수 있다. 공통 폐쇄 원칙에 대해 그다지 파악하지 못하고 있고, 재사용 가능한 요소도 알지 못하며, 컴포넌트를 생성할 때 거의 확실히 순환 의존성이 발생할 것이다. 따라서 컴포넌트 의존성 구조는 시스템의 논리적 설계에 발맞춰 성장하며 또 진화해야 한다."

 

SDP: 안정된 의존성 원칙

  • 정의: 안정성의 방향으로(더 안정된 쪽에) 의존하라.
  • 변경이 어려운 컴포넌트는 최대한 독립적으로
  • 변경이 어려운 컴포넌트에 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워짐
  • 즉, 변경하기 쉽도록 모듈을 설계해도 이 모듈에 누군가가 의존성을 메달아 버리면 이 모듈도 변경하기 어려워짐

 

안정성(stability)

  • 안정성은 변경의 발생 빈도와는 직접적인 관련이 없고, 변경을 위해 필요한 작업과 관련됨
  • 안정적이라는 것은 변경을 위해 상상한 수고를 감수해야 한다는 것
    • 컴포넌트를 변경하기 어렵게 만드는 많은 요인(컴포넌트의 크기, 복잡도, 간결함 등)이 존재하는데, 이중 다른 컴포넌트가 해당 컴포넌트에 의존하게되면 변경이 특히 어려워짐
    • 왜냐하면 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 들기 때문임

 

 

x is stable

  • X는 안정된 컴포넌트인데, 세 컴포넌트가 X에 의존하며 X는 변경하지 말아야 할 이유가 3가지나 됨
  • 이때 X는 세 컴포넌트를 책임진다고 말하며, 반대로 X는 어디에도 의존하지 않음
  • X가 변경되도록 만들 수 있는 외적인 영향이 전혀 없으므로, X는 독립적이라고 말함

 

t is unstable

  • 아래의 Y는 상당히 불안정한 컴포넌트임
  • 어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성이 없음
  • Y는 세 컴포넌트에 의존하므로 변경이 발생할 수 있는 외부 요인이 3가지이므로, Y는 의존적이라고 함

 

안정성 지표

  • 컴포넌트로 들어오고 나가는 의존성의 개수를 통해 컴포넌트의 불안정성(I)을 계산할 수 있음
    • fan-in: 안으로 들어오는 의존성으로 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수
    • fan-out: 바깥으로 나가는 의존성으로 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수
    • 불안정성(I)은 fan-out / (fan-in + fan-out)으로 계산 가능하며, [0, 1] 사이의 값을 가짐
  • 불안정성(I)가 0인 경우(X)
    • 해당 컴포넌트에 의존하는 다른 컴포넌트는 있지만, 해당 컴포넌트 자체는 다른 컴포넌트에 의존하지 않음
    • 이는 컴포넌트가 가질 수 있는 최고로 안정된 상태이며, 이러한 컴포넌트는 다른 컴포넌트를 책임지며 독립적임
    • X에게 의존하는 컴포넌트가 있으므로 변경이 어렵지만, X를 강제하는 의존성은 갖지 않음
  • 불안정성(I)가 1인 경우(Y)
    • 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만, 해당 컴포넌트는 다른 컴포넌트에 의존함
    • 최고로 불안정한 상태이며 책임성이 없으므로 의존적임
    • 의존하는 컴포넌트가 없으므로 변경하지 말아야 할 이유가 없음
    • Y가 다른 컴포넌트에 의존한다는 뜻은 Y를 변경할 이유가 있다는 것임

 

모든 컴포넌트가 안정적이어야 하는 것은 아니다

  • 우리가 기대하는 것은 불안정한 컴포넌트와 안정된 컴포넌트가 모두 존재하는 상태

 

  • 위 다이어그램은 세 컴포넌트로 구성된 시스템이 갖는 이상적인 구조임
  • 상단에는 변경 가능한 컴포넌트들이 있고, 하단의 안정된 컴포넌트에 의존함
  • 위로 향하는 화살표가 있으면 안정된 의존성 원칙(SDP)에 위배되는 것인데, 존재하지 않음

 

  • Flexible은 변경하기 쉽도록 설계한 컴포넌트임
  • 우리는 Flexible이 불안정한 상태이기를 바라지만, Stable에서 Flexible에 의존성을 걸게 되면 SDP를 위배함
  • Flexible을 변경하려면 Stable과 Stable에 의존하는 나머지 컴포넌트에도 조치를 취해야 함

 

  • 이를 해결하려면 Flexible에 대한 Stable의 의존성을 끊어야 함
  • 예를 들어 Stable의 내부 클래스 U가 Flexible의 내부 클래스 C를 사용할 때, DIP를 도입하면 이 문제를 해결할 수 있음

 

 

DIP 적용

  • US라는 인터페이스를 생성하고 이를 UServer 컴포넌트에 넣은 후 C가 해당 인터페이스를 구현하도록 만듦
  • 이를 통해 Flexible에 대한 Stable의 의존성을 끊고, 두 컴포넌트는 모두 UServer에 의존하도록 강제함
  • UServer는 매우 안정되며(I=0) Flexible은 불안정성(I=1)을 유지할 수 있고, 모든 의존성은 I가 감소하는 방향으로 향함
    • 오로지 인터페이스만을 포함하는 컴포넌트(UServer)를 생성하는 방식이 이상하게 보일 수도 있음
    • 하지만 자바와 같은 정적 타입 언어에서는 이 방식이 흔히 사용되며 꼭 필요한 전략으로 알려져 있음
    • 이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상임

 

SAP: 안정된 추상화 원칙

  • 정의: 컴포넌트는 안정된 정도만큼만 추상화되어야 한다.

고수준 정책(자주 변경해서는 안되는 소프트웨어)을 어디에 위치시켜야 하는가?

  • 고수준 정책을 캡슐화하는 소프트웨어는 안정된 컴포넌트에, 변동성이 큰 소프트웨어는 불안정한 컴포넌트에 포함시켜야 함
  • 하지만 고수준 정책을 안정된 곳에 위치시키면, 그 정책을 포함하는 소스 코드 수정이 어려워져 시스템 전체 아키텍처가 유연성을 잃음
  • 해결: 개방 폐쇄 원칙(OCP)
    • 이 원칙을 준수하는 클래스가 추상 클래스

안정된 추상화 원칙(SAP)

  • 안정된 추상화 원칙은 안정성과 추상화 정도 사이의 관계를 정의
    • 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안됨
    • 불안정한 컴포넌트는 반드시 구체 컴포넌트로써, 컴포넌트가 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있어야 함
  • 안정된 추상화 원칙(SAP)와 안정된 의존성 원칙(SDP)를 결합하면 컴포넌트에 대한 의존성 역전 원칙(DIP)

 

추상화 정도 측정하기

  • A = Na / Nc
    • Nc는 컴포넌트의 클래스 개수다.
    • Na는 컴포넌트의 추상 클래스와 인터페이스 개수다.
  • A = 0 : 컴포넌트에 추상 클래스가 하나도 없다.
  • A = 1 : 오로지 추상 컴포넌트만 있다.

 

주계열

  • 안정성(I)과 추상화 정도(A) 사이의 관계를 표현하면 다음과 같음
  • 최고로 안정적이며 추상화된 컴포넌트는 좌측 상단(0,1)
  • 최고로 불안정하며 구체화된 컴포넌트는 (1,0)에 위치함
  • 모든 컴포넌트가 이 두 지점에 위치하지는 않으며, 컴포넌트는 추상화와 안정화의 정도가 다양함
  • 컴포넌트가 위치할 수 있는 합리적인 궤적을 표현하면 다음과 같음

 

  • 고통의 영역(Zone of Pain)
    • (0, 0) 주변 구역에 위치한 컴포넌트들
    • 매우 안정적이며 구체적인데, 컴포넌트가 뻣뻣한 상태이므로 바람직하지 않음
    • 추상적이지 않으므로 확장이 어렵고, 안정적이므로 변경이 어려움
    • 제대로 설계된 컴포넌트라면 여기에 위치하지 않으며, 배제해야 하는 구역임
    • ex) 데이터베이스 스키마 or String 클래스(String은 변동성이 없으므로 해롭지는 않음) 등
  • 쓸모없는 구역(Zone of Uselessness)
    • (1, 1) 주변 구역에 위치한 컴포넌트들
    • 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않음(쓸모 없음)
    • 이는 누구도 구현하지 않은 채 남겨진 추상클래스인 경우가 많음
  • 주계열(The Main Sequence)
    • 변동성이 큰 컴포넌트들을 두 배제 구역으로부터 가능한 멀리 떨어뜨리는 선분
    • 쓸모없지 않으면서도 심각한 고통을 안겨주지도 않음
    • 가장 바람직한 지점은 주 계열의 종점이긴 하지만 일부 컴포넌트는 불가능할 수 있음
    • 주계열 바로 위에 또는 가깝게 위치할 때 가장 이상적

 

주계열과의 거리

  • 주계열로부터 얼마나 떨어져 있는지 측정하는 지표.
  • D=| A + I -1 |
  • D가 0에 가까울수록 이상적(주계열 선 위)
  • D가 0에서 가깝지 않다면 해당 컴포넌트는 재검토한 후 재구성하도록 계획 가능
    • D지표의 평균과 분산을 통해 다른 컴포넌트에 비해 예외적인 컴포넌트 추출 가능 -> 리팩

 

컴포넌트 산점도

 

  • 반대로 한 컴포넌트의 D를 시간 별(릴리즈 별)로 측정하여 주계열에서 멀리 떨어진 시점에 들어간 feature에 대해 리뷰 가능

 

  • 의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 “흘륭한” 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정함
  • 지표는 임의로 결정된 표준을 기초로 한 측정값에 지나지 않기에, 다음 단계에 대한 힌트로 사용하면 충분

 

 

728x90
반응형
반응형

1부 소개


1장 설계와 아키텍처란?

1. 설계(design)와 아키텍처(architecture) 차이: 없다.

  • 아키텍처: 저수준의 세부사항과는 분리된 고수준의 무언가를 가리킬 때
  • 설계: 저수준의 구조 또는결정사항 등을 의미

-> 저수준(세부사항) & 고수준(구조) 모두 소프트웨어 전체 설계의 구성요소; 개별로는 존재할 수 없고 경계도 뚜렷하지 않다.

2. 목표: 필요한 시스템을 만들고 유지보수하는데 투입되는 인력의 최소화

3. 함정: 지저분한 코드를 작성하면 단기간에는 더 빠르게 갈 수있고 생산성 낮아진다?

-> 빨리 가는 유일한 방법은 제대로 가는것이다.

결론

앞으로 클린 아키텍처가 무엇인지 공부해서 비용은 최소화, 생산성은 최대화 할 시스템을 만들자.


2장 두 가지 가치에 대한 이야기

1. 개발자가 높게 유지해야하는 두 가지 가치

  • 행위(behavior)
    • 요구사항 만족하도록 코드를 작성하는것
  • 구조(structure)/아키텍처
    • 변경하기 쉬워야
      • 변경사항을 적용하는데 드는 어려움은 변경되는 범위(scope)에 비례해야하며 변경사항의 형태(shape)와는 관련이 없어야 한다.
    • 아키텍처는 형태에 독립적이어야

-> 보통 행위에만 초점을 두지만, 둘 다 중요하다는 것.

2. 더 높은 가치란?

-> 구조

  • 완벽하게 동작하지만 수정이 아예 불가능한 프로그램 / 변경 비용이 창출비용을 앞서는 프로그램 -> 유지보수 불가능 -> 곧 쓰레기 (x)
  • 동작하지 않지만 변경 쉬운 프로그램 -> 동작하도록 수정 가능, 요구사항 변경시 유지보수 가능 (v)

결론

기능을 개발하기 쉽고, 간편하게 수정할 수 있으며, 확장하기 쉬운 아키텍처를 만들어야 한다.

이를 위해 투쟁하는 것이 곧 개발자의 책임

728x90
반응형
반응형

목표: Junit 코드를 분석, 리팩토링하는 과정을 공유

  • 보이스카우트 규칙: 소프트웨어 개발에서는 모듈을 체크인할 때, 반드시 체크아웃할 때 보다 아름답게(깨끗하게) 한다는 규칙
The Boy Scout Rule : "Always leave the campground cleaner than you found it."
보이 스카웃 규칙 : 언제나 처음 왔을 때보다 깨끗하게 해놓고 캠프장을 떠날 것.

 

[15-2] 코드 수정

1. prefix 제거

f: 범위 정보

public class ComparisonCompactor {

    private static final String ELLIPSIS = "...";
    private static final String DELTA_END = "]";
    private static final String DELTA_START = "[";

    private int fContextLength;
    private String fExpected;
    private String fActual;
    private int fPrefix;
    private int fSuffix;
    ...
}

 

2. 캡슐화되지 않은 조건문 수정

3. 똑같은 이름의 변수를 여기저기서 사용하지 말자

public String compact(String message) {
    if (expected == null || actual == null || areStringsEqual()) { //
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
    String expected = compactString(this.expected);  //
    String actual = compactString(this.actual); //
    return Assert.format(message, expected, actual);
}
-----------------------------------------------
public String compact(String message) {
    if (shouldNotCompact()) {
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
	String compactExpected = compactString(expected);
	String compactActual = compactString(actual);

    return Assert.format(message, expected, actual);
}

private boolean shouldNotCompact() {
    return expected == null || actual == null || areStringsEqual();
}

 

4. 긍정의 조건문을 만들자

public String compact(String message) {
    if (canBeCompacted()) {
        findCommonPrefix();
        findCommonSuffix();
        String compactExpected = compactString(expected);
        String compactActual = compactString(actual);
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

private boolean canBeCompacted() { //
    return expected != null && actual != null && !areStringsEqual();
}

 

5. 정확한 이름을 부여하라

위 compact 함수는 항상 압축(compact)을 실행하는 게 아니라 조건에 따라 하고 있으며, formatted된 string을 반환한다.

public String formatCompactedComparison(String message) { //
...
}

 

6. 함수는 한가지 일만 해야한다 / 함수, 변수 분리

...

private String compactExpected;
private String compactActual;

...

public String formatCompactedComparison(String message) {
    if (canBeCompacted()) {
        compactExpectedAndActual(); //
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

private void compactExpectedAndActual() { 
    findCommonPrefix();
    findCommonSuffix();
    compactExpected = compactString(expected);
    compactActual = compactString(actual);
}

 

7. 일관성 부족

위에서 3, 4번째 줄이 클래스 변수 세팅하는거고 1, 2번째 줄에서는 함수 안에서 해버림.. 모두 빼버리자..

8. 변수 이름 정확하게(fPrefix -> prefix -> prefixIndex)

private void compactExpectedAndActual() {
    prefixIndex = findCommonPrefix(); // 클래스 변수 세팅; 이름 바꿈
    suffixIndex = findCommonSuffix(); // "
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private int findCommonPrefix() {
    int prefixIndex = 0;
    int end = Math.min(expected.length(), actual.length());
    for (; prefixIndex < end; prefixIndex++) {
        if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) {
            break;
        }
    }
    return prefixIndex;
}

private int findCommonSuffix() { // prefixIndex 사용
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) { 
            break;
        }
    }
    return expected.length() - expectedSuffix;
}

 

9. 숨겨진 시간적인 결합(hidden temporal coupling)

변수 세팅의 순서가 중요하면, 명시적으로 보여줘야한다.

private compactExpectedAndActual() {
    prefixIndex = findCommonPrefix();
    suffixIndex = findCommonSuffix(prefixIndex); //
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private int findCommonSuffix(int prefixIndex) {
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    return expected.length() - expectedSuffix;
}

 

10. 근데 9번이 좀 억지스러워보이고 왜 순서가 필요한지 이유를 설명하지 못한다. 그래서 하나의 함수로 묶으면 좀 더 안전!

private compactExpectedAndActual() {
    findCommonPrefixAndSuffix(); //하나로 합치고
    String compactExpected = compactString(expected);
    String compactActual = compactString(actual);
}

private void findCommonPrefixAndSuffix() {
    findCommonPrefix(); //맨 첨에 실행해버림
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    suffixIndex = expected.length() - expectedSuffix;
}

private void findCommonPrefix() { //롤백
    int prefixIndex = 0;
    int end = Math.min(expected.length(), actual.length());
    for (; prefixIndex < end; prefixIndex++) {
        if (expected.charAt(prefixIndex) != actual.charAt(prefixIndex)) {
            break;
        }
    }
}

이제 저 큰 아이를 리팩토링해아겠다는 생각이 든다.

 

10. 캡슐화(for loop 조건, if문 조건)

11. 올바른 변수명 사용(index -> length)

계속 함수를 고치고보니 index가 사실 진짜 index라기보다 길이를 의미하는 것을 알게되었다.

private void findCommonPrefixAndSuffix() {
    findCommonPrefix();
    int suffixLength = 1; //
    for (; suffixOverlapsPrefix(suffixLength); suffixLength++) { //
        if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
            break;
        }
    }
    suffixIndex = suffixLength;
}

private char charFromEnd(String s, int i) {
    return s.charAt(s.length() - i);
}

private boolean suffixOverlapsPrefix(int suffixLength) {
    return actual.length() = suffixLength < prefixLength || expected.length() - suffixLength < prefixLength;
}

length는 1에서 시작하지 않는다.. 관련해서 쫙 바꿔보자

public class ComparisonCompactor {
    ...
    private int suffixLength;
    ...

    private void findCommonPrefixAndSuffix() {
        findCommonPrefix();
        suffixLength = 0;
        for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
            if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
                break;
            }
        }
    }

    private char charFromEnd(String s, int i) {
        return s.charAt(s.length() - i - 1);
    }

    private boolean suffixOverlapsPrefix(int suffixLength) {
        return actual.length() = suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength;
    }

    ...
    private String compactString(String source) {
        String result = DELTA_START + source.substring(prefixLength, source.length() - suffixLength) + DELTA_END;
        if (prefixLength > 0) {
            result = computeCommonPrefix() + result;
        }
        if (suffixLength > 0) {
            result = result + computeCommonSuffix();
        }
        return result;
    }

    ...
    private String computeCommonSuffix() {
        int end = Math.min(expected.length() - suffixLength + contextLength, expected.length());
        return expected.substring(expected.length() - suffixLength, end) + (expected.length() - suffixLength < expected.length() - contextLength ? ELLIPSIS : "");
    }
}
  • computeCommonSuffix에서 +1을 없애고
  • charFromEnd에 -1을 추가하고
  • suffixOverlapsPrefix에 <=를 사용
  • suffixIndex를 suffixLength로

 

12. 죽은 코드는 삭제

있으나마나 한 조건문(항상 참)은 과감히 지워야 한다.

 

[15-5] 최종코드...생략

 

결론

  • 코드를 리팩터링 하다보면 원래 했던 변경을 되돌리는 경우가 흔하다. 리팩터링은 코드가 어느 수준에 이를 때까지 수많은 시행착오를 반복하는 작업이기 때문이다.
  • 리팩토링에는 정답이 있는 것이 아니라 그 당시의 플랫폼이나 언어, 개발환경에 따라 달라지며, 스스로 만족하는 수준에 이를 때까지 반복되는 작업이다.

 

728x90
반응형
반응형

목표: 깨끗한 클래스 만들기

 

Class should be small

클래스 체계 / encapsulation

  • 클래스 항목의 순서

class

  • 캡슐화
    • public으로 선언된 변수는 거의 없음
    • 변수와 유틸리티 함수는 가능한 공개하지 않기
    • 테스트를 위해 protected/default로 선언하는 경우는 있음

 

클래스는 작아야 한다, 최소의 책임!

  • 클래스의 역할을 and, if, or, but을 사용하지 않고 25 단어 내외로 설명할 수 있어야

단일 책임 원칙(Single Responsibility Principle; SRP)

  • 변경해야 하는 이유가 하나뿐이어야 한다. -> 수정 이유가 같은 것들을 묶는다. -> 유지보수를 쉽게!
  • 작은 클래스 여러 개가 큰 클래스 하나보다 낫다.
  • 다른 작은 클래스와 협력하여 동작하도록

 

응집도(cohesion of class)

  • 클래스는 인스턴스 변수의 수가 적어야 한다.
  • 메서드가 인스턴스 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 높다.
  • 응집력을 잃는다면 클래스를 분리하라(응집도를 유지하면 작은 클래스가 여러 개 나온다).
  • 함수를 작게, 매개변수 목록을 짧게

 

쪼개라

변경하기 쉬운 클래스(가 되도록 쪼개라) 변경으로부터 격리(시키기 위해 쪼개라)
새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소가 되게끔 다른 기능은 보존
특정 함수에서만 사용하는 private 메서드가 있다면 쪼갤 신호 결합도를 낮춰 테스트 코드 작성을 용이하게
한 클래스에 너무 많은 책임이 있지 않도록 변경이 심하거나 외부에 영향을 받는 로직은 추상적인 개념을 분리해 인터페이스(추상클래스)로 분리한다음 테스트 클래스를 만들어서 테스트

 

쪼개야 할 클래스의 예시로 clean code의 저자는 아래를 보여준다.

before

 

after

Sql은 추상 클래스로 분리하고 각각의 책임들을 클래스로 분리했다. 이로써 얻을 수 있는 이점은: 

  • 함수 하나를 수정했다고 다른 함수가 망가질 위험이 사라짐
  • 테스트 코드로 구석구석 증명하기도 쉬워짐
  • 새로운 기능인 Update를 추가하고 싶다면 Sql을 상속하는 UpdateSql 클래스를 생성하면 손쉽게 추가할 수 있음
  • OCP

 

클래스를 쪼개다 보면 (자연스레) 객체지향의 특징인 SOLID를 갖출 수 있다.

  • 기존 기능을 변경할 때 시스템을 확장할 뿐 기존 코드를 변경하지 않는다.
  • OCP(Open-Closed Principle) 개방-폐쇄 원칙: 
    • 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙
  • 변경으로부터 격리; 결합도를 낮추면 유연성과 재사용성이 높아진다.
  • DIP(Dependency Inversion Principle) 의존 역전 원칙:
    • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다는 것(추상화에 의존해야 한다)

참고) SOLID(객체 지향 설계)

728x90
반응형

+ Recent posts