코틀린(Kotlin)/TIL

[Kotlin 문법] 객체지향 프로그래밍의 심화

초보왕보초 2023. 11. 30. 19:39
728x90
  • 접근제한자
  • 예외 처리의 활용
  • 지연초기화
  • 널 세이프티 (Null Safety)
  • 배열
  • 컬렉션
  • Single-expression function
  • 싱글턴

 

 

 

접근제한자

변수나 메서드의 접근을 제한할 수 있다

  • 코틀린에서는 public, private, internal, protected로 접근을 제한한다
  • 객체를 이용해서 변수나 메서드를 호출할 수 있는지의 여부를 접근이라고 한다

 

용어 정리

 

프로젝트 - 최상단 개념 <모듈, 패키지, 클래스>를 포함한다

 

 

모듈 - 프로젝트 아래의 개념이고 <패키지, 클래스>를 포함한다

 

 

패키지 - 모듈 아래의 개념이고 <클래스>를 포함한다, 일반적인 디렉터리

 

 

접근 제한자의 종류

public: 명시하지 않으면 기본적으로 public이다 (어디서나 접근 가능)
private: 동일한 클래스 내부에서만 접근할 수 있다 
internal: 같은 모듈 내부에서만 접근할 수 있다
protected: 기본적으로 private이지만 상속을 받은 경우에 타 모듈에서 접근할 수 있다

 

 

접근 제한자가 필요한 이유?

  • 접근권한을 통해 데이터의 무분별한 접근을 막을 수 있다
  • 클래스들 간에 접근하면 안 되는 상황을 구분하기 때문에 향후에 유지보수에 용이하다

 

 

 

예외 처리의 활용

프로그램 실행도중에 발생하는 예외를 적절하게 처리하는 문법

  • 프로그램을 실행하기 전에 알 수 있는 컴파일 에러 = 오류
  • 프로그램을 실행하는 도중에 발생하는 런타임 에러 = 예외
  • 실행 도중에 예외가 발생하면 프로그램이 비정상적으로 종료된다
  • 코틀린은 trt-catch와 throw로 예외를 처리한다

 

try-catch

fun method1() {
    try{
      예외가 발생할 가능성이 존재하는 코드
    } catch(예외종류) {
      예외가 발생했을 때 처리할 코드
    }
}

 

 

throw

fun method1(num1:Int) {
	if(num1 > 10) {
    	throw 예외종류
	}
}

 

 

예외 처리가 필요한 이유?

  • 고품질의 프로그램이란 사용성을 해치지 않아야 하기 때문에
  • 여러 측면의 사용성이 있지만 프로그램이 도중에 종료되지 않게 할 수 있기 때문에
  • 미리 예외를 생각하고 소스를 작성해야 안정성을 높인 프로그램이라고 할 수 있다

 

예시)

예외시 발생한 오류화면

 

예외를 처리한 상황 (try-catch)

    while(true) {
        try {
            var num1 = readLine()!!.toInt()
            println("내가 입력한 숫자는 ${num1}입니다")
            break
        } catch(e:java.lang.NumberFormatException) {
            println("숫자를 입력하세요")
        }
    }
    
    
    
    // 출력 - 일이삼(입력가정), 123(입력가정)
    숫자를 입력하세요
    내가 입력한 숫자는 123입니다
  • 입력한 내용을 toInt메서드로 정수 변환할 때 예외가 발생
  • 숫자를 입력할 때까지 반복문을 무한으로 실행하는 코드
  • break는 가장 가까운 반복문을 탈출하는 키워드

 

예외를 처리한 상황(try-catch-finally)

    while(true) {
        try {
            var num1 = readLine()!!.toInt()
            println("내가 입력한 숫자는 ${num1}입니다")
            break
        } catch(e:java.lang.NumberFormatException) {
            println("숫자를 입력하세요")
        } finally {
            println("키보드와의 연결은 정상적입니다")
        }
    }
    
    
    
    // 출력 - 사오육(입력가정), 456(입력가정)
    숫자를 입력하세요
    키보드와의 연결은 정상적입니다
    내가 입력한 숫자는 456입니다
    키보드와의 연결은 정상적입니다
  • 예외 처리와 관계없이 항상 실행하는 코드를 finally에 작성한다
  • 실제 예시) USB와 연결하는 코드는 반드시 사용 후에 연결을 끊어야 한다 (자원 낭비)
  • 실제 예시) GPS를 사용하는 코드는 반드시 사용 후에 연결을 끊어야 한다 (자원 낭비)

 

 

 

지연 초기화

변수나 상수의 값을 나중에 초기화할 수 있다

  • 코틀린은 클래스를 설계할 때 안정성을 위해 반드시 변수의 값을 초기화할 것을 권장한다
  • 클래스를 설계할 때 초기의 값을 정의하기 위해 난처해서 나중에 대입하기 위한 문법
  • 코틀린은 지연초기화 또는 늦은초기화를 위해 lateinit(변수), lazy(상수) 키워드를 활용한다
  • 저사양으로 제한되어 있는 환경에서는 메모리를 더욱 효율적으로 사용할 수 있다

 

예시)

변수의 지연초기화(lateinit)

// 기본 사용 

fun main(){
    var s1 = Student()
    s1.name = "참새"
    s1.displayInfo()

    s1.age = 10
    s1.displayInfo()
}

class Student {
    lateinit var name:String
    var age:Int = 0

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



// 출력
이름은: 참새 입니다.
나이는: 0 입니다.
이름은: 참새 입니다.
나이는: 10 입니다.
  • name 변수 값을 초기에 정의하기 어렵기 때문에 lateinit을 사용한다
  • 위 예시에서 name:String = " "를 이용해서 공백으로 처리할 수 있지만 가독성 측면에서 좋은 행위는 아니다
// 고급 사용

fun main(){
    var s1 = Student()
    s1.name = "참새"
    s1.displayInfo()

    s1.age = 10
    s1.displayInfo()
}

class Student {
    lateinit var name:String
    var age:Int = 0

    fun displayInfo() {
        if(this::name.isInitialized) {
           println("이름은: ${name} 입니다.")
           println("나이는: ${age} 입니다.")
        } else {
           println("name변수를 초기화해주세요.")
        }
    }
}



// 출력
이름은: 참새 입니다.
나이는: 0 입니다.
이름은: 참새 입니다.
나이는: 10 입니다.
  • 변수를 사용하기 전에 초기화되었는지 확인해야 안정성을 높일 수 있다
  • -> initialized를 활용해서 값이 초기화되었는지 확인할 수 있다 (true / false)
  • -> 사용할 때는 값이 아니라 참조 형태로 사용해야 하기 때문에 this:: 또는 ::를 붙인다

 

상수의 지연초기화(lazy)

fun main(){
    var s1 = Student()
    s1.name = "참새"
    s1.displayInfo()

    s1.age = 10
    s1.displayInfo()
}

class Student {
    lateinit var name:String
    var age:Int = 0
    val address: String by lazy {
        println("address 초기화")
        "seoul"
    }

    fun displayInfo() {
        println("이름은: ${name} 입니다.")
        println("나이는: ${age} 입니다.")
        println("주소는: ${address} 입니다.")
    }
}



// 출력
이름은: 참새 입니다.
나이는: 0 입니다.
address 초기화
주소는: seoul 입니다
이름은: 참새 입니다.
나이는: 10 입니다.
주소는: seoul 입니다.
  • 상수를 사용하는 시점에 값을 대입하고 초기화가 수행된다

 

 

 

널 세이프티 (Null Safety)

코틀린의 Null 안정성을 향상해 줄 수 있다

  • Null 예외는 프로그램의 가용성을 저하시키는 치명적인 오류이다
  • 코틀린은 Null 예외로부터 안전한 설계를 위해 자료형에 Null 여부를 명시할 수 있다
  • Null 예외로부터 안전한 설계를 위한 다양한 키워드를 지원한다
  • 코틀린은?,!!,?.,?: 로 Null 예외로부터 살아남으려 한다
  • 강제로 Null이 아니라고 하는 !!는 최대한 사용을 지양해야 한다

 

예시)

주소를 저장하는 address 변수는 Null을 저장할 수 있다고 String?으로 선언한다

// null을 저장하지 않고 설계하려면 lateinit var로 대체할 수 있다

fun main(){
    var s = Student()
    s.name = "참새"
    s.address = "서울"
    s.displayInfo()
}

class Student {
    lateinit var name:String
    var address:String? = null
    
    fun displayInfo() {
        println("이름은: ${name} 입니다")
        println("주소는: ${address} 입니다")
    }
}



// 출력
이름은: 참새 입니다
주소는: 서울 입니다

 

메서드를 호출하고 전달받은 리턴값이 Null이 아님을 !! 키워드로 보증한다

// readLine()의 결괏값을 !!로 Null이 아님을 보장한다

fun main(){
//  var data = readLine()!!.toInt()

    var inputData = readLine()!!
    var data = inputData.toInt()
    println("Null아닌 값: ${data}")
}



// 출력 - 12345(입력 가정)
Null이 아닌값: 12345

 

?. 키워드로 Null인지 확인하고 Null이 아닐 때만 참조하는 메서드

fun main(){
    var s = Student()
    s.name = "참새"
    s.displayAddressLength()
    
    s.address = "서울"
    s.displayInfo()
}

class Student {
    lateinit var name:String
    var address:String? = null

    fun displayInfo() {
        println("이름은: ${name} 입니다")
        println("주소는: ${address} 입니다")
    }
    
    fun displayAddressLength() {
        println("주소의 길이는: ${address?.length} 입니다")
    }
}



// 출력
주소의 길이는: null 입니다
이름은: 참새 입니다
주소는: 서울 입니다
  • 주소를 저장하는 address는 초기값이 null이기 때문에 null 위협에 놓여있다
  • Null이 아님을 보장해야 하는데 강제로 !!를 사용하는 것은 현 상황에 바람직하지 않다
  • ?. 는 안전호출연산자(safe-call)이라고 한다

 

?. 키워드로 안전하게 실행했지만 null이 출력되는 것을 막고 싶다면

fun main(){
    var s = Student()
    s.name = "참새"
    s.displayAddressLength()

    s.address = "서울"
    s.displayInfo()
}

class Student {
    lateinit var name:String
    var address:String? = null

    fun displayInfo() {
        println("이름은: ${name} 입니다")
        println("주소는: ${address} 입니다")
    }
    
    fun displayAddressLength() {
        println("주소의 길이는: ${address?.length ?: "초기화하세요"} 입니다")
    }
}



// 출력
주소의 길이는: 초기화하세요 입니다
이름은: 참새 입니다
주소는: 서울 입니다
  • ?: 키워드를 함께 사용해서 null 대신에 다른 문자열을 출력할 수 있다
  • ?: 를 엘비스연산자라고 한다

 

 

 

배열

변수에 순서를 매겨 활용할 수 있다

  • 일반적으로 변수를 선언하면 코틀린은 메모리에 띄엄띄엄 랜덤으로 생성한다
  • 변수의 위치정보가 연속적이지 않기 때문에 순서가 없다
  • 배열을 통해 변수에 순서를 매겨 연속적으로 활용할 수 있다
  • 코틀린은 배열을 사용하기 위해 arrayOf 메서드(키워드)를 제공한다
// java.util.Arrays 를 임포트 해줘야 한다

// arrayOf 메서드를 호출하면 배열을 리턴해준다
// 1,2,3,4,5 각각을 저장한 변수 5개를 배열형태로 arr에 저장
var arr = arrayOf(1,2,3,4,5)

// 배열요소를 모두 출력
println(Arrays.toString(arr))

// 배열의 첫번째 요소에 저장된 값을 출력
// var num1 = 1의 num1과 arr[0]은 동일
// arr[0]은 하나의 변수로 취급할 수 있다
// arr은 0~4번방(인덱스)까지 접근할 수 있다
println(arr[0])



// 출력
[1, 2, 3, 4, 5]
1

 

 

배열을 사용하는 이유?

  • 변수들을 각각 선언하면 반복적으로 변수에 접근하는 행위를 할 수 없다
  • 배열이 없다면 점수를 저장하기 위해 score1, score2, score3 이런 식으로 변수를 만들어야 하는데
  • 배열을 사용하면 socres로 점수들을 묶어서 활용하여, 효율적으로 코드를 작성할 수 있다

 

예시)

// 국어점수를 배열로 묶어 모든 점수를 출력하기

fun main() {
    var kors = arrayOf(90, 94, 96)
    for((idx, kor) in kors.withIndex()) {
        println("${idx}번째 국어 점수는 ${kor}")
    }
}



// 출력
0번째 국어 점수는 90
1번째 국어 점수는 94
2번째 국어 점수는 96

 

 

 

컬렉션

개발에 유용한 자료구조를 제공한다

  • 코틀린에서는 리스트, 맵, 집합 자료구조를 지원한다

 

List의 활용

// 읽기전용 리스트
// 0번, 1번, 2번 인덱스에 접근해서 값을 변경할 수 없다
var scores1 = listOf(값1, 값2, 값3)

// 수정가능 리스트
// 0번, 1번, 2번 인덱스에 접근해서 값을 변경할 수 있다
var scores2 = mutableListOf(값1, 값2, 값3)
scores2.set(인덱스, 값)

// 수정가능 리스트
// 0번, 1번, 2번 인덱스에 접근해서 값을 변경할 수 있다
// array로 데이터들을 저장하는 ArrayList도 mutableListOf와 동일하게 사용할 수 있다
// 저장할 데이터의 자료형을 < > 안에 지정해야 사용할 수 있다
var scores3 = ArrayList<자료형>(값1, 값2, 값3)
scores3.set(인덱스, 값)
  • 리스트는 읽기 전용과 수정가능한 종류로 구분할 수 있다
  • 배열(array)과 달리 크기가 정해져 있지 않아서 동적으로 값을 추가할 수 있다

 

Map의 활용

    // 읽기전용 맵
    // 변수명[키]로 데이터에 접근할 수 있다
    var scoreInfo1 = mapOf("kor" to 94, "math" to 90, "eng" to 92)
    println(scoreInfo1["kor"])

    // 수정가능 맵
    // 변수명[키]로 데이터에 접근할 수 있다
    var scoreInfo2 = mutableMapOf("kor" to 94, "math" to 90)
    scoreInfo2["eng"] = 92
    println(scoreInfo2["eng"])

    // 맵의 키와 값을 동시에 추출해서 사용할 수 있다
    for((k, v) in scoreInfo2) {
        println("${k}의 값은 ${v}")
    }
    
    
    
    // 출력
    94
    92
    kor의 값은 94
    math의 값은 90
    eng의 값은 92
  • 맵은 키와 값의 쌍으로 이루어진 자료형이다
  • 읽기 전용과 수정가능한 종류로 구분할 수 있다

 

Set의 활용

//  기본활용

//  읽기전용 Set
    var birdSet = setOf("닭", "참새", "비둘기")

//  수정가능 Set
//  var mutableBirdSet = mutableSetOf("닭", "참새", "비둘기")
//  mutableBirdSet.add("꿩")
//  mutableBirdSet.remove("꿩")
    println("집합의 크기: ${birdSet.size}")

    var findBird = readLine()!!

    if(birdSet.contains(findBird)) {
        println("${findBird} 종류는 존재합니다.")
    } else {
        println("${findBird}는 존재하지 않습니다.")
    }
    
    
    
    // 출력 - 닭(입력 가정)
    집합의 크기: 3
    닭
    닭 종류는 존재합니다.
  • 셋(set)은 순서가 존재하지 않고 중복 없이 데이터를 관리하는 집합 자료형이다
  • 읽기 전용과 수정가능한 종류로 구분할 수 있다
  • 다른 컬렉션들은 요소를 찾는 데에만 집중하지만, Set은 요소가 존재하는지에 집중한다
    // 고급 활용
    
    // 귀여운 새의 집합
    var birdSet = setOf("닭", "참새", "비둘기", "물오리")

    // 날수있는 새의 집합
    var flyBirdSet = setOf("참새", "비둘기", "까치")

    // 모든 새의 집합 (합집합)
    var unionBirdSet = birdSet.union(flyBirdSet)

    // 귀엽고 날수있는 새의 집합 (교집합)
    var intersectBirdSet = birdSet.intersect(flyBirdSet)

    // 귀여운 새들중에서 날수없는 새의 조합 (차집합)
    var subtractBirdSet = birdSet.subtract(flyBirdSet)

    println("=====합집합=====")
    println("모든 새의 집합 : ${unionBirdSet}")

    println("=====교집합=====")
    println("귀엽고 날수있는 새의 집합 : ${intersectBirdSet}")

    println("=====차집합=====")
    println("귀엽고 날수없는 새의 집합 : ${subtractBirdSet}")
    
    
    
    // 출력
    =====합집합=====
    모든 새의 집합 : [닭, 참새, 비둘기, 물오리, 까치]
    =====교집합=====
    귀엽고 날수있는 새의 집합 : [참새, 비둘기]
    =====차집합=====
    귀엽고 날수없는 새의 집합 : [닭, 물오리]
  • 교집합, 차집합, 합집합으로 간편하게 요소들을 추출할 수 있다

 

예시)

학생 2명의 점수를 입력받아 평균점수를 출력하기 위해 컬렉션을 활용해서 만들어보기

fun main() {
    var students = mutableListOf<Student>()
    var averages = mutableMapOf<String, Int>()

    for(idx in 0..2) {
        println("학생 이름")
        var name = readLine()!!

        println("국어 점수")
        var kor = readLine()!!.toInt()

        println("수학 점수")
        var math = readLine()!!.toInt()

        println("영어 점수")
        var eng = readLine()!!.toInt()

        var average = (kor + math + eng) / 3
        var tempStudent = Student(name, kor, math, eng)

        students.add(tempStudent)
        averages[name] = average
    }

    for(student in students) {
        var average = averages[student.name]
        student.displayInfo()
        println("평균점수: ${average}")
    }
}

class Student(name:String, kor:Int, math:Int, eng:Int) {
    var name:String
    var kor:Int
    var math:Int
    var eng:Int
    
    init {
        this.name = name
        this.kor = kor
        this.math = math
        this.eng = eng
    }

    fun displayInfo() {
        println("이름: $name")
        println("국어: $kor")
        println("수학: $math")
        println("영어: $eng")
    }
}



// 출력 - 타조,98,95,97(학생1 입력가정) / 참새,98,99,75 (학생2 입력가정)
학생 이름
이름: 타조
국어 점수
국어: 98
수학 점수
수학: 95
영어 점수
영어: 97
평균 점수: 94
학생 이름
이름: 참새
국어 점수
국어: 98
수학 점수
수학: 99
영어 점수
영어: 75
평균 점수: 90

 

 

 

Single-expression function

람다식을 이용해서 메서드를 간결하게 정의할 수 있다

  • 하나의 메서드를 간결하게 표현할 수 있는 방법
// Kotlin의 람다식 구조

{매개변수1, 매개변수2... -> 
     코드
}

 

예시)

// 세 개의 숫자의 평균을 리턴해주는 함수를 람다식으로 정의해보기

fun add(num1:Int, num2:Int, num3:Int) = (num1+num2+num3)/3
// 메서드를 선언하지 않고 로직을 저장할 수 있다

var add = {num1: Int, num2: Int, num3: Int -> (num1+num2+num3) / 3}
println("평균값: ${add(10,20,30)}")



// 출력
평균값: 20

 

 

 

싱글턴

메모리 전역에서 유일한 객체임을 보장할 수 있다

  • 보통 객체는 자원이 가능한 만큼 생성할 수 있다
  • 각자의 객체는 상이한 위치정보를 가지고 있어서 저장하는 값도 객체마다 고유하다
  • 싱글턴을 활용하면 해당 객체는 메모리 전역에서 유일함을 보장하고 위치정보가 고정이다
  • 프로그램이 실행되는 시점에 메모리에 바로 로드해서 위치를 잡는다
  • 코틀린은 companion, object 키워드로 싱글턴을 구현할 수 있다

 

싱글턴을 사용하는 이유?

  • 싱글턴 객체는 전역적으로 활용할 수 있어서 다른 클래스들에서 쉽게 접근할 수 있다
  • 전역에서 공통적으로 사용하는 정보라면 메모리를 더욱 효율적으로 활용할 수 있다
  • 객체 자원 간의 충돌을 방지할 수 있다

예시)

  • 프로그램에서 키보드 객체를 무한하게 제작한다면 입력순서가 꼬일 수 있는데
  • 이런 경우, 키보드 객체는 싱글턴으로 제작해서 사용하고 싶을 때 객체를 얻어오는 방법으로 사용한다

 

예시)

fun main() {
    Bird.fly("참새")
}

object Bird {
    fun fly(name:String) {
        println("${name}가 날아요~")
    }
}



// 출력
참새가 날아요~
  • 객체를 생성하지 않고도 클래스 정보에 접근할 수 있다 (생성자 호출 X)
fun main() {
    // trash와 같이 생성자에 매개변수 전달 가능
    var singletonObject1 = MySingletonClass.getInstance(trash = 1)
    singletonObject1.setNum(5)
    println("num값은: ${singletonObject1.getNum()}")

    // singletonObject2에서 num을 10으로 대입
    var singletonObject2 = MySingletonClass.getInstance(trash = 1)
    singletonObject2.setNum(10)

    // singletonObject1의 num이 10으로 출력됨
    // singletonObject1과 singletonObject2는 같은 객체를 공유하기 때문
    println("num값은: ${singletonObject1.getNum()}")

}

class MySingletonClass private constructor() {
    private var num:Int = 0

    companion object {
        @Volatile private var instance: MySingletonClass? = null
        private var trash = 0

        fun getInstance(trash: Int): MySingletonClass {
            this.trash = trash
            // 외부에서 요청왔을때 instance가 null인지 검증
            if(instance == null) {
		            // synchronized로 외부 쓰레드의 접근을 막음
		            // 쓰레드간의 객체상태 혼돈을 막기위해 사용한다고 이해하기
                synchronized(this) {
                    instance = MySingletonClass()
                }
            }
            return instance!!
            
//            엘비스연산자와 뒷장에서배울 scope function을 이용하면
//            아래와같이 더욱 직관적인 코드 작성이 가능하다
//            return instance ?: synchronized(this) {
//                // also는 호출한 객체를 it으로 넘김
//                // instance가 null이라면 새로 생성하고 아니면 무시함
//                instance ?: MySigletonClass().also {
//                    instance = it
//                }
//            }
        }
    }
    fun setNum(num: Int) {
        this.num = num
    }

    fun getNum(): Int{
        return this.num
    }
}



// 출력
num 값은: 5
num 값은: 10
  • 객체를 생성하지 않고도 클래스 정보에 접근할 수 있다 (생성자 호출 O)
728x90