0Pricing
Scala for Backend Engineering & Functional Programming · 강의

제네릭과 타입 매개변수

타입 안전성을 유지하면서 재사용 가능한 구성 요소를 만들기 위해 제네릭 클래스와 메서드를 작성하는 방법을 이해합니다.

제네릭과 타입 매개변수은(는) CoddyKit의 무료 Scala for Backend Engineering & Functional Programming 강의입니다. 이것은 3개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Scala for Backend Engineering & Functional Programming 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Scala for Backend Engineering & Functional Programming 강의에는 총 3개의 강의가 포함되어 있습니다.

제네릭이란 무엇인가요?

문자열, 정수 또는 사용자 정의 객체처럼 어떤 타입의 항목이든 담을 수 있는 컨테이너를 만들어야 한다고 가정해 보겠습니다. 제네릭이 없다면 타입마다 별도의 컨테이너를 작성해야 하므로 코드가 반복되거나, Any처럼 매우 포괄적인 타입을 사용해야 합니다.

제네릭을 사용하면 컴파일 시점의 타입 안전성을 유지하면서 다양한 데이터 타입에 사용할 수 있는 유연하고 재사용 가능한 코드를 작성할 수 있습니다. 나중에 여러 재료를 끼워 넣을 수 있는 설계도와 같습니다!

`Any` 타입의 함정

타입의 자리 표시자로 Any를 사용하면 문제가 발생할 수 있습니다. 무엇이든 저장할 수 있지만, 구체적인 타입 정보는 잃게 됩니다. 따라서 항목을 원래 타입으로 캐스팅해야 하는 경우가 많으며, 이는 오류가 발생하기 쉽고 캐스팅이 잘못되면 실행 중 충돌을 일으킬 수 있습니다.

다음 예제를 실행하고 발생할 수 있는 문제를 생각해 보시기 바랍니다.

class ItemHolder(val item: Any)

object Main {
  def main(args: Array[String]): Unit = {
    val stringHolder = new ItemHolder("Hello Scala")
    val numberHolder = new ItemHolder(123)

    // We need to cast, and it's unsafe if we get it wrong
    val retrievedString = stringHolder.item.asInstanceOf[String]
    println(s"Retrieved String: $retrievedString")

    // If we uncomment the line below, it compiles but will crash at runtime!
    // val wrongType = numberHolder.item.asInstanceOf[String]
    // println(s"Wrong Type: $wrongType")
  }
}

타입 매개변수 소개

제네릭은 타입 매개변수를 도입하여 Any 타입의 함정을 해결합니다. 타입 매개변수는 클래스를 인스턴스화하거나 메서드를 호출할 때 지정하는 실제 타입의 자리 표시자와 같습니다.

Scala에서는 클래스나 메서드 이름 바로 뒤에 대괄호([])를 사용하여 타입 매개변수를 정의합니다. 일반적으로 T, A, B처럼 대문자 한 글자를 사용합니다. 예를 들어 class Box[T]는 하나의 타입 매개변수 T를 받는 제네릭 클래스 Box를 선언합니다.

제네릭 Box 만들기

어떤 타입의 콘텐츠든 담을 수 있는 Box라는 간단한 제네릭 클래스를 만들어 보겠습니다. 클래스 정의 전체에서 콘텐츠의 타입을 나타내는 데 T가 사용되는 점에 주목하시기 바랍니다.

이를 통해 타입마다 별도의 클래스를 작성하지 않고도 문자열, 정수 또는 다른 타입을 담는 상자를 만들 수 있습니다.

class Box[T](val content: T) {
  def get: T = content
  override def toString: String = s"Box($content)"
}

object Main {
  def main(args: Array[String]): Unit = {
    val stringBox = new Box("Scala is fun!")
    val intBox = new Box(42)
    val booleanBox = new Box(true)

    println(stringBox)
    println(intBox)
    println(booleanBox)
  }
}

제네릭을 사용한 타입 안전성

제네릭의 가장 큰 장점은 컴파일 시점의 타입 안전성입니다. Box[String]을 만들면 컴파일러는 그 안에 문자열만 들어가야 한다는 것을 알고 있습니다. 정수를 넣거나 정수로 가져오려고 하면 컴파일러가 즉시 오류를 알려 줍니다.

이를 통해 Any를 사용하는 것처럼 타입 안전성이 낮은 방식에서 발생하는 일반적인 실행 중 오류를 많이 방지할 수 있습니다.

class Box[T](val content: T) {
  def get: T = content
}

object Main {
  def main(args: Array[String]): Unit = {
    val myStringBox: Box[String] = new Box("CoddyKit")
    val myIntBox: Box[Int] = new Box(100)

    // This works, type is String
    val s: String = myStringBox.get
    println(s"String from box: $s")

    // This works, type is Int
    val i: Int = myIntBox.get
    println(s"Int from box: $i")

    // The compiler prevents this line from compiling:
    // val wrong: String = myIntBox.get // Type mismatch error!
  }
}

메서드에도 제네릭을 사용할 수 있습니다!

클래스와 마찬가지로 개별 메서드도 제네릭이 될 수 있습니다. 제네릭 메서드는 해당 메서드에만 적용되는 타입 매개변수를 받을 수 있으므로, 클래스 전체를 제네릭으로 만들지 않고도 여러 타입에 대해 동작할 수 있습니다.

클래스 자체는 제네릭 컨테이너가 아니지만 특정 연산을 다양한 타입에 적용해야 할 때 유용합니다.

유연한 `printPair` 메서드

다음은 제네릭 메서드 printPair의 예입니다. 이 메서드는 A와 B라는 두 타입 매개변수를 받아 어떤 두 타입의 쌍이든 출력할 수 있습니다. 타입 매개변수가 메서드 이름 바로 뒤에 선언된다는 점에 주목하시기 바랍니다.

따라서 이 메서드는 다양한 데이터 조합에 매우 재사용하기 좋습니다.

object Util {
  def printPair[A, B](first: A, second: B): Unit = {
    println(s"Pair: ($first, $second)")
    println(s"Type of first: ${first.getClass.getName}")
    println(s"Type of second: ${second.getClass.getName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    Util.printPair("Hello", 123)
    Util.printPair(true, List(1, 2, 3))
    Util.printPair(4.5, 'C')
  }
}

똑똑한 타입 추론

Scala의 강력한 기능 중 하나는 타입 추론입니다. 제네릭 클래스나 메서드를 사용할 때 타입 매개변수를 명시적으로 지정하지 않아도 되는 경우가 많습니다. Scala 컴파일러는 제공된 인수를 바탕으로 타입 매개변수를 충분히 정확하게 추론할 수 있습니다.

덕분에 타입 안전성을 해치지 않으면서 코드가 더 간결하고 읽기 쉬워집니다.

class Container[T](val item: T) {
  def getItem: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    // Scala infers T as String
    val stringContainer = new Container("Inferred String")
    println(s"Content: ${stringContainer.getItem}, Type: ${stringContainer.getItem.getClass.getName}")

    // Scala infers T as Double
    val doubleContainer = new Container(3.14)
    println(s"Content: ${doubleContainer.getItem}, Type: ${doubleContainer.getItem.getClass.getName}")

    // You can also specify explicitly, but often not needed
    val explicitIntContainer: Container[Int] = new Container(500)
    println(s"Content: ${explicitIntContainer.getItem}, Type: ${explicitIntContainer.getItem.getClass.getName}")
  }
}

바운드로 제네릭 타입 제한하기

경우에 따라 특정 기능을 가진 타입만 제네릭 클래스나 메서드에서 사용하도록 제한하고 싶을 수 있습니다. 예를 들어 숫자를 더하는 메서드는 숫자 타입만 받아야 합니다.

타입 바운드를 사용하면 타입 매개변수로 사용할 수 있는 타입을 제한할 수 있습니다. 상한 바운드(<:)는 타입 매개변수가 지정된 타입의 하위 타입이어야 한다는 뜻입니다. 예를 들어 [T <: Animal]은 T가 Animal이거나 Animal의 하위 타입이어야 한다는 의미입니다.

class Animal { def speak(): String = "..." }
class Dog extends Animal { override def speak(): String = "Woof!" }
class Cat extends Animal { override def speak(): String = "Meow!" }

// This box can only hold types that are Animal or its subtypes
class AnimalShelter[T <: Animal](val animal: T) {
  def getAnimalSound: String = animal.speak()
}

object Main {
  def main(args: Array[String]): Unit = {
    val dogShelter = new AnimalShelter(new Dog())
    println(s"Dog says: ${dogShelter.getAnimalSound}")

    val catShelter = new AnimalShelter(new Cat())
    println(s"Cat says: ${catShelter.getAnimalSound}")

    // This would NOT compile because Car is not an Animal:
    // class Car
    // val carShelter = new AnimalShelter(new Car()) 
  }
}

제네릭 클래스 도전 과제

제네릭에 대한 이해도를 확인해 볼 시간입니다!

다음 문장을 주의 깊게 읽고 Scala의 제네릭 클래스와 메서드에 관해 참인 내용을 모두 선택하시기 바랍니다.

제네릭: 핵심 정리

이번 레슨에서는 유연하고 타입 안전한 코드를 작성하기 위한 강력한 기능인 Scala의 제네릭을 살펴보았습니다.

  • 제네릭을 사용하면 클래스와 메서드가 여러 타입에 대해 동작할 수 있습니다.
  • 제네릭은 실제 타입의 자리 표시자로 타입 매개변수(예: [T])를 사용합니다.
  • 제네릭은 컴파일 시점의 타입 안전성을 제공하여 실행 중 오류를 방지합니다.
  • Scala의 타입 추론 덕분에 제네릭 코드를 간결하게 작성할 수 있는 경우가 많습니다.
  • 타입 바운드(예: <:)를 사용하면 타입 매개변수를 특정 하위 타입으로 제한할 수 있습니다.

제네릭을 사용하면 다양한 데이터 타입에 맞게 재사용할 수 있고 견고하며 유연한 컴포넌트를 작성할 수 있으므로, Scala 코드가 더욱 강력해지고 오류가 발생할 가능성이 줄어듭니다.

자주 묻는 질문

“제네릭과 타입 매개변수” 강의는 무료인가요?

네 — “제네릭과 타입 매개변수” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Scala for Backend Engineering & Functional Programming 강의 전체를 잠금 해제할 수 있습니다. Scala for Backend Engineering & Functional Programming 강의에는 총 3개의 강의가 포함되어 있습니다.

“제네릭과 타입 매개변수”에서 뭘 배우나요?

타입 안전성을 유지하면서 재사용 가능한 구성 요소를 만들기 위해 제네릭 클래스와 메서드를 작성하는 방법을 이해합니다. 브라우저에서 직접 실행하는 실습 코드로 Scala for Backend Engineering & Functional Programming을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Scala for Backend Engineering & Functional Programming을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Scala for Backend Engineering & Functional Programming은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 3개 중 1번째 강의입니다.

“제네릭과 타입 매개변수” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Scala for Backend Engineering & Functional Programming 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Scala for Backend Engineering & Functional Programming 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 제네릭과 타입 매개변수
  2. 분산성: 공변성과 반공변성
  3. 타입 클래스와 암시적 기능
← Scala for Backend Engineering & Functional Programming(으)로 돌아가기