 |  |
|
난이도 : 초급 Ted Neward, Principal, Neward & Associates
옮긴이: 김도형 dwkorea@kr.ibm.com
2008 년 6 월 24 일 스칼라는 단순히 JVM에서 함수적 개념을 사용할 수 있게 해 주는 것뿐만 아니라 객체 지향 언어 설계에 대해서도 현대적인 시각을 제시합니다.
바쁜 자바 프로그래머를 위한 스칼라 입문
의 이번 글에서는 스칼라에서 특성(trait)을 이용해 어떻게 객체를 더 간단하고 쉽게 만들 수 있는지를 살펴봅니다. 앞으로 알게 되겠지만 특성은 자바(Java™) 인터페이스와 C++의 다중 상속이라는 기존의 대조되는 개념과 비슷한 면도 있고 다른 면도 있습니다.
유명한 과학자이자 연구자인 아이작 뉴튼(Isaac Newton) 경은 "내가 더 멀리 봤다고 한다면 그건 거인들의 어깨에 서 있었기 때문이다"라는 말을 했다. 열렬한 역사가이자 정치학자로서 필자는 이 위인의 말을 약간 바꿔 다음처럼 말하고 싶다. "내가 더 멀리 봤다고 한다면 그건 역사의 어깨에 서 있었기 때문이다." 이 말은 역사가인 George Santayana가 말한 "과거를 기억하지 못하는 자는 과거를 되풀이할 수 밖에 없다"라는 말을 반영한다. 다시 말해 우리가 역사를 되돌아보고 (우리 자신을 포함해) 우리보다 먼저 왔던 사람들의 실수에서 배우지 못한다면 뭔가 나아지리라 기대하기 어렵다.
이쯤 되면 대체 이 철학적인 사색이 스칼라와 무슨 관계가 있는지 궁금할 것이다. 하나를 꼽자면 상속(inheritance) 개념에도 적용된다. 자바 언어가 거의 20년 전 "객체 지향"의 전성기에 만들어졌다는 사실을 상기하자. 자바는 솔직히 말해 당시 가장 많이 사용되던 언어인 C++ 개발자를 자바 플랫폼으로 끌어오기 위해 C++를 본떠 설계되었다. 자바 설계 시 어떤 부분은 당시에만 해도 명확하고 필요하다고 생각했던 것이지만, 되돌아보면 이제 그 중 일부는 애초 설계자들이 생각했던 것보다는 유용하지 않다.
예를 들어 20년 전 자바 언어를 설계한 사람들은 C++ 스타일의 private 상속과 다중 상속을 없애는 편이 낫다고 생각했다. 그 이후 그런 결정에 대해 유감을 가질 만한 이유가 많은 자바 개발자에게 생겼다. 이번 글에서는 자바 언어에서 다중 상속과 private 상속의 역사를 다시 짚어보고, 우리 모두에게 더 큰 이점을 주기 위해 그 역사를 다시 쓰는 데 있어 스칼라가 무엇을 했는지를 살펴볼 것이다.
C++와 자바 언어의 상속
역사는 지나간 사건들에 대한, 사람들이 따르기로 결정한 대로의 각색이다.
— 나폴레옹 보나파르트
C++로 작업해 왔던 사람이라면 private 상속이 명시적으로 IS-A 관계를 만들지 않으면서 베이스 클래스의 동작을 상속하는 방법이었다는 것을 기억할 것이다. 베이스 클래스를 "private"으로 하면 새로운 클래스가 실제로는 베이스 클래스의 한 종류가 되지 않으면서도 베이스 클래스를 상속할 수 있었다. 하지만 private 상속은 그 자체로는 성공적이지 못한 특성 중 하나였다. 해당 베이스 클래스의 슈퍼 클래스 객체나 서브 클래스 객체를 해당 베이스 클래스로 타입을 변환하지 못하면서 상속을 한다는 생각 자체가 그저 엉뚱해 보였다..
 |
이 연재에 대해
저자인 Ted Neward는 이 연재를 통해 독자들에게 스칼라 프로그래밍 언어를 심층적으로 소개한다. 이 새 developerWorks 연재를 통해 스칼라에 대한 최근 스칼라를 둘러싼 떠들썩한 호평의 실체를 살펴보고, 스칼라의 언어적 특성의 일부가 실제 어떻게 사용되는지도 배우게 될 것이다. 비교가 필요할 때는 항상 스칼라 코드와 자바 코드를 나란히 보일 예정이다. 하지만 곧 알게 되겠지만 스칼라에 있는 많은 요소는 자바 언어와 직접적인 관계가 없는 것들이고, 이런 부분이 스칼라를 매력적으로 만든다. 그렇다면 자바 코드로도 할 수 있다면 왜 힘들여 스칼라를 배울까?
|
|
반면 다중 상속은 일반적으로 객체 지향 프로그래밍의 필수 요소로 받아 들여졌다. 예를 들어 탈 것의 체계를 모델링하는 경우, 분명 SeaPlane(수상 비행기)은 (startEngine()과 sail()이라는 메서드를 가진) Boat(보트)와 (startEngine()과 fly()라는 메서드를 가진) Plane(비행기) 양 쪽에서 상속 받아야 한다. SeaPlane은 Boat이기도 하고 Plane이기도 하다.
하여튼 이런 것이 C++ 전성기 때의 생각이었다. 여기서 자바 언어의 시대로 급히 돌아와 보면, 다중 상속을 private 상속만큼 잘못됐다고 생각하는 우리를 발견하게 된다. 대신 자바 프로그래머라면 누구라도 SeaPlane은 인터페이스인 Floatable(물에 뜰 수 있는 것)과 Flyable(날 수 있는 것)을 상속해야 한다고 할 것이다. (그리고 아마도 추가로 EnginePowered(엔진으로 동작하는 것)라는 인터페이스 혹은 베이스 클래스를 상속해야 한다고 할지도 모르겠다.) 인터페이스에서 상속한다는 것은 가상 다중 상속(virtual multiple inheritance)이 야기하는 골치 아픈 문제 없이 그 클래스가 필요로 하는 메서드를 구현할 수 있다는 뜻이다. (즉 SeaPlane의 startEngine()을 호출했을 때 어느 베이스 클래스의 startEngine()이 호출되어야 하는지에 대한 문제를 말하는 것이다.)
불행히 private과 다중 상속을 버리는 대신 코드 재사용 측면에서는 큰 대가를 치르게 되었다. 자바 개발자는 가상 다중 상속이 없어져 기쁠지 모르겠지만, 그 대가로 종종 더 힘들게, 실수할 소지가 높은 일을 해야 한다.
동작을 재사용할 수 있는지 다시 살펴 보자
사건은... 아마 크게 나눠서, 일어나지 않았을 수도 있는 부류와 일어나든 말든 별로 중요하지 않은 부류로 나눌 수 있을 것이다.
— 윌리엄 랄프 잉(William Ralph Inge)
자바빈즈(JavaBeans) 명세는 자바 플랫폼의 기반 중 하나로 자바 생태계의 큰 부분이 의존하는 POJO(Plain Old Java Object) 개념을 생겨 나게 했다. 우리는 Listing 1처럼 자바 코드에서 속성(property)이 get()/set() 메서드 쌍으로 관리된다는 것을 잘 알고 있다.
Listing 1. Person POJO
// 자바 코드
public class Person
{
private String lastName;
private String firstName;
private int age;
public Person(String fn, String ln, int a)
{
lastName = ln; firstName = fn; age = a;
}
public String getFirstName() { return firstName; }
public void setFirstName(String v) { firstName = v; }
public String getLastName() { return lastName; }
public void setLastName(String v) { lastName = v; }
public int getAge() { return age; }
public void setAge(int v) { age = v; }
}
|
Listing 1을 보면 비교적 단순하고 작성하기도 어렵지 않다. 하지만 여기에 다른 코드에서 POJO에 등록하고 속성이 변했을 때 호출을 받는 식으로 통보 기능을 넣으려고 한다면 어떨까? 자바빈즈 규격에 따르면 이런 경우 PropertyChangeListener 인터페이스를 구현하고 그 인터페이스가 정의하는 유일한 메서드인 propertyChange()를 정의해야 한다. 게다가 해당 POJO의 PropertyChangeListener들이 속성 변경 여부에 대해 "투표"할 수 있게 하려면, 이제는 해당 POJO가 VetoableChangeListener 인터페이스를 구현하고 거기에 정의된 vetoableChange() 메서드를 정의해야 한다.
적어도 이게 정해진 방법이다.
사실 PropertyChangeListener 인터페이스는 속성 변경을 통보 받을 가능성이 있는 수신자가 구현해야 하고, 통보를 하는 측(이 경우는 Person 클래스)은 이 인터페이스를 구현하는 객체와 리스너가 관심 있는 속성 이름을 받는 public 메서드를 제공해야 한다. 그 결과 나온 것이 Listing 2에 보인 더 복잡해진 Person 클래스다.
Listing 2. Person POJO, 2편
// 자바 코드
public class Person
{
// 각 설정자(setter) 내에 다음 같은 작업을 하는 것 빼곤 다른 부분은 이전과 동일함.
// public setFoo(T newValue)
// {
// T oldValue = foo;
// foo = newValue;
// pcs.firePropertyChange("foo", oldValue, newValue);
// }
public void addPropertyChangeListener(PropertyChangeListener pcl)
{
// pcl 값을 저장
}
public void removePropertyChangeListener(PropertyChangeListener pcl)
{
// 저장된 pcl을 찾아서 삭제
}
}
|
속성 변경 리스너에 대한 참조를 저장해야 한다는 것은 바꿔 말하면 Person POJO가 해당 참조값들을 저장하기 위해 ArrayList 같은 컬렉션 클래스를 가지고 있어야 한다는 말이다. POJO 클래스 객체가 생성되고 추가되거나 삭제되어야 한다. 또, 이런 작업은 쪼갤 수 없는 작업이 아니기 때문에(not atomic) 적절한 동기화를 통해 보호해야 한다.
마지막으로 속성값이 변하면 속성 리스너 목록에 있는 리스너들이 통보를 받아야 한다. 보통 PropertyChangeListener 집합 내부를 하나씩 짚어가면서 각각의 propertyChange() 메서드를 호출한다. 이 과정에서 PropertyChangeEvent 클래스와 자바빈즈 규격에서 지정한 대로 해당 속성과 이전 값, 새로운 값을 나타내는 새로운 PropertyChangeEvent를 넘겨줘야 한다.
우리가 작성한 POJO가 거의 리스너 통보를 지원하지 않는 것도 무리가 아니다. 상당히 많은 일이고 매번 자바빈/POJO를 만들 때마다 수작업으로 반복해야 한다.
일, 일, 일, 대안은 없나?
흥미롭게도 C++의 private 상속이 자바 언어로 넘어왔다면 자바빈즈 명세의 수수께끼 중 일부를 해결하는 데 활용할 수 있었을 것이다. 즉 베이스 클래스에서 POJO의 기본 add()와 remove() 메서드, 컬렉션 클래스, 속성값 변경을 리스너에 통보하기 위한 "firePropertyChanged()"를 제공할 수 있다.
자바 클래스로도 여전히 그렇게 할 수 있지만 자바에는 private 상속이 없기 때문에 Person 클래스는 베이스 Bean 클래스에서 상속 받아야 하고 자연히 Bean으로 타입 변환할 수 있게 된다. 결국 Person 클래스는 다른 클래스에서 상속 받을 수 없다. 다중 상속이 있다면 이 문제를 해결할 수 있을 테지만 궁극적으로 피하고 싶은 가상 상속을 다시 끌어들일 수 밖에 없게 된다.
이 문제에 대한 자바 언어의 해법은 잘 알려진 지원(support) 클래스 패턴으로, 이 경우는 PropertyChangeSupport 클래스에 해당한다. POJO 내에 지원 클래스 객체를 하나 생성하고 POJO 자체에는 필요한 pubilc 메서드를 정의한다. 그리고 각 public 메서드에서는 실제 일을 처리하기 위해 지원 클래스의 메서드를 호출한다. 다음은 PropertyChangeSupport를 사용하도록 바꾼 Person POJO다.
Listing 3. Person POJO, 3편
// 자바 코드
import java.beans.*;
public class Person
{
private String lastName;
private String firstName;
private int age;
private PropertyChangeSupport propChgSupport =
new PropertyChangeSupport(this);
public Person(String fn, String ln, int a)
{
lastName = ln; firstName = fn; age = a;
}
public String getFirstName() { return firstName; }
public void setFirstName(String newValue)
{
String old = firstName;
firstName = newValue;
propChgSupport.firePropertyChange("firstName", old, newValue);
}
public String getLastName() { return lastName; }
public void setLastName(String newValue)
{
String old = lastName;
lastName = newValue;
propChgSupport.firePropertyChange("lastName", old, newValue);
}
public int getAge() { return age; }
public void setAge(int newValue)
{
int old = age;
age = newValue;
propChgSupport.firePropertyChange("age", old, newValue);
}
public void addPropertyChangeListener(PropertyChangeListener pcl)
{
propChgSupport.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl)
{
propChgSupport.removePropertyChangeListener(pcl);
}
} |
동의할지는 모르겠지만, 위 코드의 복잡도는 차라리 다시 어셈블리 언어로 돌아가고 싶어질 정도다. 더 심한 것은 POJO를 작성할 때마다 정확히 이런 코드를 반복 작성해야 한다는 점이다. Listing 3에 들어간 노력의 반은 POJO 자체에 들어있고, 따라서 (전통적인 "긁어 붙이기(cut and paste)" 방식이 아니면) 재사용할 수 없다.
이제 스칼라가 어떻게 더 나은 대안을 제시하는지를 살펴 보자.
스칼라에서 특성과 동작의 재사용
모든 이는 자신 성격의 특성에 대해 잘 숙고해야만 한다. 또한 모든 이는 그런 특성을 충분히 조절해야 하고 다른 사람의 특성이 자신에게 더 맞을까 궁금해 하지 말아야 한다.
— 키케로(Cicero)
스칼라에서는 인터페이스와 클래스 중간쯤 되는 새로운 구문인 특성(trait)을 정의할 수 있다. 특성은 인터페이스처럼 특정 클래스에 원하는 만큼 특성들을 추가할 수 있지만 클래스처럼 동작을 포함할 수 있다는 점에서 특별하다. 또한 클래스나 인터페이스처럼 특성도 새 메서드를 추가할 수 있다. 하지만 클래스나 인터페이스와 달리 해당 특성이 실제 클래스의 일부가 되기 전까지는 특성에 정의된 동작을 확인하지 않는다. 다시 말해 클래스 정의에서 특성을 포함하기 전까지는 제대로 작성되었는지 점검되지 않는 메서드를 정의할 수 있다.
특성이 복잡해 보일지도 모르겠지만 일단 실제 쓰는 것을 보면 더 이해하기 쉽다. 우선 Person POJO를 스칼라로 다시 작성해 보자.
Listing 4. 스칼라로 작성한 Person POJO
// 스칼라 코드
class Person(var firstName:String, var lastName:String, var age:Int)
{
}
|
여기서 스칼라로 작성한 POJO에 자바 POJO 환경에서처럼 get()/set() 메서드가 정의되도록 하려면 클래스 인자인 firstName, lastName, age에 scala.reflect.BeanProperty 애노테이션(annotation)을 붙이면 된다. 하지만, 당분간은 예제를 단순하게 하기 위해 해당 메서드는 생각하지 않기로 하자.
Person 클래스가 PropertyChangeListener를 받도록 하려면, Listing 5처럼 하면 된다.
Listing 5. 스칼라로 작성한 리스너를 지원하는 Person POJO
// 스칼라 코드
object PCL
extends java.beans.PropertyChangeListener
{
override def propertyChange(pce:java.beans.PropertyChangeEvent):Unit =
{
System.out.println("Bean changed its " + pce.getPropertyName() +
" from " + pce.getOldValue() +
" to " + pce.getNewValue())
}
}
object App
{
def main(args:Array[String]):Unit =
{
val p = new Person("Jennifer", "Aloi", 28)
p.addPropertyChangeListener(PCL)
p.setFirstName("Jenni")
p.setAge(29)
System.out.println(p)
}
}
|
Listing 5에서 object를 사용함으로써 어떻게 정적 메서드를 리스너로 등록할 수 있게 됐는지를 살펴보라. 통상 자바 코드라면 명시적으로 Singleton 클래스를 생성하고 객체를 생성해야 한다. 이는 스칼라가 이전 자바 개발의 약점에서 배운다는 이론에 대한 또 하나의 증거다.
다음 할 일은 Person에 addPropertyChangeListener() 메서드를 추가하고 속성이 바뀔 때마다 각 리스너에 propertyChange() 메서드를 호출하는 일이다. Listing 6에서 보는 것처럼 스칼라에서 이런 작업을 재사용할 수 있게 하는 방법은 특성을 정의하고 사용하는 만큼 간단하다. 자바빈즈 명세에서는 "통보"되는 속성을 묶인 속성(bound property)이라고 하기 때문에 여기서는 해당 특성을 BoundPropertyBean이라고 이름 붙였다.
Listing 6. 놀라운 동작 재사용(Holy behavioral reuse, Batman!)
// 스칼라 코드
trait BoundPropertyBean
{
import java.beans._
val pcs = new PropertyChangeSupport(this)
def addPropertyChangeListener(pcl : PropertyChangeListener) =
pcs.addPropertyChangeListener(pcl)
def removePropertyChangeListener(pcl : PropertyChangeListener) =
pcs.removePropertyChangeListener(pcl)
def firePropertyChange(name : String, oldVal : _, newVal : _) : Unit =
pcs.firePropertyChange(new PropertyChangeEvent(this, name, oldVal, newVal))
}
|
여기서도 java.beans 패키지의 PropertyChangeSupport 클래스를 이용하는데, 이는 PropertyChangeSupport가 여기서 필요한 60% 가량의 상세 구현을 제공할 뿐 아니라 PropertyChangeSupport를 직접 사용하는 자바빈즈/POJO와 같은 동작을 얻을 수 있게 하기 때문이다. 또한 정의한 특성을 통해 앞으로 이 "Support" 클래스가 개선되더라도 개선점을 자동으로 활용할 수 있다. 차이점은 이제 Person POJO가 번거롭게 PropertyChangeSupport를 직접 사용하지 않아도 된다는 점이다. Listing 7을 보자.
Listing 6. 스칼라로 작성한 Person POJO, 2편
// 스칼라 코드
class Person(var firstName:String, var lastName:String, var age:Int)
extends Object
with BoundPropertyBean
{
override def toString = "[Person: firstName=" + firstName +
" lastName=" + lastName + " age=" + age + "]"
}
|
컴파일한 후에 Person 클래스 정의를 살펴보면 자바로 작성된 Person처럼 public 메서드인 addPropertyChangeListener(), removePropertyChangeListener(), firePropertyChange()가 있다는 것을 알 수 있다. 실제 스칼라로 작성된 Person은 단지 코드 한 줄을 추가해 이 새로운 메서드를 얻게 되었다. 바로 클래스 선언에서 Person 클래스가 BoundPropertyBean 특성을 상속한다는 것을 나타내는 with 절이다.
불행히 아직 다 끝난 건 아니다. Person 클래스가 리스너를 받고 제거하고 속성값 변경을 통보할 수 있게 되었지만, 스칼라가 firstName 멤버에 대해 생성하는 기본 메서드는 추가된 기능을 사용하지 않는다. 한 가지 더 마찬가지로 불행한 점은 이 글을 쓰는 시점에서는 PropertyChangeSupport 객체를 사용하는 get/set 메서드를 자동으로 마법처럼 생성해 주는 멋진 애노테이션도 없다는 점이다. 따라서 Listing 8에 보인 것처럼 직접 작성해야 한다.
Listing 8. 스칼라로 작성한 Person POJO, 3편
// 스칼라 코드
class Person(var firstName:String, var lastName:String, var age:Int)
extends Object
with BoundPropertyBean
{
def setFirstName(newvalue:String) =
{
val oldvalue = firstName
firstName = newvalue
firePropertyChange("firstName", oldvalue, newvalue)
}
def setLastName(newvalue:String) =
{
val oldvalue = lastName
lastName = newvalue
firePropertyChange("lastName", oldvalue, newvalue)
}
def setAge(newvalue:Int) =
{
val oldvalue = age
age = newvalue
firePropertyChange("age", oldvalue, newvalue)
}
override def toString = "[Person: firstName=" + firstName +
" lastName=" + lastName + " age=" + age + "]"
}
|
유용한 특성
특성은 함수적 개념이라고 보기는 어렵다. 그보다는 객체 지향 프로그래밍에 대한 10여 년간의 통찰의 결과라고 할 수 있다. 사실 간단한 스칼라 프로그램을 작성하면서 특성을 쓴다는 사실도 모른 채 다음과 같은 특성을 사용해 왔을지도 모르겠다.
Listing 9. 사라진 main()!
// 스칼라 코드
object App extends Application
{
val p = new Person("Jennifer", "Aloi", 29)
p.addPropertyChangeListener(PCL)
p.setFirstName("Jenni")
p.setAge(30)
System.out.println(p)
}
|
Application 특성은 줄곧 수작업으로 정의해 왔던 main() 메서드를 정의한다. 사실, 이 특성은 그 외 작은 유용한 기능을 포함하고 있는데, 바로 시스템 속성인 scala.time을 Application을 정의하는 코드에 넘겨주면 애플리케이션 수행 시간을 계산하는 타이머다. Listing 10을 보자.
Listing 10. 실행 시간 측정
$ scala -Dscala.time App
Bean changed its firstName from Jennifer to Jenni
Bean changed its age from 29 to 30
[Person: firstName=Jenni lastName=Aloi age=30]
[total 15ms]
|
JVM에서 특성 구현
충분히 발전한 기술이라면 마법이나 다를 바 없다.
— 아서 C. 클락(Arthur C. Clarke)
이쯤 되면 이 "메서드를 가진 인터페이스(또는 특성)"을 JVM에서 어떻게 구현하는지가 궁금할 것이다. Listing 11을 보자. 이럴 때 유용한 도구인 javap를 사용하면 이면에서 어떤 일이 일어나는지를 알 수 있다.
Listing 11. Person 클래스를 역 분석한 결과
$ javap -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
public Person(java.lang.String, java.lang.String, int);
public java.lang.String toString();
public void setAge(int);
public void setLastName(java.lang.String);
public void setFirstName(java.lang.String);
public void age_$eq(int);
public int age();
public void lastName_$eq(java.lang.String);
public java.lang.String lastName();
public void firstName_$eq(java.lang.String);
public java.lang.String firstName();
public int $tag();
public void firePropertyChange(java.lang.String, java.lang.Object, java.lang
.Object);
public void removePropertyChangeListener(java.beans.PropertyChangeListener);
public void addPropertyChangeListener(java.beans.PropertyChangeListener);
public final void pcs_$eq(java.beans.PropertyChangeSupport);
public final java.beans.PropertyChangeSupport pcs();
}
|
Person 클래스 선언을 잘 살펴 보자. 이 POJO는 BoundPropertyBean이라는 인터페이스를 구현하는데, 특성은 바로 JVM의 인터페이스로 표현된다. 하지만 특성의 메서드 구현은 어떻게 되는 걸까? 마지막 결과가 스칼라 언어의 의미론적 효과를 따르는 한 컴파일러는 어떤 편법이라도 사용할 수 있다는 점을 기억하자. 이 경우 컴파일러는 메서드 구현을 버리고 특성에 정의된 필드는 특성을 구현하는 클래스인 Person에 정의한다. 앞서 javap를 돌려 나온 마지막 두 줄에서 특성에 정의된 pcs 값을 참조하는 것을 봐서도 이 점이 명확하게 와 닿지 않는다면, javap에 -private 옵션을 주고 실행해 보면 쉽게 확인할 수 있다.
Listing 12. Person 클래스를 역 분석한 결과, 2편
$ javap -private -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
private final java.beans.PropertyChangeSupport pcs;
private int age;
private java.lang.String lastName;
private java.lang.String firstName;
public Person(java.lang.String, java.lang.String, int);
public java.lang.String toString();
public void setAge(int);
public void setLastName(java.lang.String);
public void setFirstName(java.lang.String);
public void age_$eq(int);
public int age();
public void lastName_$eq(java.lang.String);
public java.lang.String lastName();
public void firstName_$eq(java.lang.String);
public java.lang.String firstName();
public int $tag();
public void firePropertyChange(java.lang.String, java.lang.Object, java.lang.Object);
public void removePropertyChangeListener(java.beans.PropertyChangeListener);
public void addPropertyChangeListener(java.beans.PropertyChangeListener);
public final void pcs_$eq(java.beans.PropertyChangeSupport);
public final java.beans.PropertyChangeSupport pcs();
} |
사실 이 설명으로 어떻게 특성에 정의된 메서드를 실제 사용되기 전까지 점검하지 않는가에 대한 대답도 한 셈이다. 특성의 메서드는 특성이 클래스에 의해 구현되기 전가지는 어떤 클래스의 "부분"도 아니므로, 컴파일러는 메서드 내 논리의 특정 부분에 대한 검사를 나중으로 미룰 수 있다. 이는 특성이 특성을 구현하는 클래스의 실제 베이스 클래스를 알 필요 없이 super() 호출을 할 수 있다는 점에서 유용하다.
특성에 대한 유의점
BoundPropertyBean에서는 PropertyChangeSupport 객체를 만들기 위해 특성 기능을 사용했다. PropertyChangeSupport의 생성자(constructor)에는 속성값 변화를 통보할 해당 빈을 넘겨줘야 하는데, 앞서 정의한 특성에서는 "this"를 넘겨줬다. Person에서 구현되기 전까지 해당 특성은 실제 정의된 것이 아니기 때문에 "this"는 BoundPropertyBean이 아니라 Person 객체를 가리키는 것이다. 특성의 이 특이한 면, 즉 정의에 대한 분석을 미루는 점은 미묘하지만 이 같은 "늦은 연결(late-binding)"에는 매우 유용하다.
Application 특성의 경우 두 부분에서 마법이 일어난다. Application 특성이 어느 애플리케이션에서나 필요한 자바 애플리케이션 시작점인 main() 메서드를 제공한다. 또한, 실행 시간을 측정해야 하는지를 보기 위해 -Dscala.time 시스템 속성을 살핀다. 하지만 Application이 특성이기 때문에 해당 메서드는 서브 클래스(App)에 "나타난다." 이 메서드를 실행하려면 App 싱글톤을 생성해야 하므로 App 클래스의 인스턴스를 생성하고 클래스 몸체를 "실행"하게 되므로 결과적으로 애플리케이션을 실행하게 된다. 이 과정이 모두 끝나면 특성의 main()이 호출되고 실행에 걸린 시간이 표시된다.
다소 퇴보한 것 같지만 애플리케이션이 main()에 넘겨진 명령행 인자를 얻을 수 없다는 점만 제외하면 의도한 대로 동작한다. 이는 특성의 동작이 어떻게 구현하는 클래스로 "미뤄지는지" 또한 보여준다.
특성과 컬렉션
당신이 용액의 일부가 아니라면 침전물의 일부이다.
— 헨리 J. 틸먼(Henry J. Tilman)
특성은 구현하는 사람이 편리하도록 구체적인 동작을 추상적인 선언과 결합할 때 특히 유용하다. 예를 들어 기존 자바 컬렉션 인터페이스/클래스인 List와 ArrayList를 생각해 보자. List 인터페이스는 이 컬렉션의 내용을 삽입된 순서대로 하나씩 짚어갈 수 있음을 보장한다. 더 엄격하게 표현하면 "위치적 의미가 보장된다."
ArrayList는 내용을 할당한 배열에 저장하는 List의 한 종류다. 반면 LinkedList는 배열 대신 연결 목록(linked-list) 구현을 사용한다. ArrayList는 리스트 내용에 임의 순서로 접근할 때 유리하고, LinkedList는 리스트 끝 외에 다른 임의의 위치에 삽입하거나 임의의 위치에서 삭제할 때 유리하다. 하지만 그와 관계 없이 이 두 클래스의 동작 중 놀랍게 많은 부분이 같고, 따라서 둘 다 공통 베이스 클래스인 AbstractList를 상속받는다.
자바 프로그래밍에서 특성이 지원됐더라면, 특성은 "공통 베이스 클래스에서 상속받을 필요 없이 동작을 재사용"하는 이런 미묘한 문제에 대한 월등히 나은 해법이 됐을 것이다. 특성은 새로운 List 서브 타입이 List를 직접 구현해야 하는지 (여기서 RandomAccess 인터페이스도 구현해야 한다는 것을 잊을 수도 있다) 혹은 베이스 클래스인 AbstractList를 확장해야 하는지를 혼동하지 않도록 해 주는 일종의 C++의 "private 상속" 메커니즘으로 사용할 수도 있다. 루비의 믹스인(mixin)과 혼동해서는 안 되겠지만 (뒤에서 다루겠지만 스칼라 믹스인과도 혼동하지 말아야 한다) 이를 C++에서는 종종 "믹스인"이라고 불렀다.
스칼라 문서 중 전형적인 예제는 비교 및 정렬 기능을 제공하기 위한 이상한 이름의 메서드를 정의하는 Ordered 특성이다. Listing 13을 보자.
Listing 13. Ordered 특성
// 스칼라 코드
trait Ordered[A] {
def compare(that: A): Int
def < (that: A): Boolean = (this compare that) < 0
def > (that: A): Boolean = (this compare that) > 0
def <= (that: A): Boolean = (this compare that) <= 0
def >= (that: A): Boolean = (this compare that) >= 0
def compareTo(that: A): Int = compare(that)
} |
먼저 (자바 5의 제네릭스 같은 타입 인자를 가진) Ordered 특성은 A 타입의 객체를 인자로 받고 this가 그 객체보다 작으면 1보다 작은 값을, 크면 1보다 큰 값을, 같으면 0을 돌려주는 compare라는 추상 메서드를 정의한다. 그 다음으로는 관계 연산자(<, > 등)와 java.util.Comparable 인터페이스가 사용하는 더 친숙한 메서드인 compareTo()를 compare() 메서드를 이용해 정의한다.
스칼라와 자바 간 호환성
그림 한 장은 천 마디 말만큼 가치가 있고, 하나의 인터페이스는 천 장의 그림만큼 가치 있다.
— 벤 슈나이더만(Ben Shneiderman)
실제, 유사 구현 상속(pseudo-implementation-inheritance)은 스칼라에서 특성을 사용하는 가장 일반적이고 유용한 예는 아니다. 대신 스칼라에서 특성은 자바의 인터페이스를 대체하는 기본 개념이다. 스칼라를 활용하려고 하는 자바 프로그래머라면 스칼라를 사용하는 메커니즘의 하나로, 특성에 대해 친숙해져야 한다.
이 연재에서 지금까지 지적했던 것처럼, 컴파일된 스칼라 코드가 항상 자바 언어에 충실한 것은 아니다. 예를 들어 스칼라의 "이상한 이름의 메서드("+"나 "\" 같은)는 종종 자바 언어 문법 상에서 직접 사용할 수 없는 문자로 표시되기도 한다(대표적인 예로 "$"). 이 때문에 "자바에서 호출할 수 있는" 인터페이스를 생성해야 자바 코드에서 스칼라 코드를 호출하기가 쉽다.
이런 특정한 예는 다소 인위적이다. 또, 사용된 스칼라 개념에는 ("이름이 이상한 메서드"를 사용하지 않는다면) 실제로는 특성이 제공하는 간접 계층이 필요 없다. 하지만 잠시 참아보자. 여기서 개념이 바로 핵심이다. Listing 14부터 다양한 자바 객체 모델에서 흔히 보는 팩토리(factory)처럼 Student 객체를 만들어내는 전통적인 자바 스타일 팩토리를 정의해 보자. 먼저 Student에 대한 자바와 호환성 있는 인터페이스가 필요하다.
Listing 14. Student 특성
// 스칼라 코드
trait Student
{
def getFirstName : String;
def getLastName : String;
def setFirstName(fn : String) : Unit;
def setLastName(fn : String) : Unit;
def teach(subject : String)
}
|
컴파일하면 위 특성은 javap로 확인해 볼 수 있는 것처럼 POJI, 즉 Plain Old Java Interface로 바뀐다.
Listing 15. Student 특성을 javap로 살펴본 결과
$ javap Student
Compiled from "Student.scala"
public interface Student extends scala.ScalaObject{
public abstract void setLastName(java.lang.String);
public abstract void setFirstName(java.lang.String);
public abstract java.lang.String getLastName();
public abstract java.lang.String getFirstName();
public abstract void teach(java.lang.String);
}
|
다음으로 팩토리가 될 클래스가 필요하다. 보통 자바 코드에서는 팩토리는 클래스에 정의된 정적 메서드다(보통 해당 클래스는 "StudentFactory" 같은 이름을 가진다). 하지만 스칼라에는 정적 메서드란 개념이 없다. 대신 스칼라에는 인스턴스 메서드를 가진 싱글톤인 object가 있다. 여기서 필요한 것이 바로 object다. 그런고로 StudentFactory object를 생성하고 거기에 Factory 메서드를 정의해 보자.
Listing 16. 팩토리 클래스 StudentFactory
// 스칼라 코드
object StudentFactory
{
class StudentImpl(var first:String, var last:String, var subject:String)
extends Student
{
def getFirstName : String = first
def setFirstName(fn: String) : Unit = first = fn
def getLastName : String = last
def setLastName(ln: String) : Unit = last = ln
def teach(subject : String) =
System.out.println("I know " + subject)
}
def getStudent(firstName: String, lastName: String) : Student =
{
new StudentImpl(firstName, lastName, "Scala")
}
} |
중첩 클래스 StudentImpl은 Student 특성을 구현한 것으로 해당 특성이 필요로 하는 get()/set() 메서드 쌍들을 제공한다. 여기서 특성이 동작을 가질 수 있음에도 불구하고 JVM에서 인터페이스로 구현되기 때문에 특성 객체를 생성하려고 하면 Student가 abstract라는 에러가 난다는 점을 기억하자.
이 사소한 예제가 주는 중요한 보상은 당연하지만 스칼라에서 생성한 이 새로운 객체들을 사용할 수 있는 자바 애플리케이션을 작성하는 것이다.
Listing 17. StudentFactory를 사용하는 자바 코드
// 자바 코드
public class App
{
public static void main(String[] args)
{
Student s = StudentFactory.getStudent("Neo", "Anderson");
s.teach("Kung fu");
}
} |
위 예제를 실행하면 "I know Kung fu."가 출력되는 것을 확인할 수 있을 것이다(안다. 조악한 영화 참조치곤 서론이 길었다).
결론
사람들은 생각하기 싫어한다. 생각하면 반드시 결론에 도달해야 한다. 결론이 항상 유쾌하지는 않다.
— 헬렌 켈러(Helen Keller)
스칼라에서 특성은 전통적인 자바 인터페이스처럼 클라이언트가 사용할 인터페이스를 정의하거나, 특성 내에 정의된 다른 동작에 기반을 둔 동작 상속을 위한 수단으로서, 분류와 정의를 위한 효과적인 수단을 제공한다. 아마 특성과 구현하는 클래스 간의 관계를 나타내기 위해 새로운 상속 문구인 IN-TERMS-OF가 필요할지도 모르겠다.
특성을 사용하는 방법은 이 글에서 설명한 것보다 훨씬 많다. 하지만 이 연재의 목표 중 일부는 스칼라에 대한 충분한 정보를 주어 각자 더 많은 시도를 해 볼 수 있게 하는 것이다. 스칼라 구현을 받아 시험해 보고, 각자의 현재 자바 시스템에 스칼라가 끼어 들어갈 수 있는 부분을 찾아 보자. 그리고 늘 말하지만 스칼라가 유용하거나, 이 글에 대해 의견이 있거나, 코드나 본문에서 오류를 발견하면 알려 주기 바란다.
함수적인 애호가들여, 이번에는 여기까지다.
참고자료 교육
-
포드캐스트: Scala revealed(JavaWorld, June 2008): developerWorks 기고자인 Andrew Glover와 Ted Neward가 동시성(concurrency)과 데이터베이스 프로그래밍을 포함해 단순히 자바 언어와 다른 순수 OO 언어가 적합하지 않은 중요한 분야와 함께, 함수 언어와 객체 지향 언어 간의 차이에 대해 이야기한다.
-
"
바쁜 자바 프로그머를 위한 스칼라 입문: 객체 지향론자를 위한 함수 프로그래밍
" (Ted Neward, developerWorks, 2008년 1월): 연재 전체를 읽어 보자.
- "A Tour of Scala: Mixin Class Composition"(스칼라 언어 문서): 스칼라의 상속과 믹스인 클래스 조합에 대해 더 자세히 알아보자.
- "Thinking in Java: Comparing C++ and Java"(Bruce Eckel, Java Coffee Break): Bruce 저서의 일부인 이 글에서 자바 언어와 C++의 차이점을 강조한다.
- "Dinosaurs Can Take the Pain"(Cay Horstmann, Java.net, 2008년 1월): 자바 프로그래머가 진화의 막다른 점에 몰렸다는 의견을 상기시키고 일부 해법을 제시한다.
- "What are your Java pain points, really?"(Bill Venners, Artima.com, 2007년 2월): Artima 포럼의 토론 주제로 이 글을 쓰는 시점에서 264개의 댓글이 붙어 있다.
-
Interoperability happens - Languages: 언어 설계와 진화에 대한 Ted Neward의 견해에 대해 더 많은 글을 읽어 보자.
- "자바를 이용한 함수 프로그래밍"(Abhijit Belapurkar, developerWorks, 2007년 4월): 자바 개발자 관점에서 함수 프로그래밍의 이점과 용례에 대해 배운다.
- "Scala by Example"(Martin Odersky, 2007년 12월): 짧은 코드 중심의 스칼라 입문서(PDF 형식).
-
Programming in Scala(Martin Odersky, Lex Spoon, Bill Venners, Artima, 2007년 12월): 최초의 책 한 권 분량의 스칼라 입문서
-
developerWorks 자바 영역: 자바 프로그래밍의 모든 측면에 대한 수백 개의 기사가 제공된다.
제품 및 기술 얻기
토론
필자소개  | 
|  | Ted Neward는 Neward & Associates의 사장으로 자바, .NET, XML
서비스와 다른 플랫폼에 대한 컨설팅, 조언, 교육, 강연을 한다. 워싱턴 주 시애틀 근처에 산다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |