메서드 특징

메서드 특징 (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 표현을 내놓아요.

기본 파라미터 값

메서드 파라미터는 기본 값을 가질 수 있어요. 이 예제에서는 timeoutprotocol 두 파라미터 모두에 기본 값이 주어졌어요.

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_000http를 사용해요.
  • 두 번째 예제에서는 2_000timeout 값으로 주어져서 그것이 사용되고, 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)