Kotlin에서 목록 사용

사람들은 보통 매일 살아가면서 할 일 목록이나 행사 참석자 목록, 위시리스트, 식료품 목록 등 다양한 종류의 목록을 만듭니다. 프로그래밍에서도 목록은 매우 유용합니다. 예를 들어 앱 내에 뉴스 기사나 노래, 캘린더 일정, 소셜 미디어 게시물 목록이 있을 수 있습니다.

목록을 만들고 사용하는 방법을 아는 것은 도구 상자에 추가할 중요한 프로그래밍 개념이며 이를 통해 더 정교한 앱을 만들 수 있습니다.

이 Codelab에서는 Kotlin 플레이그라운드를 사용하여 Kotlin의 목록을 숙지하고 다양한 국수를 주문하는 프로그램을 만듭니다. 준비되셨나요?

기본 요건

  • Kotlin 플레이그라운드를 사용하여 Kotlin 프로그램을 만들고 수정하는 방법을 잘 알고 있어야 합니다.
  • main() 함수, 함수 인수 및 반환 값, 변수, 데이터 유형, 작업, 제어 흐름 문 등 Kotlin의 Android 기본사항 과정 1단원의 기본 Kotlin 프로그래밍 개념을 잘 알고 있어야 합니다.
  • Kotlin 클래스를 정의하고 그 클래스에서 객체 인스턴스를 만들어 속성과 메서드에 액세스할 수 있어야 합니다.
  • 서브클래스를 만들고 서브클래스가 서로 상속하는 방법을 파악할 수 있어야 합니다.

학습할 내용

  • Kotlin에서 목록을 만들고 사용하는 방법
  • ListMutableList의 차이점과 각각을 사용하는 시점
  • 목록의 모든 항목을 반복하고 각 항목에 관해 작업을 실행하는 방법

빌드할 항목

  • Kotlin 플레이그라운드에서 목록과 목록 작업을 시도합니다.
  • Kotlin 플레이그라운드에서 목록을 사용하는 음식 주문 프로그램을 만듭니다.
  • 프로그램을 통해 주문을 만들고 국수와 채소를 추가한 다음 총 주문 비용을 계산할 수 있습니다.

필요한 항목

이전 Codelab에서는 Int, Double, Boolean, String과 같은 Kotlin의 기본 데이터 유형을 알아봤습니다. 이러한 유형을 통해 변수 내에 특정 유형의 값을 저장할 수 있습니다. 그러나 값을 둘 이상 저장하려면 어떻게 해야 하나요? 이때 List 데이터 유형이 있으면 유용합니다.

목록은 특정 순서가 있는 항목의 모음입니다. Kotlin의 목록 유형에는 두 가지가 있습니다.

  • 읽기 전용 목록: List는 만든 후 수정할 수 없습니다.
  • 변경 가능한 목록: MutableList는 만든 후 수정할 수 있습니다. 즉, 요소를 추가하거나 삭제, 업데이트할 수 있습니다.

ListMutableList를 사용할 때는 포함될 수 있는 요소 유형을 지정해야 합니다. 예를 들어 List<Int>에는 정수 목록이 있고 List<String>에는 문자열 목록이 있습니다. 프로그램에서 Car 클래스를 정의하면 Car 객체 인스턴스 목록이 있는 List<Car>를 보유할 수 있습니다.

목록을 이해하는 가장 좋은 방법은 직접 사용해보는 것입니다.

목록 만들기

  1. Kotlin 플레이그라운드를 열고 제공된 기존 코드를 삭제합니다.
  2. main() 함수를 추가합니다. 다음 코드 단계는 모두 이 main() 함수 안에 들어갑니다.
fun main() {

}
  1. main() 내부에서 List<Int> 유형의 numbers라는 변수를 만듭니다. 읽기 전용 정수 목록이 포함되기 때문입니다. Kotlin 표준 라이브러리 함수 listOf()를 사용하여 새 List를 만들고 목록의 요소를 쉼표로 구분된 인수로 전달합니다. listOf(1, 2, 3, 4, 5, 6)은 1에서 6까지 읽기 전용 정수 목록을 반환합니다.
val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
  1. 변수 유형을 할당 연산자(=) 오른쪽에 있는 값에 기반하여 추측하거나 추론할 수 있으면 변수의 데이터 유형을 생략할 수 있습니다. 따라서 이 코드 줄을 다음과 같이 줄일 수 있습니다.
val numbers = listOf(1, 2, 3, 4, 5, 6)
  1. println()을 사용하여 numbers 목록을 출력합니다.
println("List: $numbers")

문자열에 $를 넣으면 평가되어 이 문자열에 추가될 표현식이 뒤따릅니다(문자열 템플릿 참고). 이 코드 줄은 println("List: " + numbers).로 작성할 수도 있습니다

  1. numbers.size 속성을 사용하여 목록의 크기를 검색하고 이 또한 출력합니다.
println("Size: ${numbers.size}")
  1. 프로그램을 실행합니다. 목록의 모든 요소 목록과 목록의 크기가 출력됩니다. 대괄호 []List임을 나타냅니다. 대괄호 안에는 쉼표로 구분된 numbers 요소가 있습니다. 또한 요소의 순서는 요소를 만든 순서와 같습니다.
List: [1, 2, 3, 4, 5, 6]
Size: 6

액세스 목록 요소

목록 관련 기능은 위치를 나타내는 정수인 색인을 통해 목록의 각 요소에 액세스할 수 있다는 것입니다. 다음은 각 요소와 이에 상응하는 색인을 보여주는 numbers 목록의 다이어그램입니다.

cb6924554804458d.png

색인은 실제로 첫 번째 요소의 오프셋입니다. 예를 들어 list[2]의 경우 목록의 두 번째 요소를 요청하는 것이 아니라 첫 번째 요소에서 오프셋 위치가 2인 요소를 요청하는 것입니다. 따라서 list[0]은 첫 번째 요소(오프셋 0)이고 list[1]은 두 번째 요소(오프셋 1), list[2]는 세 번째 요소(오프셋 2), 이런 식으로 진행됩니다.

main() 함수에서 기존 코드 뒤에 다음 코드를 추가합니다. 각 단계가 끝난 후에 코드를 실행하여 예상대로 출력되는지 확인할 수 있습니다.

  1. 색인 0에 목록의 첫 번째 요소를 출력합니다. 원하는 색인과 함께 get() 함수를 numbers.get(0)으로 호출하거나 색인을 대괄호로 묶은 짧은 구문을 numbers[0]으로 사용할 수 있습니다.
println("First element: ${numbers[0]}")
  1. 다음으로 색인 1에 목록의 두 번째 요소를 출력합니다.
println("Second element: ${numbers[1]}")

목록의 유효한 색인값('색인')은 0에서 마지막 색인(목록 크기에서 1을 뺀 값)까지 이어집니다. 즉, numbers 목록의 경우 색인은 0에서 5까지입니다.

  1. 목록의 마지막 요소를 출력하고 numbers.size - 1을 사용하여 색인을 계산하면 5가 됩니다. 다섯 번째 색인의 요소에 액세스하면 출력으로 6이 반환됩니다.
println("Last index: ${numbers.size - 1}")
println("Last element: ${numbers[numbers.size - 1]}")
  1. Kotlin은 목록에서 first()last() 작업도 지원합니다. numbers.first()numbers.last()를 호출하여 출력을 확인해보세요.
println("First: ${numbers.first()}")
println("Last: ${numbers.last()}")

numbers.first()가 목록의 첫 번째 요소를 반환하고 numbers.last()는 목록의 마지막 요소를 반환합니다.

  1. 또 다른 유용한 목록 작업은 주어진 요소가 목록에 있는지 확인하는 contains() 메서드입니다. 예를 들어 회사 직원 이름 목록이 있다면 contains() 메서드를 사용하여 특정 이름이 목록에 있는지 확인할 수 있습니다.

numbers 목록에서 목록에 있는 정수 중 하나와 함께 contains() 메서드를 호출합니다. numbers.contains(4)true 값을 반환합니다. 그런 다음 목록에 없는 정수와 함께 contains() 메서드를 호출합니다. numbers.contains(7)false를 반환합니다.

println("Contains 4? ${numbers.contains(4)}")
println("Contains 7? ${numbers.contains(7)}")
  1. 완성된 코드는 다음과 같이 표시됩니다. 주석은 선택사항입니다.
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)
    println("List: $numbers")
    println("Size: ${numbers.size}")

    // Access elements of the list
    println("First element: ${numbers[0]}")
    println("Second element: ${numbers[1]}")
    println("Last index: ${numbers.size - 1}")
    println("Last element: ${numbers[numbers.size - 1]}")
    println("First: ${numbers.first()}")
    println("Last: ${numbers.last()}")

    // Use the contains() method
    println("Contains 4? ${numbers.contains(4)}")
    println("Contains 7? ${numbers.contains(7)}")
}
  1. 코드를 실행합니다. 다음과 같이 출력됩니다.
List: [1, 2, 3, 4, 5, 6]
Size: 6
First element: 1
Second element: 2
Last index: 5
Last element: 6
First: 1
Last: 6
Contains 4? true
Contains 7? false

읽기 전용인 목록

  1. Kotlin 플레이그라운드에서 코드를 삭제하고 다음 코드로 바꿉니다. colors 목록은 Strings로 표시되는 3가지 색상 목록으로 초기화됩니다.
fun main() {
    val colors = listOf("green", "orange", "blue")
}
  1. 읽기 전용 List에서는 요소를 추가하거나 변경할 수 없습니다. 목록에 항목을 추가하려고 하거나 새 값과 같도록 설정하여 목록의 요소를 수정하려고 하면 어떻게 되는지 알아보세요.
colors.add("purple")
colors[0] = "yellow"
  1. 코드를 실행하면 오류 메시지가 여러 개 표시됩니다. 본질적으로 오류는 add() 메서드가 List에 없고 요소의 값을 변경할 수 없음을 나타냅니다.

dd21aaccdf3528c6.png

  1. 잘못된 코드를 삭제합니다.

읽기 전용 목록을 변경할 수 없는 것을 직접 확인했습니다. 그러나 목록을 변경하지는 않지만 새 목록을 반환하는 여러 작업이 목록에 있습니다. 이 중 두 개가 reversed()sorted()입니다. reversed() 함수는 요소가 역순으로 있는 새 목록을 반환하고 sorted()는 요소가 오름차순으로 정렬된 새 목록을 반환합니다.

  1. colors 목록을 역전시키는 코드를 추가합니다. 내용을 출력합니다. colors 요소가 역순으로 포함되는 새 목록입니다.
  2. 두 번째 코드 줄을 추가하여 원래 list를 출력하면 원래 목록이 변경되지 않은 것을 확인할 수 있습니다.
println("Reversed list: ${colors.reversed()}")
println("List: $colors")
  1. 출력된 두 목록은 다음과 같이 표시됩니다.
Reversed list: [blue, orange, green]
List: [green, orange, blue]
  1. sorted() 함수를 사용하여 정렬된 버전의 List를 반환하는 코드를 추가합니다.
println("Sorted list: ${colors.sorted()}")

알파벳순으로 정렬된 새로운 색상 목록이 출력됩니다. 훌륭합니다.

Sorted list: [blue, green, orange]
  1. 정렬되지 않은 숫자 목록에서 sorted() 함수를 사용해 볼 수도 있습니다.
val oddNumbers = listOf(5, 3, 7, 1)
println("List: $oddNumbers")
println("Sorted list: ${oddNumbers.sorted()}")
List: [5, 3, 7, 1]
Sorted list: [1, 3, 5, 7]

이제 목록 만들기의 유용성을 알 수 있습니다. 그러나 목록을 만들고 나서 수정할 수 있으면 더 좋으므로 이제는 변경 가능한 목록을 살펴보겠습니다.

변경 가능한 목록은 만든 후에 수정할 수 있는 목록입니다. 항목을 추가하거나 삭제, 변경할 수 있습니다. 읽기 전용 목록으로 할 수 있는 작업도 모두 할 수 있습니다. 변경 가능한 목록은 MutableList 유형이고 mutableListOf()를 호출하여 만들면 됩니다.

MutableList 만들기

  1. main()에서 기존 코드를 삭제합니다.
  2. main() 함수 내에서 변경 가능한 빈 목록을 만들어 entrees라는 val 변수에 할당합니다.
val entrees = mutableListOf()

코드를 실행하려고 하면 다음 오류가 발생합니다.

Not enough information to infer type variable T

앞서 언급했듯이 MutableListList를 만들 때 Kotlin은 전달된 인수에서 목록에 포함된 요소의 유형을 추론합니다. 예를 들어 listOf("noodles")를 작성하면 Kotlin은 개발자가 String 목록을 만들려는 것으로 추론합니다. 요소 없이 빈 목록을 초기화하면 Kotlin은 요소의 유형을 추론할 수 없으므로 유형을 명시적으로 표시해야 합니다. mutableListOflistOf 바로 뒤에 유형을 꺾쇠괄호로 묶어 추가하면 됩니다. 문서에서는 <T>로 이를 확인할 수 있습니다. 여기서 T는 type 매개변수를 나타냅니다.

  1. 변수 선언을 수정하여 변경 가능한 String 유형 목록을 만들려고 한다고 지정합니다.
val entrees = mutableListOf<String>()

오류를 수정할 수 있는 또 다른 방법은 변수의 데이터 유형을 미리 지정하는 것입니다.

val entrees: MutableList<String> = mutableListOf()
  1. 목록을 출력합니다.
println("Entrees: $entrees")
  1. 빈 목록의 경우 []가 표시되어 출력됩니다.
Entrees: []

목록에 요소 추가

변경 가능한 목록은 요소를 추가하고 삭제, 업데이트할 때 흥미로워집니다.

  1. entrees.add("noodles").를 사용하여 목록에 "noodles"를 추가합니다. add() 함수는 목록에 요소가 성공적으로 추가되면 true를 반환하고 추가되지 않으면 false를 반환합니다.
  2. 목록을 출력하여 "noodles"가 실제로 추가되었는지 확인합니다.
println("Add noodles: ${entrees.add("noodles")}")
println("Entrees: $entrees")

출력은 다음과 같습니다.

Add noodles: true
Entrees: [noodles]
  1. 목록에 다른 항목 "spaghetti"를 추가합니다.
println("Add spaghetti: ${entrees.add("spaghetti")}")
println("Entrees: $entrees")

결과 entrees 목록에는 이제 항목이 두 개 있습니다.

Add spaghetti: true
Entrees: [noodles, spaghetti]

add()를 사용하여 요소를 하나씩 추가하는 대신 addAll()을 사용하여 한 번에 여러 요소를 추가하고 목록을 전달할 수 있습니다.

  1. moreItems 목록을 만듭니다. 변경하지 않아도 되므로 val과 변경 불가능으로 만듭니다.
val moreItems = listOf("ravioli", "lasagna", "fettuccine")
  1. addAll()를 사용하여 새 목록의 항목을 모두 entrees에 추가합니다. 결과 목록을 출력합니다.
println("Add list: ${entrees.addAll(moreItems)}")
println("Entrees: $entrees")

목록이 성공적으로 추가되었다고 표시됩니다. 이제 entrees 목록에는 항목이 총 5개 있습니다.

Add list: true
Entrees: [noodles, spaghetti, ravioli, lasagna, fettuccine]
  1. 이제 이 목록에 숫자를 추가해보세요.
entrees.add(10)

다음 오류가 표시되면서 실패합니다.

The integer literal does not conform to the expected type String

entrees 목록에서는 String 유형 요소를 예상하는데 개발자는 Int를 추가하려고 하기 때문입니다. 올바른 데이터 유형의 요소만 목록에 추가해야 합니다. 그러지 않으면 컴파일 오류가 발생합니다. 이는 Kotlin이 유형 안전성으로 코드를 더 안전하게 보호하는 한 가지 방법입니다.

  1. 잘못된 코드 줄을 삭제하면 코드가 컴파일됩니다.

목록에서 요소 삭제

  1. remove()를 호출하여 목록에서 "spaghetti"를 삭제합니다. 목록을 다시 출력합니다.
println("Remove spaghetti: ${entrees.remove("spaghetti")}")
println("Entrees: $entrees")
  1. "spaghetti"를 삭제하면 true가 반환됩니다. 요소가 목록에 있어서 성공적으로 삭제할 수 있기 때문입니다. 이제 목록에 남은 항목은 4개뿐입니다.
Remove spaghetti: true
Entrees: [noodles, ravioli, lasagna, fettuccine]
  1. 목록에 없는 항목을 삭제하려고 하면 어떻게 되나요? entrees.remove("rice")를 사용하여 목록에서 "rice"를 삭제해보세요.
println("Remove item that doesn't exist: ${entrees.remove("rice")}")
println("Entrees: $entrees")

remove() 메서드가 false를 반환합니다. 요소가 없어서 삭제할 수 없기 때문입니다. 목록은 항목이 여전히 4개뿐인 채로 변경되지 않고 유지됩니다. 출력:

Remove item that doesn't exist: false
Entrees: [noodles, ravioli, lasagna, fettuccine]
  1. 삭제할 요소의 색인을 지정할 수도 있습니다. removeAt()을 사용하여 색인 0에서 항목을 삭제합니다.
println("Remove first element: ${entrees.removeAt(0)}")
println("Entrees: $entrees")

removeAt(0)의 반환 값은 목록에서 삭제된 첫 번째 요소("noodles")입니다. 이제 entrees 목록에 남은 항목은 3개입니다.

Remove first element: noodles
Entrees: [ravioli, lasagna, fettuccine]
  1. 전체 목록을 삭제하려면 clear()를 호출하면 됩니다.
entrees.clear()
println("Entrees: $entrees")

이제 빈 목록이 출력됩니다.

Entrees: []
  1. Kotlin에서는 isEmpty() 함수를 사용하여 목록이 비어 있는지 확인할 수 있습니다. entrees.isEmpty().를 출력해보세요.
println("Empty? ${entrees.isEmpty()}")

현재 포함된 요소가 없어 목록이 비어 있으므로 true가 출력됩니다.

Empty? true

isEmpty() 메서드는 목록에서 작업을 실행하거나 특정 요소에 액세스하려고 하지만 먼저 목록이 비어 있지 않은지 확인하고 싶을 때 유용합니다.

변경 가능한 목록을 위해 작성한 모든 코드는 다음과 같습니다. 주석은 선택사항입니다.

fun main() {
    val entrees = mutableListOf<String>()
    println("Entrees: $entrees")

    // Add individual items using add()
    println("Add noodles: ${entrees.add("noodles")}")
    println("Entrees: $entrees")
    println("Add spaghetti: ${entrees.add("spaghetti")}")
    println("Entrees: $entrees")

    // Add a list of items using addAll()
    val moreItems = listOf("ravioli", "lasagna", "fettuccine")
    println("Add list: ${entrees.addAll(moreItems)}")
    println("Entrees: $entrees")

    // Remove an item using remove()
    println("Remove spaghetti: ${entrees.remove("spaghetti")}")
    println("Entrees: $entrees")
    println("Remove item that doesn't exist: ${entrees.remove("rice")}")
    println("Entrees: $entrees")

    // Remove an item using removeAt() with an index
    println("Remove first element: ${entrees.removeAt(0)}")
    println("Entrees: $entrees")

    // Clear out the list
    entrees.clear()
    println("Entrees: $entrees")

    // Check if the list is empty
    println("Empty? ${entrees.isEmpty()}")
}

목록의 각 항목에 관해 작업을 실행하려면 목록을 순환하면 됩니다(목록을 반복한다고도 함). 루프는 ListsMutableLists와 함께 사용할 수 있습니다.

while 루프

루프의 한 유형이 while 루프입니다. while 루프는 Kotlin에서 while 키워드로 시작됩니다. 괄호 안의 표현식이 true인 한 계속해서 반복 실행되는 코드 블록이 중괄호 안에 포함되어 있습니다. 코드가 영구적으로 실행(무한 루프라고 함)되지 않도록 하려면 코드 블록에 표현식의 값을 변경하는 로직을 포함하여 최종적으로 표현식이 false가 되고 루프 실행이 중지되도록 해야 합니다. 이 시점에서 while 루프를 종료하고 루프 뒤에 오는 코드를 계속 실행합니다.

while (expression) {
    // While the expression is true, execute this code block
}

while 루프를 사용하여 목록을 반복합니다. 목록에서 현재 보고 있는 index를 추적하는 변수를 만듭니다. 이 index 변수는 목록의 마지막 색인에 도달할 때까지 매번 1씩 증분한 후 루프를 종료합니다.

  1. Kotlin 플레이그라운드에서 기존 코드를 삭제하고 빈 main() 함수를 보유합니다.
  2. 파티를 연다고 가정해보겠습니다. 각 요소가 각 가족에서 응답한 참석자 수를 나타내는 목록을 만듭니다. 첫 번째 가족은 가족 중 2명이 참석한다고 했습니다. 두 번째 가족은 4명이 참석한다고 했습니다. 이런 식으로 계속 이어집니다.
val guestsPerFamily = listOf(2, 4, 1, 3)
  1. 총 참석자 수가 얼마가 될지 파악합니다. 루프를 작성하여 답을 알아보세요. 총 참석자 수의 var을 만들어 0으로 초기화합니다.
var totalGuests = 0
  1. 앞에서 설명한 대로 index 변수의 var을 초기화합니다.
var index = 0
  1. while 루프를 작성하여 목록을 반복합니다. 조건은 index 값이 목록의 크기보다 작으면 코드 블록을 계속 실행하는 것입니다.
while (index < guestsPerFamily.size) {

}
  1. 루프 내에서 현재 index의 목록 요소를 가져와 총 참석자 수 변수에 추가합니다. totalGuests += guestsPerFamily[index]totalGuests = totalGuests + guestsPerFamily[index].와 같습니다.

루프의 마지막 줄은 index++를 사용하여 index 변수를 1씩 증분하므로 다음 루프 반복이 목록의 다음 가족을 살펴보게 됩니다.

while (index < guestsPerFamily.size) {
    totalGuests += guestsPerFamily[index]
    index++
}
  1. while 루프 후 결과를 출력할 수 있습니다.
while ... {
    ...
}
println("Total Guest Count: $totalGuests")
  1. 프로그램을 실행하면 다음과 같이 출력됩니다. 목록에서 숫자를 직접 더해 이 답이 올바른지 확인할 수 있습니다.
Total Guest Count: 10

다음은 전체 코드 스니펫입니다.

val guestsPerFamily = listOf(2, 4, 1, 3)
var totalGuests = 0
var index = 0
while (index < guestsPerFamily.size) {
    totalGuests += guestsPerFamily[index]
    index++
}
println("Total Guest Count: $totalGuests")

while 루프를 사용하면 색인을 추적하고 목록의 색인에 있는 요소를 가져오고 이 색인 변수를 업데이트하는 변수를 만드는 코드를 작성해야 했습니다. 목록을 반복하는 더 빠르고 간단한 방법이 있습니다. for 루프를 사용하는 것입니다.

for 루프

for 루프는 루프의 또 다른 유형입니다. 훨씬 쉽게 목록을 순환할 수 있습니다. Kotlin에서 for 키워드로 시작되고 중괄호로 코드 블록이 묶여 있습니다. 코드 블록 실행 조건은 괄호 안에 표시되어 있습니다.

for (number in numberList) {
   // For each element in the list, execute this code block
}

이 예에서 number 변수는 numberList의 첫 번째 요소와 같게 설정되고 코드 블록이 실행됩니다. 그러면 number 변수가 자동으로 numberList의 다음 요소가 되도록 업데이트되고 코드 블록이 다시 실행됩니다. 이 작업은 numberList가 끝날 때까지 목록의 각 요소에 반복됩니다.

  1. Kotlin 플레이그라운드에서 기존 코드를 삭제하고 다음 코드로 바꿉니다.
fun main() {
    val names = listOf("Jessica", "Henry", "Alicia", "Jose")
}
  1. for 루프를 추가하여 names 목록의 항목을 모두 출력합니다.
for (name in names) {
    println(name)
}

while 루프로 작성해야 하는 것보다 훨씬 쉽습니다.

  1. 출력은 다음과 같습니다.
Jessica
Henry
Alicia
Jose

목록에 관한 일반적인 작업은 목록의 각 요소로 작업을 실행하는 것입니다.

  1. 루프를 수정하여 해당하는 사람의 이름에 있는 문자 수도 출력합니다. 힌트: Stringlength 속성을 사용하여 String의 문자 수를 확인할 수 있습니다.
val names = listOf("Jessica", "Henry", "Alicia", "Jose")
for (name in names) {
    println("$name - Number of characters: ${name.length}")
}

출력:

Jessica - Number of characters: 7
Henry - Number of characters: 5
Alicia - Number of characters: 6
Jose - Number of characters: 4

루프의 코드는 원래 List를 변경하지 않았습니다. 출력된 내용에만 영향을 미쳤습니다.

목록 항목 1개에 어떤 일이 발생해야 하는지 안내를 훌륭하게 작성할 수 있고 코드는 목록 항목마다 실행됩니다. 루프를 사용하면 반복되는 코드를 수없이 입력하는 수고를 덜 수 있습니다.

목록과 변경 가능한 목록을 모두 만들고 사용해보고 루프도 알아봤으므로 이제 샘플 사용 사례에 배운 내용을 적용해보겠습니다.

지역 음식점에서 음식을 주문할 때 보통 한 고객의 음식 주문에는 여러 항목이 포함되어 있습니다. 목록을 사용하면 주문 정보를 저장하는 데 유용합니다. 클래스와 상속에 관한 지식도 활용하여 main() 함수 내에 모든 코드를 배치하는 대신 좀 더 견고하고 확장 가능한 Kotlin 프로그램을 만들어야 합니다.

일련의 다음 작업에서는 다양한 조합의 음식 주문을 허용하는 Kotlin 프로그램을 만듭니다.

먼저 최종 코드의 출력 예를 살펴보세요. 이 모든 데이터를 구성하기 위해 어떤 종류의 클래스를 만들어야 하는지 파악할 수 있나요?

Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

출력에 표시되는 내용은 다음과 같습니다.

  • 주문 목록이 표시됩니다.
  • 각 주문에는 번호가 표시됩니다.
  • 각 주문에는 국수, 채소 등 항목 목록이 포함될 수 있습니다.
  • 각 항목에는 가격이 표시됩니다.
  • 각 주문에는 개별 항목의 가격 합계인 총 가격이 표시됩니다.

Order를 나타내는 클래스와 NoodlesVegetables와 같은 각 음식 항목을 나타내는 클래스를 만들 수 있습니다. NoodlesVegetables에 유사성이 있음을 추가로 관찰할 수 있습니다. 둘 다 음식 항목이고 각각 가격이 표시되기 때문입니다. Noodle 클래스와 Vegetable 클래스가 모두 상속할 수 있는 공유 속성이 포함된 Item 클래스를 만들어 볼 수 있습니다. 이렇게 하면 Noodle 클래스와 Vegetable 클래스에서 모두 로직을 복제하지 않아도 됩니다.

  1. 다음 시작 코드가 제공됩니다. 전문 개발자는 새 프로젝트에 참여하거나 다른 개발자가 만든 기능에 추가할 때 등 다른 개발자의 코드를 읽어야 하는 경우가 많습니다. 코드를 읽고 이해할 수 있는 것이 중요합니다.

천천히 다음 코드를 살펴보며 내용을 파악하세요. 코드를 복사하여 Kotlin 플레이그라운드에 붙여넣고 실행합니다. Kotlin 플레이그라운드의 기존 코드를 삭제한 후에 새 코드를 붙여넣어야 합니다. 출력된 내용을 관찰하고 코드를 더 잘 이해하는 데 도움이 되는지 확인합니다.

open class Item(val name: String, val price: Int)

class Noodles : Item("Noodles", 10)

class Vegetables : Item("Vegetables", 5)

fun main() {
    val noodles = Noodles()
    val vegetables = Vegetables()
    println(noodles)
    println(vegetables)
}
  1. 다음과 유사하게 출력됩니다.
Noodles@5451c3a8
Vegetables@76ed5528

다음은 코드에 관한 자세한 설명입니다. 먼저 Item이라는 클래스가 있습니다. 여기서 매개변수 2개를 생성자가 사용합니다. 하나는 항목의 name(문자열)이고 다른 하나는 price(정수)입니다. 두 속성은 모두 전달된 후 변경되지 않으므로 val로 표시됩니다. Item은 상위 클래스이고 서브클래스가 상위 클래스에서 확장되므로 클래스는 open 키워드와 함께 표시됩니다.

Noodles 클래스 생성자는 매개변수를 가져오지 않지만 Item에서 확장되고 "Noodles"를 이름과 10 가격으로 전달하여 슈퍼클래스 생성자를 호출합니다. Vegetables 클래스도 비슷하지만 "Vegetables"와 5 가격으로 슈퍼클래스 생성자를 호출합니다.

main() 함수는 NoodlesVegetables 클래스의 새 객체 인스턴스를 초기화하여 출력합니다.

toString() 메서드 재정의

객체 인스턴스를 출력하면 객체의 toString() 메서드가 호출됩니다. Kotlin에서는 모든 클래스가 자동으로 toString() 메서드를 상속합니다. 이 메서드의 기본 구현에서는 인스턴스의 메모리 주소가 있는 객체 유형을 반환합니다. Noodles@5451c3a8Vegetables@76ed5528보다 좀 더 의미 있고 사용자 친화적인 내용을 반환하도록 toString()을 재정의해야 합니다.

  1. Noodles 클래스 내에서 toString() 메서드를 재정의하여 메서드에서 name을 반환하도록 합니다. Noodles는 상위 클래스 Item에서 name 속성을 상속합니다.
class Noodles : Item("Noodles", 10) {
   override fun toString(): String {
       return name
   }
}
  1. Vegetables 클래스에서도 동일하게 작업합니다.
class Vegetables() : Item("Vegetables", 5) {
   override fun toString(): String {
       return name
   }
}
  1. 코드를 실행합니다. 이제 결과가 더 명확해졌습니다.
Noodles
Vegetables

다음 단계에서는 일부 매개변수를 사용하도록 Vegetables 클래스 생성자를 변경하고 추가 정보를 반영하도록 toString() 메서드를 업데이트합니다.

주문에서 채소 맞춤설정

국수를 좀 더 다채롭게 만들려면 주문에 다양한 채소를 포함하면 됩니다.

  1. main() 함수에서 입력 인수가 없는 Vegetables 인스턴스를 초기화하는 대신 고객이 원하는 특정 채소 유형을 전달합니다.
fun main() {
    ...
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    ...
}

지금 코드를 컴파일하려고 하면 다음과 같은 오류가 발생합니다.

Too many arguments for public constructor Vegetables() defined in Vegetables

이제 문자열 인수 3개를 Vegetables 클래스 생성자에 전달하므로 Vegetables 클래스를 수정해야 합니다.

  1. 다음 코드와 같이 문자열 매개변수 3개를 사용하도록 Vegetables 클래스 헤더를 업데이트합니다.
class Vegetables(val topping1: String,
                 val topping2: String,
                 val topping3: String) : Item ("Vegetables", 5) {
  1. 이제 코드가 다시 컴파일됩니다. 그러나 이 솔루션은 고객이 정확히 채소 3개를 항상 주문하려는 경우에만 효과가 있습니다. 채소를 하나 또는 다섯 개 주문하고 싶은 고객은 주문할 수 없습니다.
  2. 각 채소의 속성을 사용하는 대신 Vegetables 클래스의 생성자에서 채소 목록(길이는 상관없음)을 허용하여 문제를 해결할 수 있습니다. List에는 Strings만 포함되어야 하므로 입력 매개변수 유형은 List<String>입니다.
class Vegetables(val toppings: List<String>) : Item("Vegetables", 5) {

main()에서 코드를 변경하여 토핑 목록을 먼저 만든 후에 Vegetables 생성자에 전달해야 하므로 가장 명쾌한 솔루션은 아닙니다.

Vegetables(listOf("Cabbage", "Sprouts", "Onion"))

이보다 더 좋은 문제 해결 방법이 있습니다.

  1. Kotlin에서 vararg 수정자를 사용하면 동일한 유형의 가변적인 인수 수를 함수나 생성자에 전달할 수 있습니다. 이렇게 하면 목록 대신 개별 문자열로 다양한 채소를 제공할 수 있습니다.

Vegetables의 클래스 정의를 변경하여 String 유형의 vararg toppings를 가져옵니다.

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
  1. main() 함수의 다음 코드가 이제 작동합니다. 여러 토핑 문자열을 전달하여 Vegetables 인스턴스를 만들 수 있습니다.
fun main() {
    ...
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    ...
}
  1. 이제 Vegetables 클래스의 toString() 메서드를 수정하여 Vegetables Cabbage, Sprouts, Onion 형식의 토핑도 언급하는 String을 반환하도록 합니다.

항목 이름(Vegetables)으로 시작합니다. 그런 다음 joinToString() 메서드를 사용하여 모든 토핑을 단일 문자열로 결합합니다. 사이에 공백이 있는 + 연산자를 사용하여 두 부분을 함께 추가합니다.

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
    override fun toString(): String {
        return name + " " + toppings.joinToString()
    }
}
  1. 프로그램을 실행하면 다음과 같이 출력됩니다.
Noodles
Vegetables Cabbage, Sprouts, Onion
  1. 프로그램을 작성할 때는 가능한 모든 입력을 고려해야 합니다. Vegetables 생성자의 입력 인수가 없으면 좀 더 사용자 친화적인 방법으로 toString() 메서드를 처리합니다.

고객이 채소를 원하지만 어떤 채소인지 표현하지 않았으므로 한 가지 솔루션은 기본값인 셰프가 선택한 채소를 제공하는 것입니다.

전달된 토핑이 없다면 Vegetables Chef's Choice를 반환하도록 toString() 메서드를 업데이트합니다. 앞서 알아본 isEmpty() 메서드를 활용합니다.

override fun toString(): String {
    if (toppings.isEmpty()) {
        return "$name Chef's Choice"
    } else {
        return name + " " + toppings.joinToString()
    }
}
  1. main() 함수를 업데이트하여 생성자 인수 없이 그리고 여러 인수로 Vegetables 인스턴스를 만드는 가능성을 모두 테스트합니다.
fun main() {
    val noodles = Noodles()
    val vegetables = Vegetables("Cabbage", "Sprouts", "Onion")
    val vegetables2 = Vegetables()
    println(noodles)
    println(vegetables)
    println(vegetables2)
}
  1. 예상대로 출력되는지 확인합니다.
Noodles
Vegetables Cabbage, Sprouts, Onion
Vegetables Chef's Choice

주문 만들기

이제 음식 항목이 생겼으므로 주문을 만들 수 있습니다. 프로그램의 Order 클래스 내에서 주문 로직을 캡슐화합니다.

  1. Order 클래스에 적합한 속성과 메서드를 생각해보세요. 도움이 되도록 여기 최종 코드의 샘플 출력을 다시 표시합니다.
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

Order #4
Noodles: $10
Vegetables Cabbage, Onion: $5
Total: $15

Order #5
Noodles: $10
Noodles: $10
Vegetables Spinach: $5
Total: $25
  1. 다음 속성과 메서드를 생각했을 수 있습니다.

주문 클래스

속성: 주문 번호, 항목 목록

메서드: 항목 추가, 여러 항목 추가, 주문 요약 출력(가격 포함)

  1. 먼저 속성에 중점을 두고 각 속성의 데이터 유형은 무엇이어야 하나요? 클래스에 공개되어야 하나요 비공개여야 하나요? 인수로 전달해야 하나요? 아니면 클래스 내에서 정의해야 하나요?
  2. 이를 구현하는 방법에는 여러 가지가 있지만 한 가지 솔루션은 다음과 같습니다. 정수 orderNumber 생성자 매개변수가 있는 class Order를 만듭니다.
class Order(val orderNumber: Int)
  1. 주문의 모든 항목을 미리 알지 못할 수 있으므로 인수로 전달할 항목 목록은 필요하지 않습니다. 대신 최상위 클래스 변수로 선언하고 Item 유형의 요소를 보유할 수 있는 빈 MutableList로 초기화할 수 있습니다. 이 클래스만 항목 목록을 직접 수정할 수 있도록 변수를 private로 표시합니다. 이렇게 하면 이 클래스 외부의 코드로 인해 예상치 못한 방식으로 목록이 수정되는 것을 방지합니다.
class Order(val orderNumber: Int) {
    private val itemList = mutableListOf<Item>()
}
  1. 클래스 정의에 메서드도 추가해 봅니다. 각 메서드에 적합한 이름을 자유롭게 선택하고 각 메서드 내의 구현 로직을 지금은 비워 두어도 됩니다. 어떤 함수 인수와 반환 값이 필요한지도 결정합니다.
class Order(val orderNumber: Int) {
   private val itemList = mutableListOf<Item>()

   fun addItem(newItem: Item) {
   }

   fun addAll(newItems: List<Item>) {
   }

   fun print() {
   }
}
  1. addItem() 메서드가 가장 간단해 보이므로 이 함수를 먼저 구현합니다. 그러면 새 Item을 가져오고 메서드는 itemList에 새 항목을 추가해야 합니다.
fun addItem(newItem: Item) {
    itemList.add(newItem)
}
  1. 다음으로 addAll() 메서드를 구현합니다. 그러면 읽기 전용 항목 목록을 가져옵니다. 이러한 항목을 모두 내부 항목 목록에 추가합니다.
fun addAll(newItems: List<Item>) {
    itemList.addAll(newItems)
}
  1. 그런 다음 총 주문 가격과 함께 모든 항목과 항목의 가격에 관한 요약을 출력하는 print() 메서드를 구현합니다.

주문 번호를 먼저 출력합니다. 그런 다음 루프를 사용하여 주문 목록의 모든 항목을 반복합니다. 각 항목과 항목의 가격을 출력합니다. 지금까지의 총 가격을 유지하고 목록을 반복하면서 계속 합산합니다. 마지막에 총 가격을 출력합니다. 이 로직을 직접 구현해보세요. 도움이 필요하다면 아래 솔루션을 확인하세요.

출력을 더 쉽게 읽을 수 있도록 통화 기호를 포함하는 것이 좋습니다. 다음은 솔루션을 구현하는 한 가지 방법입니다. 이 코드는 $ 통화 기호를 사용하지만 현지 통화 기호로 자유롭게 수정할 수 있습니다.

fun print() {
    println("Order #${orderNumber}")
    var total = 0
    for (item in itemList) {
        println("${item}: $${item.price}")
        total += item.price
    }
    println("Total: $${total}")
}

itemList에 있는 각 item의 경우 item을 먼저 출력하고(toString()item에서 호출되도록 트리거함) 항목의 price를 출력합니다. 또한 루프 전에 total 정수 변수를 0이 되도록 초기화합니다. 그런 다음 현재 항목의 가격을 total에 추가하여 총계에 계속 추가합니다.

주문 만들기

  1. main() 함수 내에 Order 인스턴스를 만들어 코드를 테스트합니다. 먼저 현재 main() 함수에 있는 내용을 삭제합니다.
  2. 이 샘플 주문을 사용하거나 직접 주문을 만들 수 있습니다. 주문 내에서 다양한 항목 조합을 시도하여 코드 내 모든 코드 경로를 테스트해야 합니다. 예를 들어 Order 클래스 내에서 addItem()addAll() 메서드를 테스트하고 Vegetables 인스턴스를 인수를 포함해 만들거나 포함하지 않고 만드는 등입니다.
fun main() {
    val order1 = Order(1)
    order1.addItem(Noodles())
    order1.print()

    println()

    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    order2.print()

    println()

    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    order3.print()
}
  1. 위 코드는 다음과 같이 출력됩니다. 총 가격이 올바르게 합산되고 있는지 확인합니다.
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

아주 좋습니다. 이제 음식 주문 같습니다.

주문 목록 유지

국수 전문점에서 실제로 사용할 프로그램을 빌드하고 있다면 모든 고객의 주문 목록을 추적하는 것이 좋습니다.

  1. 모든 주문을 저장할 목록을 만듭니다. 읽기 전용 목록인가요 아니면 변경 가능한 목록인가요?
  2. 다음 코드를 main() 함수에 추가합니다. 처음에는 목록이 비어 있도록 초기화합니다. 그리고 각 주문이 만들어지면 목록에 주문을 추가합니다.
fun main() {
    val ordersList = mutableListOf<Order>()

    val order1 = Order(1)
    order1.addItem(Noodles())
    ordersList.add(order1)

    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    ordersList.add(order2)

    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    ordersList.add(order3)
}

시간이 지남에 따라 주문이 추가되므로 목록은 Order 유형의 MutableList여야 합니다. 그런 다음 MutableList에서 add() 메서드를 사용하여 각 주문을 추가하세요.

  1. 주문 목록이 있으면 루프를 사용하여 각 주문을 출력할 수 있습니다. 주문 사이에 빈 줄을 인쇄하면 출력된 내용을 더 쉽게 읽을 수 있습니다.
fun main() {
    val ordersList = mutableListOf<Order>()

    ...

    for (order in ordersList) {
        order.print()
        println()
    }
}

이렇게 하면 main() 함수에서 중복 코드가 삭제되고 코드를 더 쉽게 읽을 수 있습니다. 출력된 내용은 이전과 같습니다.

주문의 빌더 패턴 구현

Kotlin 코드를 더 간결하게 하려면 주문을 만드는 데 빌더 패턴을 사용하면 됩니다. 빌더 패턴은 단계별 접근 방식으로 복잡한 객체를 빌드할 수 있는 프로그래밍의 디자인 패턴입니다.

  1. Order 클래스의 addItem()addAll() 메서드에서 Unit(또는 아무것도 없음)을 반환하는 대신 변경된 Order를 반환합니다. Kotlin은 키워드 this를 제공하여 현재 객체 인스턴스를 참조합니다. addItem()addAll() 메서드 내에서 this를 반환하여 현재 Order를 반환합니다.
fun addItem(newItem: Item): Order {
    itemList.add(newItem)
    return this
}

fun addAll(newItems: List<Item>): Order {
    itemList.addAll(newItems)
    return this
}
  1. main() 함수에서 이제 다음 코드와 같이 호출을 함께 연결할 수 있습니다. 이 코드는 새 Order를 만들고 빌더 패턴을 활용합니다.
val order4 = Order(4).addItem(Noodles()).addItem(Vegetables("Cabbage", "Onion"))
ordersList.add(order4)

Order(4)Order 인스턴스를 반환한 후 이 인스턴스에서 addItem(Noodles())를 호출할 수 있습니다. addItem() 메서드가 동일한 Order 인스턴스(새 상태)를 반환하면 다시 이 인스턴스에서 채소로 addItem()을 호출할 수 있습니다. 반환된 Order 결과는 order4 변수에 저장될 수 있습니다.

Orders를 만드는 기존 코드는 계속 작동하므로 변경하지 않아도 됩니다. 이러한 호출을 연결하는 것이 필수는 아니지만 함수의 반환 값을 활용하는 일반적이고 권장되는 방법입니다.

  1. 이 시점에서는 실제로 주문을 변수에 저장하지 않아도 됩니다. main() 함수에서(주문을 출력하는 최종 루프 전에) Order를 직접 만들어 orderList에 추가합니다. 각 메서드 호출이 한 줄에 하나씩 들어가면 코드를 더 쉽게 읽을 수 있습니다.
ordersList.add(
    Order(5)
        .addItem(Noodles())
        .addItem(Noodles())
        .addItem(Vegetables("Spinach")))
  1. 코드를 실행하면 다음과 같이 출력됩니다.
Order #1
Noodles: $10
Total: $10

Order #2
Noodles: $10
Vegetables Chef's Choice: $5
Total: $15

Order #3
Noodles: $10
Vegetables Carrots, Beans, Celery: $5
Total: $15

Order #4
Noodles: $10
Vegetables Cabbage, Onion: $5
Total: $15

Order #5
Noodles: $10
Noodles: $10
Vegetables Spinach: $5
Total: $25

이제 이 Codelab을 완료했습니다.

지금까지 데이터를 목록에 저장하고 목록을 변경하며 목록을 순환하는 것이 얼마나 유용한지 살펴봤습니다. 다음 Codelab에서는 여기서 배운 내용을 사용하여 Android 앱 컨텍스트에서 화면에 데이터 목록을 표시합니다.

다음은 Item, Noodles, Vegetables, Order 클래스의 솔루션 코드입니다. main() 함수는 이러한 클래스를 사용하는 방법도 보여줍니다. 이 프로그램을 구현하는 데는 여러 방법이 있으므로 코드가 약간 다를 수 있습니다.

open class Item(val name: String, val price: Int)

class Noodles : Item("Noodles", 10) {
    override fun toString(): String {
        return name
    }
}

class Vegetables(vararg val toppings: String) : Item("Vegetables", 5) {
    override fun toString(): String {
        if (toppings.isEmpty()) {
            return "$name Chef's Choice"
        } else {
            return name + " " + toppings.joinToString()
        }
    }
}

class Order(val orderNumber: Int) {
    private val itemList = mutableListOf<Item>()

    fun addItem(newItem: Item): Order {
        itemList.add(newItem)
        return this
    }

    fun addAll(newItems: List<Item>): Order {
        itemList.addAll(newItems)
        return this
    }

    fun print() {
        println("Order #${orderNumber}")
        var total = 0
        for (item in itemList) {
            println("${item}: $${item.price}")
            total += item.price
        }
        println("Total: $${total}")
    }
}

fun main() {
    val ordersList = mutableListOf<Order>()

    // Add an item to an order
    val order1 = Order(1)
    order1.addItem(Noodles())
    ordersList.add(order1)

    // Add multiple items individually
    val order2 = Order(2)
    order2.addItem(Noodles())
    order2.addItem(Vegetables())
    ordersList.add(order2)

    // Add a list of items at one time
    val order3 = Order(3)
    val items = listOf(Noodles(), Vegetables("Carrots", "Beans", "Celery"))
    order3.addAll(items)
    ordersList.add(order3)

    // Use builder pattern
    val order4 = Order(4)
        .addItem(Noodles())
        .addItem(Vegetables("Cabbage", "Onion"))
    ordersList.add(order4)

    // Create and add order directly
    ordersList.add(
        Order(5)
            .addItem(Noodles())
            .addItem(Noodles())
            .addItem(Vegetables("Spinach"))
    )

    // Print out each order
    for (order in ordersList) {
        order.print()
        println()
    }
}

Kotlin에서는 Kotlin 표준 라이브러리를 통해 데이터 컬렉션을 더 쉽게 관리하고 조작할 수 있는 기능을 제공합니다. 컬렉션은 동일한 데이터 유형의 여러 객체로 정의할 수 있습니다. Kotlin에는 목록, 집합, 지도와 같은 다양한 기본 컬렉션 유형이 있습니다. 이 Codelab에서는 목록에 특히 중점을 두었고 향후 Codelab에서 집합과 지도를 자세히 알아봅니다.

  • 목록은 특정 유형 요소의 정렬된 컬렉션입니다(예: Strings. 목록).
  • 색인은 요소의 위치를 나타내는 정수 위치입니다(예: myList[2]).
  • 목록에서 첫 번째 요소는 색인 0(예: myList[0])에 있고 마지막 요소는 myList.size-1(예: myList[myList.size-1] 또는 myList.last())에 있습니다.
  • 두 가지 목록 유형은 다음과 같습니다. List, MutableList.
  • List는 읽기 전용으로, 초기화가 완료되면 수정할 수 없습니다. 그러나 원본을 변경하지 않고 새 목록을 반환하는 sorted()reversed()와 같은 작업을 적용할 수 있습니다.
  • MutableList는 요소를 추가하거나 삭제, 수정하는 등 만든 후에 수정할 수 있습니다.
  • addAll()을 사용하여 항목 목록을 변경 가능한 목록에 추가할 수 있습니다.
  • while 루프를 사용하여 표현식이 false로 평가될 때까지 코드 블록을 실행하고 루프를 종료합니다.

while (expression) {

// While the expression is true, execute this code block

}

  • for 루프를 사용하여 목록의 모든 항목을 반복합니다.

for (item in myList) {

// Execute this code block for each element of the list

}

  • vararg 수정자를 사용하면 가변적인 인수 수를 함수나 생성자에 전달할 수 있습니다.