C++, java, kotlin 등 다양한 언어들에서는 컴파일 타임에 정의되는 상수를 정의하기 위해 const, constexpr, final 같은 키워드를 예약해두고 사용합니다.
만약 constexpr int x = 55 같이 정의되어 있다면 x는 컴파일 타임에 55라고 정의되며 x가 사용된 모든 코드 위치는 55라는 숫자로 교체됩니다.
scala에서는 상수를 어떻게 정의해야 할까요?
이에 대한 규칙들이 너무 파편화되어있고 한 번에 알아보기도 어려워서 한 번 정리해보았습니다.
Scala의 런타임 상수 정의
scala에서 런타임에 정의되는 상수를 만들고 싶다면 단순히 val 키워드를 사용하면 됩니다.
val을 사용하기만 해도 해당 변수의 재할당이 불가능해지므로 이것만으로도 런타임 상수로서의 정의가 됩니다.
만약 val로 정의된 변수를 상수로서 사용하기로 결정했다면 이름의 정의 규칙은 UpperCamelCase를 따르면 됩니다.
정확한 이유는 컴파일 타임 상수의 정의를 보면서 알아보겠습니다.
Scala의 컴파일 타임 상수 정의
scala에서 컴파일 타임 상수를 정의하고 싶으면 final val 키워드를 사용해 변수를 정의해야 합니다.
실제 정의 예시는 scala.math.package.scala 코드 내부의 Pi 키워드로 확인할 수 있습니다 (주석은 지웠습니다).
package object math {
@inline final val E = java.lang.Math.E
// final val 형태로 정의된 상수
@inline final val Pi = java.lang.Math.PI
...
}
구체적인 컴파일 타임 상수의 정의 규칙
scala에서 단순히 final val로 정의한다고 해서 컴파일 타임 상수라고 취급되지 않고 특별한 규칙들을 지켜야 합니다.
final val 선언
가장 기초적인 규칙으로 반드시 final val로 선언해야 합니다.
타입 선언 제거
scala에서는 변수에 타입을 고정시키기 위해 : 기호 이후에 실제 타입을 적을 수 있습니다.
변수를 컴파일 타임 상수로 정의하기 위해서는 반드시 타입을 적지 않아야 합니다.
해당 규칙은 scala language declaration 규칙에 적힌 언어 자체의 정의이므로 이를 우회하는 방법은 존재하지 않습니다.
자세한 규칙은 value-declarations-and-definitions 영역의 const value definition을 찾으면 됩니다.
package object 또는 object 내에서 선언
scala는 class나 object, package object 외부에서 단독적으로 변수를 선언할 수 없기 때문에 반드시 변수를 감싸는 어떤 존재가 있어야 합니다.
또한, class는 실제로 생성되고 나서야 메모리에 올라가고 각 인스턴스마다 메모리 위치가 다르기 때문에 컴파일 타임 상수를 정의하기 위해서는 singleton 형태로 하나만 존재하는 package object나 object에 선언하여야 합니다.
해당 규칙은 Scala Naming Convention에 적혀있습니다.
UpperCamelCase 형태로 선언
scala는 패턴 매치에서 상수를 활용하기 위해 반드시 선언을 UpperCamelCase 형태로 선언해야 합니다.
만약 UpperCamelCase로 선언하지 않으면 이를 모든 패턴을 매칭시키는 변수 패턴으로 인식하게 됩니다.
import math.{E, Pi}
val pi = Pi
E match {
case pi => "strange math? Pi = " + pi
case _ => "OK"
}
// 컴파일 결과
On line 2: warning: patterns after a variable pattern cannot match (SLS 8.1.1)
case _ => "OK"
변수인 pi에 상수인 Pi를 대입하고 상수인 E를 pi와 패턴 매칭시키는 코드입니다.
결과는 당연히 _에 매칭되어 "OK"가 나올 것 같지만 컴파일해보면 case _ 에는 영원히 도달하지 못한다고 나옵니다.
이는 pi를 변수도 상수도 아닌 변수 패턴으로 인식해 모든 결과를 매칭시키기 때문입니다.
상수를 만들고 싶으면 패턴 매칭도 고려해야 하므로 반드시 UpperCamelCase로 만들어야 합니다.
자세한 사항은 Programming in scala 15.2의 변수 패턴, 변수 또는 상수? 절을 읽으면 됩니다.
컴파일 타임 상수와 아닐 때의 빌드 결과 비교로 확인해보기
각 규칙에 따라 어떤 결과물이 나오는지는 단순히 예상하는 것보다 실제로 만들어 보는 것이 기억에 남기 좋으므로 예상되는 시나리오들을 만들고 해당 코드를 디컴파일해서 확인해보겠습니다.
예시는 다음과 같습니다
- val 일 경우
- val이면서 UpperCamelCase 일 경우
- private[this] 일 경우
- private[this] 이면서 UpperCamelCase 일 경우
- final val 일 경우
- final val 이면서 UpperCamelCase 일 경우
- final val 이면서 UpperCamelCase 이면서 inline 일 경우
- final val 이면서 camel case 일경우
object ConstTest {
val x = 11
val X = 22
private[this] val y = 33
private[this] val Y = 44
final val z = "str1"
final val Z = "str2"
@inline final val InlineZ = "inlineStr"
final val fail: String = "failStr"
def foo(): Unit = {
val testX1 = x
val testX2 = X
val testY1 = y
val testY2 = Y
val testZ1 = z
val testZ2 = Z
val testZ3 = InlineZ
val testFail = fail
}
}
위의 코드를 디컴파일하면 다음과 같은 결과가 나옵니다.
public final class ConstTest$ {
public static final ConstTest$ MODULE$ = new ConstTest$();
private static final int x = 11;
private static final int X = 22;
private static final int y = 33;
private static final int Y = 44;
private static final String fail = "failStr";
public int x() {
return x;
}
public int X() {
return X;
}
public final String z() {
return "str1";
}
public final String Z() {
return "str2";
}
public final String InlineZ() {
return "inlineStr";
}
public final String fail() {
return fail;
}
public void foo() {
int testX1 = this.x();
int testX2 = this.X();
int testY1 = y;
int testY2 = Y;
String testZ1 = "str1";
String testZ2 = "str2";
String testZ3 = "inlineStr";
String testFail = this.fail();
}
private ConstTest$() {
}
}
x 는 단순히 val인 런타임 상수이므로 x() 함수가 만들어지고 this.x()로 호출되었습니다.
X는 val이면서 UpperCamelCase 이지만 컴파일 타임 상수로 취급되지 않아 X() 함수가 만들어지고 this.X()로 호출되었습니다.
y는 private[this] 여서 x 와 다르게 y() 함수는 만들어지지 않았고 y로 즉시 대입되었지만 컴파일 타임 상수는 아닙니다.
Y 역시 y() 함수는 만들어지지 않았고 Y로 즉시 대입되었지만 컴파일 타임 상수는 아닙니다.
z는 static final 변수는 만들어지지 않고 getter가 만들어졌으며 컴파일 타임 상수로 취급되어 값으로 대체되었습니다.
Z 역시 z와 동일하게 컴파일 타임 상수로 취급되어 값으로 대체되었습니다.
InlineZ는 동일하게 컴파일 타임 상수로 취급되었습니다. @inline은 단순히 컴파일러에게 inline 규칙을 적용할 수 있으면 적용하라는 마크일 뿐이므로 실제 결과는 상황에 따라 다를 수 있습니다.
fail은 컴파일 타임 상수 규칙을 대부분 지켰지만 타입을 정의하지 말라는 규칙을 지키지 않아 런타임 상수로 취급되었습니다.
결론
컴파일 타임 상수를 정의하고 싶다면 다음의 규칙을 지키면 됩니다
- object 또는 package object에 정의한다
- final val로 정의한다
- 타입을 명시하지 않는다
- (scala 패턴 매칭을 위해) UpperCamelCase로 정의한다
- (필요하다면) 인라인 어노테이션을 붙여준다. (상수가 인라인이 아닐 필요는 없다)
C++, java, kotlin 등 다양한 언어들에서는 컴파일 타임에 정의되는 상수를 정의하기 위해 const, constexpr, final 같은 키워드를 예약해두고 사용합니다.
만약 constexpr int x = 55 같이 정의되어 있다면 x는 컴파일 타임에 55라고 정의되며 x가 사용된 모든 코드 위치는 55라는 숫자로 교체됩니다.
scala에서는 상수를 어떻게 정의해야 할까요?
이에 대한 규칙들이 너무 파편화되어있고 한 번에 알아보기도 어려워서 한 번 정리해보았습니다.
Scala의 런타임 상수 정의
scala에서 런타임에 정의되는 상수를 만들고 싶다면 단순히 val 키워드를 사용하면 됩니다.
val을 사용하기만 해도 해당 변수의 재할당이 불가능해지므로 이것만으로도 런타임 상수로서의 정의가 됩니다.
만약 val로 정의된 변수를 상수로서 사용하기로 결정했다면 이름의 정의 규칙은 UpperCamelCase를 따르면 됩니다.
정확한 이유는 컴파일 타임 상수의 정의를 보면서 알아보겠습니다.
Scala의 컴파일 타임 상수 정의
scala에서 컴파일 타임 상수를 정의하고 싶으면 final val 키워드를 사용해 변수를 정의해야 합니다.
실제 정의 예시는 scala.math.package.scala 코드 내부의 Pi 키워드로 확인할 수 있습니다 (주석은 지웠습니다).
package object math { @inline final val E = java.lang.Math.E // final val 형태로 정의된 상수 @inline final val Pi = java.lang.Math.PI ... }
구체적인 컴파일 타임 상수의 정의 규칙
scala에서 단순히 final val로 정의한다고 해서 컴파일 타임 상수라고 취급되지 않고 특별한 규칙들을 지켜야 합니다.
final val 선언
가장 기초적인 규칙으로 반드시 final val로 선언해야 합니다.
타입 선언 제거
scala에서는 변수에 타입을 고정시키기 위해 : 기호 이후에 실제 타입을 적을 수 있습니다.
변수를 컴파일 타임 상수로 정의하기 위해서는 반드시 타입을 적지 않아야 합니다.
해당 규칙은 scala language declaration 규칙에 적힌 언어 자체의 정의이므로 이를 우회하는 방법은 존재하지 않습니다.
자세한 규칙은 value-declarations-and-definitions 영역의 const value definition을 찾으면 됩니다.
package object 또는 object 내에서 선언
scala는 class나 object, package object 외부에서 단독적으로 변수를 선언할 수 없기 때문에 반드시 변수를 감싸는 어떤 존재가 있어야 합니다.
또한, class는 실제로 생성되고 나서야 메모리에 올라가고 각 인스턴스마다 메모리 위치가 다르기 때문에 컴파일 타임 상수를 정의하기 위해서는 singleton 형태로 하나만 존재하는 package object나 object에 선언하여야 합니다.
해당 규칙은 Scala Naming Convention에 적혀있습니다.
UpperCamelCase 형태로 선언
scala는 패턴 매치에서 상수를 활용하기 위해 반드시 선언을 UpperCamelCase 형태로 선언해야 합니다.
만약 UpperCamelCase로 선언하지 않으면 이를 모든 패턴을 매칭시키는 변수 패턴으로 인식하게 됩니다.
import math.{E, Pi} val pi = Pi E match { case pi => "strange math? Pi = " + pi case _ => "OK" } // 컴파일 결과 On line 2: warning: patterns after a variable pattern cannot match (SLS 8.1.1) case _ => "OK"
변수인 pi에 상수인 Pi를 대입하고 상수인 E를 pi와 패턴 매칭시키는 코드입니다.
결과는 당연히 _에 매칭되어 "OK"가 나올 것 같지만 컴파일해보면 case _ 에는 영원히 도달하지 못한다고 나옵니다.
이는 pi를 변수도 상수도 아닌 변수 패턴으로 인식해 모든 결과를 매칭시키기 때문입니다.
상수를 만들고 싶으면 패턴 매칭도 고려해야 하므로 반드시 UpperCamelCase로 만들어야 합니다.
자세한 사항은 Programming in scala 15.2의 변수 패턴, 변수 또는 상수? 절을 읽으면 됩니다.
컴파일 타임 상수와 아닐 때의 빌드 결과 비교로 확인해보기
각 규칙에 따라 어떤 결과물이 나오는지는 단순히 예상하는 것보다 실제로 만들어 보는 것이 기억에 남기 좋으므로 예상되는 시나리오들을 만들고 해당 코드를 디컴파일해서 확인해보겠습니다.
예시는 다음과 같습니다
- val 일 경우
- val이면서 UpperCamelCase 일 경우
- private[this] 일 경우
- private[this] 이면서 UpperCamelCase 일 경우
- final val 일 경우
- final val 이면서 UpperCamelCase 일 경우
- final val 이면서 UpperCamelCase 이면서 inline 일 경우
- final val 이면서 camel case 일경우
object ConstTest { val x = 11 val X = 22 private[this] val y = 33 private[this] val Y = 44 final val z = "str1" final val Z = "str2" @inline final val InlineZ = "inlineStr" final val fail: String = "failStr" def foo(): Unit = { val testX1 = x val testX2 = X val testY1 = y val testY2 = Y val testZ1 = z val testZ2 = Z val testZ3 = InlineZ val testFail = fail } }
위의 코드를 디컴파일하면 다음과 같은 결과가 나옵니다.
public final class ConstTest$ { public static final ConstTest$ MODULE$ = new ConstTest$(); private static final int x = 11; private static final int X = 22; private static final int y = 33; private static final int Y = 44; private static final String fail = "failStr"; public int x() { return x; } public int X() { return X; } public final String z() { return "str1"; } public final String Z() { return "str2"; } public final String InlineZ() { return "inlineStr"; } public final String fail() { return fail; } public void foo() { int testX1 = this.x(); int testX2 = this.X(); int testY1 = y; int testY2 = Y; String testZ1 = "str1"; String testZ2 = "str2"; String testZ3 = "inlineStr"; String testFail = this.fail(); } private ConstTest$() { } }
x 는 단순히 val인 런타임 상수이므로 x() 함수가 만들어지고 this.x()로 호출되었습니다.
X는 val이면서 UpperCamelCase 이지만 컴파일 타임 상수로 취급되지 않아 X() 함수가 만들어지고 this.X()로 호출되었습니다.
y는 private[this] 여서 x 와 다르게 y() 함수는 만들어지지 않았고 y로 즉시 대입되었지만 컴파일 타임 상수는 아닙니다.
Y 역시 y() 함수는 만들어지지 않았고 Y로 즉시 대입되었지만 컴파일 타임 상수는 아닙니다.
z는 static final 변수는 만들어지지 않고 getter가 만들어졌으며 컴파일 타임 상수로 취급되어 값으로 대체되었습니다.
Z 역시 z와 동일하게 컴파일 타임 상수로 취급되어 값으로 대체되었습니다.
InlineZ는 동일하게 컴파일 타임 상수로 취급되었습니다. @inline은 단순히 컴파일러에게 inline 규칙을 적용할 수 있으면 적용하라는 마크일 뿐이므로 실제 결과는 상황에 따라 다를 수 있습니다.
fail은 컴파일 타임 상수 규칙을 대부분 지켰지만 타입을 정의하지 말라는 규칙을 지키지 않아 런타임 상수로 취급되었습니다.
결론
컴파일 타임 상수를 정의하고 싶다면 다음의 규칙을 지키면 됩니다
- object 또는 package object에 정의한다
- final val로 정의한다
- 타입을 명시하지 않는다
- (scala 패턴 매칭을 위해) UpperCamelCase로 정의한다
- (필요하다면) 인라인 어노테이션을 붙여준다. (상수가 인라인이 아닐 필요는 없다)