References: Do it! 코틀린 프로그래밍

코틀린에서의 함수 호출방법에 대해 알아봅니다.

 

 

1. 값에 의한 호출

 

값에 의한 호출로 람다식이 전달되는 경우 람다함수는 값으로 처리되어 바로 람다함수가 수행되며 그 후 결과 값을 전달합니다.

다음 예시를 통해 확인해 보겠습니다.

fun callByValue(b: Boolean): Boolean{
    println("Called callByValue function.")
    return b
}
val lambdaFunc: ()->Boolean = {
    println("Called lambdaFunc function")
    true
}
fun main() {
    val result = callByValue(lambdaFunc())
    println(result)
}

callByValue(lambdaFunc()) > 람다함수는 값으로 처리되어 즉시 수행됨. 결과값 true를 리턴.

callByValue(lambdaFunc()) > 람다함수가 실행된 결과값인 true를 인자로 함수가 실행됨.

 

위에 설명한 순서대로라면 결과값은 다음과 같이 출력됩니다.

 

 

2. 이름에 의한 호출

 

람다함수를 이름으로만 호출해보겠습니다.

코드를 다음과 같이 변경해줍니다.

fun callByName(b: ()->Boolean): Boolean{
    println("Called callByName function.")
    return b()
}
val lambdaFunc: ()->Boolean = {
    println("Called lambdaFunc function")
    true
}
fun main() {
    val result = callByName(lambdaFunc)
    println(result)
}

fun callByValue(b: ()->Boolean): Boolean > 람다식을 이름으로만 호출하기 위해 자료형을 람다식으로 변경해줍니다.

return b() > 람다식을 리턴합니다.

 

실행순서는 값에 의한 호출과 다릅니다.

A. callByValue(lambdaFunc) > 람다함수가 저장된 변수가 함수의 인자로 전달됩니다.

B. fun callByValue(b: ()->Boolean): Boolean > 인수를 받아온 함수가 실행됩니다.

C. return b() > 인수가 리턴됩니다.

D. val lambdaFunc: ()->Boolean > 리턴된 인수안에 담겨있는 람다함수가 실행됩니다.

E. true > 람다함수의 결과값인 true가 리턴됩니다.

 

위에 설명한 순서대로라면 결과값은 다음과 같게 출력됩니다.

 

 

 

반응형

'Programming' 카테고리의 다른 글

[Kotlin] 14. 인라인 함수  (0) 2019.08.23
[Kotlin] 13. 익명함수  (0) 2019.08.23
[Kotlin] 11. 람다식  (0) 2019.08.22
[Kotlin] 10. 함수의 매개변수  (0) 2019.08.21
[Kotlin] 09. 함수의 선언과 간략화  (0) 2019.08.21

References: Do it! 코틀린 프로그래밍

코틀린에서의 람다식과 사용예시에 대해 알아봅니다.

 

 

1. 람다식

 

람다식은 람다 대수에서 유래했다고 합니다. 람다식의 예시는 다음과 같습니다.

{ x, y -> x + y }

예시를 보면 함수에 이름이 없고 화살표가 사용되었습니다. 

수학에서의 람다 대수는 이름없는 함수로 2개 이상의 입력을 하나의 값으로 단순화 한다는 개념입니다.

프로그래밍에서도 유사하게 1. 함수의 이름이 없으며 2. 여러 입력값을 하나로 리턴합니다.

 

 

2. 람다함수의 사용예시.

 

실제로 람다 함수는 다음과 같이 사용됩니다.

 

fun sumFun(sum: (Int, Int) -> Int, a: Int, b: Int): Int{
    return sum(a, b)
}

fun main(){
    println(sumFun( { x, y -> x + y }, 1, 2))
}

 

fun sumFun(sum: (Int, Int) -> Int, a: Int, b: Int): Int > 람다식으로 사용할 매개변수 이름.

fun sumFun(sum: (Int, Int) -> Int, a: Int, b: Int): Int > 자료형을 람다식으로 선언.

return sum(a, b) > 매개변수로 선언한 람다식을 사용

sumFun( { x, y -> x + y }, 1, 2) 인자로 람다식을 전달

 

람다식을 변수에 저장하여 사용할 수도 있습니다.

fun main(){
    var result: Int
    val multi = { a: Int, b: Int -> a * b }
    result = mulit(2,4)
}

multi를 보면 변수에 람다식을 저장해 함수처럼 사용할 수 있는것을 알 수 있습니다.

 

val multi = { a: Int, b: Int -> a * b }

 

위 식을 좀 더 자세히 써보겠습니다.

 

val multi: (Int, Int) -> Int = { a: Int, b: Int -> a * b }

 

람다식 자료형을 모두 표현한 식입니다.

이 식은 다시 다음과 같이 나타낼 수 있습니다.

 

val multi: (Int, Int) -> Int = { a, b -> a * b }

 

첫 예시는 변수선언의 람다식 자료형을 생략한 표현이고 위의 예시는 람다식내의 매개변수 자료형을 생략한 표현입니다.

물론 두개 다 생략하면 코틀린은 더이상 자료형을 추론할 수 없어 오류가 발생합니다.

 

매개변수가 없는 람다식 표현의 경우 다음과 같이 작성하면 됩니다.

 

val printHello: () -> Unit = { println("Hello") } //OR val printHello = { println("Hello") }

 

반응형

'Programming' 카테고리의 다른 글

[Kotlin] 13. 익명함수  (0) 2019.08.23
[Kotlin] 12. 함수 호출 방법  (0) 2019.08.22
[Kotlin] 10. 함수의 매개변수  (0) 2019.08.21
[Kotlin] 09. 함수의 선언과 간략화  (0) 2019.08.21
[Kotlin] 08. 스마트 캐스트  (0) 2019.08.21

References: Do it! 코틀린 프로그래밍

함수의 매개변수에 대해 알아봅니다.

 

 

1. 매개변수의 기본값 설정하기.

 

함수 매개변수의 기본값은 다음과 같이 설정합니다.

 

fun addUser(id: String, email: string = "Default"){
    ...
}

 

이후 기본값이 있는 매개변수는 함수 호출시 인자를 넘겨주지 않아도 됩니다.

addUser("smoh", "seungmuk92@gmail.com") //OK
addUser("smoh") //OK

 

 

2. 매개변수를 이름과 함께 호출하기.

 

만약 함수의 매개변수가 많아지면 어떤 인자를 어떤 매개변수에 전달해야하는지 헷갈릴 수 있습니다.

그럴때를 위해 코틀린은 매개변수의 이름과 함께 인자를 전달할 수 있는 방법을 제공합니다.

fun connect(host: String = "localhost", port: int = 1521, user: String = "Default", password: String){
    ...
}

위와 같이 매개변수가 많은 함수가 있을 때 다음과 같이 호출할 수 있습니다.

connect(host = "127.0.0.1", password = "12345") //OK, 나머지 값은 기본값 사용.
connect(password = "12345") //OK, 나머지 값은 기본값 사용.

단, 기본값이 지정되지않은 password는 항상 전달해야 합니다.

 

 

3. 매개변수가 고정되지 않은 함수.

 

매개변수의 갯수가 고정되지 않은 함수는 가변인자(Variable Argument)를 사용하면 만들 수 있습니다.

가변인자를 이용한 함수는 다음과 같이 정의합니다.

fun printArgs(vararg words: String){
    for(word in words) { print("$word ") }
    print("\n")
}

이후 해당 함수는 다음과 같이 호출 할 수 있습니다.

printArgs("Hello", "World", "!") //OK
printArgs("Hello", "World", "&", "Hello", "Kotlin") //OK

 

 

 

 

반응형

'Programming' 카테고리의 다른 글

[Kotlin] 12. 함수 호출 방법  (0) 2019.08.22
[Kotlin] 11. 람다식  (0) 2019.08.22
[Kotlin] 09. 함수의 선언과 간략화  (0) 2019.08.21
[Kotlin] 08. 스마트 캐스트  (0) 2019.08.21
[Kotlin] 07. Null 허용 및 NPE 검사  (0) 2019.08.21

References: Do it! 코틀린 프로그래밍

함수의 선언방법과 간략히 하는 방법에 대해서 알아봅니다.

 

 

1. 함수의 선언

 

함수는 다음과 같이 선언합니다.

fun sum(a: Int, b: Int): Int {
    var result = a + b;
    return result;
}

fun sum(a: Int, b: Int): Int > 함수의 이름을 의미합니다.

fun sum(a: Int, b: Int): Int > 함수의 매개변수를 의미합니다.

fun sum(a: Int, b: Int): Int > 함수의 반환 타입을 의미합니다.

 

만약 반환형이 없는 만들고 싶으면 fun voidFunc(): Unit 처럼 "Unit"를 사용하거나 자료형을 생략하면 됩니다.

코틀린은 함수의 반환형이 없으면 자동으로 Unit로 추론합니다.

 

** void함수와 Unit를 반환하는 함수는 다릅니다.

** void함수는 아무것도 반환하지 않지만 Unit함수는 Unit 객체를 반환합니다.

 

 

2. 함수 간략화

 

코틀린에서 지원하는 함수를 간략하게 하는법을 알아봅니다.

 

위의 예시함수는 아래와 같이 간략하게 만들 수 있습니다.

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

다른 언어에서도 위와같은 방법으로 많이 사용합니다.

코틀린에선 중괄호 내의 코드가 한줄이면 중괄호와 return을 생략 할 수 있습니다.

fun sum(a: Int, b: Int): Int = a + b

여기서 우리가 만든 sum 함수는 Int + Int = Int이므로 함수의 반환형인 Int도 생략할 수 있습니다.

fun sum(a: Int, b: Int) = a + b

 

 

반응형

'Programming' 카테고리의 다른 글

[Kotlin] 11. 람다식  (0) 2019.08.22
[Kotlin] 10. 함수의 매개변수  (0) 2019.08.21
[Kotlin] 08. 스마트 캐스트  (0) 2019.08.21
[Kotlin] 07. Null 허용 및 NPE 검사  (0) 2019.08.21
[Kotlin] 06. 자료형 String  (0) 2019.08.21

References: Do it! 코틀린 프로그래밍

자료형의 스마트 캐스트와 Any및 as에 대해 알아봅니다.

 

 

1. 스마트 캐스트

 

스마트 캐스트란 어떤 값이 정수일수도 있고 실수일수도 있는 상황에서 컴파일러가 자동으로 형을 변환해주는 기능을 말합니다.

말한 예시대로 정수와 실수 즉 숫자형에 대한 스마트 캐스트가 적용되는 자료형은 Number가 있습니다.

var num: Number = 11.1

위의 코드에서 num은 스마트 캐스트에 의해 자동으로 Float형이 됩니다.

 

 

2. Any

 

자료형을 결정하지 않은채 변수를 선언하고 싶을땐 Any형을 사용하면 됩니다.

val value: Any
value = "Hello kotlin!"
if(value is String) { println(value) }

value를 선언 할 때 Any라는 타입으로 변수가 선언됩니다.

이후 "Hello kotlin!"이 할당된 후(아직 자료형은 Any로 남아있습니다) is를 통해 형을 검사할때 자동으로 String으로 스마트 캐스팅 되어 조건문을 수행하게 됩니다.

 

Any는 모든 자료형 클래스의 최상단에 위치합니다. 다시말하면 코틀린의 모든 클래스는 Any라는 슈퍼 클래스를 가집니다.

이로인해 Any를 사용한 자료형은 묵시적 변환에 의해 어떤 자료형으로든 변경될 수 있습니다. 따라서 아래와 같이 구분할 수도 있습니다.

fun checkArgType(input: Any)
    if(input is String) { println("input value is String") }
    if(input is Int) { println("input value is Int)}
}

 

 

3. as

 

as를 사용해 스마트 캐스트를 수행 할 수도 있습니다. 

단, as는 형변환이 가능하지 않으면 예외를 발생시킵니다.

val str2: String = str1 as String

위의 코드는 str1이 null이 아니면 String으로 형변환 되어 str2에 할당하는 코드입니다.

만약 str1이 null이면 형변환이 불가능하므로 예외를 발생합니다. 따라서 위의 코드는 다음과 같이 바꿔 수행하는게 더 안전합니다.

val str2: String? = str1 as? String
반응형

References: Do it! 코틀린 프로그래밍

Null을 허용하는 변수 설정과 NullPointerException(NPE)을 방지하는 법에 대해 알아봅니다

 

 

1. Null을 허용하는 변수 설정하기.

 

코틀린에서는 기본적으로 변수에 NULL값을 허용하지 않습니다.

위와 같이 String 타입 변수에 null을 넣으려고 하면 에러를 반환합니다.

 

직접 null을 할당하려면 다음과 같이 해야합니다.

1. 명시적으로 자료형을 선언해줘야 하며

2. NULL을 허용한다는 의미로 ? 를 자료형 뒤에 입력해줘야 합니다.

 

 

2. 세이프 콜

 

위의 예시에서 value의 길이를 구해봅시다.

세이프 콜 (?.)이나 non-null(!!.)만 허용한다는 에러를 확인할 수 있습니다.

 

세이프 콜이란 null이 할당되어 있을 가능성이 있는 변수를 검사하여 안전하게 호출하도록 도와주는 기법입니다.

이번엔 세이프콜을 적용하여 길이를 구해봅시다.

fun main() {
    var value:String? = "Hello kotlin"
    value = null;
    var length = value?.length
    println(length);
}

실행하면 결과가 어떻게 나오나요? 

세이프 콜은 변수를 검사 한후 값이 있으면 길이를, 값이 null이면 null을 출력합니다. 위의 코드를 실행하면 null이 출력될 것 입니다.

 

 

3. non-null 단정 기호.

 

이번엔 non-null 단정기호인 !!.을 사용해 봅시다.

코드를 아래와 같이 수정한 후 실행해봅니다.

fun main() {
    var value:String? = "Hello kotlin"
    value = null;
    var length = value!!.length
    println(length);
}

물론 컴파일러에서 에러를 보여주긴 합니다.

그러나 정상적으로 컴파일은 되고 실행하면 NPE가 발생합니다.

Exception in thread "main" kotlin.KotlinNullPointerException

 

 

4. 조건문을 활용한 변수 검사

 

기본적으로 조건문을 사용해 변수의 null 가능성을 검사할 수도 있습니다.

fun main() {
    var value:String? = "Hello kotlin"
    value = null;
    val length = if(value != null) value.length else -1
    println(length);
}

 

 

5. 세이프 콜과 엘비스(Elvis)를 사용한 변수 검사.

 

세이프 콜(?.)과 엘비스 연산자(?:)를 함께 사용하면 좀 더 안전하게 변수를 검사할 수 있습니다.

fun main() {
    var value:String? = "Hello kotlin"
    value = null;
    val length = value?.length ?: -1
    println(length);
}

결과값은 -1을 출력합니다.

 

"value?.length ?: -1"에 대해 알아봅시다.

세이프 콜인 value?.length는 위에서 살펴본 바와 같이 null을 리턴합니다.

엘비스 연산자는 좌측의 값이 null이면 우측의 값을 리턴하고, null이 아니면 좌측의 값을 그대로 리턴합니다.

따라서 null ?: -1이므로 -1을 리턴하게 됩니다.

 

 

반응형

References: Do it! 코틀린 프로그래밍

String 자료형에 대해 알아봅니다.

 

 

1. 문자열 자료형 선언과 저장 방식.

 

문자열을 다음과 같이 선언 합니다.

var str1: String = "Hello"
var str2 = "World"
var str3 = "Hello"

위와같이 문자열을 선언 하면 실제 메모리엔 다음과 같이 저장됩니다.

Heap: StringPool[ (A1:Hello), (A2:World) ]

Stack: Address[ (A1: str1), (A2: str2), (A1: str3) ]

실제 str1과 str3이 바라보는 곳은 A1입니다.

따라서 str1 == str3의 결과는 true를 반환합니다.

 

 

2. 문자열 출력하기

 

변수의 값을 문자열에 입력하기 위해 $ 기호를 사용할 수 있습니다.

다음 코드를 봅시다.

var value = 100
var str = "value is $value"

str 변수를 초기화 하고 value의 값을 사용합니다. 따라서 str은 "value is 100"으로 초기화 됩니다.

 

만약 변수를 그대로 사용하지 않고 계산 결과를 입력하고 싶으면 어떻게 해야할까요

표현식을 사용하면 가능합니다. 표현식은 중괄호({})를 통해 사용할 수 있습니다.

다음 코드를 확인해 봅시다.

var value = 100
var str1 = "value is $value"
var str2 = "value +10 is ${value +10}"

위의 코드에서 ${value +10}은 실제 value인 100에 10을 더한값. 즉 110이 할당됩니다.

따라서 str2는 "value +10 is 110"으로 초기화됩니다.

 

반응형

References: Do it! 코틀린 프로그래밍

변수 선언 키워드와 자료형 그리고 자료형 추론에 대해 알아봅니다.

 

1. 변수 선언 키워드: val과 var

 

변수는 val과 var 키워드를 이용하여 선언합니다. 

val: 최초로 지정한 변수의 값으로 초기화 하며 더이상 수정할 수 없습니다.
var: 초깃값이 있더라도 이후에 값을 변경할 수 있습니다.

val은 const와 같은 개념으로 보입니다.

 

 

2. 자료형을 이용한 변수의 선언

 

변수는 다음과 같이 선언합니다.

val name: String = "smoh"

"val"은 변수 선언 키워드입니다.

"name"은 변수의 이름입니다.

"String"은 변수의 자료형입니다.

"smoh"는 변수의 값 입니다.

 

 

3. 변수의 자료형 추론

 

자료형을 명시적으로 지정하지 않으면 코틀린이 초기화 한 값을 바탕으로 자료형을 자동으로 찾아줍니다.

val name = "smoh"

위와 같은 선언의 경우 코틀린이 "smoh"를 바탕으로 name의 자료형을 추론해 String으로 결정합니다.

crtl+shift+P를 누르면 해당 변수를 어떤 자료형으로 추론하였는지 확인할 수 있습니다.

 

단, 다음과 같은 경우는 허용되지 않습니다.

var name

위와 같은 경우는 추론해야할 대상이 존재하지 않으므로 자료형 추론이 불가능하며 에러를 발생시킵니다.

 

 

4. 자료형 검사

 

변수의 자료형을 알아내려면 is 키워드를 사용하면 됩니다.

fun main() {
    var num = 123
    if(num is Int) { println("num is int!") }
    if(num !is Int) { println("num is not Int!") }
}

결과값은 "num is Int!"를 출력합니다.

반응형

References: Do it! 코틀린 프로그래밍

사용자가 직접 정의한 클래스를 사용하는 방법을 알아봅니다.

 

 

1. 클래스정의.

 

프로젝트를 생성 후 패키지를 만듭니다. 그리고 그 아래 클래스를 하나 만들어 둡니다.

defineMyClass 패키지 아래 Person 클래스를 생성하였습니다.

이 클래스를 다른 패키지인 useMyClass의 메인함수에서 사용해 보겠습니다.

 

 

2. 클래스 사용하기.

 

Main.kt에 다음과 같이 코딩합니다.

package useMyClass

import defineMyClass.Person

fun main() {
    val user1 = Person("smoh", 28);
    println(user1.name)
    println(user1.age)
}

이후 실행하면 다음과 같은 결과값을 확인할 수 있습니다.

그런데 만약 useMyClass 패키지내에 Person클래스가 이미 존재하면 어떻게 쓸 수 있을까요 ?

이를 테스트하기 위해 useMyClass에도 Person클래스를 추가합니다. 그리고 다음과 같이 코딩합니다.

(** 물론 입력하는 자료형리 다르다면 똑똑한 컴파일러는 알아서 구분해주긴 합니다.)

package useMyClass
class Person(val name: String, val dob: Int)

이제 main에서 같은 패키지 내의 Person클래스를 사용해 보려고 합니다.

뭔가 문제가 생긴것을 볼 수 있습니다. user2는 같은 패키지 안에 있는 Person을 사용하고 싶은데 자동으로 defineMyClass 안에 있는 Person을 생성해 버립니다.

 

이 둘을 구분하여 쓰기 위해 "as"를 사용해 별명을 붙입니다.

코드를 다음과 같이 수정합니다.

as를 사용해 별도로 이름을 지정해 주면 같은 이름의 클래스를 구분할 수 있습니다.

 

 

 

반응형

References: Do it! 코틀린 프로그래밍

코틀린에서의 모듈, 패키지, 클래스 개념에 대해 알아봅니다.

 

 

1. 프로젝트.

 

최상단 개념입니다. 프로젝트는 모듈, 패키지, 클래스를 포함합니다.

한 프로젝트는 여러 모듈을 가질 수 있습니다.

처음 프로젝트 생성 후 좌측 리스트를 보면 기본적으로 프로젝트 단위로 구성을 볼 수 있습니다.

 

 

2. 모듈

 

프로젝트 바로 아래의 개념입니다. 모듈은 패키지와 클래스를 포함합니다.

한 모듈은 여러 패키지를 가질 수 있습니다.

프로젝트 우클릭 후 New > Module을 클릭해 모듈을 프로젝트 아래에 추가할 수 있습니다.

프로젝트 아래에 모듈이 추가된 것을 확인할 수 있습니다.

보기단위를 패키지로 바꾸면 좀 더 명확하게 구분지어 볼 수 있습니다.

 

 

3. 패키지

 

모듈의 아래 개념입니다.

기본(default)패키지와 사용자가 명명한 패키지가 있습니다.

이름이 별도로 지정되지 않은 파일의 경우 모두 기본 패키지에 속하게 됩니다.

모듈 아래 자동 생성된 src에 우클릭 후 New > Package를 클릭하면 패키지를 만들 수 있습니다.

 

패키지의 이름은 보통 사이트 이름을 뒤집어서 많이 씁니다. 

예를 들어 sub.domain.com인 경우 패키지 이름은 com.domain.sub이런식으로 짓습니다.

그리고 필요에따라 기능별로 뒤에 단어를 추가합니다.

예를 들어 com.domain.sub.do.somthing이런식으로 구분하면 관리가 좀 더 용이해집니다.

 

예시로 다음과 같이 패키지를 생성했습니다.

com.exampple.ktmodule패키지를 생성 후 실제 폴더가 어떻게 생성되었는지 확인해 봅시다.

[.]단위로 구분되어어 src 아래에 com\example\ktmodule폴더 순서로 생성된 것을 확인할 수 있습니다.

 

 

3-1. 기본패키지

 

기본패키지란 코틀린 프로그래밍에서 자주 사용되는 클래스와 함수등을 미리 만들어 둔 것으로 import를 별도로 사용하지 않아도 바로 사용할 수 있는것들 입니다.

 

기본패키지는 kotlin-stdlib-sources.jar에 위치합니다.

IntelliJ에서 String을 정의 한 후 String에 ctrl + B를 누르면 확인할 수 있습니다.

기본패키지는 다음과 같습니다.

kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.io.*
kotlin.ramges.*
kotlin.sequences.*
kotlin.text.*

 

4. 클래스

 

패키지 아래에 위치한 개념이며 실제 코틀린 파일에 정의됩니다.

위에 만든 패키지를 우클릭 해 새 코틀린 파일을 생성합니다.

 

생성한 코틀린 파일엔 다음과 같은 코드가 자동으로 입력된 것을 확인할 수 있습니다.

package com.example.ktmodule

 

이전에 만든 "HelloKotlin.kt" 파일을보면 차이가 있습니다.

fun main() {
    println("Hello kotlin!")
}

package의 유무를 확인할 수 있습니다.

package가 명시적으로 입력되지 않은 모든 파일(클래스)는 기본(default) 패키지에 속하게 됩니다.

실제 코틀린 파일의 위치를 보면 "HelloKotlin.kt"는 패키지 아래에 위치하지 않음을 확인할 수 있습니다.

 

같은 패키지 내에서 모든 클래스의 이름은 유일해야 합니다.

다르게 말하면 패키지가 다르면 같은 이름의 클래스 이름을 사용할 수 있습니다.

 

테스트를 해보겠습니다.

KotlinModule에 기본패키지에 코틀린 파일을 하나 추가합니다.

그 후 두 코틀린 파일에 다음과 같이 클래스를 정의해 봅시다.

class person(val name: String, val age: Int)

 

별 문제 없이 정의가 됩니다. 이제 package com.example.ktmodule를 주석처리 해 봅시다.

오류가 발생한 것을 확인할 수 있습니다.

 

"KotlinModuleClass.kt"파일의 Person 클래스를 KotlinModuleClass로 변경해 봅시다.

package com.example.ktmodule
class KotlinModuleClass(val name: String, val age: Int)

파일명과 클래스명이 같으면 해당 파일의 확장자가 숨김처리 됩니다.

Person 클래스를 갖고있는 DefaultKotlinClass는 .kt 확장자가 그대로 남아있음을 확인할 수 있습니다.

반응형

+ Recent posts