20230724_TIL

이상훈·2023년 7월 23일

TIL

목록 보기
4/83

프로그래밍 심화 주차(Kotlin)

  1. 유용한 기능
    자료형을 변환할 수 있다.

일반 자료형간의 변환 예시

  • 숫자 자료형끼리는 to자료형() 메소드를 활용할 수 있다

  • 문자열을 숫자로 변경할때에는 별도의 메소드가 필요하다

    		var num1 = 20

    var num2 = 30.2

    var num3 = num2.toInt()
    var num4 = num1.toDouble()

    var strNum5 = "10"
    var strNum6 = "10.21"

    var num5 = Integer.parseInt(strNum5)
    var num6 = strNum6.toDouble()

    println("num3: $num3")
    println("num4: $num4")
    println("num5: $num5")
    println("num6: $num6")

객체 자료형간의 변환 예시

  • 객체 자료형간의 변환은 상속관계에서 가능하다

업 캐스팅(자식클래스를 부모클래스의 자료형으로 객체 생성)

fun main() {
println("몇 마리를 생성하시겠습니까?")
var count = readLine()!!.toInt()
var birds = mutableListOf()

for(idx in 0..count-1) {
    println("조류의 이름을 입력해주세요")
    var name = readLine()!!

			// as Bird는 생략가능
    birds.add(Sparrow(name) as Bird)
}
println("============조류 생성완료============")
for(bird in birds) {
    bird.fly()
}

}

open class Bird(name: String) {
var name: String

init {
    this.name = name
}

fun fly() {
    println("${name}이름의 조류가 날아요~")
}

}

class Sparrow(name: String): Bird(name) {

}

다운 캐스팅(부모클래스를 자식클래스의 자료형으로 객체 생성)

fun main() {
println("몇 마리를 생성하시겠습니까?")
var count = readLine()!!.toInt()
var birds = mutableListOf()

for(idx in 0..count-1) {
    println("조류의 이름을 입력해주세요")
    var name = readLine()!!

    birds.add(Sparrow(name) as Bird)
}
println("============조류 생성완료============")
for(bird in birds) {
    bird.fly()
}
// 다운캐스팅 오류
// Sparrow는 Bird가 가져야할 정보를 모두 가지고 있지 않기 때문임

// var s1:Sparrow = birds.get(0)
}

open class Bird(name: String) {
var name: String

init {
    this.name = name
}

fun fly() {
    println("${name}이름의 조류가 날아요~")
}

}

class Sparrow(name: String): Bird(name) {

}

자료형의 타입을 확인할 수 있다

코틀린은 is 키워드를 활용해서 자료형의 타입을 확인할 수 있다.

	if(name is String) {
    println("name은 String 타입입니다")
} else {
    println("name은 String 타입이 아닙니다")
}

여러 인스턴스를 리턴할 수 있다

설명

  • 메소드는 기본적으로 하나의 데이터를 리턴한다
  • 두 개 이상의 데이터를 포함하는 데이터클래스를 설계하고 인스턴스를 리턴하면 가능하다
  • 하지만 매번 불필요한 클래스를 만드는 행위는 비효율적이다.

Kotlin의 복수 데이터 리턴 방법

Pair를 활용해서 두 개의 인스턴스 리턴

	var chicken = Chicken()
var eggs = chicken.getEggs()
var listEggs = eggs.toList()

// first, second로 관리
// var firstEgg = eggs.first
// var secondEgg = eggs.second

// 리스트로 관리
var firstEgg = listEggs[0]
var secondEgg = listEggs[1]

println("달걀의 종류는 ${eggs} 입니다.")
println("리스트 달걀의 종류는 ${listEggs} 입니다.")
println("첫번째 달걀의 종류는 ${firstEgg} 입니다.")
println("두번째 달걀의 종류는 ${secondEgg} 입니다.")

}

class Chicken {
fun getEggs(): Pair<String, String> {
var eggs = Pair("달걀", "맥반석")
return eggs
}
}

Triple를 활용해서 세 개의 인스턴스 리턴

fun main() {
var chicken = Chicken()
var eggs = chicken.getThreeEggs()
var listEggs = eggs.toList()

// first, second, third로 관리
// var firstEgg = eggs.first
// var secondEgg = eggs.second
// var eggTime = eggs.third

// 리스트로 관리
var firstEgg = listEggs[0]
var secondEgg = listEggs[1]
var eggTime = listEggs[2]

println("달걀의 정보는 ${eggs} 입니다.")
println("리스트 달걀의 정보는 ${listEggs} 입니다.")
println("첫번째 달걀의 종류는 ${firstEgg} 입니다.")
println("두번째 달걀의 종류는 ${secondEgg} 입니다.")
println("달걀은 ${eggTime}에 나왔습니다.")

}

class Chicken {
fun getTwoEggs(): Pair<String, String> {
var eggs = Pair("달걀", "맥반석")
return eggs
}

fun getThreeEggs(): Triple<String, String, Int> {
    var eggs = Triple("달걀", "맥반석", 20230101)
    return eggs
}

}

자기 자신의 객체를 전달해서 효율적인 처리를 할 수 있다.

설명

  • 코틀린에서는 Scope Functions들을 제공한다.
  • 객체를 사용할때 임시로 Scope를 만들어서 편리한 코드 작성을 도와준다.

Kotlin의 Scope Function 종류

let function의 활용

  • 중괄호 블록안에 it으로 자신의 객체를 전달하고 수행된 결과를 반환한다.

    		var strNum = "10"

    var result = strNum?.let {
    // 중괄호 안에서는 it으로 활용함
    Integer.parseInt(it)
    }

    println(result!!+1)

with function의 활용

  • 중괄호 블록안에 this로 자신의 객체를 전달하고 코드를 수행한다.

  • this는 생략해서 사용할 수 있으므로 반드시 null이 아닐때만 사용하는 게 좋다.

    	var alphabets = "abcd"
    
    with(alphabets) {

    // var result = this.subSequence(0,2)
    var result = subSequence(0,2)
    println(result)
    }

also function의 활용

  • 중괄호 블록안에 it으로 자신의 객체를 전달하고 객체를 반환해준다.
  • apply와 함께 자주 사용한다

fun main() {
var student = Student("참새", 10)

var result = student?.also {
    it.age = 50
}
result?.displayInfo()
student.displayInfo()

}

class Student(name: String, age: Int) {
var name: String
var age: Int

init {
    this.name = name
    this.age = age
}

fun displayInfo() {
    println("이름은 ${name} 입니다")
    println("나이는 ${age} 입니다")
}

}

apply function의 활용

  • 중괄호 블록안에 this로 자신의 객체를 전달하고 객체를 반환해준다
  • 주로 객체의 상태를 변화시키고 바로 저장하고 싶을때 사용한다.

fun main() {
var student = Student("참새", 10)

var result = student?.apply {
    student.age = 50
}
result?.displayInfo()
student.displayInfo()

}

class Student(name: String, age: Int) {
var name: String
var age: Int

init {
    this.name = name
    this.age = age
}

fun displayInfo() {
    println("이름은 ${name} 입니다")
    println("나이는 ${age} 입니다")
}

}

run function의 활용

객체에서 호출하지 않는 경우의 예시

	var totalPrice = run {
    var computer = 10000
    var mouse = 5000

    computer+mouse
}
println("총 가격은 ${totalPrice}입니다")

객체에서 호출하는 경우의 예시

  • with와 달리 null체크를 수행할 수 있으므로 더욱 안전하게 사용가능하다

fun main() {
var student = Student("참새", 10)
student?.run {
displayInfo()
}
}

class Student(name: String, age: Int) {
var name: String
var age: Int

init {
    this.name = name
    this.age = age
}

fun displayInfo() {
    println("이름은 ${name} 입니다")
    println("나이는 ${age} 입니다")
}

}

Scope Functions 정리

수신객체와 람다함수간의 긴밀한 관계가 존재한다

Scope Functions은 크게 두 가지로 구분할 수 있다

  • 명시적으로 수신객체 자체를 람다로 전달하는 방법
  • 수신객체를 람다의 파라미터로 전달하는 방법

수신객체, 람다와의 관계를 자세히 알아본다

  • T는 수신객체를 의미한다.
  • block: 내부는 람다함수의 소스코드다.
  • 수신객체는 it으로 사용할 수 있다

fun main() {
var student = Student("참새", 10)
student?.run {
displayInfo()
}
}

class Student(name: String, age: Int) {
var name: String
var age: Int

init {
    this.name = name
    this.age = age
}

fun displayInfo() {
    println("이름은 ${name} 입니다")
    println("나이는 ${age} 입니다")
}

}

모든 수신객체를 it으로 활용하면 문제가 발생할 수 있다.

  • Child Function에서 Shadow가 되어서 제대로 참조하지 못할 수 있다
  • 그래서 it은 다른 이름으로 변경해서 사용하기도 한다.

// Scope Function을 중첩으로 사용할 경우 누가 누구의 범위인지 알수 없다!
// Implicit parameter 'it' of enclosing lambda is shadowed 경고 발생!

data class Person(
var name: String = "",
var age: Int? = null,
var child: Person? = null
)

// 잘못된 예시
Person().also {
it.name = "한석봉"
it.age = 40
val child = Person().also {
it.name = "홍길동" // 누구의 it인지 모른다!
it.age = 10 // 누구의 it인지 모른다!
}
it.child = child
}

// 수정한 예시
Person().also {
it.name = "한석봉"
it.age = 40
val child = Person().also { c ->
c.name = "홍길동"
c.age = 10
}
it.child = child
}

  1. 확장함수

기존 클래스에 쉽게 메소드를 추가할 수 있다.

설명

  • 코틀린에서는 자바와 달리 외부에서 클래스의 메소드를 추가할 수 있다.
  • 과도하게 사용하면 코드의 가독성을 해칠 수 있지만 장점도 존재한다.
  • 원하는 메소드가 있지만 내가 설계한 클래스가 아닐때 외부에서 메소드를 관리한다
  • 내 목적을 위해 외부에서 관리하기 때문에 원본 클래스의 일관성을 유지할 수 있다.

코드로 예시를 알려준다.

예시 코드

이름 나이만 출력하는 displayInfo 메소드가 있는데 추가로 등급까지 조회하고 싶다.

클래스를 변경하지 못하는 상황에서 확장함수로 메소드를 추가해서 사용할 수 있다

fun main() {
fun Student.getGrade() = println("학생의 등급은 ${this.grade} 입니다")
var student = Student("참새", 10, "A+")
student.displayInfo()
student.getGrade()
}

class Student(name: String, age: Int, grade: String) {
var name: String
var age: Int
var grade: String

init {
    this.name = name
    this.age = age
			this.grade = grade
}

fun displayInfo() {
    println("이름은 ${name} 입니다")
    println("나이는 ${age} 입니다")
}

}

  1. 실습: 텍스트 RPG 게임의 코드 효율성 개선_

// CashShop.kt
class CashShop private constructor() {
private val bowPrice = 150
private val staffPrice = 120

companion object {
    @Volatile private var instance: CashShop? = null

    fun getInstance(): CashShop {
        // 외부에서 요청왔을때 instance가 null인지 검증
        if(instance == null) {
            // synchronized로 외부 쓰레드의 접근을 막음
            // 쓰레드는 다음챕터에서 소개합니다!
            // 쓰레드간의 객체상태 혼돈을 막기위해 사용한다고 이해해주세요
            synchronized(this) {
                instance = CashShop()
            }
        }
        return instance!!
    }
}

fun purchaseWeapon(character:Character){
    if(character is Archer) {
        character?.run {
            if(money >= bowPrice) {
                println("[구매 후 금액]: [${money} - ${bowPrice}] = ${money-bowPrice}")
                money -= bowPrice
                weapons.add("슈퍼 활")
            } else {
                println("돈이 부족합니다.")
            }
        }
    } else if(character is Wizard) {
        character?.run {
            if(money >= staffPrice) {
                println("[구매 후 금액]: [${money} - ${staffPrice}] = ${money-staffPrice}")
                money -= staffPrice
                weapons.add("슈퍼 스태프")
            } else {
                println("돈이 부족합니다.")
            }
        }
    }
}

}

// WorldMain.kt
fun main() {
val worldName = "스코월드"

var myName = inputMyInfo("name").toString()

var myAge = inputMyInfo("age").toString().toInt()

var myJob = inputMyInfo("job").toString()

var myGender = inputMyInfo("gender").toString()

var myMoney = inputMyInfo("money").toString().toInt()

var myHp = inputMyInfo("hp").toString().toInt()

var isNamePass = true
var isAgePass = true
var isJobPass = true

var names = mutableListOf("참새", "꿩", "비둘기")
for(name in names) {
    if(myName == name) {
        println("중복된 이름이 존재합니다.")
        isNamePass = false
        break
    }
}

if(myAge < 12) {
    println("12세 미만은 이용할 수 없습니다.")
    isAgePass = false
}
if(myJob == "전사") {
    println("일시적으로 전사를 이용할 수 없습니다.")
    isJobPass = false
}

// 모든 조건을 통과한 경우에만 환영
if(isNamePass && isAgePass && isJobPass) {
    // 새로 이름 추가
    names.add(myName)
    displayInfo(worldName, myName, myAge, myJob)

    if(myJob == "마법사") {
        println("마법사는 초기 mp도 입력해주세요")
        var myMp = inputMyInfo("mp").toString().toInt()
        var myCharacter = Wizard(myName, myAge, myGender, myMoney, myHp, myMp)

        while(true) {
            println("[1] 슬라임동굴, [2] 좀비마을, [3] 캐쉬샵, [4] 종료")
            var selectNumber= inputMyInfo("selectNumber").toString().toInt()

            when(selectNumber) {
                1 -> {
                    selectWorld(1, myCharacter)
                }
                2 -> {
                    selectWorld(2, myCharacter)
                }
                3 -> {
                    openCashShop(myCharacter)
                }
                4 -> {
                    println("게임 종료")
                    break
                }
                else -> {
                    break
                }
            }
        }
    } else if(myJob == "궁수") {
        println("궁수를 선택했군요")
        var myCharacter = Archer(myName, myAge, myGender, myMoney, myHp)

        while(true) {
            println("[1] 슬라임동굴, [2] 좀비마을, [3] 캐쉬샵, [4] 종료")
            var selectNumber= inputMyInfo("selectNumber").toString().toInt()

            when(selectNumber) {
                1 -> {
                    selectWorld(1, myCharacter)
                }
                2 -> {
                    selectWorld(2, myCharacter)
                }
                3 -> {
                    openCashShop(myCharacter)
                }
                4 -> {
                    println("게임 종료")
                    break
                }
                else -> {
                    break
                }
            }
        }
    }
}

}

fun displayInfo(worldName:String, myName:String, myAge:Int, myJob:String) {
println("==================${worldName}에 오신것을 환영합니다==================")
println("당신의 정보는 다음과 같습니다.")
println("이름: ${myName}입니다.")
println("나이: ${myAge}입니다.")
println("직업: ${myJob}입니다.")
println("모험을 떠나 볼까요?")
}

fun selectWorld(selectWorld:Int, myCharacter: Character) {
if(selectWorld == 1) { // 슬라임 던전
if(myCharacter is Archer) {
var slime1 = Slime("초록슬라임", "초록", 30.2, 200, 10)
slime1.attack()
myCharacter.windArrow()
slime1.poison()
} else if(myCharacter is Wizard) {
var slime1 = Slime("파랑슬라임", "파랑", 30.2, 200, 10)
slime1.attack()
myCharacter.fireBall()
slime1.poison()
}
} else if(selectWorld == 2) { // 좀비 던전

    if(myCharacter is Archer) {
        var zombie1 = Zombie("파랑좀비", "파랑", 142.2, 500, 25)
        zombie1.virus()
        myCharacter.windJump("건물1")

    } else if(myCharacter is Wizard) {
        var zombie1 = Zombie("파랑좀비", "파랑", 142.2, 500, 25)
        zombie1.virus()
        myCharacter.teleport(10, 20)
    }

}

}

fun inputMyInfo(type:String): Any? {
return when(type) {
"name" -> {
println("이름을 입력해주세요")
while(true) {
try {
var originName = readLine()
if(originName?.first() != '_' && originName?.first() != '!') {
return originName
} else {
println("이름을 다시 입력해주세요")
}
} catch(e:Exception) {
println("이름을 다시 입력해주세요")
}
}
}
"age" -> {
println("나이를 입력해주세요")
while(true) {
try {
var originAge:String? = readLine()
return originAge?.toInt() ?: -1
} catch(e:Exception) {
println("나이를 다시 입력해주세요")
}
}
}
"job" -> {
println("직업을 입력해주세요")
while(true) {
try {
var originName = readLine()
if(originName?.equals("궁수") == true || originName?.equals("마법사") == true) {
return originName
} else {
println("직업을 다시 입력해주세요")
}
} catch(e:Exception) {
println("직업을 다시 입력해주세요")
}
}
}
"gender" -> {
println("성별을 입력해주세요")
while(true) {
try {
var originGender = readLine()
if(originGender?.equals("남") == true || originGender?.equals("여") == true) {
return originGender
} else {
println("성별을 다시 입력해주세요")
}
} catch(e:Exception) {
println("성별을 다시 입력해주세요")
}
}
}
"money" -> {
println("초기자본을 입력해주세요")
while(true) {
try {
var originMoney:String? = readLine()
return originMoney?.toInt() ?: -1
} catch(e:Exception) {
println("초기자본을 다시 입력해주세요")
}
}
}
"hp" -> {
println("초기체력을 입력해주세요")
while(true) {
try {
var originHp:String? = readLine()
return originHp?.toInt() ?: -1
} catch(e:Exception) {
println("초기체력을 다시 입력해주세요")
}
}
}
"mp" -> {
println("초기마나를 입력해주세요")
while(true) {
try {
var originMp:String? = readLine()
return originMp?.toInt() ?: -1
} catch(e:Exception) {
println("초기마나를 다시 입력해주세요")
}
}
}
"selectWorld" -> {
println("월드를 선택해주세요")
while(true) {
try {
var selectWorld:String? = readLine()
return selectWorld?.toInt() ?: -1
} catch(e:Exception) {
println("월드를 다시 선택해주세요")
}
}
}
"selectNumber" -> {
println("번호를 선택해주세요")
while(true) {
try {
var selectNumber:String? = readLine()
return selectNumber?.toInt() ?: -1
} catch(e:Exception) {
println("번호를 다시 선택해주세요")
}
}
}
else -> {
return "no"
}
}
}

fun openCashShop(character: Character) {
var cashShop = CashShop.getInstance()

if(character is Archer) {
    println("구매전 무기: ${character.weapons}")
    cashShop.purchaseWeapon(character)
    println("구매후 무기: ${character.weapons}")
} else if(character is Wizard) {
    println("구매전 무기: ${character.weapons}")
    cashShop.purchaseWeapon(character)
    println("구매후 무기: ${character.weapons}")
}

}

  1. 비동기 프로그래밍

설명

  • 순서대로 하나의 작업씩 수행하는 행위를 동기적 프로그래밍이라고 한다.
  • 동기적 프로그래밍은 순차적으로 수행하기 때문에 앞선 작업에 영향을 받는다.
  • 만약 앞선작업이 끝나지 않는다면 뒷작업은 영원히 수행할 수 없다.
  • 꼭 동기적으로 실행하지 않아도 되는 기능은 비동기적으로 실행하는 게 좋다.

여러가지의 로직들이 완료 여부에 관계없이 실행되는 방식을 의미한다.

예시

  • 5GB 영상 다운로드 -> 메일전송 -> 알림의 순서를 가진 로직이 있다
  • 해당 작업을 순차적으로 진행한다고 생각했을때 다른 작업을 하지못하고 앱이 멈추는등의 문제가 발생할 수 있다
  • 다른 작업을 하고있다가 영상 다운로드가 완료됬을때 알림이 발생하면 좋을 것 같다.

동기/비동기를 정리

동기 프로그래밍

  • 요청 보내고 결과값을 받을 때까지 작업을 멈춘다.
  • 한 가지씩 작업을 처리한다.

비동기 프로그래밍

  • 요청 보내고 결과값을 받을 때까지 멈추지 않고 또 다른 일을 수행한다.
  • 다양한 일을 한 번에 수행한다.
  1. 쓰레드

로직을 동시에 실행될 수 있도록 도와준다.

설명

  • 프로그램은 하나의 메인 쓰레드(실행흐름)가 존재한다.
  • 하나의 메인 쓰레드는 ㅡ> fun main() <- ㅡ 메인함수를 의미한다.
  • 실습 프로그램은 메인 쓰레드 위에서 로직을 실행해서 동시처리가 불가능했다.
  • 별도의 자식 쓰레드를 생성해서 동시에 로직을 실행할 수 있다.
  • 코틀린은 thread 키워드로 쓰레드를 생성할 수 있다.

전문용어로 설명한다

사전지식이 필요하다

프로세스(Process)

  • 프로그램이 메모리에 올라가서 실행될때 이를 프로세스 1개라고 한다.
  • 보통 프로그램을 더블클릭하면 프로세스가 당긴다.

쓰레드(Thread)

  • 쓰레드는 프로세스보다 더 작은 단위다.
  • 프로세스 안에서 더 작은 작업의 단위를 쓰레드라고 부른다.
  • 쓰레드는 생성되서 수행할때 각 독립된 메모리 영역인 STACK을 가진다.
  • 즉 쓰레드를 한 개 생성하면 스택메모리의 일정 영역을 차지한다.
  1. 코루틴

운영체제의 깊이있는 지식이 없어도 쉽게 비동기 프로그래밍을 할 수 있다.

설명

  • 최적화된 비동기 함수를 사용한다.
  • 하드웨어 자원의 효율적인 할당을 가능하게 한다.
  • 안정적인 동시성, 비동기 프로그래밍을 가능하게 한다.
  • 코루틴이 쓰레드보다 더욱 가볍게 사용할 수 있다.
  • 로직들을 협동해서 실행하자는게 목표이고 구글에서 적극 권장한다.

코루틴은 빌더와 함께 사용한다.

코루틴 빌더의 종류

  • 일반적으로 launch와 async 빌더를 가장 많이 사용한다
  • launch는 결과값이 없는 코루틴 빌더를 의미한다.

launch는 Job객체로 코루틴을 관리한다.

  • Job 객체는 다양한 함수를 가지고 있다.
  • join : 현재의 코루틴이 종료되기를 기다린다.
  • cancel: 현재의 코루틴을 즉시 종료한다.

async는 결과값이 있는 코루틴이다.

코루틴은 스코프로 범위를 지정할 수 있다.

  • GlobalScope : 앱이 실행된 이후에 계속 수행되어야할때 사용한다.
  • CoroutineScope : 필요할때만 생성하고 사용후에 정리가 필요하다.

코루틴을 실행할 쓰레드를 Dispatcher로 지정할 수 있다.

  • Dispatchers.Main : UI와 상호작용하기 위한 메인쓰레드
  • Dispatchers.IO : 네트워크나 디스크 I/O 작업에 최적화되어있는 쓰레드
  • Dispatchers.Default : 기본적으로 CPU 최적화되어 있는 쓰레드
  • 안드로이드에서는 특히 Dispatcher 간의 변환을 해야하는 작업을 고려해야 한다
profile
열심히 하자

1개의 댓글

comment-user-thumbnail
2023년 7월 24일

이런 유용한 정보를 나눠주셔서 감사합니다.

답글 달기