728x90
반응형
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
반응형
반응형

목표: 올바른 객체/자료구조의 사용이 무엇인지 확인, 상황에 알맞게 사용하자.


객체(object) vs 자료구조(data structure)

  • 객체: 추상화 뒤로 자료(field)를 숨긴 채 자료를 다루는 함수만 공개
  • 자료구조: 자료를 그대로 공개하며 별다른 함수를 제공하지 않음
  • 잡종 구조(hybrid): 반반무마니 구조, 새로운 함수는 물론이고 새로운 객체를 추가하는 것도 어려움(양쪽의 단점만 모은 구조)
    -> 되도록 사용하지 않도록!

객체지향 코드(OOP) vs 절차지향 코드(procedural)

  객체지향 절차지향
특징 bottom-up
기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉬움
새로운 함수를 추가하기 어려움 -> 모든 클래스를 고쳐야
top-down
기존 자료구조를 변형하지 않으면서 새 함수를 추가하기 쉬움
새로운 자료구조를 추가하기 어려움 -> 모든 함수를 고쳐야
단점 1. 느린 개발 속도(설계시간)
2. 느린 실행/처리 속도
1. 유지보수의 어려움
2. 엄격하게 순서가 정해짐
3. 프로그램 분석이 어렵다
장점 1. 생산성/유지보수가 상대적으로 쉬움
2. 자원의 재사용성
1. 컴퓨터 처리 방식과 유사하기 때문에 실행 속도가 빠름
예시

: 어떤 스타일이 항상 맞고, 항상 정답이진 않는다. 각각 상황에 알맞은 설계를 가져가는 것이 핵심.

  • 새로운 자료타입을 추가하는 유연성이 필요? 객체지향적으로!
  • 새로운 동작을 추가하는 유연성이 필요? 절차지향적으로!

The Law of Demeter; 디미터의 법칙

Don't Talk to Strangers
Principle of least Knowledge

 

"객체가 어떤 데이터를 가지고 있는가?"           "객체가 어떤 메시지를 주고받는가?

: 구현을 모른 채 핵심을 조작할 수 있도록, 객체의 내부를 모르게 하라(결합도를 낮추자).

 

기차 충돌(train wreck)

여러 개의 dot을 사용하지 말라.

기차충돌 예시

 

그럼, 단순히 dot이 많다고 디미터의 법칙을 어긴 것인가?

: NO, stream or 일반적인 자료구조는 dot을 사용할 수밖에

  • 객체라면 내부 구조를 숨겨야 하기에 디미터 법칙을 위반한다.
  • 자료구조라면 이미 노출된 내부 구조를 사용하기에 디미터 법칙을 위반하지 않는다.

  • best practice: 뭔가를 하라고 해야지, 속내를 드러내면 안된다.

디미터 원칙 > 행위 노출


DTO: 공개된 변수만 있고 함수(비즈니스 로직)는 없는 클래스

활성 레코드: DTO + 탐색 함수(save, find); 자료 구조로 취급하라

-> 비즈니스 로직을 가지고 있지 않도록 주의!(잡종 구조가 되어버린다)


참고: 객체지향 vs 절차지향 

https://usefultoknow.tistory.com/entry/%EC%A0%88%EC%B0%A8%EC%A7%80%ED%96%A5Procedural-Programming-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5Object-Oriented-Programming-%EC%9E%A5%EB%8B%A8%EC%A0%90-%EB%B0%8F-%EC%B0%A8%EC%9D%B4%EC%A0%90

728x90
반응형

+ Recent posts