메서드 특징
메서드 특징 (Method Features)
이 절에서는 Scala 3에서 메서드를 정의하고 호출하는 다양한 측면을 소개해요.
출처: Scala 3 Book
본문
메서드 정의하기
Scala 메서드는 많은 특징을 갖고 있어요. 그중 일부는 이래요.
- 타입 파라미터(type parameter)
- 기본 파라미터 값(default parameter value)
- 여러 파라미터 그룹(multiple parameter group)
- 컨텍스트 제공 파라미터(context-provided parameter)
- 이름-에 의한 파라미터(by-name parameter)
- 그 외 더…
이 특징들 중 일부는 이 절에서 보여드리지만, 이런 것들을 쓰지 않는 "단순한" 메서드를 정의할 때의 문법은 이렇게 생겼어요.
def methodName(param1: Type1, param2: Type2): ReturnType =
// the method body
// goes here
end methodName // this is optional
이 문법에서:
- 메서드를 정의할 때는
def키워드를 써요. - Scala 표준은 메서드 이름을 camel case 관례를 따라 짓는 거예요.
- 메서드 파라미터는 항상 타입과 함께 정의해요.
- 메서드 반환 타입 선언은 선택 사항이에요.
- 메서드는 여러 줄로도, 한 줄로도 구성할 수 있어요.
- 메서드 본문 뒤의
end methodName부분도 선택 사항이고, 긴 메서드에서만 권장돼요.
다음은 add라는 한 줄짜리 메서드의 두 가지 예제예요. Int 입력 파라미터 두 개를 받죠. 첫 번째 버전은 Int 반환 타입을 명시적으로 보여주고, 두 번째는 그렇지 않아요.
def add(a: Int, b: Int): Int = a + b
def add(a: Int, b: Int) = a + b
공개적으로 보이는 메서드에는 반환 타입을 붙여 주는 걸 권장해요. 반환 타입을 선언해 두면 몇 달, 몇 년 뒤 다시 보거나 다른 사람의 코드를 볼 때 이해하기 쉬워지거든요.
메서드 호출하기
메서드를 호출하는 건 간단해요.
val x = add(1, 2) // 3
Scala 컬렉션 클래스에는 수십 개의 내장 메서드가 있어요. 이 예제들은 그걸 어떻게 호출하는지 보여줘요.
val x = List(1, 2, 3)
x.size // 3
x.contains(1) // true
x.map(_ * 10) // List(10, 20, 30)
눈여겨볼 점:
size는 인자를 받지 않고, 리스트의 요소 개수를 반환해요.contains메서드는 인자 하나, 찾을 값을 받아요.map은 인자 하나, 함수를 받아요. 이 경우 익명 함수가 전달됐죠.
여러 줄 메서드
메서드가 한 줄보다 길면, 메서드 본문을 두 번째 줄에서 시작해서 오른쪽으로 들여써요.
def addThenDouble(a: Int, b: Int): Int =
// imagine that this body requires multiple lines
val sum = a + b
sum * 2
이 메서드에서:
sum은 불변 로컬 변수라서 메서드 밖에서는 접근할 수 없어요.- 마지막 줄이
sum의 값을 두 배로 만들고, 이 값이 메서드에서 반환돼요.
이 코드를 REPL에 붙여 넣으면 원하던 대로 동작하는 걸 볼 수 있어요.
scala> addThenDouble(1, 1)
res0: Int = 4
메서드 끝에 return 문이 필요 없다는 점을 눈여겨보세요. Scala에서는 거의 모든 것이 표현식(expression)—즉 각 줄의 코드가 값을 반환(또는 평가)한다는 뜻—이어서 return을 쓸 필요가 없어요.
이 메서드를 한 줄로 줄여 쓰면 더 명확해져요.
def addThenDouble(a: Int, b: Int): Int = (a + b) * 2
메서드 본문은 언어의 다양한 특징을 모두 사용할 수 있어요.
if/else표현식match표현식while루프for루프와for표현식- 변수 할당
- 다른 메서드 호출
- 다른 메서드 정의
현실 세계의 여러 줄 메서드의 예로, 이 getStackTraceAsString 메서드는 입력받은 Throwable을 잘 포맷된 String으로 변환해요.
def getStackTraceAsString(t: Throwable): String =
val sw = StringWriter()
t.printStackTrace(PrintWriter(sw))
sw.toString
이 메서드에서:
- 첫 번째 줄이 새
StringWriter인스턴스를 값 바인더sw에 할당해요. - 두 번째 줄이 스택 트레이스 내용을
StringWriter에 저장해요. - 세 번째 줄이 스택 트레이스의
String표현을 내놓아요.
기본 파라미터 값
메서드 파라미터는 기본 값을 가질 수 있어요. 이 예제에서는 timeout과 protocol 두 파라미터 모두에 기본 값이 주어졌어요.
def makeConnection(timeout: Int = 5_000, protocol: String = "http") =
println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
// more code here ...
파라미터들이 기본 값을 가지므로 메서드는 이렇게 호출할 수 있어요.
makeConnection() // timeout = 5000, protocol = http
makeConnection(2_000) // timeout = 2000, protocol = http
makeConnection(3_000, "https") // timeout = 3000, protocol = https
이 예제들에 관한 핵심 포인트:
- 첫 번째 예제에서는 인자가 주어지지 않으므로 메서드가 기본 값인
5_000과http를 사용해요. - 두 번째 예제에서는
2_000이timeout값으로 주어져서 그것이 사용되고,protocol은 기본 값을 사용해요. - 세 번째 예제에서는 두 파라미터 모두에 값이 주어져서 둘 다 사용돼요.
기본 파라미터 값을 쓰면, 사용하는 측에서 세 개의 서로 다른 오버라이드된 메서드를 쓰는 것처럼 보인다는 점을 눈여겨보세요.
이름 붙은 파라미터
원한다면 메서드를 호출할 때 메서드 파라미터의 이름을 사용할 수도 있어요. 예를 들어 makeConnection은 이렇게도 호출할 수 있어요.
makeConnection(timeout=10_000)
makeConnection(protocol="https")
makeConnection(timeout=10_000, protocol="https")
makeConnection(protocol="https", timeout=10_000)
어떤 프레임워크에서는 이름 붙은 파라미터가 많이 사용돼요. 여러 메서드 파라미터가 같은 타입일 때도 아주 유용하죠.
engage(true, true, true, false)
IDE의 도움 없이는 이 코드를 읽기 어려워요. 하지만 이 코드는 훨씬 명확하고 뚜렷하죠.
engage(
speedIsSet = true,
directionIsSet = true,
picardSaidMakeItSo = true,
turnedOffParkingBrake = false
)
파라미터가 없는 메서드에 대한 제안
메서드가 파라미터를 받지 않으면 arity-0 메서드라고 해요. 비슷하게, 메서드가 파라미터 하나를 받으면 arity-1 메서드예요. arity-0 메서드를 만들 때:
- 메서드가
println호출 같은 부수 효과를 수행하면, 빈 괄호로 메서드를 선언해요. - 메서드가 부수 효과를 수행하지 않으면—컬렉션의
size를 얻는 것처럼, 컬렉션의 필드에 접근하는 것과 비슷한—괄호를 뺀다.
예를 들어 이 메서드는 부수 효과를 수행하므로 빈 괄호로 선언돼요.
def speak() = println("hi")
이렇게 하면 메서드 호출자들이 메서드를 호출할 때 열린 괄호를 사용해야 해요.
speak // error: "method speak must be called with () argument"
speak() // prints "hi"
이건 단지 관례일 뿐이지만, 이걸 따르면 코드 가독성이 크게 좋아져요. arity-0 메서드가 부수 효과를 수행하는지 한눈에 이해하기 쉬워지거든요.
if를 메서드 본문으로 사용하기
if/else 표현식은 값을 반환하므로 메서드 본문으로 쓸 수 있어요. 다음은 Perl의 true/false 정의를 구현한 isTruthy라는 메서드예요.
def isTruthy(a: Any) =
if a == 0 || a == "" || a == false then
false
else
true
이 메서드가 어떻게 동작하는지 보여주는 예제들이에요.
isTruthy(0) // false
isTruthy("") // false
isTruthy("hi") // true
isTruthy(1.0) // true
match를 메서드 본문으로 사용하기
match 표현식도 메서드 전체의 본문으로 사용할 수 있고, 실제로 흔히 그렇게 써요. match 표현식으로 다시 쓴 isTruthy의 또 다른 버전이에요.
def isTruthy(a: Matchable) = a match
case 0 | "" | false => false
case _ => true
이 메서드는 앞서 if/else 표현식을 쓴 메서드와 똑같이 동작해요. 여기서는 파라미터 타입으로 Any 대신 Matchable을 써서, 패턴 매칭을 지원하는 어떤 값이든 받을 수 있게 했어요. Matchable 트레이트에 대한 더 자세한 내용은 Reference 문서를 참고하세요.
클래스에서 접근 제어하기
클래스, 객체, 트레이트, enum에서 Scala 메서드는 기본적으로 public이에요. 그래서 여기서 만든 Dog 인스턴스가 speak 메서드에 접근할 수 있어요.
class Dog:
def speak() = println("Woof")
val d = new Dog
d.speak() // prints "Woof"
메서드는 private으로 표시할 수도 있어요. 그러면 현재 클래스에만 비공개가 되어, 서브클래스에서 호출하거나 오버라이드할 수 없어요.
class Animal:
private def breathe() = println("I'm breathing")
class Cat extends Animal:
// this method won't compile
override def breathe() = println("Yo, I'm totally breathing")
메서드를 현재 클래스에 비공개로 만들면서도 서브클래스에서 호출하거나 오버라이드할 수 있게 하려면, protected로 표시하면 돼요. 이 예제의 speak 메서드처럼요.
class Animal:
private def breathe() = println("I'm breathing")
def walk() =
breathe()
println("I'm walking")
protected def speak() = println("Hello?")
class Cat extends Animal:
override def speak() = println("Meow")
val cat = new Cat
cat.walk()
cat.speak()
cat.breathe() // won't compile because it's private
protected 설정의 의미는 이래요.
- 메서드(또는 필드)를 같은 클래스의 다른 인스턴스들이 접근할 수 있어요.
- 현재 패키지의 다른 코드에서는 보이지 않아요.
- 서브클래스에서 사용할 수 있어요.
객체는 메서드를 담을 수 있어요
앞에서 트레이트와 클래스가 메서드를 가질 수 있다는 걸 봤어요. Scala의 object 키워드는 싱글턴 클래스를 만드는 데 쓰이고, 객체도 메서드를 담을 수 있어요. 이는 "유틸리티" 메서드들의 집합을 묶기에 좋은 방법이에요. 예를 들어 이 객체는 문자열을 다루는 메서드 모음을 담고 있어요.
object StringUtils:
/**
* Returns a string that is the same as the input string, but
* truncated to the specified length.
*/
def truncate(s: String, length: Int): String = s.take(length)
/**
* Returns true if the string contains only letters and numbers.
*/
def lettersAndNumbersOnly_?(s: String): Boolean =
s.matches("[a-zA-Z0-9]+")
/**
* Returns true if the given string contains any whitespace
* at all. Assumes that `s` is not null.
*/
def containsWhitespace(s: String): Boolean =
s.matches(".*\\s.*")
end StringUtils
확장 메서드 (Extension methods)
닫힌 클래스에 기능을 추가하고 싶은 상황이 많아요. 예를 들어 Circle 클래스가 있는데, 그 소스 코드를 바꿀 수 없다고 해요. 서드파티 라이브러리에서 이렇게 정의됐을 수 있죠.
case class Circle(x: Double, y: Double, radius: Double)
이 클래스에 메서드를 추가하고 싶다면, 확장 메서드(extension method)로 정의하면 돼요.
extension (c: Circle)
def circumference: Double = c.radius * math.Pi * 2
def diameter: Double = c.radius * 2
def area: Double = math.Pi * c.radius * c.radius
이제 aCircle이라는 Circle 인스턴스가 있으면, 이런 메서드들을 이렇게 호출할 수 있어요.
aCircle.circumference
aCircle.diameter
aCircle.area
더 알아볼 것들
메서드에 대해 더 알아야 할 부분도 있어요. 예를 들면:
- 슈퍼클래스의 메서드 호출하기
- 이름-에 의한 파라미터 정의하고 사용하기
- 함수 파라미터를 받는 메서드 작성하기
- 인라인 메서드 만들기
- 예외 처리하기
- vararg 입력 파라미터 사용하기
- 여러 파라미터 그룹(부분 적용 함수)을 가진 메서드 작성하기
- 타입 파라미터를 가진 메서드 만들기
이 특징들에 대한 더 자세한 내용은 이 책의 다른 챕터들을 참고하세요.
더 알아보기
- "Methods" 챕터 (Scala 3 Book)
- "Main Methods in Scala 3" 챕터 (Scala 3 Book)
- "Extension Methods" 관련 챕터 (Scala 3 Book)