[Kotlin] Android 톺아보기 3-1. Kotlin 함수.md

이소은·2022년 10월 4일
0

Android 기본기 잡기

목록 보기
8/9

드디어 드디어 기본 문법에서 탈출하고 함수를 알아보자. Kotlin은 함수형 언어인 만큼, 함수에 대한 개념과 알아둬야 할 것이 매우 많다..ㅠㅠ 차근히 하나하나 알아보자.



함수란 무엇일까?

함수는 여러 값을 입력받아 기능을 수행하고 결과값을 반환하는 코드의 모음이다. 함수는 코드를 재사용하기 위해 사용한다. 예로 자동차를 만드는 함수가 f1이라고 할 때, f1의 input 값으로 빨강, 마세라티를 넣어서 자동차를 만들 수 있고, 파랑, 벤틀리를 넣어서 또 다른 자동차를 만들 수 있다. 입력값이 달라도 "자동차를 만드는" 같은 기능을 수행하는 것이다. 벤틀리를 만드는 함수, 마세라티를 만드는 함수를 따로 만들지 않고 코드를 재사용하는 것이 바로 함수의 핵심이다.



함수 구조 자세히 보기

fun sum(a: Int, b: Int): Int{
  return a + b
}

함수를 자세히 뜯어보자.

  1. fun: 모든 함수는 fun 키워드로 시작한다.
  2. sum: 함수 이름이다. 함수 이름은 camelCase로 짓는다.
  3. a: Int, b: Int: 매개변수이다. 매개변수란, 함수에서 입력한 값을 받는 변수이다. 매개변수는 여러개를 쓸 수 있으며 반드시 : 과 함께 자료형을 명시해야 한다. ex) 변수명 : 자료형
  4. : Int : 함수의 반환값 자료형이다. (반환값이 없으면 생략 가능)
  5. {}: 블록안에 함수의 코드를 작성한다.
  6. return a + b: 함수의 반환값에 맞는 자료형으로 최종 결과물을 반환한다.

보통 함수는 위와 같이 작성할 수 있지만, 보다 더 간단히 만들 수 있다.

fun sum(a: Int, a: Int): Int = a + b //{} 생략
fun sum(a: Int, a: Int) = a + b //반환값 자료형 생략


인자 vs 매개변수
  • 인자: 함수를 호출할 때 부르는 명칭
  • 매개변수: 함수를 선언할 때 부르는 명칭


함수 사용하기

함수를 사용하는 것을 "함수를 호출한다"라고 한다. 즉, 지금까지는 함수를 작성한 것이고, 작성한 함수를 사용하려면 "호출"해야 한다.

fun sum(a: Int, b: Int): Int{	// 3. 5. 
  return a + b
}

fun main(){										// 1. 
  val result1 = sum(3, 2)			// 2. 
  val result2 = sum(10, 3)		// 4. 
  
  println(result1)						// 6. 
}

위의 예시 코드에서 함수가 호출되는 순서를 알아보자.


1 => 2 => 3 => 4 => 5=> 6 순으로 실행된다. 여기서 중요한 것은 Kotlin에서는 main함수가 프로그램 진입점이라는 것과 main에서 다른 함수 A를 호출 시 A 함수로 이동하고, A 함수의 return 문 혹은 A 함수의 마지막 } 에서 main 함수로 다시 돌아온다는 점이다. 즉, 함수를 호출하면 잠시 그 함수에 들어갔다가 해당 함수를 호출한 지점으로 다시 되돌아온다.



함수의 호출과 메모리

이번에는 함수가 호출될 때 메모리가 어떻게 변하는지 알아보자. 이 부분은 CS 관련 지식으로도 많이 나오니 꼭 알아두자!

코드와 함수의 스택 프레임을 같이 보자. (스택 프레임은.. 방대한 내용이나 여기선 최대한 기본 개념만 짚고 넘어가자)



스택 프레임

자, 스택 프레임을 알려면 우선 스택이 뭔지 알아야 한다. 간단히 메모리 구조를 보고 가자.

메모리 영역은 다음 그림과 같이 크게 스택, 힙, 데이터, 코드 영역 4가지로 구성된다.


  1. Stack: 함수 인자, 지역 변수, 반환 주소 등이 저장되는 영역으로 함수 호출 전반적인 처리를 하는 메모리 영역이다. 상위 메모리 주소에서 하위 메모리 주소로 데이터가 저장되어 일명 거꾸로 자란다고 한다. (운영체제의 Kernal을 절대 침범할 수 없도록 하기 위해 Heap과 Stack을 마주보게 자라도록 구성한거라 한다.)

    Stack은 LIFO로 먼저 들어올수록 나중에 나가는 구조이다. 즉, 아래 그림처럼 한 쪽만 뚫려있어서 출입구가 1개이고, 먼저 들어온 애들이 밑에부터 쌓이다 보니 나중에 들어온 애들이 먼저 나가는 구조이다.


  1. Heap: 개발자가 동적으로 할당하는 메모리 영역이다. Stack 밑 부분이고 메모리 주소가 낮은 곳에서 높은 곳으로 순차적으로 할당된다. 따라서 Stack과 겹칠 수 있다. (Stack이 메모리를 너무 많이 차지하면 Heap 영역과 충돌하는데 이를 스택오버플로우 라고 한다.)

  1. Data: 전역변수와 정적 변수가 저장된 영역이다.

=> Heap과 Stack은 프로그램이 실행되는 동안 크기가 바뀌는 동적 할당 영역이다. 즉, 함수가 몇 번 호출되는 지, 변수를 얼만큼이나 동적으로 생성할 지 등에 따라 메모리 영역 크기가 바뀐다. 반면에 Data, Text 영역은 프로그램이 실행 되기 전에 이미 크기가 정해져서 바뀌지 않는 고정된 영역이다.



다시 돌아와서 스택 프레임이란, 함수가 호출될 때 그 함수만의 스택 영역을 구분하기 위해 생성되는 공간이다. 이 공간에는 함수와 관계된 지역 변수, 매개변수가 저장되고 함수가 호출될 때 생성되었다가 함수가 종료되면 소멸한다.


이젠 스택 프레임이 뭔지 개념적으로 알았으니, 함수와 스택 프레임의 관계를 알아보자.


fun main(){
  val num1 = 10
  val num2 = 3
  val result: Int
  
  result = max(num1, num2)
  
  println(result)
}

fun max(a: Int, b: Int) = if (a > b) a else b

위의 예시 코드를 스택 프레임으로 나타내면 다음과 같다.



main() 프레임에는 지역변수들이 호출된 순서대로 (args, num1, num2, result) 차례로 쌓여있다. 여기서 지역변수란, 함수 안에서만 쓸 수 있는 변수로, 해당 함수가 종료되면 스택 프레임에서 함께 사라지는 변수이다. 즉, 다른 함수에서는 지역변수에 접근 할 수 없다. (프레임이, 곧 영역이 다르니까!)


max() 프레임은 main() 함수에서 max()를 호출할 때 생성된다. max() 프레임에는 a 와 b 값인 3, 10이 들어있다. max() 가 종료되면 a, b 도 같이 사라지고 main()으로 돌아와 max()의 반환값이 main의 result 변수에 저장된다.



크게 크게 스택 프레임을 함수 단위로 보자. 아래 그림처럼 함수가 호출되면 스택에 프레임이 생긴다. 스택 프레임은 분리되어서 함수에 선언된 변수도 분리되어 있다. 즉, 서로 다른 영역의 변수엔 접근 할 수 없는 것이다. 이러한 변수들을 지역 변수라고 한다. 예로 main()프레임과 max() 프레임에 둘다 같은 이름의 변수 c가 존재하더라도, 두 변수는 다른 프레임(영역) 에 있으므로 프로그램 상 전혀 문제가 없다.


앞서 스택이 LIFO 방식이라고 했던 것처럼, main()이 호출되어 아래에 쌓이고, max()가 다음으로 호출되어 main() 위에 쌓이는 것을 볼 수 있다. 이후 max()가 종료되면 max()가 스택에서 먼저 사라지고 이후 main() 도 소멸된다.


즉, 함수를 A(), B(), C() 순서로 호출했다면 C(), B(), A() 순으로 스택에서 소멸된다.



함수 제대로 활용하기

함수의 CS적인 지식까지 알았으니 이제 함수를 실전에서 제대로 활용할 수 있는 꿀팁을 알아보자.


반환값이 없는 함수

함수의 반환값은 코드에 따라 있을 수도, 없을 수도 있는데 반환값이 없는 경우에는 4가지 방식으로 표현할 수 있다.

fun printSum(a: Int, b: Int): Unit{
  println("a+b :${a+b}")
}

fun printSum(a: Int, b: Int){
  println("a+b :${a+b}")
}

fun printSum(a: Int, b: Int): Unit{
  println("a+b :${a+b}")
  return Unit
}

fun printSum(a: Int, b: Int){
  println("a+b :${a+b}")
  return Unit
}
  1. return문 x Unit 자료형 x
  2. return문 x Unit 자료형 o
  3. return문 o Unit 자료형 x
  4. return문 o Unit 자료형 o

여기서 Unit이란, Kotlin에서 다루는 특수한 자료형 중 하나로 반환값이 없을 때 사용한다. 일반적으로 반환값이 없는 경우에 다른 언어에서는 void라고 표현하는데, Unit과 void는 조금 다르다. Unit은 특수한 객체를 반환하는 것이고, void는 정말로 아무것도 반환하지 않는 것이다.



매개변수에 default 값 지정하기

함수를 호출할 때, 매개변수의 기본값을 미리 지정할 수 있는데, 기본값을 미리 정할 경우 함수 호출 시 해당 매개변수를 넘겨주지 않아도 된다. (이 경우 기본값이 자동으로 채워짐) 예시를 보자.

fun add(name: String, email: String = "default"){
  
}

add("Soeun") // name만 줘도 됨

fun sumNum(a: Int = 100, b: Int = 200): Int{
  return a+b
}

sumNum() // 아무것도 안 줘도 됨

위의 코드와 같이 email 매개변수에 "default"라는 기본 값을 주었다. 따라서 add()를 호출할 때 email 없이 name만 넘겨주어도 add() 함수를 사용할 수 있다. 만약, 모든 매개변수의 기본값을 지정한다면 인자를 하나도 넘겨주지 않아도 호출할 수 있다.



매개변수 이름으로 호출하기

매개변수가 너무 많은 함수를 호출하다 보면 가끔 어떤 인자를 전달한건지 헷갈리는 경우가 있다. 이럴 경우 Kotlin에서는 매개변수 이름과 값을 같이 전달할 수 있다. 예시를 보자.


fun main(){
  namedParam(a = 200, b = 100)
}

namedParam(a: Int, b: Int): Int{
  return a+b
}

위의 예시와 같이 namedParam(a = 200, b = 100) 처럼 함수를 호출하는 부분만 봐도 어떤 값을 어떤 매개변수에 전달했는지 한 눈에 들어온다.



매개변수 개수가 고정되지 않은 함수 사용하기

가끔 함수의 매개변수 개수가 가변적인 경우가 있다. 예로 함수 인자로 1, 2, 3을 전달하면 1, 2, 3을 출력하고 1, 2, 3, 4를 전달하면 1, 2, 3, 4를 출력하는 함수가 있다고 하자. 이런 함수를 작성하려면 숫자 3개를 받는 함수와 숫자 4개를 받는 함수를 따로 작성해야 한다. 허나 이 방식은 같은 기능을 하는데 인자 개수 때문에 함수를 여러 개 정의해야 해서 매우 비효율적이다.


이럴 때 vararg를 사용하면 된다. 이는 가변 인자를 뜻하는데, 인자의 개수가 변한다는 뜻이다. 가변 인자를 사용하면 함수는 한 개만 정의해 놓고 여러 개의 인자를 받을 수 있다. 예시를 보자.


fun main(){
  exampleVarargs(1, 2, 3)
  exampleVarargs(1, 2, 3, 4)
}

fun exampleVarargs(vararg counts: Int){
  for(num in counts){
    println(num)
  }
}

위와 같이 가변 인자를 사용할 함수 매개변수 앞에 vararg 키워드를 사용하면 끝난다. 받은 매개변수는 for문을 통해 원소 하나하나에 접근 할 수 있다. 즉, 위의 예시에서 counts 변수는 Int array라고 보면 된다.


여기까지 함수의 기본 개념과 사용법, 선언하는 법 등을 배웠다. 다른 언어와 비슷한게 많지만 Kotlin은 함수형 언어인 만큼 함수를 다시 잘 짚고 넘어가기 위해 정리해보았다. 다음 장부터는 함수 파티가 시작될 예정이다.



Reference

profile
안드로이드 개발자

0개의 댓글