Scala in Action의 8장의 내용을 확실히 이해하고자 되새김질 차원에서 하나하나 타이핑해가며 번역하였다.
Scala in Action의 8장 제목은 ‘Building scalable and extensible components’이다. 8장에서 소개되는 문법(추상 타입 멤버, 셀프 타입 멤버, 타입 투영, 팬텀 타입, 타입 클래스 등)에 대한 부분은 생략하고 실무사례로 예로 들고 있는 급여명세 시스템에 대한 부분만 간략히 번역해 보았다.
재사용
먼저, 재사용 가능한 컴포넌트를 Scala로 어떻게 작성하는지 간단히 살펴보기 위해 일반적인 주문시스템을 만들어보자.
일반적인 주문시스템은 다음의 컴포넌트로 구성된다.
주문(Order) – 고객이 주문한 내용을 기술.
창고(Inventory) – 상품을 저장하고 있는 컴포넌트, 주문 전에 상품이 있는지 확인해야 함.
배송(Shipping) – 고객 주문 처리 방법을 기술.
실제는 이보다 더 복잡하지만, 더 큰 컨텍스트로 쉽게 확장할 수 있기 때문에 이 정도로 단순화시키자.
추상 타입 멤버(abstract type memeber)로 주문시스템 컴포넌트를 아래와 같이 추상화할 수 있다.
OrderSystem은 3가지 추상 멤버를 선언하고 있다. 동시에 각 타입은 상한을 정하고 있다. 타입 O는 Order 타입의 하위 타입이다. 마찬가지로 I와 S는 Inventory와 Shipping의 하위 타입이다. 따라서, 각 컴포넌트에 대해 Order, Inventory, Shipping의 로직은 다음과 같이 정의할 수 있다.
placeOrder 메소드는 위에서 언급된 모든 단계들을 셀프 타입 주석(self type annotation)의 은총으로 구현된다. Ordering은 이제 itemExists 메소드의 Inventory와 sheduleShipping 메소드의 Shipping에 의존하게 된다. with 키워드로 다중 셀프 타입 주석을 기술할 수 있음을 주목하자. 이상 다음은 모든 부분들을 합친 주문시스템 컴포넌트이다.
OrderingSystem의 추상 타입 멤버들은 이 컴포넌트가 구체적인 구현에 의존하지 않는 서비스임을 나타낸다. 이는 여러가지 상황에서 재사용할 수 있다. 결합 기능으로 Inventory와 Shipping 트레이트를 조립해서 Odering 트레이트를 만들 수 있고, 셀프 타입으로 Ordering은 트레이트 결합으로 제공되는 서비스를 사용할 수 있다. 이 모든 추상은 Scala에서 확장성 있고 재사용성 있는 컴포넌트를 만들 수 있는 블딩블럭을 제공한다. 도서 주문시스템을 구현하고자 할 경우, OderingSystem을 아래와 같이 쉽게 재사용할 수 있다.
소프트웨어 컴포넌트를 확장해서 현재 소프트웨어 시스템에 기존 소스 코드를 바꾸지 않고 연동하는 것은 소프트웨어 공학이 지닌 근본적인 도전이다. 많은 닝겐들이 Expression problem 을 사용해 객체 지향 상속이 소프트웨어 컴포넌트 확장의 면에서 실패하는 것을 보여주었다. Expression problem은 유형별로 데이터 타입을 정의하는 도전인데, 재컴파일 하지 않고 정적 타입 안정성을 유지하며 데이터 타입의 새 유형과 연산을 추가할 수 있어야 한다는 것이다. 보통 이런 도전은 프로그래밍 언어의 강점과 약점을 증명하는데 쓰인다. Scala에서는 이 문제를 어떻게 풀 수 있는지 살펴보자.
목표는 데이터 타입과 기존 코드의 재컴파일 없이 그러나 정적 타입 안정성(static type safety)은 유지하며 새로운 데이터 타입과 새로운 연산을 정의하는 것이다.
Expression problem은 다음의 요구조건을 모두 만족하며 구현되어야 한다.
두 가지 차원에서 확장 가능성. 새 타입의 정의와 모든 타입에 대해 동작하는 연산의 추가.
문제는 전부를 재빌드해야 하기 때문에, expression problem의 제약으로 인해 되돌아가서 트레이트를 수정할 수 없다는 것이다. 수정하지 않고 어떻게 기존 시스템에 기능을 추가할 것인지는 실제적인 문제이다. expression problem 해법의 어려움을 이해하기 위해, 방문자(Visitor) 패턴을 사용해 이 문제를 풀어보자. 다음과 같이 직원 급여를 처리하는 방문자 하나를 만들자.
USPayroll과 CanadaPayroll 타입 모두 급여명세(payroll) 방문자를 억셉트한다. 직원들의 급여를 처리하기 위해 EmployeePayrollVisitor 인스턴스를 사용할 것이다. 계약자의 매달 급여를 처리하기 위해, ContractorPayrollVisitor라 불리는 새 클래스를 다음과 같이 쉽게 만들 수 있다.
방문자 패턴을 사용하면, 새 연산을 추가하는 것은 쉽지만, 타입은 어떠한가? JapanPayroll이라 불리는 새 타입을 추가하려면, 되돌아가서 모든 방문자가 JapanPayroll를 허용하도록 수정해야 한다. 첫 번째 해법은 새 타입을 쉽게 추가할 수 있고, 두 번째 해법은 새 연산을 쉽게 추가할 수 있다. 그러나 우리는 두 가지 측면을 모두 처리할 수 있는 해법을 원한다.
객체지향 스타일
Scala에서 추상 타입 멤버와 트레이트 결합을 사용해 이 문제를 어떻게 풀어가는지 살펴보자. 동일한 급여명세 시스템을 사용해서, 어떻게 새로운 타입을 추가하는 동시에 타입 안정성을 깨지않고 급여명세 시스템에 새로운 연산을 쉽게 추가할 수 있는지 살펴보자.
모든 것을 한 트레이트에 두게 되면, 이를 모듈로 다룰 수 있다. P 타입은 Payroll 트레이트의 어떤 하위 타입을 의미하는데, 직원들의 급여를 처리하기 위해 추상 메소드로 선언되어 있다. processPayroll 메소드는 주어진 Payroll 타입에 대해 급여명세(payroll)을 처리하기 위해 구현되어야 할 필요가 있다. 아래는 트레이트가 미국과 캐나다 급여명세에 대해 어떻게 확장될 수 있는지 보여준다.
ContractorPayrollSystem 내에 정의된 Payroll 트레이트는 오버라이딩되지 않고 PayrollSystem으로부터 Payroll 타입의 이전 정의를 가리운다. (※. 이를 쉐도잉(shadowing)이라고 하는 듯..) 이전 ContractPayrollSystem 컨텍스트 안에서 이전 정의는 super 키워드를 사용해 접근할 수 있다. 쉐도잉은 코드에서 예기치 않은 오류를 가져다 줄지도 모르나 이 상황에서는 오버라이딩하지 않고 낡은 Payroll 정의를 확장한다.
주목할만한 또 한가지는 추상 멤버 타입 P를 재정의하고 있다는 것이다. P는 processEmployees 메소드와 processContractors 메소드 모두를 이해하는 Payroll의 하위 타입이 될 필요가 있다. 계약자(contractor)를 미국과 캐나다 모두에 대해 처리하기 위해, ContractPayrollSystem 트레이트를 확장하자.
USPayroll과 CanadaPayroll의 이전 정의를 쉐도잉하고 있다. 또한 processContractors 메소드를 구현하기 위해 Payroll 트레이트의 새로운 정의를 결합하고 있다. 타입 안정성 요구사항을 기억하자. Payroll 트레이트를 결합하지 않으면, USContractorPayrollSystem나 CanadaContractorPayrollSystem의 구체적인 구현을 만들 때 오류를 얻게 된다. 마찬가지로 processContractors 연산를 JapanPayrollSystem에 추가할 수 있다.
Scala 일급 모듈(first-class module) 지원을 사용하면, 모든 트레이트와 클래스를 한 객체로 포장하고 전부를 재컴파일하지 않으면서 또 동시에 타입 안정성을 유지하며 기존 소프트웨어 컴포넌트를 확장할 수 있다. 구버전과 새버전의 Payroll 모두 이용할 수 있다는 점, 구성하는 트레이트에 의해 동작들이 조정되는 점을 주목하라. 직원과 계약자들 모두를 처리하기 위해 새 Payroll을 사용하려면, ContractorPayrollSystem 트레이트 중 하나를 결합해야 한다. 다음의 예제는 USContractorPayrollSystem 인스턴스를 어떻게 만드는지를 보여준다.
processPayroll 메소드는 Payroll 트레이트의 processEmployees와 processContractors 둘 다를 호출하지만, 대신에 미국 직원의 급여를 어떻게 처리해야 할지 아는 기존 급여명세 시스템을 쉽게 사용할 수 있다. 왜냐하면 USPayroll 트레이트를 여전히 확신할 수 있기 때문이다. 남은 것은 추가적인 processContractors 부분을 구현하는 것이다.
이상, 우리는 객체 지향 추상 가능성(Object oriented abstractions available)을 사용해 풀어보았다.
함수형 스타일
이제 함수형 프로그래밍 차원에서 접근해보자.
급여명세(payroll) 프로세스는 두 가지 추상화에 의해 주도된다. 하나는 급여명세를 처리하는 국가(country)고 하나는 피지불인(payee)이다.USPayroll 클래스는 다음과 같을 것이다.
C는 급여명세 유형을 나타내는 고차 존재(higher-kinded) 타입이다. 고차 존재 타입인 이유는 USPayroll과 CanadaPayroll 둘 다 타입 매개변수를 취하기 때문이다. A는 피지불인 유형을 나타내는 타입이다. 팬텀 타입(phamtom type)처럼 매개변수화된 타입을 제외하고는 C를 사용하지 않는다는 것을 주목하라. 타입 클래스의 두 번째 빌딩블럭인 PayrollProcessor 트레이트의 임플리시트(implicit, 이하 임플리시트) 정의는 다음과 같다.
12345678
caseclassEmployee(name:String,id:Long)implicitobjectUSPayrollProcessorextendsPayrollProcessor[USPayroll, Employee]{defprocessPayroll(payees:Seq[Employee])=Left("us employees are processed")}implicitobjectCanadaPayrollProcessorextendsPayrollProcessor[CanadaPayroll, Employee]{defprocessPayroll(payees:Seq[Employee])=Left("canada employees are processed")}
국가에 따라 적절한 PayrollProcessor의 정의를 식별하기 위해 PayrollProcessor의 첫번째 타입 매개변수를 어떻게 사용하는지 살펴보자. 임플리시트 정의를 가져다 쓸려면, USPayroll 과 CanadaPayroll 타입에 모두에 대해 임플리시트 클래스 매개변수를 다음과 같이 정의해야 한다.
packagechap08.payroll.typeclassimportscala.langage.higherkindsobjectPayrollSystemWithTypeclass{caseclassEmployee(name:String,id:Long)traitPayrollProcessor[C[_], A]{defprocessPayroll(payees:Seq[A]):Either[String, Throwable]}caseclassUSPayroll[A](payees:Seq[A])(implicitprocessor:PayrollProcessor[USPayroll, A]){defprocessPayroll=processor.processPayroll(payees)}caseclassCanadaPayroll[A](payees:Seq[A])(implicitprocessor:PayrollProcessor[CanadaPayroll, A]){defprocessPayroll=processor.processPayroll(payees)}}objectPayrollProcessors{importPayrollSystemWithTypeclass._implicitobjectUSPayrollProcessorextendsPayrollProcessor[USPayroll, Employee]{defprocessPayroll(payees:Seq[Employee])=Left("us employees are processed")}implicitobjectCanadaPayrollProcessorextendsPayrollProcessor[CanadaPayroll, Employee]{payees:Seq[Employee])=Left("canada employees are processed")}}objectRunPayroll{importPayrollSystemWithTypeclass._importPayrollProcessors._defmain(args:Array[String]):Unit=rundefrun={valr=USPayroll(Vector(Employee("a",a))).processPayrollprintln(r)}}
모든 임플리시트 정의들은 함께 그룹화되어, RunPayroll 객체 안으로, 임포트될 수 있게 거들고 있다. 직원 콜렉션을 제공하는 USPayroll를 인스턴스화할 때 임플리시트 프로세서가 공급됨을 주목하라. 이 경우에는 USPayrollProcessor가 이에 해당한다.
이제 타입 안정성(type satefy)도 지녔는지 검증해보자. Contractor라 불리는 새 타입을 만들자.
1
caseclassContractor(name:String)
피지불인 유형에 대한 제약사항이 없기 때문에, 쉽게 계약자 콜렉션을 만들어 USPayroll에 전달할 수 있다.
1
USPayroll(Vector(Contractor("a"))).processPayroll
그러나 위 라인을 컴파일하는 순간, 컴파일 에러가 발생한다. 왜냐하면 아직 USPayroll 과 Contractor에 대한 암묵적인 정의가 없기 때문이다.
현재 구성에서 새 타입을 추가하는 것은 매우 쉽다. 새 클래스를 추가해서 급여명세 프로세서에 임플리시트를 정의하면 된다.
1234567891011121314151617
objectPayrollSystemWithTypeclassExtension{importPayrollSystemWithTypeclass._caseclassJapanPayroll[A](payees:Vector[A])(implicitprocessor:PayrollProcessor[JapanPayroll, A]){defprocessPayroll=processor.processPayroll(payees)}caseclassContractor(name:String)}objectPayrollProcessorsExtension{importPayrollSystemWithTypeclassExtension._importPayrollSystemWithTypeclass._implicitobjectJapanPayrollProcessorextendsPayrollProcessor[JapanPayroll, Employee]{defprocessPayroll(payees:Seq[Employee])=Left("japan employees are processed")}}
계약자 B에 대한 급여를 지불하는 새 연산도 매우 간단하다. 아래와 같이 계약자에 대한 임플리시트를 정의하면 된다.
123456789
implicitobjectUSContractorPayrollProcessorextendsPayrollProcessor[USPayroll, Contractor]{defprocessPayroll(payees:Seq[Contractor])=Left("us contractors are processed")}implicitobjectCanadaContractorPayrollProcessorextendsPayrollProcessor[CanadaPayroll, Contractor]{defprocessPayroll(payees:Seq[Contractor])=Left("canada contractors are processed")}implicitobjectJapanContractorPayrollProcessorextendsPayrollProcessor[JapanPayroll, Contractor]{defprocessPayroll(payees:Seq[Contractor])=Left("japan contractors are processed")}
위의 임플리시트 정의들을 PayrollProcessorsExtension 객체 안에 추가하면, 모두를 함께 그룹지을 수 있다. 다음의 코드 조각은 직원과 계약자 모두의 급여를 지불하기 위한 코드를 어떻게 사용하지는 보여주고 있다.
보다시피 필요한 클래스와 임플리시트 정의를 모두 임포트해서 일본에 대해 급여를 처리하고 있다. 이번에는 함수형 프로그래밍 기술을 사용하여 Expression problem을 거듭 성공적으로 풀었다. 자바 프로그래머들은 타입 클래스에 익숙해지는데에 다소 시간이 걸릴지도 모르지만, 한번 익숙해지게 되면 변경에 재빠르게 반응하는 소급 모델(retroactive model)의 파워를 갖추게 될 것이다.
나가며
Scala in Action의 8장에서는 Scala의 재사용성과 확장성을 보여주기 위해 Expression problem을 실무 사례와 비슷한 급여명세 시스템에 적용하며 객체지향 프로그래밍(OOP)과 함수형 프로그래밍(FP) 스타일의 해법을 제시하고 있다. OOP에서는 추상 멤버 타입과 트레이트 결합을, FP에서는 팬텀 타입(Phantom Type)과 임플리시트(implicit)를 사용하고 있다. 개인적으로는 아무래도 OOP 스타일이 더 익숙하지만, 임플리시트로 팬텀 타입(Phantom Type)에서 사용하는 타입을 준비하고 타입 매개변수에서 필요한 특정 타입을 지정하면 나머지는 컴파일러가 알아서 처리하는 FP 스타일의 간결함과 유연함이 좀 더 좋아보인다.