 |
|
난이도 : 초급 Ted Neward, 사장, Neward & Associates
옮긴이: 김도형 dwkorea@kr.ibm.com
2008 년 10 월 07 일 실생활에서는 코드를 참조하고 관련 있는 코드끼리 묶을 수 있어야 합니다.
바쁜 자바 프로그래머를 위한 스칼라 입문
연재의 일곱 번째인 이번 글에서는 스칼라의 패키지와 접근 제한 지시어(access modifier)를 살펴봄으로써 그 동안 간과해 왔던 문제들을 바로잡아 봅니다. 그 다음에는 스칼라의 함수적 측면에 대해 계속해서 살펴봅니다. 이번에는 “apply” 개념을 간단하게 알아봅니다.
최근 독자 의견을 통해 이 연재에서 스칼라 언어의 중요한 측면을 빠뜨렸다는 것을 알았다. 바로 스칼라의 패키지와 접근 제한 지시어다. 따라서 함수 언어적 특성인 apply 개념에 대해 상세히 살펴보기 전에 잠시 지면을 할애해 이 주제를 먼저 다루고 넘어갈까 한다.
패키징(Packaging)
코드 간에 서로 충돌이 없도록 분리하기 위해, 자바(Java™) 코드는 클래스가 선언되는 어휘적 이름공간(namespace)을 만들 수 있도록 package 키워드를 제공한다. 클래스 Foo를 com.tednewward.util 패키지에 넣는다는 것은 본질적으로 정식 클래스 이름을 com.tedneward.util.Foo로 바꾸는 것이다. 따라서 참조할 때도 그 이름을 사용해야 한다. 아마 자바 프로그래머라면 실제 프로그래밍을 할 때는 그렇게 긴 이름을 사용하지 않는다는 점을 금방 지적할 것이다. 해당 패키지를 import하면 코드에서 긴 정식 이름을 타이핑할 필요가 없다. 그 말이 맞기는 하지만 실제로는 프로그래머를 대신해 컴파일러와 바이트코드가 정식 이름을 사용해 주는 것뿐이다. javap를 돌려보면 쉽게 확인할 수 있다.
 |
이 연재에 대해
저자인 Ted Neward는 이
연재를 통해 독자들에게 스칼라 프로그래밍 언어를 심층적으로 소개한다. 이 새 developerWorks 연재를 통해 스칼라에 대한 최근 스칼라를 둘러싼 떠들썩한 호평의 실체를 살펴보고, 스칼라의 언어적 특성의 일부가 실제 어떻게 사용되는지도 배울 것이다. 비교가 필요할 때는 항상 스칼라 코드와 자바 코드를 나란히 보일 예정이다. 하지만 곧 알겠지만 스칼라에 있는 많은 요소는 자바 언어와 직접적인 관계가 없는 것들이고, 이런 부분이 스칼라를 매력적으로 만든다. 그렇다면 자바 코드로도 할 수 있다면 왜 힘들여 스칼라를 배울까?
|
|
하지만 자바 언어의 패키지는 몇몇 부자연스러운 면이 있다. 우선 패키지 선언은 반드시 해당 패키지에 속하는 클래스가 정의되는 .java 파일의 선두에 나와야 한다(이 때문에 패키지에 애노테이션을 적용하려고 할 때 언어 차원에서 심각한 혼란을 일으킨다). 또 패키지 선언은 전체 파일에 영향을 미친다. 이는 두 클래스가 패키지 경계를 넘어 밀접하게 연관된 드문 경우에 소스 파일을 둘로 분리해야 한다는 뜻이다. 이 때문에 코드를 훑어보는 사람이 두 클래스 간의 밀접한 연관을 알아채기 어려울 수도 있다.
스칼라는 패키징에 있어 약간 다른 접근법을 택했는데, 자바 언어의 declaration(선언) 방식과 C#의 scope(범위 지정) 방식 모두를 사용할 수 있다. 이 점을 염두에 두면 자바 프로그래머는 기존 자바 방식대로 .scala 파일의 선두에 package 선언을 놓을 수도 있다. 이 경우 패키지 선언은 자바 코드처럼 소스 파일 전체에 영향을 끼친다. 스칼라 개발자라면 여기에 추가로 패키지 “범위 지정(scoping)” 방식을 사용할 수도 있다. 이 경우 package 문의 범위는 Listing 1에서 보듯이 ‘{ }’로 지정한다.
Listing 1. 간편한 패키징 방식
package com
{
package tedneward
{
package scala
{
package demonstration
{
object App
{
def main(args : Array[String]) : Unit =
{
System.out.println("Howdy, from packaged code!")
args.foreach((i) => System.out.println("Got " + i) )
}
}
}
}
}
}
|
이 코드는 App이라는 클래스 하나를 선언했는데 정확하게는 com.tedneward.scala.demonstration.App이라는 이름의 클래스를 선언한 것이다. 스칼라에서도 패키지 이름을 ‘.’로 구분할 수 있으므로 Listing 1은 Listing 2에서 보는 것처럼 간단하게 바꿀 수 있다.
Listing 2. 간편한 패키징 방식(다른 방식)
package com.tedneward.scala.demonstration
{
object App
{
def main(args : Array[String]) : Unit =
{
System.out.println("Howdy, from packaged code!")
args.foreach((i) => System.out.println("Got " + i) )
}
}
}
|
두 코드 모두 똑같은 결과를 만들어 내므로 어떤 방식이든 적합한 쪽을 선택해 사용하면 된다(scalac는 javac와 마찬가지로 패키지에 선언된 디렉터리 내에 .class를 생성한다).
Import
물론 논리적으로 패키징의 이면은 import다. 스칼라에서는 import를 통해 현재의 어휘적 이름공간에 이름을 불러들인다. 이 연재를 계속 읽었다면 몇몇 예제에서 import를 봤을 것이다. 하지만 이번에는 자바 개발자가 놀랄 만한 import의 특성을 다루려고 한다.
먼저 스칼라 소스 파일의 선두뿐 아니라 어느 위치에서든 import를 사용할 수 있다. 따라서 import가 영향을 끼치는 범위를 한정할 수도 있다. Listing 3에서 java.math.BigInteger import는 App 객체 내에 정의된 메서드에 한정되고 다른 부분에는 영향을 끼치지 않는다. 만약 mathfun 내 다른 클래스나 객체가 java.math.BigInteger를 사용하려면 App 정의 밖 패키지 수준에서 import를 사용해야 한다. 이 경우 이 패키지 범위 내 모든 클래스가 BigInteger를 import한 것이 된다.
Listing 3. Import 범위 지정
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == BigInteger.ZERO) BigInteger.ONE
else arg multiply (factorial (arg subtract BigInteger.ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
|
하지만 스칼라 import의 특이한 점은 이것만이 아니다. 스칼라에서는 최상위 멤버와 중첩된 멤버를 구분할 이유가 없기 때문에 import를 하면 어휘 영역(lexical scope)에 중첩된 타입뿐 아니라 멤버까지 추가한다. 일례로 java.math.BigInteger 내부의 이름을 모두 import함으로써 ZERO나 ONE을 참조할 때 Listing 4처럼 범위 지정을 위한 BigInteger를 생략할 수 있다.
Listing 4. static이 없는 정적 import
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger
import BigInteger._
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == ZERO) ONE
else arg multiply (factorial (arg subtract ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
|
‘_’를 사용함으로써(스칼라의 와일드카드 문자를 기억하는지?) 스칼라 컴파일러에 BigInteger 내의 모든 멤버를 범위에 가져 오라고 지시하는 것이다. 그리고 BigInteger는 바로 직전 import로 범위에 불러들였기 때문에 명시적으로 클래스 이름 앞에 패키지 이름을 써 줄 필요는 없다. 사실 import에 ‘,’로 구분해 여러 대상을 지정할 수 있기 때문에 이 두 import조차 하나로 묶을 수도 있다(Listing 5를 보라).
Listing 5. 동시에 여러 대상을 import
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger, BigInteger._
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == ZERO) ONE
else arg multiply (factorial (arg subtract ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
|
이렇게 하면 한두 행을 줄일 수 있다. 하지만 이 두 import를 하나로 묶을 수는 없다. 첫 import는 BigInteger 클래스를 들여오고 다음 import는 그 클래스의 다양한 멤버를 들여온다.
상수 멤버가 아닌 다른 멤버를 들여오기 위해 import를 사용할 수도 있다. 예를 들어 Listing 6의 math 유틸리티 라이브러리(유용성은 의문이지만 여전히…)를 보자.
Listing 6. 엔론(Enron)의 회계 코드(역주: 2001년 회계 부정으로 도산한 미국 회사)
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object BizarroMath
{
def bizplus(a : Int, b : Int) = { a - b }
def bizminus(a : Int, b : Int) = { a + b }
def bizmultiply(a : Int, b : Int) = { a / b }
def bizdivide(a : Int, b : Int) = { a * b }
}
}
}
}
}
|
멤버를 사용할 때마다 BizarroMath를 써야 하기 때문에 이 라이브러리를 사용하는 것이 점차 짜증날 수도 있다. 하지만 스칼라에서는 BizarroMath의 각 멤버를 전역 함수인 것처럼 최상위 어휘적 이름공간에 import할 수 있다(Listing 7을 보라).
Listing 7. 엔론의 지출 계산
package com
{
package tedneward
{
package scala
{
package demonstration
{
object App2
{
def main(args : Array[String]) : Unit =
{
import com.tedneward.scala.mathfun.BizarroMath._
System.out.println("2 + 2 = " + bizplus(2,2))
}
}
}
}
}
}
|
스칼라에는 개발자가 더 자연스럽게 2 bizplus 2와 같은 표현을 사용할 수 있는 흥미로운 구문이 있다. 하지만 여기에 대해서는 나중에 소개하기로 한다(심하게 오용될 가능성이 있는 이 스칼라의 특성에 대해 궁금한 독자라면 “Odersky, Spoon, Venners가 쓴 Programming in Scala에서 implicit 구문을 찾아본다).
접근(Access)
패키징(그리고 import)이 스칼라의 캡슐화(encapsulation)와 패키징의 한 부분이기는 하지만, 더 중요한 것은 자바 코드처럼 어떤 멤버에 대한 접근을 선택적으로 제한할 수 있는 방법이다. 다시 말해 특정 멤버를 “public”, “private”이나 이 둘 중 어떤 상태로 표시할 수 있는 방법이 있어야 한다.
자바 언어는 4단계의 접근 제한이 있다. 즉 public, private, protected, 패키지 수준 접근(혼란스럽게도 어떤 키워드도 안 적으면 적용된다)으로 나뉜다. 스칼라의 접근 제한은 다음과 같다.
- (어떤 방법으로) 패키지 수준 지정을 사용하지 않는다.
- 기본으로 “public”을 사용한다.
- “이 범위에서만 접근할 수 있다”는 의미로 “private”을 명시한다.
반면 “protected”는 자바의 protected와 완전히 다르다. 자바의 protected 멤버를 서브클래스와 멤버가 정의된 패키지 내에서 접근할 수 있는 것과 달리 스칼라에서는 서브클래스에서만 접근할 수 있다. 이는 스칼라의 protected가 자바보다 더욱 제한적이라는 뜻이다(이견의 여지는 있는데 제한적이지만 더 직관적이다).
하지만 스칼라가 자바와 완전히 다른 부분은 스칼라의 접근 제한 지시어는 패키지 이름으로 “제한(qualified)”될 수 있어 해당 멤버에 접근할 수 있는 접근 수준을 지정할 수 있다는 점이다. 예를 들어 BizarroMath 패키지가 특정 클래스 멤버에 대해 같은 패키지 내 다른 멤버에서 접근할 수 있도록 허용하고 싶을 때(하지만 서브클래스에서는 접근할 수 없도록 하고 싶을 때) Listing 8처럼 할 수 있다.
Listing 8. 엔론의 회계 코드
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object BizarroMath
{
def bizplus(a : Int, b : Int) = { a - b }
def bizminus(a : Int, b : Int) = { a + b }
def bizmultiply(a : Int, b : Int) = { a / b }
def bizdivide(a : Int, b : Int) = { a * b }
private[mathfun] def bizexp(a : Int, b: Int) = 0
}
}
}
}
}
|
여기서 private[mathfun] 표현에 주목하자. 본질적으로 이 접근 제한 지시어는 이 멤버가 패키지 mathfun까지에만 private하다고 지정하는 것이다. 이는 패키지 mathfun 내 어떤 멤버라도 bizexp를 사용할 수 있지만 서브클래스를 포함해 해당 패키지 밖에서는 사용할 수 없다는 뜻이다.
이와 같이 “private”이나 “protected” 선언에 com(혹은 심지어 최상위 이름공간을 지칭하는 _root_를 지정할 수도 있어 private[_root_]라고 하면 “public”과 같은 의미로 사용할 수도 있다)에 이르는 어떤 패키지든 지정할 수 있다. 이는 자바 언어에 비해 훨씬 유연한 접근 제한 명시를 가능하게 한다.
사실 스칼라는 또 하나의 접근 제한 명세 기능을 제공하는데 바로 private[this]라고 지정하는 object-private 명세다. 이는 지정한 멤버를 같은 객체 내 멤버에서만 사용할 수 있도록 지정한다. 이 경우 심지어 같은 타입의 다른 객체에서도 해당 멤버를 사용할 수 없다(이로써 자바 프로그래밍 면접 질문에서나 유용했지 하등 쓸 데 없는 자바 접근 제한 명세 체계 내의 작은 구멍을 메우게 된 셈이다).
접근 제한 지시어는 적정 수준에서 JVM 상에 대응되어야 한다는 점을 상기하자. 결과적으로 접근 제한 지시어의 일부 미묘한 부분은 클래스로 컴파일되고 일반 자바 코드에서 호출될 때 사라지게 된다. 예를 들어 위 (private[mathfun]으로 선언된 bizexp가 있는) BizarroMath 예제는 (javap로 보면) Listing 9와 같은 클래스 정의를 생성한다.
Listing 9. 엔론의 회계 라이브러리, JVM 관점
Compiled from "packaging.scala"
public final class com.tedneward.scala.mathfun.BizarroMath
extends java.lang.Object
{
public static final int $tag();
public static final int bizexp(int, int);
public static final int bizdivide(int, int);
public static final int bizmultiply(int, int);
public static final int bizminus(int, int);
public static final int bizplus(int, int);
}
|
컴파일된 BizarroMath 클래스의 두 번째 줄에서 보듯이 bizexp() 메서드는 JVM 수준에서는 public으로 지정되어 있다. 즉, 일단 스칼라 컴파일러가 접근 제한 검사를 마치고 나면 미묘한 private[mathfun]의 구분은 없어진다는 것이다. 따라서 개인적으로는 자바 코드와 사용할 스칼라 코드를 작성할 때는 기존 “private”과 :”public” 정의만을 사용하는 편이다(심지어 때론 “protected”도 JVM 수준 “public”으로 대응된다. 따라서 확실하지 않을 때는 실제 컴파일된 바이트코드에 javap를 돌려보는 편이 좋다).
적용(Application)
이 연재의 앞선 글에서("컬렉션 타입") 스칼라의 배열(정확히는 Array[T])에 대해 언급하면서 “배열의 i 번째 요소를 얻는 것”은 사실 “이상한 이름의 메서드 중 하나”라고 설명한 바 있다. 사실 그 글에서 더 상세한 내용을 다룰 생각이 없기는 했지만 엄밀히 말해 맞는 말은 아니다.
그렇다. 솔직히 말하면 거짓말을 한 셈이다.
기술적으로 Array[T] 클래스에 대해 괄호를 사용하는 것은 단순히 “이상한 이름을 가진 메서드”보다 다소 더 복잡하다. 스칼라는 그런 특정 문자열(즉, 왼쪽 괄호들과 오른쪽 괄호들이 나오는 문자열)에 대해 특별한 명칭 연관(nomenclature association)을 지정해 두고 있는데, 이는 그 표현이 특정 의도를 염두에 두고 너무 자주 사용되기 때문이다. 즉, 어떤 것을 “수행하려는(doing)” 의도로 사용되기 때문이다(함수적으로 이야기하면 어떤 것을 어떤 것에 “적용(applying)”하는 것이다).
바꿔 말하면 스칼라는 “적용(application)” 연산자 “()”에 대해 특별한 문법(더 정확하게는 특별한 문법적 관계)을 지원한다. 정확하게 말해 스칼라는 객체를 ()로 호출했을 때 호출할 메서드로 apply라는 메서드를 인지한다. 예를 들어 특정 클래스가 functor(함수처럼 사용하는 객체)로 동작하길 원한다면 함수나 메서드 같은 의미를 제공하기 위해 apply 메서드를 정의하면 된다.
Listing 10. functor 예제
class ApplyTest
{
import org.junit._, Assert._
@Test def simpleApply =
{
class Functor
{
def apply() : String =
{
"Doing something without arguments"
}
def apply(i : Int) : String =
{
if (i == 0)
"Done"
else
"Applying... " + apply(i - 1)
}
}
val f = new Functor
assertEquals("Doing something without arguments", f() )
assertEquals("Applying... Applying... Applying... Done", f(3))
}
}
|
호기심 많은 독자라면 functor가 무명 함수나 클로저(closure)와 뭐가 다른지 궁금할 것이다. 알겠지만 이 관계는 아주 명확하다. 표준 스칼라 라이브러리에 포함된 Funtion1 타입(인자를 하나 받는 함수를 의미)에는 apply 메서드가 정의되어 있다. 스칼라 무명 함수에 대해 생성된 스칼라 무명 클래스를 살펴보면 생성된 클래스들이 Function1(또는 함수가 인자를 몇 개 받는지에 따라 Function2, Function3 등)에서 상속받는다는 것을 알 수 있다.
이는 무명이나 명칭이 있는 함수가 바람직한 설계 방식과 꼭 맞는 경우가 아니라면 functor 클래스를 만들어 필드에 저장되는 몇몇 초기 데이터를 지정하고 (기존 전략(Strategy) 패턴 구현과 달리) 특정 공통 베이스 클래스 없이도 ()를 사용해 실행할 수 있다는 것을 의미한다.
Listing 11. 심화된 functor 예제
class ApplyTest
{
import org.junit._, Assert._
// ...
@Test def functorStrategy =
{
class GoodAdder
{
def apply(lhs : Int, rhs : Int) : Int = lhs + rhs
}
class BadAdder(inflateResults : Int)
{
def apply(lhs : Int, rhs : Int) : Int = lhs + rhs * inflateResults
}
val calculator = new GoodAdder
assertEquals(4, calculator(2, 2))
val enronAccountant = new BadAdder(50)
assertEquals(102, enronAccountant(2, 2))
}
}
|
적절한 인자를 가진 apply 메서드가 있는 어떤 클래스라도 인자의 수와 타입만 맞으면 ()로 호출할 수 있다.
결론
스칼라의 패키징, import, 접근 제한 지시어 개념은 기존 자바 프로그래머가 누릴 수 없었던 세밀한 제어와 캡슐화를 제공한다. 예를 들어 특정 객체의 메서드를 import할 수 있게 해, 기존 전역 함수의 문제점 없이 마치 전역 함수처럼 사용할 수 있도록 해 준다. 또 특히 그런 메서드들이 이 연재에서 소개했던 tryWithLogging 함수처럼(“루프 때문에 당황하지 말자”) 고차 기능(high-order functionality)을 제공하는 메서드인 경우 해당 메서드를 아주 사용하기 쉽게 해 준다.
비슷하게 “application” 개념은 스칼라가 함수적 외형의 이면에 있는 구체적인 실행 방식을 숨길 수 있게 해, 프로그래머가 심지어 자신이 호출하는 것이 실제 함수가 아니라 상당한 복잡도를 내포한 객체라는 사실을 모르게(혹은 신경 쓰지 않도록) 해 준다. 이 개념은 스칼라의 함수적인 특성에 또 하나의 차원을 부가한다. 즉, 자바 언어(C#, C++도 마찬가지다)에서도 구현할 수는 있지만 스칼라가 제공하는 수준의 문법적 순수성은 없다.
여기까지다. 다음 글로 만날 때까지 스칼라 프로그래밍을 즐기자.
다운로드 하십시오 | 설명 | 이름 | 크기 | 다운로드 방식 |
|---|
| 이 글의 예제 스칼라 코드 | j-scala07298.zip | 95KB | HTTP |
|---|
참고자료 교육
- "
바쁜 자바 프로그래머를 위한 스칼라 입문: 객체 지향론자를 위한 함수 프로그래밍"(Ted
Neward, 한국 developerWorks, 2008년 4월): 이 연재의 첫 편으로 스칼라 전반에 대해 소개하고 여러 요소 중 스칼라의 동시성(concurrency)에 대한 함수적인 접근에 대해 설명한다. 이 연재의 다른 글은 다음과 같다.
- "클래스 동작"(2008년 4월): 스칼라의 클래스 문법과 의미에 대해 상세히 다룬다.
- "루프 때문에 당황하지 말자!"(2008년 5월): 스칼라의 제어 구문에 대해 상세히 알아본다.
- "특성과 동작"(2008년 6월): 스칼라 판 자바 인터페이스를 다룬다.
- "구현 상속"(2008년 9월): 스칼라의 다형성(polymorphism)에 대해 다룬다.
- "컬렉션 타입"(2008년 9월): 튜플, 배열, 리스트에 대해 다룬다.
- "자바를 이용한 함수 프로그래밍"(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월): 최초의 책 한 권 분량의 스칼라 입문서
-
Bjarne Stroustrup의 FAQ: 스스로 “더 나은 C”라고 부른 C++를 설계하고 구현했다.
- The developerWorks 자바 기술 존: 자바 프로그래밍의 모든 측면에 대한 수백 개 기사가 제공된다.
제품 및 기술 얻기
-
Scala 내려받기: 이 연재와 함께 스칼라를 배워보자.
-
SUnit: 표준 스칼라 배포판의 일부로 scala.testing 패키지 내에 있다.
토론
필자소개  | 
|  | Ted Neward는 Neward & Associates의 사장으로 자바, .NET, XML 서비스와 다른 플랫폼에 대한 컨설팅, 조언, 교육, 강연을 한다. 워싱턴 주 시애틀 근처에 산다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|