Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

니름: 쉬운 SOA 단위 테스트

1,008 views

Published on

니름은 마이크로서비스를 위한 인터페이스 정의 언어(IDL) 컴파일러이자 원격 프로시저 호출(RPC) 프레임워크입니다. 스포카에서 서비스 지향 설계(SOA)를 적극적으로 도입하면서 쓰기에 적합하도록 구현되었습니다.


제품을 개선하기 위해서는 코드를 고쳐야 합니다. 그런데, 고친 코드가 행여 제품을 망가뜨리는 것이 아닐까 망설이고 고민할 때가 많습니다. 단위 테스트가 있다면 제품을 안전하고 빠르게 개선할 수 있습니다. 하지만 서비스 지향 설계로 제품을 만들다 보면 여러 개의 서비스들이 서로 통신하게 됩니다. 그리고 다른 서비스에 통신하는 기능도 단위 테스트를 작성해야 합니다.


서비스 간 단위 테스트는 까다로운 처리가 많이 필요합니다: 단위 테스트 안에서 요청을 흉내 내기, 실제 서비스를 띄워서 단위 테스트에서 테스트용 서비스에 요청하거나, 또는 요청과 응답을 흉내 내기, 요청한 내용을 역직렬화하고 응답할 내용을 직렬화 하기 등… 니름을 사용하여 서비스를 작성하면 서비스의 인터페이스와 구현을 분리할 수 있습니다.


요청이나 직렬화 등의 작업도 니름이 대신 처리하므로 추상화됩니다. 따라서 단위 테스트를 쉽게 작성할 수 있습니다. 서비스 지향 설계에서 니름을 사용하여 단위 테스트를 작성하면서 느낀 장점과 이것이 기존 방법들과 어떤 차이가 있는지 공유하고 싶습니다.


  • Hey guys! Who wants to chat with me? More photos with me here 👉 http://www.bit.ly/katekoxx
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

니름: 쉬운 SOA 단위 테스트

  1. 1. 니름: 쉬운SOA 단위테스트 파이콘한국 2017 강효준kanghyojun.org https://github.com/spoqa/nirum
  2. 2. 니름에대하여소개합니다 니름의장점 니름사용방법
  3. 3. 서비스지향설계(SOA)에서단위테스트 간단하게 단위테스트소개 다른서비스에의존하는단위테스트구현법 니름을사용해여러서비스에걸친단위테스트를쉽게 작성하는법
  4. 4. 니름(Nirum) 오픈소스프로젝트https://github.com/spoqa/nirum 이영도소설"눈물을마시는새"에서이름을따옴 인터페이스정의언어(IDL) 컴파일러 마이크로서비스를위한원격 프로시져호출(RPC) 프레임워크
  5. 5. 니름IDL이필요한이유 Python에 class 가 없다면? def store_location(lat: float, lng: float) -> Mapping: row = db.execute(''' SELECT name, lat, lng FROM store WHERE earth_distance( ll_to_earth(lat, lng), ll_to_earth(:lat, :lng) ) <= 1.0 ''', lat=lat, lng=lng) return { 'name': row['name'], 'lng': float(row['lng']), 'lat': float(row['lat']), } 위도/경도와매장의정보를하나로묶을수없음
  6. 6.  class 가 없을때속성추가하기 def store_location(lat: float, lng: float) -> Mapping: ... return {..., 'closed': bool(row['closed'])} def store_name(name: str) -> Mapping: ... return {..., 'closed': bool(row['closed'])} 추상화수준이낮은상태 함수의인자가 반복되므로, 여러번고쳐써야하고 실수할확률이큼
  7. 7.  class 로구현하기 class Coord: def __init__(self, lat: float, lng: float): self.lat = lat self.lng = lng class Store: def __init__(self, name: str, coord: Coord): self.name = name self.coord = coord def store_location(coord: Coord) -> Store: ... return Store(...) def store_name(name: str) -> Store: ... return Store(...)  class 로객체의속성을묶어줌
  8. 8. 새로운속성추가하기 class Store: def __init__(self, name: str, coord: Coord, closed: bool): self.name = name self.closed = closed self.coord = coord def store_location(coord: Coord) -> Store: ... return Store(...) def store_name(name: str) -> Store: ... return Store(...) 매장의폐업여부속성이추가되더라도클래스만고침 안고치면 TypeError 가 알려줌
  9. 9. 함수를다른서비스로분리하기 payload = request.get_json() if 'coord' not in payload: raise ValueError() if not isinstance(payload['coord'], dict): raise ValueError() ... 응답에요청한값이존재하는지확인해야함 값이내가 원하는형식의값인지확인해야함 문자열, 정수, 시각(RFC 3339)
  10. 10. 역직렬화(Deserialize) if 'lat' not in payload['coord']: raise ValueError() if not isinstance(payload['coord']['lat'], float): raise ValueError() if 'lng' not in payload['coord']: raise ValueError() if not isinstance(payload['coord']['lng'], float): raise ValueError() store_location( Coord(lat=payload['coord']['lat'], lng=payload['corordinate']['lng']) )
  11. 11. 역직렬화는지루하다 형식에맞는지검사하는코드가 반복되고 많아짐 다른서비스에요청하게 바뀌면서데이터를구조를알기 어려워짐 결국  class 없이프로그래밍하는것과 같음
  12. 12. 니름사용하기 record coord ( # Coordinator float32 lat, # latitude float32 lng, # longitude ); 필요한개념의모양(인터페이스)을정의 이파일을원하는언어로컴파일할수있음
  13. 13. Python으로바뀐니름 class Coord(object): __slots__ = ('lat', 'lng') __nirum_record_behind_name__ = 'coord' __nirum_field_names__ = name_dict_type([ ('lat', 'lat'), ('lng', 'lng') ]) def __init__(self, lat: float, lng: float): ... def __nirum_serialize__(self) -> Mapping[str, Any]: ... @classmethod def __nirum_deserialize__(cls, value) -> 'Coord': ...
  14. 14. 니름사용하여직렬화(Serialize)/역직렬화하기 coord = deserialize_meta(Coord, request_json) store_location(coord) print(serialize_meta(coord)) # {"_type": "coord", "lat": 37.5137, "lng": 127.0582} 니름에정의한 coord 의형식에맞추어서JSON으로부터 Coord 의인스턴스를생성 니름이생성한 Coord 클래스와인스턴스가 역직렬화/직렬화를쉽게 해줌
  15. 15. 니름, 원격 프로시져호출(RPC) 프레임워크 여러서비스끼리통신하기 서비스지향설계(SOA)에서는기능단위로여러서비스나눔 나누어진서비스의기능을잘엮어서필요한서비스를만듦
  16. 16. 여러서비스끼리통신하기 (2) 통신할방법은다양하지만, 스포카에서는HTTP로통신함 요청서비스는인스턴스를JSON으로직렬화하고  requests 사용해서요청 응답서비스는JSON을역직렬화하여인스턴스로만들고 원하는작업을수행함
  17. 17. 백엔드서비스 @app.route('/', methods=['GET']) def store_location(): if 'lat' not in request.args: raise BadRequest('`lat` is required.') if 'lng' not in requst.args: raise BadRequest('`lng` is required.') try: lat = float(request.args['lat']) except ValueError: raise BadRequest('`lat` expected to be `float`.') ... 역직렬화코드와알맞은형식이아닐때에러메시지를적어주는코드가 대부분임
  18. 18. 백엔드서비스(2) ... rows = db.execute( ''' SELECT name, lat, lng FROM store WHERE earth_distance( ll_to_earth(lat, lng), ll_to_earth(:lat, :lng) ) <= 1.0 ''', lat=lat, lng=lng ) return jsonify([ {'name': r[0], 'lat': r[1], 'lng': r[2]} for r in rows ]) 결과값을원하는형식으로맞추는작업이필요
  19. 19. 프론트엔드서비스 @app.route('/<float:lat>,<float:lng>/', methods=['GET']) def store_location(lat: float, lng: float): payload = {'lat': lat, 'lng': lng} response = get('http://store-service.com/', params=payload) if not response.ok: raise InternalServerError() payload = response.json() ... 백엔드서비스가 정의한형식에맞춰서직렬화해야함 백엔드서비스에서응답이잘왔는지확인함
  20. 20. 프론트엔드서비스(2) if 'name' not in payload: ... if 'closed' not in payload: ... return render_template( 'stores.html', name=payload['name'], closed=payload['closed'] ) 원하는형태로사용하기 위해형식검사
  21. 21. Back to the Slide 11 if 'lat' not in payload['coord']: raise ValueError() if not isinstance(payload['coord']['lat'], float): raise ValueError() ... 역직렬화는지루하다 결국  class 없이프로그래밍하는것과 같음
  22. 22. Back to the Slide 11 (2) 서비스지향설계(SOA)에서는여러개의내부서비스가 통신하기 때문에, 다른외부서비스와통신하는 것과 다름없음 인터페이스정의언어(IDL)을사용하지않으면프로그램의절반은형식을검사하는코드 결국  class 없이프로그래밍하는것과 같음 서비스지향설계(SOA)에서는이렇게 코드를구현할수밖에없을까?
  23. 23. 니름RPC 사용하기 record store ( text name, ); service store-service ( store location (coord coord), ); 서비스에필요한메서드의모양을정의함 컴파일하면서비스구현과 요청에필요한인터페이스가 만들어짐.
  24. 24.  service 컴파일결과 class StoreService(service_type): def location(coord: Coord) -> Store: raise NotImplementedError() class StoreService_Client(client_type, StoreService): ... 컴파일된결과는상속받아서WSGI 애플리케이션으로만들수있음 만들어진WSGI 애플리케이션에 StoreService_Client 를사용해서바로요청할수있음.
  25. 25.  service 구현 from store_schema import StoreService class StoreServiceImpl(StoreService): def location(coord: Coord) -> Store: row = db.execute( ''' SELECT name, lat, lng FROM store WHERE earth_distance( ll_to_earth(lat, lng), ll_to_earth(:lat, :lng) ) <= 1.0 ''', lat=coord.lat, lng=coord.lng ) return Store(name=row[0], coord=Coord(lat=row[1], lng=row[2])) if __name__ == '__main__': app = WsgiApp(StoreServiceImpl()) ...
  26. 26.  client 사용 s = StoreService_Client( HttpTransport(url) ) @app.route('/<float:lat>,<float:lng>/', methods=['GET']) def store_location(lat: float, lng: float): store = s.location(coord=Coord(lat=lat, lng=lng)) return render_template( 'stores.html', store=store )
  27. 27. 니름RPC가 좋은점 서비스가 받는인자의형식과 반환하는값의형식의모양을모두정의해서직렬화/역직렬화로직이없음 서비스응답의상태체크도니름이자동으로만든클라이언트가 대신해줌 귀찮을일들을프로그래머대신해줌
  28. 28. 단위테스트에니름적용하기 단위테스트란? 특정단위의코드가 의도한대로동작하는지확인하는테스트기법 Python에선pytest 같은서드파티(third‑party) 라이브러리를사용하거나unittest 같은표준라이브 러리를사용할수있음
  29. 29. 간단한단위테스트예제 def sigma(until: int) -> int: s = 0 count_ = 1 while count_ <= until: s += count_ count_ += 1 return s def test_sigma(): assert sigma(10) == 55 의도한동작을보장함 1부터10까지전부다더하면55가 나와야함 단위테스트가 작성되었으므로 sigma 는조금 더쉽게 고칠(혹은리팩토링) 수있음
  30. 30. 과감하게 끊임없이고치기 def sigma(n): return sum(range(1, n)) def test_sigma(): assert sigma(10) == 55 코드의의도는테스트가 보장하므로, 프로그래머는조금 더쉽게 코드를고칠수있음 지속적통합에도움이됨 지속적통합을통해서비스개선에도도움을줌
  31. 31. 서비스지향설계(SOA)에서단위테스트짜기 단위테스트에서테스트해야하는함수가 다른서비스에의존할수있음 실제서버에단위테스트가 의존한다면, 제대로된단위테스트를할수없음 다른방법이필요
  32. 32. 다른방법으로테스트구현하기 CI(Continuous Integration) 서버에서필요한다른서비스를직접실행하기 서비스를가짜로따라해서(mocking) 테스트하기
  33. 33. 다른서비스실행해서테스트하기 def sigma(n): r = get(URL, {‘number’: n}) resp = json.loads(r) return resp[‘result’] def test_sigma(): assert sigma(10) == 55 CI 서버(CircleCI, TravisCI 등)에다른서비스를직접띄우기가 힘들수있음 테스트도중다른서비스가 테스트외의조건 때문에오류를일으킬수있음 일관적인테스트결과를얻기가 힘듬
  34. 34. 가짜로따라하기 def sigma(n, session): … return resp[‘result’] def test_sigma(): session = Session() adapter = requests_mock.Adapter() session.mount(‘mock’, adapter) adapter.register_uri(‘GET’, URL, text=55) assert sigma(10) == 55 requests는requests‑mock을사용해서requests로만들어지는모든요청을가로채서흉내낼수있 음 httpretty, httmock 등다양한라이브러리가 있음 단위테스트에서의도한응답을내려줄수있도록코드를짜야함
  35. 35. 가짜코드이기 때문에완전히같지않다 GET /sigma/?number=1 HTTP/1.1 Host: example.com Accept: application/json HTTP/1.1 400 Bad Request Content-Type: application/json "`n` is required." 다른서비스에서형태를바꾸더라도알아차릴수없음 다른서비스의형태를바꿀때마다가짜로짠코드도바꿔줘야함
  36. 36. 가짜코드를구현하기 def mock(requst, context): j = request.json() if 'lat' not in j: assert False ... return {'name': 'hello'} def test_store_location(): ... adapter.register_uri(‘GET’, URL, mock) assert store_location(0.1, 0.1) ... 가짜코드가 다른서비스처럼동작하도록코드를채워넣어야함 형식검사코드가 절반이상
  37. 37. 니름을단위테스트에사용해보기 필요한것은단위테스트안에서다른서비스가 원하는형식으로잘전달하고 있는지여부 니름을사용해서서비스의모양과 통신로직을쉽게 만들수있음
  38. 38.  sigma-service 구현하기 service sigma-service ( int64 sigma(int64 number), ); class SigmaService(service_type): ... class SigmaService_Client(client_type): ... 니름파일을컴파일한패키지를보면다른서비스의모양을알수있음 이모양을바로단위테스트에적용할수있음
  39. 39.  sigma-service 테스트에적용하기 class SigmaServiceTest(SigmaService): def sigma(n: int) -> int: return 55 def get_client(test: bool) -> SigmaService: if test: c = SigmaServiceTest() else: c = SigmaService_Client(HttpTransport(URL)) return c def test_sigma(): client = get_client(test=True) assert client.sigma(10) 서비스의모양을바로적용할수있음 테스트의설정을보고 가짜테스트구현체를사용하든지, 서비스에요청하든지할수있음
  40. 40. 모양바뀐것 알아채기 service sigma-service ( int64 sigma ( int64 from , int64 to ), ); def test_sigma(): client = get_client(test=True) # TypeError 일어남. assert client.sigma(10) 모양이바뀌면, 컴파일된패키지의인자도바뀌므로쉽게 API의breaking change를알아차릴수있음 패키지버전은올려줘야함
  41. 41. 형식검사가 필요없음 class SigmaServiceTest(SigmaService): def sigma(n: int) -> int: return 55 컴파일된니름패키지안에서형식검사는전부한후에서비스의메서드로값이들어옴 IDL/RPC를사용하면서얻는장점을테스트코드에서도얻을수있음
  42. 42. 감사합니다 https://github.com/spoqa/nirum 14일, 15일스프린트진행 https://speakerdeck.com/admire93/nireum‑swiun‑soa‑danwi‑teseuteu Spoqa https://www.spoqa.com/

×