이 글은 Programming in Scala 4/e
를 읽고 작성한 글입니다.
규모가 큰 프로그램을 작성할 때는 프로그램의 여러 부분이 서로 의존하는 정도를 나타내는 커플링(coupling)
을 최소화하는 것이 중요하다.
프로그램의 한 부분에서 언뜻 보기에 아무 문제 없을 것 같은 코드를 조금 변경했는데, 다른 부분에 엄청난 문제가 생길 수 있다. 이런 위험을 커플링을 줄임으로써 감소시킬 수 있다.
커플링을 최소화하는 방법 중 하나는 모듈화 스타일로 프로그램을 작성
하는 것이다.
모듈화 스타일
프로그램을 더 작은 여러 모듈로 나눈다. 각 모듈에는내부
와외부
가 있다.
모듈의 내부(구현)에 대해 작업할 때는 같을 모듈을 가지고 작업하는 프로그래머와 협력하면 된다.
모듈의 외부(인터페이스)를 변경해야 하는 경우에만 다른 모듈을 가지고 협력하면 된다.
스칼라 코드는 Java 플랫폼의 전역 패키지(global package) 계층 안에 있다.
현 챕터 이전의 예제 코드들은 모두 이름 없는 패키지 안에서 작성했었다. 스칼라에서는 두 가지 방법으로 이름이 있는 패키지 안에 코드를 작성할 수 있다.
package bobsrockets.navigation
class Navigator
이 예시문의 package절은 bobsrockets.navigation
패키지 안에 Navigator
라는 클래스를 넣는 코드이다.
위의 패키지절에서 bobsrockets
라는 조직/단체에서 개발한 navigation
소프트웨어라는 것을 짐작할 수 있다.
참고
스칼라 코드는 자바 생태 시스템의 일부이므로 공개하는 패키지 이름을 만들 때 자바의 관습대로 도메인 이름을 역으로 사용하는 것이 좋다. 따라서 위 패키지절의 더 좋은 이름은 com.bobsrockets.navigation이 더 좋을 것이다.
C#의 네임스페이스와 유사한 형태로 package절 다음에 중괄호를 넣는 방법 역시 패키지 안에 코드를 작성하는 방법이다.
package bobsrockets.navigation {
class Navigator
}
위와 같이 packag절 다음 중괄호가 오고 중괄호 안에 있는 정의는 모두 해당 패키지 속하는 것이다.
간단한 예시라면 pacakge절을 사용할 것이며, 한 파일 안에 여러 패키지를 넣을 때는 패키징을 사용할 것이다.
package bobsrockets {
package navigation {
// bobsrockets.navigation 패키지 안
class Navigator
package tests {
class NavigatorSuite
}
}
}
위의 코드는 같은 파일 안에 테스트 코드를 포함시키되 별도의 패키지에 테스트 코드를 집어넣는 것이다. 즉, 커플링을 최소화하는 것이다.
코드를 패키지 계층으로 나누는 이유는 코드를 훑어볼 때 도움을 주기 위해서도 있지만, 컴파일러도 같은 패키지 안에 있는 코드가 서로 관련 있음을 알 수 있게 하기 때문이다.
이러한 연결성을 통해 전체 경로를 포함하지 않는 간단한 이름을 사용할 수 있다.
package bobsrockets {
package navigation {
class Navigator {
val map = new StarMap
}
class StarMap
}
class Shop {
// bobsrockets.navigation.Navigator로 전체 경로를 쓸 필요가 없다.
val nav = new navigation.Navigator
}
package fleets {
class Fleet {
//bobsrockets.Ship이라는 전체 경로를 쓸 필요가 없다.
def addShip() = new Ship
}
}
}
위와 같이 전체 경로를 포함하지 않아도 참조할 수 있다.
프로그래밍을 하다보면, 패키지들이 뒤섞여서 상호참조를 하는 경우가 발생하곤 한다.
// launch.scala 파일
package launch {
class Booster3
}
// bobsrockets.scala 파일
package bobsrockets{
package navigation {
package launch {
class Booster1
}
class MissionControl {
val booster1 = new launch.Booster1
val booster2 = new bobsrockets.launch.Booster2
val booster3 = new _root_.launch.Booster3
}
}
package launch {
class Booster2
}
}
위 코드에서 우리는 _root_
라는 코드가 보일 것이다.
_root_
는 스칼라가 제공하는 사용자가 작성한 모든 패키지 외부에 존재하는 패키지이다.
즉, 최상위 패키지는 _root_
의 멤버라고 생각하면 된다.
스칼라에서 패키지와 그 멤버는 import
절을 통해 불러올 수 있다.
package bobsdelights
abstract class Fruit {
val name: String
val color: String
}
object Fruits {
object Apple extends Fruit("apple", "red")
object Orange extends Fruit("orange", "orange")
object Pear extends Fruit("pear", "yellowish")
val menu = List(Apple, Orange, Pear)
}
위와 같은 패키지가 있다고 생각해보자.
// Fruit에 간단하게 접근
import bobsdelights.Fruit
// bobsdelights의 모든 멤버에 간단하게 접근
import bobsdelights._
// Fruits의 모든 멤버에 간단하게 접근
import bobsdelights.Fruits._
위와 같은 import문들로 필요에 따라 접근할 수 있다.
첫번째 import문인 import bobsdelights.Fruit
는 자바의 상글타입 임포트에 해당한다.
두번째 import문은 자바의 주문식 임포트와 같다. Java에서는 *
을 사용하는 것과 달리 스칼라에서는 _
을 사용한다.
세번째 임포트 절은 정적 클래스 필드를 불러오는 자바 임포트에 속한다.
스칼라의 import
위의 예시만 보면 스칼라와 자바의 import와 같다고 생각할 수 있지만, 크게 3가지가 차이 난다.
1. Scala는 어느곳에서나 나타날 수 있다.
2. 패키지 뿐만 아니라 싱글톤 또는 일반 객체도 참조할 수 있다.
3. 불러온 멤버 이름을 숨기거나 다른 이름을 지칭할 수 있다.
import Fruits.{Apple, Orange}
위와 같이 중괄호로 임포트 셀렉터(import selector)를 사용하면 원하는 멤버들만 임포트하며, 멤버들을 감추거나 이름을 바꿀 수 있다.
import Fruits.{Apple => McIntosh, Orange}
위의 임포트 셀렉터는 Apple이라는 객체를 Fruits.Apple이나 McIntosh로 이름을 바꾸어 참조할 수 있는 것이다.
즉, =>
를 사용하여 이름을 바꾸어 임포트할 수 있는 것이다.
import Fruits.{Pear=>_, _}
그렇다면 위의 임포트문은 어떻게 동작할까? 이는 Pear를 제외한 모든 구문을 불러온다는 것이다.
원래이름 => _
절은 불러올 이름 중에서 "원래 이름만" 제외하는 효과가 있다.
스칼라는 모든 프로그램에 몇 가지 import를 항상 추가시킨다.
import java.lang._
import scala._
import Predef._
위 3개의 패키지는 항상 포함한다.
스칼라는 접근 수식자의 의미를 지정자(quialifier)
로 확장할 수 있다. private[X]
나 protected[X]
의 형태로 지정 가능하다.
이러한 접근 지정자가 있는 수식자는 매우 세밀한 접근 제어를 가능케 한다.
package bobsrockets
package navigation {
private[bobsrockets] class Navigator {
protected[navigation] def useStarChart() {}
class LegOfJourney {
private[this] val speed = 200
}
}
}
위와 같이 접근제어자 않에 패키지명을 넣음으로써 접근 가능한 곳들을 제한할 수 있다.