dev-course day57

2rlokr·2025년 5월 27일

dev-course

목록 보기
38/43
post-thumbnail

오늘 배운 것

함수

함수

가변인자 (Variadic Argument)

Kotlin에서는 vararg 키워드를 사용하여 하나의 매개변수에 여러 개의 값을 전달할 수 있다.

  • 함수 호출 시 해당 인자에 값을 여러 개 나열하거나 이미 존재하는 배열을 * 연산자 (스프레드 연산자)를 붙여 전달할 수도 있다.
fun printCartList(size: Int, vararg items: String) {
    println("$size 크기의 장바구니에 ${items.joinToString(", ")}(이)가 들어있습니다.")
}

printCartList(4, "성찬", "앤톤", "은석", "쇼타로", "원빈", "소희")

val cartList = arrayOf("감자", "고구마", "계란", "파")
printCartList(cartList.size, *cartList)
  • 가변인자의 자료형이 String인데, cartList는 배열 타입이다. 그럴 때는 스프레드 연산자를 사용해서 풀어 넣어줘야 한다.
fun logging(type : String = "INFO", vararg msg : String) {
    for (s: String in msg) {
        println("[${type}] $s")
    }
}
logging(msg=arrayOf("로그1", "로그2", "로그3"))
  • 이 경우는 for문에서 스마트 캐스트로 인해 array로 넣어도 가능한 것이다.

함수형 프로그래밍 (Functional Programming)

Kotlin은 함수형 프로그래밍객체지향 프로그래밍을 모두 지원하는 다중 패러다임 언어이다.

함수형 프로그래밍은 순수 함수를 작성하여 프로그램의 부작용을 줄이는 프로그래밍 기법을 의미한다.

  • 수학에서의 함수처럼 기능한다. 외부의 값에 의지하지 않는다.

순수 함수 (Pure Function)

입력이 같으면 항상 같은 값을 반환하고, 함수가 함수 외부의 어떤 상태도 바꾸지 않는다.

  • 사이드 이펙트(Side Effect)가 없는 함수를 의미한다.
// 순수 함수
fun power(x:Int) {
	return x*x;
}

// 순수 함수 X, 비순수 함수
var counter: Int = 0
fun increaseCounter(x:Int) {
	counter++
    return x + counter
}
  • 테스트에 용이하고, 병렬처리하는 것도 유리하다.

일급 객체 (First Class Citizen)

함수형 프로그래밍에서는 함수를 일급 객체로 취급한다. 일급 객체란 매개변수, 함수의 반환값, 할당 명령문의 대상, 동일 비교의 대상이 될 수 있는 것들을 의미한다.

  • 다른 객체들에게 일반적으로 적용가능한 모든 연산을 지원하는 객체를 의미한다.
  • 함수의 인자로 전달될 수 있고, 함수의 반환값에 사용될 수 있고, 변수에 담을 수 있는 것이 특징이다.

고차 함수 (High-Order Function)

다른 함수를 인자로 사용하거나 함수를 결과값으로 반환하는 함수를 의미한다. 일급 객체를 서로 주고받을 수 있는 함수가 고차함수가 될 수 있게 된다.

fun times(x:Int) : (Int) -> Int {
	return { target -> x * target }
    // 매개변수 -> 반환값
}

fun main() {
	val fiveTimes =times(5)
    val threeTimes = times(3)
    
    println("fiveTimesFive = ${fiveTimes(5)}") // 25
    println("threeTimesFive = ${threeTimesFive(5)}") // 15
}
  • 고차 함수는 인자나 반환값으로 함수를 사용한다.

익명 함수 (Anonymous Function)

익명 함수는 이름 없이 바로 정의해서 사용할 수 있는 함수이다. fun 키워드를 사용하지만 별도의 함수 이름 없이, 주로 짧은 동작을 전달하거나 일회성 작업을 할 때 사용된다.

fun main() {
	val process1 = fun(v:String) { // 익명함수
    	println(v)
    }
    
    process1("abc") // abc
    
    val process2 = fun(v: String): String {
    	return "processed $v"
    }
    
    process2("abc") // processed abc
}
  • 익명 함수는 반환 타입을 명시할 수도 있고, 생략할 수도 있다.
  • 람다 표현식과 비슷하지만, 익명 함수는 return을 사용할 때 익명 함수 자체에서 빠져나가는 특징이 있다.
val sayHello: () -> Unit = fun() {
    println("Hello World!")
}

sayHello()

// Hello World!
  • Kotlin의 함수는 일급 객체이기 때문에 정의 후 변수에 할당하는 것이 가능하다.

람다 표현식 (Lambda Expression)

fun main() {
	val sum = { x:Int, y:Int -> // 람다 표현식 매개변수
    	println("입력한 x = ${x}, y = $y")
    }
    sum(1,2)
}
fun main() {
	val sum = { x:Int, y:Int -> x + y }
    
    val sumResult = sum(5,5)
    println(sumResult) // 10
}
  • 중괄호{} 안에 화살표 연산자->를 사용하여 표현하는 것이 특징이다.
  • 인라인 함수가 아닌 람다 표현식에서는 return을 사용할 수 없다.
  • 람다 표현식은 익명 함수처럼 동작하기 때문에 return을 사용하면 컴파일 오류가 발생한다.
  • 코드가 길 때는 제일 마지막 줄의 명령문을 자동으로 반환한다.
val ap: (Int, Int) -> Int = ap@{ x, y ->
	if (x > y) {
    	return@ap x
    }
    x + y
}

val app1 = ap(100, 200)
val app2 = ap(200, 100)

println("app1 = ${app1}") // 300
println("app2 = ${app2}") // 300
  • 항상 맨 마지막 것을 반환 (x+y)하기 때문에, 라벨을 붙여주기 전까지는 300으로 같은 값을 리턴한다.
  • 라벨을 붙여주면 람다식에서도 return을 사용할 수 있다.

후행 람다 함수

fun main() {
	sayHello("성찬") {
    	println("반갑습니다!")
    }
}

fun sayHello(target:String, greeting: () -> Unit) {
	println("${target}님, ")
    greeting()
}
  • 함수의 매개변수 가장 마지막에 함수가 있을 때는, 후행 람다 함수를 사용하여 중괄호 안에 매개변수로 전달되는 함수를 작성할 수 있다.
  • 만약, 제일 마지막에 있지 않다면, 사용할 수 없다.

인라인 함수 (Inline Function)

인라인 함수는 함수 호출을 줄이기 위하여 컴파일 시 함수 본문을 호출한 곳에 그대로 복사하는 방식으로 동작하는 함수이다. 함수를 호출할 때는 호출 스택에 함수가 올라가게 되는데, 그 과정이 필요없게 되는 것이다. 인라인 함수는 inline이라는 키워드를 함수 정의 키워드 fun 앞에 붙이는 방식으로 사용한다.

  • 인라인 함수는 호출 오버헤드를 없애고 실행 성능을 높여준다.
fun main() {
	numbering(1) { n -> 
    	println("입력받은 숫자 : $n")
    }
    
    inline fun numbering(number:Any, func:(Any) -> Unit) {
    	println("숫자세기 시작!")
        func(number)
        println("숫자세기 끝!")
    }
}

위의 함수를 호출하게 되면 아래와 같아진다.

fun main() {
	var number = 1
    
    println("숫자세기 시작!")
    println("입력받은 숫자 : $number")
    println("숫자세기 끝!")
    
    inline fun numbering(number:Any, func:(Any) -> Unit) {
    	println("숫자세기 시작!")
        func(number)
        println("숫자세기 끝!")
    }
}

중위 표현식 (Infix Notation)

중위 표현법은 메서드를 마치 연산자처럼 사용할 수 있게 해주는 문법이다. infix 키워드를 함수 앞에 붙이는 방식으로 사용할 수 있으며, infix가 추가되어있으면 객체와 객체 사이에 함수 이름을 두고 호출할 수 있게 된다.

class Person(val name: String) {
    infix fun eat(sth: String) {
        println("$name${sth}을(를) 먹습니다!")
    }
}

val person1 = Person("성찬")
person1 eat "사과"

// 성찬 는 사과을(를) 먹습니다!

오버로딩 (Overloading)

같은 이름의 함수이지만, 함수 시그니처가 동일하지 않은 함수를 구현하는 것을 의미한다.

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

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

객체지향(OOP)와 Kotlin

객체지향 패러다임

객체지향의 4가지 특징

캡슐화(Encapsulation)

객체의 데이터와 그 데이터를 처리하는 메서드를 하나로 묶어 외부로부터 객체의 내부 구현을 숨기는 원칙이다.

  • 객체의 상태(데이터)는 외부에서 직접 접근하거나 변경할 수 없으며, 객체가 제공하는 공용 메서드를 통해서만 데이터를 조작할 수 있다.
  • 외부에는 꼭 필요한 기능만 공개(public)하고, 나머지는 감추는 방식이다.

상속(Inheritance)

기존의 클래스(부모 클래스)로부터 새로운 클래스(자식 클래스)를 생성하여, 부모 클래스의 속성과 메서드를 자식 클래스가 재사용할 수 있도록 하는 원칙이다.

  • 자식 클래스는 부모 클래스의 모든 특성을 상속받으며, 이를 기반으로 추가적인 속성이나 메서드를 정의하거나, 기존의 메서드를 수정(오버라이딩)할 수 있다.

다형성(Polymorphism)

같은 타입의 객체가 상황에 따라 다른 동작을 하는 성질을 말한다.

  • 메서드 오버라이딩(Override)과 인터페이스 구현(Interface Implementation)을 다형성의 대표적인 예시이다.

추상화(Abstraction)

복잡한 시스템의 중요한 부분만을 강조하고, 불필요한 세부 사항은 숨기는 원칙이다.

  • 객체가 공통적으로 수행할 수 있는 기능을 정의하고, 이 기능에 대한 구체적인 세부 사항은 숨긴다.
  • Kotlin에서는 추상 클래스(abstract class)와 인터페이스(interface)를 통해 추상화를 구현할 수 있다.

클래스 (Class)

class 클래스이름 { // 클래스 선언
	// 정의
    val field1 = "value" // 속성 (Field)
    
    fun behavior(): Unit {
    
    } // 함수 (Method)
}

인스턴스 (Instance)

클래스라는 설계도를 바탕으로 생성된 구체적인 객체(Object)를 의미한다.

  • 클래스는 필드(변수)와 메서드(함수)를 정의하지만, 실제 메모리에 할당되어 사용될 수 있는 실체가 아니다.
  • 클래스를 통해 실제 객체를 인스턴스로 만드는 행위를 인스턴스화라고 한다.

생성자 (Constructor)

객체를 초기화하는 특별한 종류의 메서드이다. 생성자는 클래스를 인스턴스화할 때 호출하며 객체의 초기 상태를 설정하는 데에 사용된다.

class Car constructor(name:String, c
					,color : String
                    ,size : Int
                    , isGasoline:Boolean) {
	var name: String = name
    var color: String = color
    var size: Int = size
    var isGasoline :Boolean = isGasoline
}

주 생성자
주 생성자는 클래스 이름과 함께 정의되는 생성자로서, constructor를 생략하여 작성하는 것도 가능하다.

  • 보통 클래스 선언과 동시에 프로퍼티를 초기화하거나 init 블록을 이용해 추가 작업을 한다.
  • 한 클래스에 주 생성자는 하나만 가질 수 있다.
class Car (var name: String
	, var color: String
	, var size: Int
	, var isGasoline: Boolean) {

}
  • 주 생성자의 매개변수에 var 혹은 val을 추가한다면 클래스 내부에서는 속성(Property)를 생략할 수 있다.

부 생성자 (Secondary Constructor)
일번 메서드와 마찬가지로 클래스의 본문에 함수와 같이 선언할 수 있다. 그리고 필요하다면 매개변수를 다르게 하여 여러 개 오버로딩 하는 것이 가능하다.

class Car {
    
    var name: String
    var color: String
    var size: Int
    var isGasoline: Boolean

    constructor(name: String, color: String, size: Int, isGasoline: Boolean) {
        this.name = name
        this.color = color
        this.size = size
        this.isGasoline = isGasoline
    }
    
}

init 블록

init 블록은 Kotlin 클래스에서 객체가 생성될 때 자동으로 실행되는 초기화 블록이다. 주생성자가 호출될 때 클래스가 생성되면서 init 블록 안에 작성된 코드가 가장 먼저 실행된다. 이 블록은 주생성자에게서 받은 매개변수를 검증하거나, 복잡한 초기화 작업이 필요한 경우에 주로 실행된다.

class Car (var name: String
	, var color: String
	, var size: Int
	, var isGasoline: Boolean) {

    init {
        println("""
            자동차가 생성되었습니다!
            자동차 이름: $name
            자동차 색상: $color
            자동차 크기: $size
            주유 타입: ${if (isGasoline) "가솔린" else "디젤"} 
        """.trimIndent())

    }
}

fun main() {
    Car("붕붕이", "검정색", 100, true)
    
    // 자동차가 생성되었습니다!
		// 자동차 이름: 붕붕이
		// 자동차 색상: 검정색
		// 자동차 크기: 100
		// 주유 타입: 가솔린
    
}

속성 (Property)

클래스가 생성된 후 해당 객체가 가지고 있는 고유한 값이나 상태를 의미한다. 이러한 속성은 클래스 안에 정의된 변수로 표현된다.

정보 은닉화 (Data Hiding)

정보 은닉화란 객체 지향 프로그래밍에서 객체의 내부 상태나 구현 세부사항을 외부로부터 숨기는 개념이다. 외부에서는 객체가 어떤 방식으로 데이터를 저장하거나 처리하는지 알 필요가 없으며, 단지 제공된 인터페이스만을 통해 기능을 이용하면 된다.

  • GetterSetter를 사용하는 것이 정보 은닉화의 대표적인 방법인데, Kotlin에서는 속성에 대해 기본적으로 Getter와 Setter를 자동으로 생성해준다.
class Member(_id: Int, _name: String, _nickname: String) {

    val id: Int = _id
    var name: String = _name
    var nickname: String = _nickname

}

fun main() {

    val memberA = Member(1, "admin", "admin")

    // Getter
    println("memberA id = ${memberA.id}")

    // Setter
    // memberA.id = 2 // Member 클래스의 id가 val로 되어있기 때문에 Setter 이용 불가능
    memberA.name = "memberA"
    memberA.nickname = "memberA"

    // Getter
    println("memberA name = ${memberA.name}")
    println("memberA nickname = ${memberA.nickname}")

}
  • valvar에 따라 생성되는 getter와 setter가 다르게 동작한다.
    • val : 읽기 전용 속성, getter만 생성, setter 생성 x
    • var : 읽기, 쓰기 모두 가능, getter와 setter 모두 생성 o
class Member(_id: Int, _name: String, _nickname: String) {

    val id: Int = _id
        get() {
            return field
        }
        
    var name: String = _name
        get() {
            return field
        }
        set(value) {
            field = value
        }
        
    var nickname: String = _nickname
        get() {
            return field
        }
        set(value) {
            field = value
        }
}
  • Getter와 Setter를 커스터마이징할 수 있다.
  • field는 해당 속성의 실제 값을 가리키는 백킹 필드(backing field)이다.
    • Getter와 Setter 내부에서 속성 이름을 그대로 사용하면 해당 속성의 getter 또는 setter를 다시 호출하게 되어 무한 재귀 호출이 발생할 수 있다.
      이를 방지하기 위해 field 키워드를 사용해 속성 값을 직접 참조해야 한다.
  • 또한, value는 Setter 내부에서 자동으로 제공되는 변수로, 새로 할당되는 값을 의미한다.

지연초기화

객체의 속성을 나중에 초기화하고 싶을 때 지연 초기화(late initialization) 기능을 사용할 수 있다. 지연 초기화는 lateinit 키워드로 수행할 수 있다.

  • lateinit 키워드는 var로 선언된 non-null 타입 프로퍼티에만 사용 가능하며, 주 생성자나 초기화 블록에서 바로 초기화되지 않고 나중에 값을 할당할 수 있게 해준다.
class Animal {

    lateinit var name: String

    fun sayName() {
        if ( ::name.isInitialized ) {
            println("제 이름은 $name 이에요!")
        } else {
            println("아직 이름이 없습니다!")
        }
    }

}

fun main() {

    val animal = Animal()
    animal.sayName()        // 아직 이름이 없습니다!

    animal.name = "나비"
    animal.sayName()        // 제 이름은 나비 이에요!
}
  • lateinit 변수를 초기화하지 않고 사용하면 UninitializedPropertyAccessException 예외가 발생한다.

lazy
val을 이용하여 lateinit과 같이 지연초기화를 할 때 사용할 수 있다.

  • 일반적으로 어떤 변수가 프로그램 실행 중 반드시 사용된다는 보장이 없거나, 초기화 비용이 큰 경우에는 해당 변수를 미리 초기화하지 않고 실제로 처음 접근할 때 초기화하는 것이 더 효율적이다.
class Animal {
    val name: String by lazy {
    	println("이름 짓는 중...")
        "나비"
    }
}

fun main() {

    val animal = Animal()
    println("animal.name = ${animal.name}") // 이 시점에서 초기화
    println("animal.name = ${animal.name}") // 이미 초기화된 값 사용
    
    /*
    이름 생성중..!
    animal.name = 나비
    animal.name = 나비
     */ 
}
  • lazy는 해당 프로퍼티가 처음 호출될 때 딱 한 번 실행되고 이후에는 캐시된 값을 반환하는 방식으로 동작한다.

가시성 지시자 (Visibility Modifier)

가시성(Visibility)이란 코드에서 어떤 요소(클래스, 함수, 변수 등)가 다른 코드에 의해 보일 수 있는 범위를 의미한다. 즉, 어떤 코드 조각이 외부에서 접근 가능한지, 아니면 내부에서만 사용할 수 있는지를 구분하는 개념이다.

  • 가시성 지시자는 클래스, 인터페이스, 함수, 프로퍼티 등에 붙여서 이 요소가 어디까지 접근 가능한지를 명시적으로 제어한다.
  • public, internal, protected, private 같은 가시성 지시자를 제공하며, 각각 접근 범위를 다르게 설정할 수 있다.
  • private : 외부에서 접근 불가능
  • public : 어디서든지 접근 가능
  • protected : 상속받은 하위 요소에서만 접근 가능
  • internal : 같은 정의의 모듈 내부에서 접근 가능
  • 가시성 지시자를 사용하지 않고 정의하면 기본적으로 public 취급한다.
  • 모듈이란, 같은 코틀린 프로젝트 내를 의미한다.

느낀 점

내용이 굉장히.. 많다. 팀원 분 중 한 분이 오늘 내용 고봉밥이라고 했는데,, 동의요 ㅎ.. 뭐랄까 이해는 되는데, 흡수해야 할 내용들이 많아서 좀 힘들었던 것 같다.
그치만 복습하고나니까 이해도 잘 되고, 괜찮은 것 같다 !
근데 솔직히 함수를 왜 저렇게 쓰지.. 싶은 게 많다. 그냥 다 fun 하면 안될까? 왜 굳이 굳이 익명함수를 쓰고 그걸 변수에 넣어서.. 그 변수를 호출하는 식으로 쓰는걸까... 하하하하
아직 화요일이라는 것을 난 믿을 수 없다. 믿길 거부한다 !!!! 악!

0개의 댓글