Android/TDD

[UnitTest][ch6(2/3편)] 단위테스트, 함수형 프로그래밍, 그리고 함수형 아키텍처 (그리고 헥사고날 아키텍처 첨가)

네모메모 2023. 12. 1. 11:38
반응형

 

 

 

 

 

 

 

 

 

 

전편 포스팅에서 이어지는 내용입니다.

 

 

 

 

 

'출력 기반 단위 테스트' 스타일을 '함수형'이라고도 한다.

 

'출력 기반 테스트'를 적용할 수 있는 것은 '순수 함수' 뿐이다.
'순수 함수'방식으로 사용한 프로그래밍을 '함수형 프로그래밍'이라고 부른다.

 

즉, '출력 기반 단위 테스트' 스타일은 코드를 '함수형 프로그래밍'을 이용해 '순수 함수' 방식으로 작성해야 한다.

 

 


그러므로 함수형 프로그래밍과 함수형 아키텍처에 대한 개념부터 알아보자!


 

함수형 프로그래밍(Functional Programming)이란?

: 순수 함수(Pure function)를 사용한 프로그래밍

 

- 목표 : ["비즈니스 로직을 처리하는 코드  '사이드 이펙트를 일으키는 코드'를 분리하는 것"]

 


 

순수함수(Pure function)란?

- '수학적 함수(mathematical function)'라고도 함

특성

  - 함수의 모든 입출력이 (메서드 이름, 인수, 반환 타입으로 구성된) '메서드 시그니처'에 명시되어 있다. (=숨은 입출력이 없다)

  - 항상 동일한 입력에 항상 동일한 출력값을 생성하는 함수 (출력횟수와 무관하게)

  - 함수의 실행이 프로그램의 실행에 영향을 미치지 않는다. 즉, 함수 내부에서 인자의 값을 변경하거나 프로그램 상태를 변경하는 'Side effect' 없다.

 

특징

- '출력 기반 테스트'를 적용할 수 있는 것은 '순수 함수' 뿐이다.  

- 순수함수 ex)

fun calculateDiscount(product: List<Product>): Double {
	  val discount: Double = product.count() * 0.01
	  return min(discount, 0.2)
}

  ㄴ> 하나의 입력(Product 배열)과 하나의 출력(decimal 타입의 discount)이 있으며, 둘 다 메서드 시그니처에 명시돼 있다.
  ㄴ> 숨은 입출력이 없다.
     => 이로써 CalculateDiscount()는 순수 함수이다.

 

 

입출력을 명시한 '순수함수(Pure function)'의 장점

- 테스트가 짧고 간결하며 이해하기 쉽고 유지보수 하기 쉬우므로 테스트가 쉽다.

다시 말하지만, "출력 기반 테스트를 적용할 수 있는 것은 수학적 함수 뿐이다." 즉, 유지보수성이 뛰어나고 '거짓 양성' 빈도가 낮다.

 

 


 

 

'순수 함수'의 '숨은 입출력'이 없다는 특성을 어긴 

'숨은 입출력' 

단점

- 위 장점의 모든 걸 반대로 가지고 있다.

- 테스트하기 힘들게 한다. (가독성도 떨어진다.)

 

종류

1. 사이드 이펙트 (Side effect)
: 메서드 시그니처에 표시되지 않은 출력이며 숨어있다. 

ㄴ> '숨은 출력'의 가장 일반적인 유형이다.

ㄴ> 어떤 '사이드 이펙트'도 일으키지 않는 애플리케이션을 만들 수는 없다.

 

2. 예외
  :  메서드 시그니처가 전달하지 않은 출력이다.

 ㄴ> 메서드가 예외를 던지면, 프로그램 흐름에 메서드 시그니처에 설정된 계약을 우회하는 경로를 만든다. 
        호출된 예외는 호출 스택의 어느 곳에서도 발생할 수 있으므로, 메서드 시그니처가 전달하지 않은 출력을 추가한다.

 

3. 내외부 상태에 대한 참고

  : 메서드 시그니처에 없는 실행 흐름에 대한 입력이며, 따라서 숨어있다.

 ㄴ> ex1) DateTime.Now와 같이 정적 속성을 사용해 현재 날짜와 시간을 가져오는 메서드

 ㄴ> ex2) 데이터베이스에서 데이터를 질의할 수 있다.

 ㄴ> ex3) 비공개 변경 가능 필드를 참조할 수도 있다.

 

 

 


 

순수함수(Pure function) 판별법

- 프로그램의 동작을 변경하지 않고 해당 메서드에 대한 '호출'을 '반환값'으로 대체할 수 있는지 확인하는 것이다.

   ㄴ> 이렇게 대체하는 것을 '참조 투명성'이라고 한다.

+) '참조 투명성(referential transparency) : 메서드 호출을 해당 값으로 바꾸는 것

 

- ex1) 순수 함수

public int Increment(int x)
{
    return x+1;
}

 

ㄴ> 아래 두 구문은 서로 동일하기 때문에 이 메서드는 수학적 함수다. 

int y = Increment(4);
int y = 5;

 

 

- ex2) 순수함수가 아닌 '사이드 이펙트 (Side effect)'

int x = 0;
public int Increment()
{
    x++;
    return x;
}

 

ㄴ> 반환 값이 메서드의 출력을 모두 나타내지 않으므로 반환 값으로 대체할 수 없으므로 순수함수가 아니다.

ㄴ> 숨은 출력인 필드 x의 변경이 '사이드 이펙트'이다.

 

 

- ex3) "더 순수 함수처럼 보이지만" 순수함수가 아닌 '사이드 이펙트 (Side effect)'

public Comment AddComment(string text)
{
    var comment = new Comment(text);
    _comments.Add(comment); // 사이드 이펙트
    return comment;
}

 

ㄴ> 글(text)를 입력하고 댓글(Comment)를 반환하고 둘 다 메소드 시그니처에 표현되어 있지만,

ㄴ> 추가적인 숨은 출력인  '사이드 이펙트'를 가지고 있어 순수함수가 아니다.

 

 

 


 함수형 아키텍처(Functional Architecture)란?

: 시스템 전체의 아키텍처에 '함수형 프로그래밍' 원칙을 적용하는 것을 목표로 하는 아키텍처

   -> 시스템 아키텍처에 '함수형 프로그래밍'의 원칙을 확장한 개념

 

  함수형 프로그래밍 함수형 아키텍처
정의 프로그램을 수학적인 함수의 조합으로 바라보는 프로그래밍 패러다임 시스템 전체의 아키텍처에 함수형 프로그래밍 원칙을 적용하는 아키텍처
범위 코드 작성 방법을 기준으로 한 패러다임 시스템 아키텍처에 '함수형 프로그래밍'의 원칙을 확장한 개념
강조 키워드 - (코드 수준에서) 부작용을 최소화하고, 불변성을 강조
- 함수 조합과 고차 함수를 통해 코드를 작성하는 방식을 강조
(아키텍처 수준에서도) 부작용 최소화와 불변성을 강조


강조를 통해 유연하고 확장 가능한 프로그래밍 유연하고 확장 가능한 시스템을 설계합니다.

 

 

- "'사이드 이펙트를' 비즈니스 연산 끝으로 몰아서 '비즈니스 로직'을 '사이드 이펙트'와 분리"(=함수형 프로그래밍의 목표)를 이룬다.

   ㄴ> 위 '함수형 프로그래밍'의 목표의 책임을 모든 곳에서 고려하면 복잡도가 배가되고 장기적으로 코드의 유지 보수성을 방해하므로, '함수형 아키텍처'가 적용되어야 한다.

 

 

 


 

- 아래 2가지 코드 유형을 구분해서 '비즈니스 로직'을 '사이드 이펙트'와 분리(=함수형 프로그래밍의 목표)할 수 있다.

 

1. 함수형 코어 (functional code, 불변 코어, immutable core)

   : '결정을 내리는 코드'

   - 순수 함수를 사용해 구현되며 애플리케이션에서 모든 결정을 내린다. 

   - '사이드 이펙트'가 필요 없기 떄문에 '순수 함수'를 사용해 구현된다.

 

2. 가변셸 (mutable shell)

   : 결정에 따라 작용하는 코드

   - 순수 함수에 의해 이뤄진 모든 결정을 가시적인 부분(ex) 데이터베이스의 변경이나 메시지 버스로 전송된 메시지 등)으로 전환한다.

   - '1. 함수형 코어'에 입력 데이터를 제공하고, 프로세스 외부 의존성(ex)DB 등)에 부작용을 적용해 그 결정을 해석한다.

 

 

 

'함수형 코어'와 '가변 셸'의 동작방식

  - '가변 셸'은 모든 입력을 수집한다.

  - '함수형 코어'는 결정을 생성한다. 

  - '셸'은 결정을 '사이드 이펙트'로 반환한다.

 

- {불변 코어}는 {가변 셸}에 의존하지 않는다.
   자급할 수 있고 외부 계층과 격리돼 작동할 수 있어 테스트하기 쉽다. -> {가변 셸}에서 {불변 코어}를 완전히 떼어내 셸이 제공하는 입력을 단순한 값으로 모방할 수 있다.

 


 

이 두 계층을 계속 잘 분리하려면,

'가변 셸'이 의사 결정을 추가하지 않게끔 '결정을 내는 클래스'에 정보가 충분히 있는지 확인해야 한다.

다시 말해, '가변 셸'은 가능한 한 아무 말도 하지 않아야 한다.

 


목표

- '함수형 코어' 여러 '출력 기반 테스트'로 맡긴다.

- '가변 셸'은  적은 수의 '통합 테스트'로 맡긴다.

 

 

 

 

 


'함수형 아키텍처(Functional Architecture)'와 '육각형 아키텍처(Hexagonal Architecture)' 비교

 

- 함수형 아키텍처는 육각형 아키텍처의 하위 집합이다. 극단적으로는 함수형 아키텍처를 육각형 아키텍처로 볼 수도 있다.

 


 

공통점

- 둘 다 관심사 "분리"라는 아이디어를 기반으로 한다. (그러나 분리를 둘러싼 구체적인 내용은 다양하다.)

   ㄴ>  '육각형 아키텍처'는 {도메인 계층}과 {애플리케이션 서비스 계층}을 구별한다.

              {도메인 계층} 은 비즈니스 로직에 책임이 있는 반면, {애플리케이션 서비스 계층}은 (데이터베이스나 SMTP 서비스와 같이) 외부 애플리케이션의 통신이 책임이 있다.

   ㄴ> 함수형 아키텍처'는 '결정{함수형 코어}'과 '실행{가변 셀}'을 분리한다.

 

- 의존성 간의 단방향 흐름

   ㄴ> '육각형 아키텍처'에서 {도메인 계층} 내 클래스는 서로에게만 의존해하고, {애플리케이션 서비스 계층}의 클래스에 의존해서는 안된다.

   ㄴ> '함수형 아키텍처'의 {불변 코어}는 {가변 셸}에 의존하지 않는다. 자급할 수 있고 외부 계층과 격리돼 작동할 수 있다.
           이로 인해 함수형 아키텍처를 테스트하기 쉽다. {가변 셸}에서 {불변 코어}를 완전히 떼어내 셸이 제공하는 입력을 단순한 값으로 모방할 수 있다.

 


 

차이점

- '사이드 이펙트'에 대한 처리가 다르다.

   ㄴ> '함수형 아키텍처'는 모든 '사이드 이펙트'를 {불변 코어}에서 '비즈니스 연산 가장자리'로 밀어낸다.
           이 가장자리는 {가변 셸}이 처리한다.

   ㄴ> '육각형 아키텍처'는 {도메인 계층}에 제한하는 한, {도메인 계층}으로 인한 사이드 이펙트도 문제없다.

       ㄴㄴ> 육각형 아키텍처의 모든 수정 사항은 {도메인 계층} 내에 있어야 하며, 계층의 경계를 넘어서는 안 된다.
                 ex) 도메인 클래스 인스턴스는 데이터베이스에 직접 저장할 수 없지만, 상태는 변경할 수 있다. 애플리케이션 서비스에서 이 변경 사항을 데이터베이스에 적용한다.

 

 

 

 

 

나머지는 3편 포스팅으로 >>

 

 


출처

- 책 정보 : http://www.acornpub.co.kr/book/unit-testing

- https://dongyyy.github.io/java/2019/04/11/java-signature.html

- https://velog.io/@weekbelt/6.-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8A%A4%ED%83%80%EC%9D%BC#631-%ED%95%A8%EC%88%98%ED%98%95-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80

 

 

 

반응형