본문 바로가기

IT/자바

코틀린 기본 문법

반응형

함수

규칙 : fun + 함수명 +(+ 파라미터명 + 파라미터 변수형+)+ : + 반환 변수형 = 비즈니스 로직 

이때 파라미터 변수형과 반환 변수형이 같은 경우 생략이 가능함.

 

자바의 void 함수를 표현할 경우 코틀린에선 unit 으로 선언함.

fun main() {
    println("Hello World!")

    println(sum(1, 5))
    println(sum1(1, 5))
    sum2(1, 5)
}

//반환값을 정의한 함수
fun sum(a: Int, b: Int):Int = a+b
//파라미터와 반환값이 같은 경우 반환형 생략 가능
fun sum1(a: Int, b: Int) = a+b
//반환값이 없는 void 함수인 경우 unit으로 선언할 수 있음
fun sum2(a: Int, b: Int):Unit = println(a+b)

 

 

변수 

val : 변경 불가능한 변수

var : 변경 가능한 변수

  • 함수 와 마찬가지로 클래스 외부에 선언 가능
  • 타입 추론 가능
  • 변수명 먼저 선언 후 타입 선언

$변수명 사용하면 해당 변수를 문자열로 바로 삽입 가능

${수식(코틀린)} 사용하면 수식도 문자열로 바로 삽입 가능

fun main() {
    val i: Int = 123
    val j = 321

    var age: Int = 20
    age = 23

    val number = 20
    println("Please give me $number dollors")
    println("I only have ${if (number <10 ) number else 10} dollor")
}

 

 

클래스

코틀린을 쓸 경우 자바에서 선언한 롬복을 자동으로 선언해줌

자바 클래스 : 필드를 갖는 생성자 선언, field+getter+setter 프로퍼티 생성 필요

@Getter
@Setter
@AllArgsConstructor
public class Person {
    private String name;
    private int age;
    private boolean isMarred;
}

 

코틀린 클래스 : 필드를 모두 갖는 생성자 자동 선언, field+getter+setter 프로퍼티 자동 생성, 커스텀이 필요한 경우에만 getter, setter 선언

class Person(val name: String, val age: Int, val isMarried: Boolean)

 

 

Blooean 프로퍼티 getter, setter 선언 방식

Java : getter(isMarried()), setter(setMarried())

Kotlin : getter/setter(isMarried)

 

코틀린으로 생성한 클래스는 java로 디컴파일 한 경우 아래처럼 소스가 생성됨

public final class Person {
   @NotNull
   private final String name;
   private final int age;
   private final boolean isMarried;

   public Person(@NotNull String name, int age, boolean isMarried) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.age = age;
      this.isMarried = isMarried;
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public final boolean isMarried() {
      return this.isMarried;
   }
}

 

 

또한 코틀린에서는 getter를 아래처럼 커스텀해서 쓸 수 있음

class Rectangle (val height: Int, val width: Int) {
    val isSquare:Boolean
        get(){
            return height == width
        }

    //자바에서는 별도 구분 함수를 통해 값을 검증하는 방식을 권장하지만
    //코틀린에서는 프로퍼티 관련 검증 함수가 자동으로 생성됨으로 위 코드처럼 변수를 선언한 뒤 속성의 프로퍼티 사용하는 걸 권장함
    fun isSquareV2() = height == width
}

 

root인 최상위 함수는 아래처럼 함수명으로 import 해서 사용할 수 있음

fun createRandomRectangle(): Rectangle {
    val random = Random()

    return Rectangle(random.nextInt(), random.nextInt())
}

fun main() {
    println(createRandomRectangle().isSquare)
}

 

[디컴파일]

public final class MainKt {
   @NotNull
   public static final Rectangle createRandomRectangle() {
      Random random = new Random();
      return new Rectangle(random.nextInt(), random.nextInt());
   }

   public static final void main() {
      Person person = new Person("James", 32, true);
      String var1 = person.getName();
      System.out.println(var1);
      int var3 = person.getAge();
      System.out.println(var3);
      boolean var4 = person.isMarried();
      System.out.println(var4);
      Rectangle rectangle = new Rectangle(4, 4);
      boolean var2 = rectangle.isSquare();
      System.out.println(var2);
      var2 = createRandomRectangle().isSquare();
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] args) {
      main();
   }
}

디컴파일 할 경우 최상위 함수로 선언한 createRectangle 함수가 별도의 클래스의 public static 함수로 선언된 걸 확인할 수 있음

=> 자바랑 다르게 코틀린은 디렉토리 구조와 패키지 경로가 다를 수 있고 하나의 파일에 여러 클래스와 함수, 최상위 변수 모두 선언이 가능하다.

 

하나의 파일에 여러 클래스와 함수, 최상위 변수를 선언해서 사용할 경우 장점

  • 코틀린에서 편리해서 자주 활용되는 부분임
  • 결합도가 높은 클래스, 유사한 클래스, enum 클래스 등 한곳에 모아 놓으면 관리와 사용이 편리해짐

 

When 과 Enum 

코틀린에서 When 을 사용한 경우 반드시 enum을 적용해야 되는데 이는 아래처럼 enum이 추가될 경우 when 절에서 해당 enum이 없음을 컴파일 시점에 알 수 있어 매우 유용함

 

enum class Color(val r: Int, val g: Int, val b: Int){
    RED(255, 0, 0),
    ORANGE(255, 166, 0),
    YELLOW(255, 255, 0),
    GREEN(0, 255, 0);// 아래 함수와 구분을 위해서 세미콜론 추가

    fun rgb() = (r * 256 + g) *256 + b
}

fun getKoreanColor(color: Color): String =
    when(color) {
        RED -> "빨강"
        ORANGE -> "오렌지"
        YELLOW -> "노랑"
        GREEN -> "녹색"
    }

fun main(){
    println(getKoreanColor(RED))
}

 

인자가 없는 When

fun mix(c1: Color, c2: Color) =
    //인자가 없는 when의 경우 반드시 else을 정의해야 됨
    when {
        c1 == RED && c2 == YELLOW -> ORANGE
        c1 == YELLOW && c2 == BLUE -> GREEN
        else -> throw RuntimeException()
    }

 

 

When으로 스마트캐스트(타입 검증 및 타입 캐스트 자동으로 가능)

- if 문에서도 가능하고 직접 형변환도 가능함 (as 명령어를 통해)

fun printObject(obj: Any): Unit = when(obj){
    is String -> println(obj.lowercase())
    is Duration -> println(obj.nano)
    is LocalDateTime -> println(obj.month)
    else -> println("Unknown type")
}

fun printObjectIf(obj: Any): Unit = if (obj is String) {
    println(obj.lowercase())
} else {
    println("nothing")
}

fun main(){
    printObjectIf("OBNJECT")
    printObject(Duration.ofNanos(3456))
    printObject(LocalDateTime.now())
}

 

반복문

while 문 Java와 똑같고 for 반복문은 조금 다름

 - 기본적으로 i in 1..100 이면 양쪽 끝 부분 포함, until은 마지막 값 미포함

fun evenOrOdd(n: Int):String = when {
    n % 2 == 0 -> "even"
    else -> "odd"
}

fun main(){
    for (i in 1.. 10){
        println(evenOrOdd(i))
    }

    // 마지막 10 미포함 -> until
    for (i in 1 until 10){
        println(evenOrOdd(i))
    }
}

 

map과 이터레이션

mutableMapOf로 map 간단히 선언 가능

  • listOf, mapOf, setOf도 있음

c in Object -> for-each랑 동일함

(key, value) in mapObject -> map과 같은 객체의 key, value를 자동으로 분해해서 값을 세팅하여 가져다 쓸 수 있음

fun main(){
    // 값 변경이 가능한 map 객체
    val students = mutableMapOf<Int, String>()
    students[1] = "Jack"
    students[2] = "Diana"
    students[3] = "Frost"

    for (student in students) {
        println("번호:${student.key}, 이름:${student.value}")
    }

    //unmutable 변경 불가능한 map 객체 생성 (실무에서 다 많이 씀)
    val st = mapOf(
        1 to "Jock",
        2 to "Diana",
        3 to "Frost"
    )

    for ((key, value) in st){
        println("번호:${key}, 이름:${value}")
    }
}

 

in 키워드는 범위 검사할때도 사용할 수 있음

fun recognize(c: Char): String= when(c) {
    in '0'..'9' -> "숫자"
    in 'a'..'z', in 'A'..'Z' -> "알파벳"
    else -> "숫자도 알파벳도 아님"
}

fun main(){
    println("b는 ${recognize('b')}")
    println("3는 ${recognize('3')}")
    println("{는 ${recognize('}')}")
}

 

예외처리

try catch finally 구문도 if, when 처럼 값으로 취급할 수 있음

자바랑 다른 부분은 throw 할때 new로 Exception 클래스를 선언할 필요가 없고 checked Excpetion을 호출 함수에도 throw Exception을 별도로 표기할 필요가 없음

fun parse(numberStr: String): Int = try {
    Integer.parseInt(numberStr)

    throw IOException("일부로 발생시키는 checked exception") // java랑 다르게 new 없음
} catch (e: IOException) {
    throw RuntimeException("UnChecked exception으로 변환") // 롤백될 수 있게 예외 변환 
} finally {
    println("무조건 실행되는 코드블록")
}

fun main(){
    println(parse("1"))
}

 

Kotlin + Spring에서 @Transactional 을 활용할 때 주의점

CheckedException은 롤백되지 않음!!

  • 자바에서는 checkedException은 컴파일 에러로 잡아주기 때문에 그전에 개발자가 직접 필요에 따라 RuntimeException으로 전환해서 롤백 처리를 하거나 안하거나 선택할 수 있음
  • 코틀린에서는 별도로 checkedException을 컴파일에러로 잡지 않기 때문에 예외가 발생해도 롤백이 되지 않을 수 있음

그러므로 예외가 발생한 포인트에서는 반드시 try catch를 걸어주는게 좋음

 

코틀린 함수 활용

함수를 통해 이름으로 인자를 정의할 수 있으며 디폴트 파라미터 값 선언을 통해 인자 갯수에 따른 별도 메소드를 생성하는 오버로딩 작업을 하지 않아도 됨

fun getMyname(
    firstName: String = "gildong",
    lastName: String = "hong"
): String = "$firstName $lastName"

fun main(){
    //파라미터 세팅 혼돈이 발생할 수 있음
    println(getMyname("jinyoon", "yang"))
    //파라미터 지정이 가능함
    println(getMyname(firstName = "jinyoon", lastName = "yang"))

    //디폴트 인자 세팅이 가능함 (장점 : 인자 값에 따른 오버로딩을 할 필요가 없음)
    println(getMyname())
    //특정 인자에만 값 세팅이 가능함
    println(getMyname(lastName = "kim"))
}

 

 

최상위 함수와 프로퍼티 선언이 가능함 (장점 : 자바처럼 별도의 유틸 클래스를 생성해야 되는 불편함 해소)

//최상위 프로퍼티 선언 가능 (상수)
const val ONE = 1
const val TWO = 2

//최상위 함수 선언 가능 (유틸 클래스랑 유사)
fun sum(
    num1: Int,
    num2: Int
): Int = num1 + num2

 

-> 자바 코드로 작성할 경우 

public final class NumberUtils {
    public static final Integer ONE = 1;
    public static final Integer TWO = 2;
    private NumberUtils() {
    }
    public static Integer sum(Integer num1, Integer num2) {
        return num1 + num2;
	} 
}

 

확장함수 & 확장 프로퍼티

//확장함수
fun String.double():String = this + this

//확장프로퍼티
val String.lastChar:Char
    get() = get(this.length - 1)

//중위함수
infix fun String.add(postfix :String) = this + postfix

fun main() {
    println("do".double())
    println("dodo".lastChar)
    println("do" add "do")
    println(setOf(1, 4, 3, 6, 10).maxOrNull())

    //구조분해 (자동으로 key, value의 값을 정의해서 호출할 수 있음)
    mapOf(
        "one" to 1,
        "tow" to 2
    ).forEach {
        (key, value) -> println("$key : $value")
    }

}

확장함수로 자바와 다르게 상속을 받아 커스텀 함수를 정의할 필요가 없음 또한 컬렉션의 추가 메소드들은 모두 확장함수 형태로 선언되어 있어 호출해서 쓰기 편함

 - 코틀린 확장 함수는 내부적으로 정적 메소로 생성됨으로 오버라이드 할 수 없음.

 

중위 호출함수는 확장함수이면서 파라미터가 한개인 함수에서만 활용이 가능하며 mapof 에 많이 활용됨. 확장함수 형태지만 .으로 호출할 필요없이 띄여쓰기로 함수를 호출할 수 있어 가독성이 향상됨

 

참고 : 코틀린 가변 인자 함수는 vararg로 선언하면 됨

 

인터페이스

  • 자바와 동일함 (인터페이스를 만들고 안에 메소드를 정의함)
  • 자바에서는 인터페이스를 구현할때 implements나 extends로 정의하지만 코틀린에서는 : 으로 정의하면 됨
  • 인터페이스의 메서드 구현을 할 경우 반드시 override를 선언해야 됨.
interface Clickable {
    fun click(): Unit
}

interface Focused {
    fun focus(): Unit
    // 기본 메소드 (구현체 정의되어 있음)
    fun showOff(): Unit=println("showOff")
}

// 다중 상속
class Button : Clickable, Focused{
    override fun click() {
        println("clicked!!")
    }

    override fun focus() {
        println("focused!!")
    }
}

fun main() {
    Button().click()
    Button().focus()
    Button().showOff()
}

상속

코틀린은 기본적으로 상속이 불가능한 final 로 생성됨 그러므로 반드시 open으로 선언해줘야 상속이 가능함

(이유 : 무분별한 상속을 방지하기 위함, 여러 개발자의 손을 타면서 원 작성자의 의도를 파악해서 잘 상속해서 클래스를 구현할 수 있는지 .. 이 문제를 막기 위함 - 취약한 기반 클래스 문제)

// 오픈 클래스 : 상속 가능
open class RichButton : Clickable {
    // final method : 오버라이드 불가능
    fun disable() {}
    // open method : 오버라이드 가능
    open fun animate() {}
    // 오버라이드 메소드 : 오버라이드 가능
    override fun click() {}
}

 

 

베이스 클래스 상속하는 방법 

[버전 1]

open class Parent(val firstName:String)

class Chlid(val subName:String, firstName: String): Parent(firstName = firstName)

[버전 2]

//자바랑 비슷한 형태
class Child : Parent{
    private val subName:String
    constructor(subName:String, firstName: String):super(firstName){
        this.subName = subName
    }
}

 

부생성자 선언 방법

  • 클래스 구현부에 construct를 통해 별도의 생성자를 추가로 선언할 수 있고 this - 다른 생성자, super - 상속 클래스 생성자를 호출함
class Child : Parent{
    private val subName:String

    constructor(subName: String):this(subName, "")
    
    constructor(subName:String, firstName: String):super(firstName){
        this.subName = subName
    }
}

[간소화 버전]

class Child(val subName:String, firstName: String = "") : Parent(firstName)

 

backing Field - getter, setter 커스텀 할때 해당 필드를 의미하는 키워드

class Account{
    var balance:Int = 0
        set(value) {
            if (value < 0) throw IllegalArgumentException("value is wrong")
            else field = value
        }

    var accountName:String = ""
        get() = "계좌이름 : "+field
}

fun main() {
    val account = Account()
    account.balance = 100
    account.accountName = "급여계좌"

    println("${account.accountName} , 잔액 : ${account.balance}")
}

 

public, private, internal, protected : 접근자

-자바랑 코틀린 차이점

자바는 기본적으로 package-private 인데 코틀린에선 사용하지 않음. 코틀린은 기본적으로 public임

internal : 모듈 안에서만 볼 수 있은 가시성 (멀티 모듈인 경우 제한을 위해 새로 생김)

private : 클래스 내에서 선언하면 자바처럼 해당 클래스에서만 사용할 수 있고 파일내에 선언하면 해당 파일 안에서만 사용할 수 있음

 

코틀린 클래스 생성 방식 변화

버전 1, 2를 거치면서 마지막 클래스 생성 방식으로 많이 간소화되었음

// version 1
/*
class User constructor(_username:String){
    val username:String

    init {
        username = _username
    }
}
*/

//version2
/*
class User (_username:String){
    val userName:String = _username
}
*/

class User(val userName:String)
class User2(val userName:String, val level:Int=1)

fun main() {
    println(User(userName = "Jenny").userName)

    println(User2(userName = "Jenny").level)
    println(User2(userName = "Jenny", level = 3).level)
}

 

sealed class : 봉인된 클래스

  • 확장을 제한하여 편의성을 향상시키는 클래스 타입

sealed 클래스 내부에 해당 클래스의 구현체를 모두 나열한 뒤 when 절에서 해당 클래스를 처리할 경우 else 구문을 정의할 필요가 없으며 sealed 클래스에 구현체가 신규로 추가될 경우 컴파일 에러로 수정 위치를 찾기 수월해짐

 

[인터페이스 버전]

interface Error

class FileError(val filename:String) : Error

class DatabaseError(val database:Database) : Error

enum class Database{
    ORACLE, MYSQL, MARIADB;
}

fun getMassage(error: Error)=when(error){
    is FileError -> "error is fileError ${error.filename}"
    is DatabaseError -> "error is databaseError ${error.database.name}"
    else -> throw IllegalArgumentException("error is unknown")
}

fun main() {
    println(getMassage(DatabaseError(Database.MYSQL)))
}

 

[Sealded 클래스 버전]

sealed class Error2{
    class FileError(val filename:String) : Error2()
    class DatabaseError(val database:Database) : Error2()
    class RedisError(val host:String): Error2()
}

enum class Database2{
    ORACLE, MYSQL, MARIADB;
}

fun getMassage2(error: Error2) =when(error){
    is Error2.DatabaseError -> "error is databaseError ${error.database.name}"
    is Error2.FileError -> "error is fileError ${error.filename}"
    is Error2.RedisError -> "error is redisError ${error.host}"
}

fun main() {
    println(getMassage2(Error2.RedisError("127.0.0.1")))
}

 

Sealed Class의 장점 : Error 클래스에 새로 구현체가 추가될 경우 When 절 코드에서 컴파일 에러로 잡히기 때문에 추가해야 되는 위치를 한번에 찾아 처리하기 편함

 

Private 처리를 통해 set, get 함수의 접근자를 변경할 수 있음.

class Account{
    var balance:Int = 0
        private set(value) {
            if (value < 0) throw IllegalArgumentException("value is wrong")
            else field = value
        }
    
    fun increaseBalance(amount : Int) {
        this.balance += amount
    }
    
    fun decreaseBalance(amount: Int) {
        this.balance -= amount
    }

    var accountName:String = ""
        get() = "계좌이름 : "+field
}

fun main() {
    val account = Account()
//    account.balance = 100
    account.increaseBalance(100)
    account.accountName = "급여계좌"

    println("${account.accountName} , 잔액 : ${account.balance}")
}

 

 

Data Class

데이터를 저장하는 클래스 타입을 처리할때 사용하며 Lombok 으로 @Data 선언하는거랑 유사함

 - 데이터 처리에 필요한 기본 기능을 자동으로 제공해줌 ( + toString, equalsOf, hashCode)

클래스 앞에 Data 키워드만 추가하면 됨

data class User(val name: String, val age: Int)

 

Object Class

자바에서의 싱글톤 패턴 적용을 원하는 경우 Object 키워드를 추가해주면 됨 

 - 싱글톤 또는 Util 클래스 정의할때 Object 클래스로 생성하면 더 편리함 

(익명 클래스에서도 Object 클래스 선언 가능함)

object NumberUtil {
    fun sum(a:Int, b:Int) = a+b
    fun multiply(a:Int, b:Int) = a*b
}

fun main() {
    println(NumberUtil.multiply(10 ,2))
}

 

단, Util 클래스에서 Statict 메소드만 존재하고 별도 인스턴스 생성이 필요없는 경우에도 사용할 수 있지만 파일에 최상위 함수로 선언해서 사용하는 것이 더 좋음

 

Compainon Object : 자바의 static method를 대체함

 - static으로 상수값을 저장하거나 팩토리 생성자를 만들때 사용하면 됨.

 

data class Child (val name:String, val age:Int){
    companion object {
        const val MAX_CHILDREN = 4

        fun ofDefaultAge(subName:String):Child{
            return Child(subName, 0)
        }
    }
}

fun main() {
    println(Child.ofDefaultAge("Jinyoon"))
}

 

강좌 링크 : 

https://www.inflearn.com/course/%EC%BD%94%ED%8B%80%EB%A6%B0-%EB%AC%B8%EB%B2%95-%EC%8B%A4%EB%AC%B4-%EC%9E%90%EB%B0%94to%EC%BD%94%ED%8B%80%EB%A6%B0/dashboard

 

코틀린 문법부터 실무까지 (자바 to 코틀린 실무) 강의 | 양세열 - 인프런

양세열 | 이 강의를 통해 Kotlin 문법을 학습하고 자바 프로젝트를 코틀린으로 안전하게 전환하는 방법을 배울 수 있습니다. 코틀린을 실무 코드 기반으로 배우고 직접 활용해서 장점을 누려보세

www.inflearn.com

 

반응형