반응형

제목: 분산 시스템 테스팅 사례 연구(A Case Study in Testing Distributed Systems)

저자: Brad Long 1, 호주

문서유형: 학계 기술보고서( 10페이지), 2001

 

3-티어 시스템의 중간에 위치한 미들웨어 소프트웨어를 테스트 한 사례를 기술한 자료



테스트 대상 시스템: BlastServer 미들웨어

  • 자바로 개발된 BlastServer 미들웨어는 복수의 자바 클라이언트가 백엔드 데이터 소스로부터 정보를 요청하는 것을 허용하는 제품
  • 애플리케이션의 중간 계층(middle layer)에 위치하여 애플리케이션 아키텍쳐에 큰 변경 없이 백엔드 데이터 소스가 교환(interchanged)되는 것을 허용 함
  • BlastServer는 애플리케이션을 멀티 티어 시스템으로 실행하게 해주는 두 개 컴포넌트(클라이언트 컴포넌트와 서버 컴포넌트)로 구성됨. 전형적인 애플리케이션에서는 자바 클라이언트 프로그램이 원격 서버에게 리퀘스트를 보내면, 원격 서버의 프로그램이 실행되면서 관계 데이터베이스로부터 데이터를 조회하고, 그 결과가 다시 클라이언트에 보내진다.


1. BlastServer 미들웨어의 주요 특징

  • 3-티어 아키텍쳐(three-tier architecture): 멀티 티어 애플리케이션 환경에서 미들 티어 역할을 함(, 사용자 인터페이스, 비즈니스 로직, 데이터 액세스를 디커플링)
  • 복수 클라이언트 지원(multiple client support): 여러 클라이언트 애플리케이션이 단일 서버로부터 서비스를 요청하는 것을 허용함(서버에서 자바 멀티 쓰레딩 활용)
  • 개별 클라이언트의 복수 연결(multiple connections per client): 서버로의 각 연결이 클라이언트 오브젝트를 통해 이루어지며 하나의 클라이언트 애플리케이션 내에 여러 클라이언트 오브젝트가 존재할 수 있음
  • 랑데뷰 메시지 전달 프로토콜(rendezvous message passing protocol): 아래 그림과 같은 클라이언트 서버간의 상호작용을 지원하는 랑데뷰(또는 request-reply) 프로토콜을 사용
  • 여러 클라이언트 소프트웨어 지원(supports any client software): 클라이언트 클래스가 Java로 쓰여졌기 때문에 어떤 프론트엔드 언어든지 자바와 인터페이스 되어야 하는 것이 현재 요구사항. 하지만 클라이언트 컴포넌트를 타 언어로 구현하여 플러그인 하는 것도 가능함(해당 언어가 소켓 통신을 지원하고 BlastServer의 메시지 전달 프로토콜이 정확하게 구현되는 것이 전제 조건)
  • 여러 서버 데이터 소스 지원(supports any server data source): 미들웨어 소프트웨어가 서버 측의 어떤 언어 및 데이터 소스와도 통신할 수 있음(해당 언어/데이터 소스가 표준 입출력 스트림에서 데이터를 송수신 할 수 있어야 하는 것이 전제 조건)

[랑데뷰 메시지 전달 프로토콜]


2. 시스템 설계(System Design)

  • 아래 예처럼 BlastServer는 클라이언트 컴포넌트와 서버 컴포넌트로 구성됨(애플리케이션에 특정한 개발 코드와 제 3자 제품은 회색으로 표시)
  • 클라이언트 컴포넌트는 GUI 또는 서브렛 코드(servlet code)에 의해 사용될 수 있음
  • 디코더는 BlastServer 커맨드를 데이터 소스 커맨드로 해석하는 주문 제작(custom-written) 코드이며 데이터 소스 각 타입에 대해 한번씩만 개발되면 된다. 테스팅 중에는 C로 쓰여진 간단한 스터브(stub)를 사용하여 디코더와 데이터 소스를 대체함
  • 비즈니스 컴포넌트는 RDBMS와 상호작용하는 비즈니스 로직 애플리케이션 코드

[애플리케이션 아키텍쳐 예]


3. BlastServer 클라이언트 컴포넌트(Client Component)

클라이언트 컴포넌트는 아래 그림처럼 3개 클래스로 구성된다.

  • Client: 리퀘스트(request)를 보내고 서버로부터 응답(reply)을 받는 클라이언트 애플리케이션의 인터페이스
  • ArgList: Client 의한 서버 프로세스로의 전송에서 argument 패키징을 위해 사용됨
  • ResultSet: 서버로부터 반환된 결과 데이터(순서가 있는 row들의 집합)를 보관. rowjava.util.List 인터페이스를 구현하는 오브젝트이며, row들의 집합을 가로질러 이동하는 전진(forward) 및 후진(backward) 스크롤링이 지원됨

[BlastServer 클라이언트 측 클래스]


4. BlastServer 서버 컴포넌트(Server Component)

서버 컴포넌트는 아래 그림처럼 5개 클래스로 구성된다.

  • BlastServer: 클라이언트 리퀘스트를 청취하고, 발생된 리퀘스트를 커넥션 큐에 넣고, 클라이언트 커넥션 리퀘스트와 서버 구성 패러미터에 의해 요구되는 신규 BlastHandler 오브젝트를 생성함
  • BlastHandler: 커넥션이 이루어진 클라이언트 애플리케이션과의 일대일 통신을 처리함
  • BlastProcess: 커맨드 디코더를 통해 비즈니스 로직 및 데이터 소스와의 통신을 제공
  • BlastClient: 서버와 클라이언트 애플리케이션 간의 통신 관로(conduit)를 제공
  • BlastLogger: 서버 메시지를 로그(기록)

[BlastServer 서버 측 클래스]


BlastServer 미들웨어 테스팅

  • 테스팅은 클래스(단위) 테스팅, 동시성 테스팅, 통합 테스팅, 시스템 테스팅으로 구성됨
  • 미들웨어 시스템의 정확한 기능성과 동시성(concurrency)의 정확한 구현을 테스팅 하는데 중점을 둠. 테스팅이 LAN 상에서 수행되므로 요청과 응답이 즉각적임. 따라서 네트워크 실패(network failures), 결함 허용(fault tolerance), 보안 등의 이슈는 고려하지 않음
  • 전통적인 테스트 도구와 기법이 적용 가능한 곳에서는 이를 활용하고 분산 시스템에 고유한 속성인 동시성을 테스트하기 위해서는 특수한 기법을 도입하여 테스트를 보강함
  • 상향식(bottom-up)으로 테스트가 수행됨. , Java SDK에서 의존하는 클래스(dependent classes)가 없는 낮은 레벨 클래스들이 먼저 테스트 되고, 이미 테스트된 클래스는 이에 의존하는 다른 클래스에 의해 사용될 수 있음
  • 외부 시스템, 아직 테스트 안 된 컴포넌트, 다른 물리적 기계 상에 상주하는 컴포넌트들을 대신하기 위해 스터브를 생성하여 활용함
  • 테스트 케이스 선정 및 테스트 구현 전략을 문서화하기 위해 테스트 계획서(Test plans)가 사용됨


1. 단위 테스팅(Unit Testing)

  • ResultSet, ArgList, BlastLogger 클래스들의 단위 테스트를 수행하는데 기존 Roast 테스팅 도구를 사용함
  • Roast 도구는 저 레벨의 반복적인 프로그래밍을 최소화하여 테스터가 실제 테스트 케이스에 집중할 수 있게 해주고, 테스트의 실제 값과 예상 값(또한 실제 exception과 예상 exception)을 비교 체크해 준다.
  • 아래 그림은 데이터 테이블로의 커서(cursor)를 제공하는 ResultSet을 위한 테스트 계획이다. ArgListBlastLogger도 이와 유사한 방식으로 테스트 됨

[ResultSet의 테스트 계획서]


2. 동시성 테스팅(Concurrency Testing)

  • BlastServer 클래스의 메쏘드들이 단위 테스트 되기는 했지만 여러 쓰레드가 BlastServer 오브젝트에 액세스할 수 있으므로 동시성 테스팅이 요구됨. , 해당 오브젝트가 쓰레드 안전(thread-safe)” 한지 확인이 필요
  • 동시성을 가진 프로그램(concurrent programs)을 테스팅 하는 방법은 크게 정적 분석 기법(프로그램 모델을 사용하여 분석)과 동적 분석 기법(실제 실행을 통해 프로그램에 대한 정보를 수집)으로 구분된다.
  • BlastServer 클래스의 동시성 테스팅에는 동적 분석 기법이 적용됨


자바 Monitor(동기화를 위한 클래스)를 테스트 하기 위해 제안된 기존 방법을 활용하여 아래와 같이 BlastServer의 동기화된(synchronized) 버퍼 모니터 구현에 대한 동시성 테스트를 수행 함

  • 테스트 프로그램은 테스팅을 위해 BlastServer 오브젝트(모니터)를 인스턴스화 한다. 이 모니터는 두 개의 동기화된 메쏘드(enqueue, getSocket)로 구성된 클래스 임. 동시성 테스팅 프레임워크의 한 부분으로 MonitorClockMonitorTimer 오브젝트도 인스턴스화 한다.
  • 테스트 프로그램은 생산자 및 소비자 프로세스의 실행 순서를 정한다. 생산자(producer)는 모니터 내에 있는 버퍼로 넣어지는 소켓 커넥션(socket connections)을 생성한다. 하나의 서버 소켓으로 구성된 단순한 테스트 스터브 프로그램이 localhost 상에서 생산자 커넥션을 지속적으로 청취함
  • 소비자(consumer)는 모니터 내에 포함된 버퍼로부터 소켓을 회수하기 위해 getSocket을 호출한다. 모니터로의 호출 순서를 통제하기 위해 모니터 클락의 await 메쏘드가 사용됨. , await(t)가 호출되면 클락이 시간 t에 도달할 때까지 오브젝트 호출이 일시 중지된다(suspended).
  • Monitor를 테스트하기 위해 8개 테스트 케이스로 구성된 3개 테스트 시퀀스가 선택됨(이 테스트 케이스들은 Monitor의 각 브랜치가 실행되도록 만든다). 아래는 모니터 버퍼의 정확한 오퍼레이션을 테스트 하는 모니터 호출 시퀀스를 보여준다.
    -
    버퍼에 소켓을 집어 넣고(enqueue) 빼 내기(retrieve) 위해 생산자 쓰레드와 소비자 쓰레드가 생성되며, Ti는 각 모니터 호출(monitor call)이 이루어지는 시간이다.
    -
    표에서 (wait) 표시는 생산자 쓰레드가 소켓 오브젝트를 버퍼에 넣을 때까지 소비자 쓰레드가 중지되는 것을 나타냄
    -
    완료 시간(completion time)은 쓰레드 호출의 완료 시점을 나타냄. 모든 중단 쓰레드(suspended threads)가 모니터 호출을 이 시간에 완료한다. 다수의 completion time은 유효한 시간의 범위를 나타냄
    -
    무기한으로 중지된 쓰레드가 있는 경우 타이머가 이를 발견하고 ‘liveness 에러(데드락)’를 보고한다. 또한 쓰레드가 예상 completion time과 다른 시간에 깨어나면 잠재적인 안전성 에러(a potential safety error)를 의미한다. 수행된 BlastServer 동시성 테스트에서는 이런 에러가 발견되지 않았음

[BlastServer 동시성 테스트 계획서 – 일부 발췌]


3. 통합 테스팅(Integration Testing)

  • 클래스 ResultSet, ArgList, BlastLogger를 독립적으로 테스트 하는 것이 완료되면 클라이언트 측과 서버 측을 따로따로 테스트하는 단계로 이동함
  • ResultSetArgList가 이미 개별적으로 테스트 되었으므로 클라이언트 측 테스팅은 Client 클래스에 집중한다.
  • 서버 측 테스팅은 외부 통신 채널과 관련된 클래스(구체적으로 BlastClientBlastProcess)에 집중한다.


3.1 클라이언트 측 테스팅(Testing the client-side)

  • 서버와의 통신을 담당하는 Client 클래스의 메쏘드 중 특별히 관심 대상이 되는 것은 run이다. run 메쏘드가 원격 머신 상의 프로그램 실행을 요청하고 ResultSet 오브젝트를 반환함
  • 클라이언트 측 테스팅을 위해 아래와 같은 두 가지 방법이 검토됨. 첫 번째 방법은 테스트 스터브의 사용을 피하지만 원래 코드의 변경을 요구하며, 두 번째 방법은 코드 변경을 요구하지 않는다.


방법 1: 소켓 스트림을 스트링 스트림으로 덮어쓰기(Override Socket Streams with String Streams)

  • Client 클래스를 확장하는 서브클래스인 ClientTest를 생성하여 Client를 테스트 함. ClientTest는 통신 채널을 가로질러 테스트 스터브를 세우는 것을 피하기 위해 Client 클래스의 소켓 스트림을 스트링 스트림으로 덮어 쓴다(override).
  • 장점: 원격 프로세스로의 통신 채널이 없기 때문에 해당 프로세스들을 위한 테스트 스터브를 세울 필요가 없으며 따라서 결과 체킹이 단순화 된다.
  • 단점: 원래의 클래스 구현에 변경이 요구된다(DataInputStreamDataOutputStream 애트리뷰트가 private가 아닌 protected가 되도록 코드 변경이 필요)
  • 메시지를 스트링 스트림에 밀어 넣기 위해 사용되는 메쏘드인 setReturnRowsetEndOfMessageClient에는 존재하지 않고 ClientTest에만 존재하며, 이렇게 스트링 스트림에 보내진 메시지들은 Client에 의해 읽혀져서 ResultSet 오브젝트로 바뀐다(unpacked).


방법 2: 서버 스터브와 통신(Communication with Server Stub)

  • 구현을 변경하는 것을 피하기 위해 로컬 머신 상에서 실행되는 서버 스터브를 생성함. 클라이언트는 localhost 상의 소켓에 연결하여 메시지를 전송하고, 서버 스터브는 특정 포트에서 청취를 하다가 결과를 반환한다(이 결과는 클라이언트에 의해 읽혀지고 unpack 된다).
  • 이 방법은 서버 측 테스팅에서 통신 채널에 관련된 오브젝트를 테스팅 하기 위해서도 사용된다(다음 섹션에서 설명)
  • 아래 그림은 Roast 드라이버와 서버 스터브 간의 상호작용을 나타낸다. Client 오브젝트는 데이터를 소켓으로 보내는 run 메쏘드를 실행하며, 이 데이터는 서버 스터브에 의해 읽혀지고(BlastClient 클래스의 receive 메쏘드) 다시 Client에게로 보내진다(BlastClientsend 메쏘드). 

[테스트 드라이버와 서버 스터브 간의 상호작용]


3.2 클라이언트로의 서버 통신을 테스팅(Testing server communication to the client)

  • 서버 측의 한 부분인 BlastClient 클래스는 미들웨어와 클라이언트 머신 간의 통신 채널을 제공한다. , 분산된 두 개의 프로세스 간의 메시지를 보내고 받는 기능을 제공하며 기본적으로 두 개 메쏘드(sendreceive)로 구성된다.
  • 통신 채널(소켓)을 생성하기 위해 서버 역할을 하는 간단한 ServerStub 클래스가 생성되고, 두 개의 BlastClient 오브젝트가 인스턴스화 된다(하나는 클라이언트 프로세스에서 생성되고 다른 하나는 서버 프로세스에서 생성됨)
  • BlastClient 오브젝트는 클라이언트를 통해 서버 스터브 오브젝트로 데이터를 전송하고 수신한다. 서버 스터브는 클라이언트로 다시 보내지는 것을 단순히 그대로 되풀이(echo)한다.
  • 이 간단한 테스트는 Roast 테스팅 프레임워크와 ServerStub 클래스를 사용하여 생성되었고, 통신 오브젝트가 정확하게 전송하고(send), 수신하고(receive), 전송을 끊는 것(hangs up transmissions)을 보장한다.


4. 시스템 테스팅(System Testing)

  • BlastHandler를 제외한 모든 클래스들이 앞에 기술된 방법에 의해 테스트 됨. 클라이언트와 서버 간의 프로토콜 해석기(a protocol interpreter)BlastHandler는 시스템 테스트의 한 부분으로 테스트 된다.
  • 시스템 테스팅은 디코더 및 데이터 소스를 위한 스터브와 Client 오브젝트를 사용하는 클라이언트 프로그램(ExampleApp.java)을 생성하여 수행됨. BlastServer 프로그램과 ExampleApp 프로그램은 각각 다른 Java 가상 머신(VM)에서 시작된다.
  • ExampleApp는 입력 텍스트 파일로부터 메시지들을 읽고 각각의 메시지를 서버로 보내며, 서버는 이 메시지(리퀘스트)를 디코더와 데이터 소스 스터브로 전달한다. 스터브로부터 데이터 반환이 이루어지고 이는 최종적으로 클라이언트로 보내진다.
  • ExampleApp 프로그램의 출력(, 반환된 데이터)은 출력 텍스트 파일로 보내진다. 입력 텍스트 파일과 출력 텍스트 파일 간의 차이가 없으면 테스트가 성공적인 것으로 간주됨
  • 두 개의 ExampleApp 프로그램을 동시에 실행 시키는(각각 별개의 Java VM에서 실행됨) 추가적인 테스트도 수행됨. 또한 개별 클라이언트의 복수 연결속성을 테스트 하기 위하여 하나의 ExampleApp 프로그램으로부터 두 개의 Client 오브젝트를 인스턴스화 하는 테스트도 수행됨


테스트 수행 결과

1. 테스팅에서 수집된 측정 데이터(Metrics collected during testing)

  • 아래 표는 테스트된 모듈의 크기와 달성된 테스트 커버리지를 보여줌. 1 열은 테스트 대상 클래스(class under test), 3열은 테스트 대상 클래스의 코드 라인 수, 4열은 클래스를 테스트 하는데 사용된 Roast 스크립트의 라인 수(그리고 Roast 스크립트로부터 생성된 자바 코드의 라인 수), 5열은 테스팅에서 달성된 일차적인 구문 커버리지(statement coverage), 6열은 최종 달성된 구문 커버리지, 7열은 테스팅 결과로 클래스에 수행된 설계 변경 건수, 8열은 테스팅에서 발견된 결함 건수를 나타낸다.
  • 상용 시스템 치고 코드의 총 라인 수가 작은 편지만( 1000 라인) 내용적으로 쉬운 코드는 아님. Roast 드라이버 스크립트의 라인 수는 상응하는 테스트 대상 클래스의 라인 수와 비슷하거나 적다.
  • 코드 커버리지를 측정하기 위해서 JProbe Coverage 도구가 사용됨

[테스트 커버리지 메트릭]


2. 발견된 결함(Faults Found)

  • 테스트 대상 시스템이 상당 기간 동안 사용되어 왔음에도 불구하고 결함과 일부 클래스의 재설계 필요성이 발견됨
  • BlastClient가 클라이언트로 반환되는 메시지를 그리고 BlastProcess는 데이터 소스로 반환되는 메시지를 각각 부정확하게 초기화 함. 이 에러는 서버로부터 반환되는 데이터가 없는 테스트 케이스에 의해서만 발견됨
  • ResultSet에서 중요한 설계 변경 발생. 현실에서 ResultSet은 대개 데이터 셋을 가로질러 전진 이동하는데 주로 사용됨. 하지만 전진과 후진으로 방향을 조합한 테스팅이 일부 미정의 된 동작을 드러나게 하였고 결과적으로 ResultSet 클래스의 재설계를 불러옴
  • 최종 커버리지 테스팅에서 Client 클래스에 있는 결함 하나를 찾게 됨. Client에는 각기 다른 signature를 가진 여러 run 메쏘드가 존재하는데, 이 중 하나가 스트링 값이 필요한 필드에 null 값을 전달하고 있었고 따라서 NullPointerException이 발생함(이전 테스트에서는 모든 run 메쏘드가 일일이 호출되지 않았었고 따라서 이 결함을 발견하지 못하였음)


반응형

+ Recent posts