반응형

 

3. 아이템 상세 화면 ItemActivity

리사이클러뷰에서 제공하는 아이템뷰는 보통 사이즈를 작게 만들기에 주요정보만 제공하는 편입니다. 그렇기에 아이템뷰를 클릭했을 때 아이템의 상세정보를 볼 수 있는 별도의 상세화면을 사용합니다. 

이번에는 아이템의 정보를 보다 상세하게 볼 수 있는 상세화면 액티비티를 만들어 보겠습니다. 액티비티의 이름은 ItemActivity 라고 명명하겠습니다.

 

 

 

이번에는 EdgeToEdge UI 를 그대로 두고 구현하겠습니다.

상세화면 구현 4단계
1) MainActivity 에서 아이템뷰 클릭했을 때 ItemActivity 로 전환하는 코드 작성 ( 아이템 정보 전달 및 확인 )
2) 상세화면 레이아웃 xml 파일 작성
3) ItemActivity 에서 아이템 정보를 보여주기
4) 액티비티 전환 효과 구현

 

1) MainActivity 에서 아이템뷰를 클릭했을 때 상세화면(ItemActivity)로 전환하기

 

상세화면으로 전환되는 이벤트는 아이템뷰를 클릭했을 때 입니다. 하여 아이템뷰에 클릭이벤트를 처리하는 리스너를 설정하도록 하겠습니다. 아이템뷰는 보이기에는 MainActivity 화면에 있지만 실제 만들어지는 위치는 MyAdapter 클래스 내부 입니다. 그래서 아이템뷰의 클릭이벤트 처리 역시 MyAdapter 클래스 안에서 설정해야 합니다. 

아이템뷰의 클릭이벤트 처리는 ViewHolder에서 하거나 onBindViewHolder()에서 처리가 가능합니다. 보통 아이템 데이터를 참조하고 있는 onBindViewHolder() 메소드 안에서 처리하는 경우가 더 많습니다. 클릭이벤트는 람다표현보다 간결한 SAM 변환으로 처리를 해보겠습니다. 또한 ItemActivity 상세화면에서 Item 데이터를 보여줘야 하기에 Intent 객체에게 Extra 데이터로 전달하도록 하겠습니다.

//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
//## RecyclerView.Adapter 상속 ##
class MyAdapter constructor(val context: Context, val items:MutableList<Item>) : RecyclerView.Adapter<MyAdapter.VH>(){

    .........

    //3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
    override fun onBindViewHolder(holder: VH, position: Int) {

        val item= items.get(position)  //현재 아이템뷰에 보여줄 Item 데이터 얻어오기

        holder.tvTitle.setText(item.title)  // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
        holder.tvMsg.text= item.msg         // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정

        //holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
        // 이미지 로딩 라이브러리 Glide 로 이미지 설정
        Glide.with(context).load(item.img).into(holder.iv)
        
        //---------------------------------------------------------

        //SAM conversion 이용하여 이벤트처리
        holder.itemView.setOnClickListener{ v ->

            // ItemActivity 를 실행하기 위한 Intent 객체 생성
            val intent= Intent(context, ItemActivity::class.java)
            
            // 액티비티 전환할 때 Item 데이터 전달을 위해 Intent 에 Extra 데이터 추가
            intent.putExtra("title", item.title)
            intent.putExtra("msg", item.msg)
            intent.putExtra("img", item.img)
            
            // 위 인텐트 객체를 이용하여 ItemActivity 실행
            context.startActivity(intent)

        }
        
    }//onBindViewHolder method....

}//MyAdapter class..

 

상세화면 ItemActivity 에 전달된 Item 데이터(제목, 메세지, 이미지)를 받아 변수에 저장해 놓겠습니다.

class ItemActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_item)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        //넘어온 Intent 객체가 이미 이 액티비티의 property로 존재함
        val title= intent.getStringExtra("title")
        val msg= intent.getStringExtra("msg")
        val img= intent.getIntExtra("img", R.drawable.ic_launcher_foreground) //전달받은 이미지가 없다면 기본 앱아이콘 이미지 보여주기
        
    }//onCreate method..
    
}//ItemActivity class....

 

받아온 Item 데이터를 보여주는 뷰들이 필요하겠네요. 글씨 2개(제목, 메세지)와 이미지 1개(도시 이미지)를 보여주는 레이아웃 파일을 만들어 보겠습니다.

 

 

2) 상세화면 레아이웃 xml 파일 만들기

 

Item 데이터(제목, 메세지, 도시 이미지)를 보여주는기 위해 뷰들을 배치하겠습니다. 툴바에는 제목, 이미지뷰에는 도시 이미지를, 메세지는 이미지뷰 아래 텍스트 뷰를 통해 보여주도록 하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ItemActivity">
    
    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/design_default_color_primary"
        app:title="아이템 제목"
        app:titleTextColor="@color/white"
        app:navigationIcon="@drawable/baseline_arrow_back_24"/>

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:contentDescription="city image"/>

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:padding="8dp"
        android:text="This is message."/>

</LinearLayout>

 

 

3) 상세화면(ItemActivity)에서 아이템 정보 보여주기

 

레이아웃 xml 파일에서 만든 뷰 3개(툴바, 이미지뷰, 텍스트뷰)에 대한 참조변수를 먼저 만들어줍니다. 코틀린은 프로퍼티(멤버변수)를 만들 때 반드시 초기화를 해야 하기에 by lazy 를 이용하여 늦은 초기화로 수행했습니다. 또한 상세화면에서 메인화면으로 다시 돌아가기 위해 제목줄(툴바)에 네비게이션 아이콘 버튼을 통해 뒤로 돌아가도록 하였습니다.

import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.bumptech.glide.Glide
import com.google.android.material.appbar.MaterialToolbar

class ItemActivity : AppCompatActivity() {

    // 뷰들의 참조변수 3개 준비 - 참조변수의 참조값을 변경할 상황이 거의 없기에 val 키워드로 선언.
    // 초기값을 나중에 수행되도록 늦은 초기화 기법으로 by lazy 사용
    val toolbar: MaterialToolbar by lazy { findViewById(R.id.toolbar) }
    val iv: ImageView by lazy { findViewById(R.id.iv) }

    //tv변수를 만들때 명시적으로 자료형이 주어지지 않으면 대입되는 자료형의 값에 따라 추론처리되는데 findViewById()는 어떤 뷰 객체인지 추론이 불가하기에 <>제네릭으로 타입을 지정하면서 대입해줘야 함
    val tv by lazy { findViewById<TextView>(R.id.tv) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_item)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        //넘어온 Intent 객체가 이미 이 액티비티의 property로 존재함
        val title= intent.getStringExtra("title")
        val msg= intent.getStringExtra("msg")
        val img= intent.getIntExtra("img", R.drawable.ic_launcher_foreground) //전달받은 이미지가 없다면 기본 앱아이콘 이미지 보여주기

        // ---- Item 데이터(제목, 메세지, 도시 이미지)를 뷰들에 설정하기 -------------------

        // 툴바에 제목 title 표시
        toolbar.title= title

        // 텍스트뷰에 메세지 msg 표시
        tv.text= msg

        // 이미지뷰에 도시 이미지 img 표시 - 이미지 로드 라이브러리 Glide 사용
        Glide.with(this).load(img).into(iv)
        // ----------------------------------------------------------------------
        
        // 툴바(제목줄)에 뒤로가기 아이콘을 클릭했을 때 이전 화면(MainActivity)로 돌아가기. [ 뒤로가기 디스패처에게 뒤로가기 동작 실행 요청 ]
        toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }


    }//onCreate method..

}//ItemActivity class....

 

여기까지만 제작해도 충분히 상세화면을 보여주는 목적은 달성한 코드입니다. 다만, 사용자에게 앱의 사용에 대한 몰입감을 주는 UX를 구현하기 위해 액티비티를 전환할 때 전환효과를 주도록 하겠습니다.

 

4) 액티비티 전환할 때 전환 효과 주기

 

전환효과는 메인화면의 작은 아이템뷰에 있던 도시 이미지가 확대되면서 상세화면의 큰 이미지뷰로 연결되는 듯한 효과를 줄 겁니다.

 

전환효과를 적용하려면 이전화면의 특정 뷰와 다음 화면의 특정 뷰에 같은 이름의 '별명'을 지정해주고 '장면전환 SceneTransition'객체를 만들어 액티비티를 전환할 때 옵션으로 지정하면 적용됩니다.

 

4.1 먼저, 아이템뷰가 클릭되었을 때 화면이 전환되므로 아이템뷰가 있는 MyAdapter 클래스 안에서 기존 액티비티 전환코드를 수정하겠습니다. 장면전환 효과 SceneTransitionAnimation 객체를 만들면서 아이템뷰에 있는 도시 사진을 보여주는 이미지뷰에 '별명'을 지정하고 액티비티 옵션객체로 생성하여 인텐트로 ItemActivity를 실행시키는 startActivity()에 두번째 파라미터로 전달하면 전환효과를 적용할 수 있습니다. 주석으로 ~~ 표시 다음에 있는 코드가 새로 추가한 전환효과 코드 입니다. 

별명은 "img" 라는 이름으로 명명하였습니다.

import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide

//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
//## RecyclerView.Adapter 상속 ##
class MyAdapter constructor(val context: Context, val items:MutableList<Item>) : RecyclerView.Adapter<MyAdapter.VH>(){

    .........

    //3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
    override fun onBindViewHolder(holder: VH, position: Int) {

        val item= items.get(position)  //현재 아이템뷰에 보여줄 Item 데이터 얻어오기

        holder.tvTitle.setText(item.title)  // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
        holder.tvMsg.text= item.msg         // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정

        //holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
        // 이미지 로딩 라이브러리 Glide 로 이미지 설정
        Glide.with(context).load(item.img).into(holder.iv)

        //---------------------------------------------------------

        //SAM conversion 이용하여 이벤트처리
        holder.itemView.setOnClickListener{ v ->

            // ItemActivity 를 실행하기 위한 Intent 객체 생성
            val intent= Intent(context, ItemActivity::class.java)

            // 액티비티 전환할 때 Item 데이터 전달을 위해 Intent 에 Extra 데이터 추가
            intent.putExtra("title", item.title)
            intent.putExtra("msg", item.msg)
            intent.putExtra("img", item.img)

            // 위 인텐트 객체를 이용하여 ItemActivity 실행
            //context.startActivity(intent)

            //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            //액티비티의 전환시에 뷰들에 연결 효과주기.. ActivityOptions - SceneTransition   [ Pair()객체 - 전화효과를 줄 뷰에게 별칭을 연결.. 실행될 액티비티에서 연결된 뷰도 같은 별칭을 주면 자동으로 전환효과 보여짐 ] - ex. 당근마켓앱에 구현되어 있음.
            val optionsCompat:ActivityOptionsCompat= ActivityOptionsCompat.makeSceneTransitionAnimation(context as MainActivity, Pair(holder.iv, "img")) //context를 Activity로 형변환 - 코틀린에서 클래스들의 형변환 연산자 as
            context.startActivity(intent, optionsCompat.toBundle())

        }

    }//onBindViewHolder method....

}//MyAdapter class..

 

 

4.2 이제 새로 실행될 상세화면 ItemActivity 에서 전환효과로 연결될 아이템의 도시 사진을 보여주는 큰 이미지뷰에 이전에 지정했던 것과 같은 '별명'을 지정해주면 시스템이 같은 '별명 img' 를 가진 뷰와 이전 화면의 뷰 사이에 전화효과 애니메이션을 자동으로 구현시켜줍니다. 역시 주석 ~~ 표시 다음에 별명을 지정하는 한줄코드를 추가하겠습니다. 

class ItemActivity : AppCompatActivity() {

    // 뷰들의 참조변수 3개 준비 - 참조변수의 참조값을 변경할 상황이 거의 없기에 val 키워드로 선언.
    // 초기값을 나중에 수행되도록 늦은 초기화 기법으로 by lazy 사용
    val toolbar: MaterialToolbar by lazy { findViewById(R.id.toolbar) }
    val iv: ImageView by lazy { findViewById(R.id.iv) }

    //tv변수를 만들때 명시적으로 자료형이 주어지지 않으면 대입되는 자료형의 값에 따라 추론처리되는데 findViewById()는 어떤 뷰 객체인지 추론이 불가하기에 <>제네릭으로 타입을 지정하면서 대입해줘야 함
    val tv by lazy { findViewById<TextView>(R.id.tv) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_item)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        //넘어온 Intent 객체가 이미 이 액티비티의 property로 존재함
        val title= intent.getStringExtra("title")
        val msg= intent.getStringExtra("msg")
        val img= intent.getIntExtra("img", R.drawable.ic_launcher_foreground) //전달받은 이미지가 없다면 기본 앱아이콘 이미지 보여주기

        // ---- Item 데이터(제목, 메세지, 도시 이미지)를 뷰들에 설정하기 -------------------

        // 툴바에 제목 title 표시
        toolbar.title= title

        // 텍스트뷰에 메세지 msg 표시
        tv.text= msg

        // 이미지뷰에 도시 이미지 img 표시 - 이미지 로드 라이브러리 Glide 사용
        Glide.with(this).load(img).into(iv)
        // ----------------------------------------------------------------------

        // 툴바(제목줄)에 뒤로가기 아이콘을 클릭했을 때 이전 화면(MainActivity)로 돌아가기. [ 뒤로가기 디스패처에게 뒤로가기 동작 실행 요청 ]
        toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
        
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        
        //iv 이미지뷰에 전환효과 주기위에 별칭 부여 [ 이전 액티비티에서 ActivityOptions 에서 Pair()로 설정한 별칭과 같은 명칭
        iv.transitionName = "img"


    }//onCreate method..

}//ItemActivity class....

 

네. iv.transitionName="img"  이 한줄만으로 자동으로 전환효과가 적용됩니다. 참 쉽네요.

 

[ 전환효과 적용 전적용 후 ]

 

 

화면이 전활될 때 이미지가 자연스럽게 연결되는 느낌을 주어 사용자의 몰입감을 유지시켜주는 사용자경험을 제공합니다.

 


 

여기까지 해서 앱을 구현할 때 가장 많이 사용하는 액티비티 전환과 리사이클러뷰를 코틀린으로 만들어 보았습니다.

앞으로의 앱 개발에 대한 소개는 코틀린언어를 통해 소개하도록 하겠습니다. 모든 코틀린 문법을 소개한 것은 아니기에 앞으로 앱개발에 대한 기술을 소개하면서 조금씩 코틀린언어에 대해 익숙해지면서 못다한 내용도 소개하도록 하겠습니다.

반응형
반응형

 

2. 메인 화면 - 리사이클러뷰를 통해 대량의 데이터를 보여주는 화면 MainActivity

대부분의 앱에서 대량의 데이터를 보여주는 용도로 많이 구현하는 RecyclerView 를 코틀린언어를 사용하여 만들어 봄으로서 앞으로의 안드로이드 앱 개발은  코틀린을 이용한 앱개발로 자연스럽게 전환하겠습니다.

 

 

 리사이클러뷰를 사용하여 대량의 데이터를 보여주는 앱들의 UI [ 크몽, 여기어때, 오늘의 집 ]

리사이클러뷰를 사용하는 앱들의 UI

 

 

이번 글은 리사이클러뷰에 대한 학습이라기 보다 코틀린으로 구현하는 리사이클러뷰에 대한 내용이기에 위의 앱들과 같은 모양의 UI 구현은 너무 오래걸릴 듯 화여 최대한 간결하게 Image 1개, Text 2개를 가진 아이템뷰만 간단하게 만들어보겠습니다.

 

리사이클러뷰를 이용한 대량의 데이터를 보여주는 화면을 만들려면 여러 단계와 파일들을 만들어야 합니다. 그래서 초보자들의 경우에는 관려예제를 보고 따라 만드는 것 조차 쉽게 수행하지 못하는 경우가 많습니다. 그래서 필자의 경우에는 리사이클러뷰를 구현할 때 나름의 순서를 기준으로 하여 구현하는 편입니다. 정답은 아니지만 제가 선호하는 순서를 소개하고 그대로 만들어 보겠습니다. 혹시 리사이클러뷰 구현에 어려움을 겪고 있으시다면 이 방법을 써보시길 권해드립니다.

리사클러뷰 구현 단계
1) 액티비티 레이아웃 xml 파일에 RecyclerView 추가 및 액티비티에서 참조변수 선언
2) 대량의 데이터 준비( List , Data class )
3) 아이템뷰 1개의 레이아웃 xml 파일 작성
4) 대량의 데이터(List) 개수만큼 아이템뷰(아이템뷰 1개의 레이아웃 모양) 객체를 생성하는 아답터 클래스 설계
5) 아답터 객체 생성 및 리사이클러뷰에 아답터로 설정하여 완성

 

 

1) RecyclerView 추가 및 참조변수 만들기

 

레이아웃 파일은 간단하게 리사이클러뷰 1개만 배치하도록 하겠습니다.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

기본으로 만들어져 있는 MainActivity 클래스 파일을 보겠습니다. 역시 컴포즈 개발방법의 등장과 함께 도입된 화면 전체를 사용하는 Edge To Edge 와 Navigation Bar 사이즈를 얻어와서 패딩을 주는 기능이 작성되어 있습니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()    //화면 전체를 사용하는 기능
        setContentView(R.layout.activity_main)
        
        //Navigation Bar 의 사이즈만큼 액티비티 패딩 설정.
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }
}

 

MainActivity 역시 이전 IntroActivity 와 마찬가지로 리사이클러뷰의 사용에만 집중하기 위해 불필요한 EdgeToEdge 는 제거하고 시작하겠습니다. 

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)        
        setContentView(R.layout.activity_main) //화면에 보여줄 뷰로 레이아웃 xml 파일 설정
        
    }//onCreate method...
}//MainActivity class.....

 

이제 레이아웃 xml 파일에서 추가한 리사이클러뷰를 제어하기 위한 참조변수를 만들겠습니다. 이전에는 var 키워드를 사용해보았습니다. 

하지만 뷰 참조변수들은 한번 뷰객체를 참조하면 다른 뷰객체로 바꾸어 참조하는 경우가 거의 없기에 var보다 val을 사용해도 됩니다. 저는 이런 이유로 값변경이 불가능한 val 변수로 만드는 것을 선호합니다.
val 변수도 역시 초기화를 하지 않으면 에러입니다. 하지만 RecyclerView의 객체 참조값을 지금 초기화 할 수는 없습니다. 이전에서 소개했듯이 onCreate() 에서 findViewById()를 해야 하기에 나중에 해야 합니다. 그렇다고 nullable 변수로 만들면 null 값을 다른 참조값을 바꿀 수 없기에 사용할 수 없습니다. 그래서 늦은초기화 문법을 사용하겠습니다.

단, lateinit 은 var에만 사용할 수 있기에 by lazy 지연 초기화 문법으로 초기화를 진행하겠습니다. lazy 중괄호 { } 안의 작성 내용을 참조변수가 처음 사용될 때까지 좀 게으르게(lazy) 지연하여 초기화를 수행하기에 문제없이 RecyclerView 객체의 생성 이후 참조값을 얻어올 수 있습니다.

class MainActivity : AppCompatActivity() {

    // 뷰 참조변수들은 한번 뷰객체를 참조하면 다른 뷰객체로 바꾸어 참조하는 경우가 거의 없기에 var보다 val을 선호함
    // 이때, 초기화 안하면 에러 - 하지만 recycler의 객체 참조값을 지금 초기화 할 수 없음 [ onCreate() 에서 findViewById()를 해야 하기에..]
    // 그래서 늦은초기화 사용[ lateinit 은 var에만 사용할 수 있기에 by lazy 지연초기화 사용]
    val recycler: RecyclerView by lazy { findViewById(R.id.recycler) }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }//onCreate method...
}//MainActivity class.....

 

보다시피 한 줄에 참조변수 선언과 findViewById()를 작성할 수 있어서 lateinit var 늦은 초기화 보다 코드의 가독성이 좋아서 저는 더 선호하는 편입니다. 개발자 마다 뷰의 초기화 참조변수를 만드는 방법은 약간씩 차이가 있으니 어떤 것이 정답이라는 생각보다는 본인이 편하고 좋다고 생각하시는 방법으로 뷰 참조변수를 만드시길 바랍니다.


 

 

앞서 언급했듯이 가급적 간결하게 아이템뷰를 구성하고자 합니다. 여행지 정보를 보여주는 앱이라고 가정하고 아래 모양처럼 구성하고자 합니다. 아이템뷰 당 이미지 1개, 제목글씨 1개, 메세지 글씨 1개로 구성하고자 합니다.

 

 

2) 리사이클러뷰가 보여줄 대량의 데이터 준비

 

※ 여행지 이미지 파일 3개 : newyork.jpg, paris.jpg, sydney.jpg     

newyork.jpg
0.23MB
paris.jpg
0.19MB
sydney.jpg
0.10MB

 

실제 앱들의 이미지는 서버에 있는 이미지를 불어와서 보여주기에 프로젝트 폴더 안에 이미지파일이 존재하지 않지만 실습의 편의를 위해 위 3개의 사진파일을 res 폴더 > drawable 폴더안에 넣어놓고 리소스 ID 로 불러와서 보여주도록 하겠습니다.

 

 

다음으로 아이템뷰가 보여줄 아이템 1개( 이미지, 제목, 메세지 ) 데이터를 저장하기 위한 데이터 클래스를 하나 만들겠습니다. 클래스명은 단순하게 Item 이라고 명명하겠습니다. 

 

코틀린은 클래스 중에서 data class 라는 것이 있습니다. class 앞에 data 키워드를 추가하면 데이터 클래스로 만들어집니다. 이름 그대로 데이터를 저장하는 용도의 클래스를 만들 때 사용합니다. 

데이터 클래스 : 데이터만 저장하는 목적은 클래스
- 일반 class와 다르게 자동으로 equals()할때 객체 참조주소의 비교가 아니라 주생성자의 멤버변수를 비교해주는 특별한 클래스 
- 별도의 기능 메소드를 가지고 있지 않는 클래스 이기에 클래스의 설계를 위한 중괄호{} 영역을 작성하지 않음.

 

Android Studio data class 만들기

//데이터를 저장하는 용도의 클래스 ( 제목, 메세지, 이미지 파일 리소스 경로)
data class Item constructor(var title:String, var msg:String, var img:Int) //- 특별한 기능메소드를 작성할 필요없기에 보통은 {}도 생략함.

 

대량의 데이터는 보통 서버나 DB에서 불어와야 하지만 지금은 리사이클러뷰 구현에 집중하기 위해 테스트용으로 더미 데이터를 직접 작성하여 추가하도록 하겠습니다. 

리사이클러뷰를 보여주는 MainActivity에서 Item 데이터 객체를 여러개 보관할 리스트를 멤버변수로 만들고 더미데이터를 추가하도록 하겠습니다.

class MainActivity : AppCompatActivity() {

    // 리사이클러뷰 참조변수 - 늦은 초기화
    val recycler: RecyclerView by lazy { findViewById(R.id.recycler) }

    //대량의 데이터 property[속성:멤버변수]
    var items= mutableListOf<Item>() //Java의 ArrayList<Item>와 비슷하게 동작
    //var items= arrayListOf<Item>()  //이렇게 ArrayList 객체를 만들어도 됨

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //대량의 데이터들 추가 [테스트목적]
        items.add( Item("NEW YORK","Hello. Kotlin", R.drawable.newyork) )
        items.add( Item("PARIS","Nice to meet you", R.drawable.paris) )
        items.add( Item("SYDNEY","Have a good day", R.drawable.sydney) )
        items.add( Item("뉴욕","Do you have fun", R.drawable.newyork) )
        items.add( Item("파리","Nice to meet you", R.drawable.paris) )
        items.add( Item("시드니","Have a good day", R.drawable.sydney) )
        items.add( Item("new york","Do you have fun", R.drawable.newyork) )
        items.add( Item("paris","Nice to meet you", R.drawable.paris) )
        items.add( Item("sydney","Have a good day", R.drawable.sydney) )
        
    }
    
}

 

 

3) Item 데이터를 보여줄 아이템뷰의 1개의 모양 디자인을 위한 레이아웃 파일 만들기

 

Item 객체 1개의 데이터( 제목, 메세지, 이미지)를 보여줄 아이템뷰 1개의 모양 디자인을 위한 레이아웃 xml 파일을 만들어 보겠습니다.

 

[ 디자인 스케치 - 도형 ]             |             [ 구현된 아이템뷰 1개의 실제 모양 스크린샷 ]

 

recycler_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="160dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp"
    android:layout_margin="12dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@drawable/newyork"
            android:scaleType="centerCrop"/>
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TITLE"
            android:textStyle="bold"
            android:textSize="40sp"
            android:textColor="@color/white"
            android:layout_alignParentRight="true"
            android:layout_margin="16dp"/>
        <TextView
            android:id="@+id/tv_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is message"
            android:textColor="@color/white"
            android:textStyle="bold"
            android:textSize="18sp"
            android:layout_alignParentRight="true"
            android:layout_alignParentBottom="true"
            android:layout_margin="16dp"/>

    </RelativeLayout>

</com.google.android.material.card.MaterialCardView>

 

 

4) 리사이클러뷰가 보여줄 아이템 뷰를 리스트 개수만큼 객체로 만들어서 제공하는 아답터 객체 생성

 

이제 테스트 목적으로 직접 추가한 9개의 Item 데이터를 사용자가 볼 수 있는 아이템뷰 객체로 만들어주는 아답터 클래스를 설계해 보겠습니다. 아이템 1개의 모양을 설계한 recycler_item.xml 파일의 레이아웃 모양으로 아이템 뷰를 리스트 개수만큼 만들어서 리사이클러뷰에 제공하도록 구현하겠습니다.

리사이클러뷰를 구현할 때 가장 핵심적인 역할을 수행하는 클래스로서 RecyclerView.Adapter 클래스를 상속받아 구현합니다. 또한 상속을 받을 때 아이템뷰의 자식뷰들의 참조값을 저장하는 ViewHolder 클래스를 <>제네릭 타입으로 지정하여 만듭니다.

 

아탑터 클래스의 이름은 내가 만든 아답터라는 의미로 MyAdapter 로 명명해 보겠습니다.

 

MyAdapter 클래스를 만들때 2개의 프로퍼티(멤버변수)를 만들겠습니다.

- context : 아답터 클래스 안에서 운영체제의 기능들을 사용해야 하는 경우가 많기에 보통 Context 를 멤버변수(프로퍼티)로 준비함.

- items : 대량의 데이터를 아이템뷰객체로 만들어 주기에 대량의 데이터인 items:MutableList 멤버변수(프로퍼티)로 준비함.

 

코틀린은 멤버변수를 만들고 생성할 때 초기화를 시키고자 한다면 주 생성자(Primary Constructor)를 사용할 것을 권장합니다. 주 생성자의 파라미터를 만들때 var or val 키워드를 추가하면 멤버변수면서 매개변수가 되기에 코드가 훨씬 간결해 집니다.

MyAdapter.kt
import android.content.Context

//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
class MyAdapter constructor(val context: Context, val items:MutableList<Item>){
}

 

이제 RecyclerView를 보여주는 MainActivity 에서 MyAdapter 객체를 생성할 때 context와 대량의 Item 데이터인 items를 생성자 파라미터에 전달해 주면 됩니다.

 

앞에서 소개했듯이 아답터는 아이템뷰에 데이터인 Item의 값들을 설정해 주어야 하기에 아이템뷰의 자식뷰들을 참조하여 저장하는 뷰홀더클래스를 이너클래스로 만들어 사용합니다. 코틀린은 자바와 다르게 이너클래스를 만들 때 inner class 로 만들어야 온전하게 이너클래스가 됩니다. 또한 이 뷰홀더 클래스는 RecyclerView.ViewHolder 를 상속하여 만들어야 합니다. 

그럼. MyAdater 클래스의 이너 클래스로 뷰홀더 클래스를 만들어 보겠습니다. 이름은 뷰홀더라는 의미로 VH 라고 명명하겠습니다.

import android.content.Context
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
class MyAdapter constructor(val context: Context, val items:MutableList<Item>){

    //아이템뷰 1개의 자식뷰들을 참조하는 참조변수를 저장하는 뷰홀더 클래스 (이너클래스) - 아이템뷰를 생성자 파라미터로 받아서 사용
    inner class VH constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
        // 아이템뷰의 자식뷰( 제목을 보여주는 텍스트뷰, 메세지를 보여주는 텍스트뷰, 이미지를 보여주는 이미지뷰 )
        // by lazy : 지연초기화를 이용하여 자식뷰들 참조
        val tvTitle: TextView by lazy { itemView.findViewById(R.id.tv_title) }
        val tvMsg: TextView by lazy { itemView.findViewById(R.id.tv_msg) }
        val iv: ImageView by lazy { itemView.findViewById(R.id.iv) }
    }
    
}

 

이제 리사이클러뷰의 아답터로서 반드시 구현해야 할 메소드 3개를 만들기 위해 MyAdapter에 RecyclerView.Adapter 를 상속해 주도록 하겠습니다. 이때 이너클래스로 설계한 VH 클래스를 제네릭으로 지정해 주면 뷰홀더 클래스로 활용할 수 있게 됩니다.

RecyclerView.Adapter 클래스는 3개의 추상메소드를 가지고 있어서 반드시 구현해야 합니다. 이를 통해 개발자가 아답터의 필수 구현기능 3개를 실수없이 제작할 수 있도록 강제하게 됩니다.

import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨. 
//## RecyclerView.Adapter 상속 ##
class MyAdapter constructor(val context: Context, val items:MutableList<Item>) : RecyclerView.Adapter<MyAdapter.VH>(){

    //아이템뷰 1개의 자식뷰들을 참조하는 참조변수를 저장하는 뷰홀더 클래스 (이너클래스) - 아이템뷰를 생성자 파라미터로 받아서 사용
    inner class VH constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
        // 아이템뷰의 자식뷰( 제목을 보여주는 텍스트뷰, 메세지를 보여주는 텍스트뷰, 이미지를 보여주는 이미지뷰 )
        // by lazy : 지연초기화를 이용하여 자식뷰들 참조
        val tvTitle: TextView by lazy { itemView.findViewById(R.id.tv_title) }
        val tvMsg: TextView by lazy { itemView.findViewById(R.id.tv_msg) }
        val iv: ImageView by lazy { itemView.findViewById(R.id.iv) }
    }

    //반드시 구현해야 할 추상 메소드 3개. ------------------------------------------------------------------

    //1] 아이템뷰 생성 기능 : recycler_item.xml 레이아웃 모양의 아이템뷰 객체를 생성하여 뷰홀더에 전달하여 리턴하는 메소드
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        TODO("Not yet implemented")
    }

    //2] 아답터가 만들 아이템뷰의 총 개수를 리턴하는 기능 : 대량의 데이터인 items 의 개수.
    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }

    //3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
    override fun onBindViewHolder(holder: VH, position: Int) {
        TODO("Not yet implemented")
    }

}

 

이제 필수 기능 메소드 3개의 코드를 차례로 작성해 보겠습니다. 이미 자바언어를 통해 리사이클러뷰의 아답터를 제작해 보았다는 것을 전제로 소개하고 있기에 코드에 대한 소개는 간략하게 주석으로 소개하겠습니다.

 

1] 아이템뷰 생성 기능 : recycler_item.xml 레이아웃 모양의 아이템뷰 객체를 생성하여 뷰홀더에 전달하여 리턴하는 메소드

//1] 아이템뷰 생성 기능 : recycler_item.xml 레이아웃 모양의 아이템뷰 객체를 생성하여 뷰홀더에 전달하여 리턴하는 메소드
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
    val inflater= LayoutInflater.from(context)  //xml 레이아웃 파일 모양으로 뷰 객체를 생성해 주는 객체 소환
    val itemView= inflater.inflate(R.layout.recycler_item, parent, false) //아이템뷰 객체 생성
    val holder= VH(itemView) //아이템뷰의 자식뷰들을 참조해주는 뷰홀더 객체 생성
    return holder //뷰홀더 객체 리턴
}

 

2] 아답터가 만들 아이템뷰의 총 개수를 리턴하는 기능 : 대량의 데이터인 items 의 개수

//2] 아답터가 만들 아이템뷰의 총 개수를 리턴하는 기능 : 대량의 데이터인 items 의 개수.
override fun getItemCount(): Int {
    return items.size  //멤버변수인 대량의 Item 리스트의 총 개수 리턴.
}

 

3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정

//3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
override fun onBindViewHolder(holder: VH, position: Int) {
    
    val item= items.get(position)  //현재 아이템뷰에 보여줄 Item 데이터 얻어오기

    holder.tvTitle.setText(item.title)  // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
    holder.tvMsg.text= item.msg         // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정

    holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
 
}

 

* 참고로 이미지뷰에 이미지를 설정할 때는 기본 기능인 setImageResource()보다는 이미지로딩 용 외부 라이브러리 사용을 권해 드립니다. 기본 기능과 다르게 이미지의 용량이 아주 클때 자동으로 scale 을 해주며, gif 와 같은 동적 이미지와 네트워크 이미지 로딩도 지원해 줍니다. 그래서 위 코드에서 마지막 holder.iv 에 이미지를 불러와 보여주는 코드는 Glide 라는 이미지로딩 외부 라이브러리를 사용하도록 수정해 보겠습니다. Glide 라이브러리 추가는 스스로 하실 수 있다고 보고 설명을 생략하도록 하겠습니다.

 

build.gradle.kts(Module : app)

 

Glide 를 추가했으니 이미지 설정 코드를 수정해 보겠습니다.

//3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
override fun onBindViewHolder(holder: VH, position: Int) {

    val item= items.get(position)  //현재 아이템뷰에 보여줄 Item 데이터 얻어오기

    holder.tvTitle.setText(item.title)  // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
    holder.tvMsg.text= item.msg         // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정

    //holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
    // 이미지 로딩 라이브러리 Glide 로 이미지 설정
    Glide.with(context).load(item.img).into(holder.iv)
}

 

완성된 MyAdapter 클래스의 전체 코드 입니다.

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide

//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
//## RecyclerView.Adapter 상속 ##
class MyAdapter constructor(val context: Context, val items:MutableList<Item>) : RecyclerView.Adapter<MyAdapter.VH>(){

    //아이템뷰 1개의 자식뷰들을 참조하는 참조변수를 저장하는 뷰홀더 클래스 (이너클래스) - 아이템뷰를 생성자 파라미터로 받아서 사용
    inner class VH constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
        // 아이템뷰의 자식뷰( 제목을 보여주는 텍스트뷰, 메세지를 보여주는 텍스트뷰, 이미지를 보여주는 이미지뷰 )
        // by lazy : 지연초기화를 이용하여 자식뷰들 참조
        val tvTitle: TextView by lazy { itemView.findViewById(R.id.tv_title) }
        val tvMsg: TextView by lazy { itemView.findViewById(R.id.tv_msg) }
        val iv: ImageView by lazy { itemView.findViewById(R.id.iv) }
    }

    //반드시 구현해야 할 추상 메소드 3개. ------------------------------------------------------------------

    //1] 아이템뷰 생성 기능 : recycler_item.xml 레이아웃 모양의 아이템뷰 객체를 생성하여 뷰홀더에 전달하여 리턴하는 메소드
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val inflater= LayoutInflater.from(context)  //xml 레이아웃 파일 모양으로 뷰 객체를 생성해 주는 객체 소환
        val itemView= inflater.inflate(R.layout.recycler_item, parent, false) //아이템뷰 객체 생성
        val holder= VH(itemView) //아이템뷰의 자식뷰들을 참조해주는 뷰홀더 객체 생성
        return holder //뷰홀더 객체 리턴
    }

    //2] 아답터가 만들 아이템뷰의 총 개수를 리턴하는 기능 : 대량의 데이터인 items 의 개수.
    override fun getItemCount(): Int {
        return items.size  //멤버변수인 대량의 Item 리스트의 총 개수 리턴.
    }

    //3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
    override fun onBindViewHolder(holder: VH, position: Int) {

        val item= items.get(position)  //현재 아이템뷰에 보여줄 Item 데이터 얻어오기

        holder.tvTitle.setText(item.title)  // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
        holder.tvMsg.text= item.msg         // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정

        //holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
        // 이미지 로딩 라이브러리 Glide 로 이미지 설정
        Glide.with(context).load(item.img).into(holder.iv)
    }

}

 

 

5) 아답터 객체 생성 및 리사이클러뷰에 아답터로 설정하여 완성하기

 

이제 리사이클러뷰를 보여주는 MainActivity 에서 MyAdapter 클래스를 객체로 생성하여 리사이클러뷰에 설정해 주겠습니다.

class MainActivity : AppCompatActivity() {

    // 뷰 참조변수들은 한번 뷰객체를 참조하면 다른 뷰객체로 바꾸어 참조하는 경우가 거의 없기에 var보다 val을 선호함
    // 이때, 초기화 안하면 에러 - 하지만 recycler의 객체 참조값을 지금 초기화 할 수 없음 [ onCreate() 에서 findViewById()를 해야 하기에..]
    // 그래서 늦은초기화 사용[ lateinit 은 var에만 사용할 수 있기에 by lazy 지연초기화 사용]
    val recycler: RecyclerView by lazy { findViewById(R.id.recycler) }

    //대량의 데이터 property[속성:멤버변수]
    var items= mutableListOf<Item>() //Java의 ArrayList<Item>와 비슷하게 동작
    //var items= arrayListOf<Item>()  //이렇게 ArrayList 객체를 만들어도 됨

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //대량의 데이터들 추가 [테스트목적]
        items.add( Item("NEW YORK","Hello. Kotlin", R.drawable.newyork) )
        items.add( Item("PARIS","Nice to meet you", R.drawable.paris) )
        items.add( Item("SYDNEY","Have a good day", R.drawable.sydney) )
        items.add( Item("뉴욕","Do you have fun", R.drawable.newyork) )
        items.add( Item("파리","Nice to meet you", R.drawable.paris) )
        items.add( Item("시드니","Have a good day", R.drawable.sydney) )
        items.add( Item("new york","Do you have fun", R.drawable.newyork) )
        items.add( Item("paris","Nice to meet you", R.drawable.paris) )
        items.add( Item("sydney","Have a good day", R.drawable.sydney) )

        // ----------------------------------------------------------------------------
        
        //리사이클러뷰안에 이미 아답터 프로퍼티(멤버변수)가 있어서 아답터객체를 대입해 주면 됨
        recycler.adapter= MyAdapter(this, items)

    }//onCreate method...
}//MainActivity class.....

 

마지막으로, 리사이클러뷰가 아이템뷰를 세로 목록형으로 나열되도록 레이아웃 매니저를 LinearLayoutManager 로 설정해 주겠습니다.

class MainActivity : AppCompatActivity() {

    // 뷰 참조변수들은 한번 뷰객체를 참조하면 다른 뷰객체로 바꾸어 참조하는 경우가 거의 없기에 var보다 val을 선호함
    // 이때, 초기화 안하면 에러 - 하지만 recycler의 객체 참조값을 지금 초기화 할 수 없음 [ onCreate() 에서 findViewById()를 해야 하기에..]
    // 그래서 늦은초기화 사용[ lateinit 은 var에만 사용할 수 있기에 by lazy 지연초기화 사용]
    val recycler: RecyclerView by lazy { findViewById(R.id.recycler) }

    //대량의 데이터 property[속성:멤버변수]
    var items= mutableListOf<Item>() //Java의 ArrayList<Item>와 비슷하게 동작
    //var items= arrayListOf<Item>()  //이렇게 ArrayList 객체를 만들어도 됨

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //대량의 데이터들 추가 [테스트목적]
        items.add( Item("NEW YORK","Hello. Kotlin", R.drawable.newyork) )
        items.add( Item("PARIS","Nice to meet you", R.drawable.paris) )
        items.add( Item("SYDNEY","Have a good day", R.drawable.sydney) )
        items.add( Item("뉴욕","Do you have fun", R.drawable.newyork) )
        items.add( Item("파리","Nice to meet you", R.drawable.paris) )
        items.add( Item("시드니","Have a good day", R.drawable.sydney) )
        items.add( Item("new york","Do you have fun", R.drawable.newyork) )
        items.add( Item("paris","Nice to meet you", R.drawable.paris) )
        items.add( Item("sydney","Have a good day", R.drawable.sydney) )

        // ----------------------------------------------------------------------------

        //리사이클러뷰안에 이미 아답터 프로퍼티(멤버변수)가 있어서 아답터객체를 대입해 주면 됨
        recycler.adapter= MyAdapter(this, items)

        //리사이클러뷰에 레이아웃매니저 붙이기
        recycler.layoutManager= LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

    }//onCreate method...
}//MainActivity class.....

 


 

이렇게 해서 리사이클러뷰를 보여주는 코드를 코틀린으로 구현해보는 작업을 완료했습니다.

잘 구현되었는지 확인해 볼까요?

 

 

 

이번 글은 좀 길었네요.

다음글에서 마지막으로 아이템뷰를 클릭하였을 때 Item 상세 화면 액티비티로 전환하도록 만들겠습니다.

반응형
반응형

이제까지 코틀린 언어의 문법을 대략적으로 알아보았으니 실제 앱 개발을 해봐야 겠죠.

안드로이드 앱 개발에서 가장 기본적인 기술인 액티비티 화면 전환과 리사이클러뷰를 통한 대량의 데이터를 보여주는 앱을 만들어 보겠습니다. 아마 이 예제를 만들어보면 코틀린을 이용한 앱 개발이 어떤 건지 대략적인 감이 올 겁니다. 더불어 자바언어로 개발했을 때에 비해 얼마나 효과적인지 느껴보실 수 있을 겁니다.

액티비티 전환 방법과 리사이클러뷰에 대한 기술 소개는 다른 글에서 소개했으니 이번에는 코틀린언어의 적용 모습에 포커스를 두고 살펴보시길 바랍니다. 


실습을 위한 프로젝트를 생성하겠습니다. 

 

Project Template : Empty Views Activity

 

Name : KotlinRecyclerViewApp

Language : Kotlin

Minimum SDK : API 26

Build configuration language : Kotlin DSL


 

 

대부분의 앱은 1개 이상의 화면을 보유하고 있는 만큼 화면을 담당하는 액티비티 전환을 코틀린으로 구현해보기 위해 화면 구성은 총 3개를 만들어 볼 예정입니다.

 

화면(Activity) 3개

1. IntroActivity  : 진입 화면 - 화면이동 버튼

2. MainActivity : 리사이클러뷰를 보여주는 화면

3. ItemActivity : 아이템 상세화면 ( 리사이클러뷰의 아이템뷰 클릭시에 이동될 아이템 상세화면 ~ 화면전환 애니메이션 적용 )

 

 

♣ 새로운 Activity 생성 [ 코틀린 클래스 파일.kt, 레이아웃 파일.xml, AndroidManifest.xml 에 등록 ]

New > Activity > Empty Views Activity

 

이렇게 안드로이드 스튜디오의 Activity 생성 메뉴를 통해 새로운 액티비티를 생성하면 Activity를 상속한 코틀린 클래스 파일과 이 액티비티에서 보여줄 화면을 구성하는 레이아웃 xml 파일을 자동으로 생성해 줍니다. 또한 앱의 구성요소(Component)를 등록하는 앱의 시작파일인 AndroidManifest.xml 파일에 등록까지 해줍니다.

 

 

1. 인트로 화면 - 진입화면

앱의 진입 화면으로 사용하는 액티비티 이기에 생성할 때 [ Launcher Activity ] 체크박스를 체크해 주시기 바랍니다.

 

런처 액티비티로 생성하였기에 AndroidManifest.xml 에 등록될 때 <Intent-Filter> 가 추가되어 있습니다. 사실 이 상태 그대로 코드를 작성해도 되지만 기존에 런처 액티비티였던 MainActivity는 더이상 진입화면으로 사용하지 않을 것이기에 인텐트필터를 제거하고 외부에서 호출할 수 없도록 exported 속성을 false 로 변경하겠습니다.

변경 전 변경 후

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity
    android:name=".MainActivity"
    android:exported="false">
</activity>

 

 

먼저, 인트로 화면의 레이아웃 xml 파일을 작성해 보겠습니다. 실제 앱을 구현하는게 목적이 아니고 단지, 액티비티 전환을 코틀린으로 구현하는 것이 목적인 만큼 별도의 디자인 없이 Button 만 배치 하여 MainActivity로 전환할 수 있도록 하겠습니다.

버튼의 클릭이벤트에 반응하는 리스너 설정은 우선 익명클래스로 구현해 보겠습니다.

activity_intro.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".IntroActivity">

    <!-- 익명클래스로 리스너 처리   -->
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="go to Main"
        android:layout_centerInParent="true"/>

</RelativeLayout>

 

이제 액티비티 클래스 코드를 작성해 보겠습니다.

우선 필요한 코드에 집중하기 위해 불필요한 코드는 제거하고 시작하겠습니다. 안드로이드 컴포즈가 등장하면서 Edge-To-Edge UI 를 기본적으로 구현된 상태로 액티비티가 만들어집니다.

 

♣ Edge-To-Edge UI 

액티비티는 기본적으로 디스플레이 화면 전체를 사용하지 않습니다. 상태표시줄(Status bar)과 네비게이션바(Navigation bar) 영역을 제외한 부분만을 사용합니다. 예전에는 화면전체를 사용하려면 WindowManager 를 통해 Flag 설정을 직접 해야 했습니다. 하지만 안드로이드 버전이 변경되면 기존 Flag 설정으로 적용되지 않는 등의 문제가 있어 개발자들을 조금 불편하게 했습니다. 

구글은 컴포즈 개발방법을 도입하면서 UI 적으로 화면 전체를 사용하게 함으로서 사용자에게 몰입감을 주고자 화면을 상단 끝(Edge)에서 하단 끝(Edge) 까지 사용하는 것을 권장합니다. 그래서 화면 전체 사용을 편하게 적용하게 하기 위해 enableEdgeToEdge() 기능을 제공하며 기본 보일러플레이트 코드(미리 작성되어 있는 코드)로 추가하였습니다. Android 15버전 부터는 모든 앱에 기본적으로 적용된다고 합니다.

 

class IntroActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()    // 화면전체 사용 ON
        setContentView(R.layout.activity_intro)  // 액티비티가 보여줄 뷰를 설정
        
        //Edge-To-Edge 일때 하단 네비게시션바 사이즈 만큼 패딩 적용
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }
}

 

enableEdgeToEdge() 기능을 사용하면 상태표시줄 뿐만 아니라 하단의 네비게이션바 영역까지 액티비티가 확장됩니다. 하지만 네비게이션바는 시스템바 이기에 없어진 것은 아니고 액티비티 위에 오버레이 되어 그려집니다. 그러다 보니 액티비티의 가장 아래쪽은 네비게이션바에 의해 콘텐츠가 일부 가려지는 문제가 발생할 수 있습니다. 하여 네비게이션바 사이즈를 얻어와서 그 만큼 Window 의 안쪽 패딩을 적용하는 코드까지 추가되었습니다. 네비게이션 바의 종류(제스처, 2버튼, 3버튼)나 디바이스의 종류에 따라 사이즈가 다르기에 리스너를 통해 시스템바의 사이즈를 동적으로 얻어와서 패딩을 적용하는 코드입니다. 덕분에 코드가 지저분해 보이기도 합니다. 

 

이번 예제에서는 코틀린을 이용한 앱 개발 코드에 집중하기 위해 EdgeToEdge 관련 코드는 모두 제거하고 액티비티가 보여줄 뷰를 설정하는 setContentView()만 남겨두고 설명을 이어가겠습니다.

import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class IntroActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro) //액티비티가 보여줄 뷰 설정
        
    }//onCreate method....
    
}//MainActivity class...

 

예전버전에서 액티비티를 만들었을 때의 기본 코드와 같습니다.

 

이제 레이아웃 xml 파일에서 만든 Button 을 참조하기 위한 참조변수를 만들어보겠습니다. 보통 액티비티가 보여주는 뷰들은 액티비티 전체에서 사용될 가능성이 많기에 멤버변수(프로퍼티)로 선언하는 것을 권장합니다. 우선 값 변경이 가능한 var 키워드로 변수를 만들어 보겠습니다.

class IntroActivity : AppCompatActivity() {

    var btn:Button   //ERROR - 코틀린은 초기화 없이 멤버변수를 만들 수 없음.
    
    ....
    
 }

 

다만, 코틀린은 멤버변수를 선언할 때 초기화를 하지 않으면 문법적 에러가 발생합니다. 

 

그렇다고 뷰들 찾아와서 객체의 참조값을 리턴해주는 findViewById() 를 호출하여 초기화하면 문법적 에러는 아니지만 런타임(실행 중) 예외가 발생합니다. 이미 자바를 통해 앱을 구현해 보신 분들은 잘 알고 있듯이 xml에 작성한 레이아웃용 뷰들은 액티비티 객체가 생성된 후 자동으로 발동하는 라이프사이클 콜백 메소드인 onCreate()메소드 안에서 setContentView()를 통해 액티비티에 뷰로서 설정되면서 객체로 만들어 집니다. 하지만 멤버변수 btn 은 액티비티가 객체로 생성될 때 초기화를 위해 Button 객체의 참조값을 요구합니다. 즉, Button 객체가 아직 만들어 지기 전에 객체를 찾아오는 findViewById()를 호출하여 참조변수 btn 에 초기화를 하려 하기에 결국 찾지 못하여 null 값이 리턴됩니다. 

class IntroActivity : AppCompatActivity() {

    var btn:Button= findViewById(R.id.btn)    //초기화를 안하면 문법에러 라고 지금 여기서 find를 하면 에러.. 멤버변수가 만들어진 후 onCreate()가 호출되기에 아직 뷰가 만들어지지 않았음.
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro) //이때 xml 레이아웃파일의 뷰 객체들이 생성됨.
        
    }
}

 

해결 방법은 여러가지가 있기에 앞으로 하나씩 새로운 방법으로 소개하겠습니다.

첫번째 방법으로 자바언어를 사용했을 때 처럼 일단 null 값으로 초기화를 하는 btn 참조변수를 만들고 onCreate() 메소드 안에서 Button 객체를 찾아와서 참조하도록 하겠습니다. 단, 코틀린은 null 값을 저장할 수 있는 변수는 ? 가 표시된 nullable 변수로 만들어야 합니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro)

        //btn 참조하기
        btn= findViewById(R.id.btn)
        
    }//onCreate method....
}//IntroActivity class........

 

자바로 개발 할 때와 같은 구조 입니다.

이제 버튼 클릭이벤트에 반응하는 리스너를 설정하겠습니다. 클릭에 반응하는 OnClickListener 는 interface 이기에 곧바로 객체를 만들 수 없습니다. 별도의 class 를 만들어 구현(implement)하거나, 객체를 생성하면서 인터페이스를 구현하는 익명클래스를 이용할 수 있습니다.  이 버튼의 클릭에만 반응하는 전용 리스너인 만큼 익명클래스로 구현해 보겠습니다.

자바와코틀린은 익명클래스를 만드는 문법이 완전히 다릅니다. 코틀린에서는 키워드 자체에서 '나 객체야.' 라는 것을 직관적으로 알수 있도록 object 라는 키워드를 사용하여 익명클래스를 만듭니다. 근데 그냥 object 라고만 쓰면 객체이긴 한데 어떤 인터페이스를 구현했는지 알수 없으니 implements 문법으로 사용하는 : 기호를 붙이고 OnClickListener 를 구현하면 됩니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro)

        //btn 참조하기
        btn= findViewById(R.id.btn)
        
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                
            }
        })
        
    }//onCreate method....
}//IntroActivity class........

 

btn 변수가 nullable 타입이기에 null 안전 연산자인 ?. 을 이용하여 NPE 에 대응해야만 합니다. nullable 변수를 사용할 때 다소 불편한 점 중에 하나입니다.

문법적 표기법의 차이만 다소 있을 뿐 클릭이벤트 처리 기법을 동일하고 모양도 비슷하여 크게 어렵지 않게 보실 수 있을 겁니다.

 

이제, 버튼의 기능을 onClick()메소드 안에 추가하겠습니다. 이 버튼은 MainActivity 로 전환을 해주는 버튼입니다. 액티비티를 전환하기 위해 Intent 객체를 생성하고 실행하고자 하는 MainActivity의 클래스 정보를 기입하겠습니다. 

 

자바와 다른점

1) new 키워드를 사용하지 않고 Intent 객체 생성

2) 익명클래스 안에서 아웃터클래스(IntroActivity) 객체를 지칭하는 아웃터클래스.thisthis@아웃터클래스명 으로 표기법 변경

 

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro)

        //btn 참조하기
        btn= findViewById(R.id.btn)
        
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                //MainActivity로 전환
                val intent: Intent = Intent(this@IntroActivity, MainActivity::class.java)
                startActivity(intent)
                
            }
        })
        
    }//onCreate method....
}//IntroActivity class........

 

 

[ IntroActivity -> MainActivity 액티비티 전환 실행화면 사진 & GIF ]

IntroActivit -> MainActivity 로 전환

 

 

기능적으로는 특별히 어려운 내용은 없습니다. 단지 코틀린에서 달라진 문법적 표기법을 살펴보았습니다.

 

위에서 살펴본 익명클래스를 이용한 버튼 클릭 리스너 처리를 코드가 다소 지저분 합니다. 익명 클래스의 영역 중괄호{} 안에 추상메소드를 구현하기 위한 onClick() 메소드의 중괄호가 중첩으로 구성되어 가독성이 떨어집니다. 이 함수 처리를 간결하게 해주는 문법이 람다표기법입니다. 자바에도 존재하지요. 코틀린에도 람다 표현이 가능합니다. 

 

이번에는 람다 표현을 이용한 버튼 클릭이벤트 처리를 실습해보기 위해 IntroActivity에 [액티비티 종료] 버튼을 하나 더 추가해 보겠습니다. 화면의 우상단 위치에 배치하기 위해 activity_main.xml 레이아웃 파일에 버튼을 하나 더 추가하겠습니다.

activity_intro.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".IntroActivity">

    <!-- 익명클래스로 리스너 처리   -->
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="go to Main"
        android:layout_centerInParent="true"/>


    <!-- (람다표현)으로 클릭이벤트 처리   -->
    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:layout_alignParentRight="true"
        android:text="EXIT"
        android:backgroundTint="#FFFF3300"/>

</RelativeLayout>

 

 

이제 이 EXIT 버튼 클릭 처리를 위해 IntroActivity의 코틀린 파일을 수정하겠습니다.

새로 추가한 버튼의 참조변수도 필요합니다. 위에서는 nullable 변수를 사용했었는데 null 안전 문법을 사용하는 것이 불편하니 초기화를 늦게 적용하는 것을 요청하는 '늦은초기화 문법 lateinit' 을 사용해 보겠습니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    //늦은 초기화 lateinit
    lateinit var btn2: Button
    
    ....
    
}//IntroActivity class.....

 

lateinit 을 통해 늦은 초기화를 하면 멤버변수의 초기화 시점이 참조변수가 처음 사용될 때 실행 됩니다. 

이제 btn2 의 초기화를 onCreate()영역 안에서 수행하겠습니다.

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    //늦은 초기화 lateinit
    lateinit var btn2: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro)

        //btn 참조하기
        btn= findViewById(R.id.btn)
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                val intent: Intent = Intent(this@IntroActivity, MainActivity::class.java)
                startActivity(intent)
            }
        })

        //btn2 참조하기 [lateinit - 늦은초기화 문법으로 ] 
        btn2= findViewById(R.id.btn2)  //이때 초기화 됨
        
    }//onCreate method...
    
}//IntroActivity class...

 

이제 버튼 클릭처리를 익명클래스인 object 를 사용하지 않고 람다 표현으로 간결화 하겠습니다. 이 람다 표현만으로도 이미 많이 간결해 집니다. 근데 코틀린은 이것 또한 더 줄여서 작성하는 SAM (Single Abstract Method) 변환 이란 문법을 도입했습니다. 메소드의 파라미터를 받기위한 소괄호 () 안에 람다 표현의 중괄호 {} 가 중첩으로 되어 있는 것이 지저분해 함수의 소괄호()가 생략된 문법입니다. 짧게 소개하기는 여의치 않아 별도의 글로 SAM 변환에 대해 소개할 예정입니다. 지금은 앱의 클릭이벤트 처리를 코틀린으로 구현하는 것이 목적이기에 깊게 소개하지 않고 문법적인 사용모습을 보여드리고자 하오니 대략 이렇게 사용하는 구나. 정도로 넘어가시길 바랍니다. 추후 별도로 소개하겠습니다. 얼마나 간결한지 한번 보시죠.

package com.kitesoft.kotlinrecyclerviewapp

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class IntroActivity : AppCompatActivity() {

    var btn: Button?= null      //이렇게 시작은 null 값으로 지정 [nullable type]

    //늦은 초기화 lateinit
    lateinit var btn2: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro)

        //btn 참조하기
        btn= findViewById(R.id.btn)
        //Button?타입이기에 null safe 연산자 ?. 사용
        btn?.setOnClickListener(object : View.OnClickListener{  //익명클래스 사용해보기
            override fun onClick(p0: View?) {
                val intent: Intent = Intent(this@IntroActivity, MainActivity::class.java)
                startActivity(intent)
            }
        })

        //btn2 참조하기 [lateinit - 늦은초기화 문법으로 ]
        btn2= findViewById(R.id.btn2)

        //리스너 설정을 람다 표기법을 이용하면 더 쉽게 코딩 가능. 단, 그 리스너 인터페이스의 추상메소드가 1개일 때[단일 추상 메소드 : SAM (Single Abstract Method]변환으로 구현 가능
        btn2.setOnClickListener {
            finish()   //액티비티 종료
        }
    }//onCreate Method...

}//MainActivity class...

 

클릭 이벤트처리가 정말 간결하네요. 아직 조금만 살펴봤지만 코틀린의 매력을 조금씩 느껴갈 수 있기를 바랍니다. 자바에 비해 분명 요즘의 트랜디한 모던 코딩 문법이어서 맘에 드실겁니다.

 

[ IntroActivity 전체 실행화면 사진 & GIF ]

IntroActivity 의 버튼 클릭에 따른 액티비티전환 및 종료 실행화면

 


 

앱 구현의 기본기능인 클릭이벤트 처리, 액티비티 전환 및 종료에 대하여 코틀린으로 구현해보았습니다.

이제 앱 개발에서 가장 많이 구현하는 기능인 대량의 데이터를 보여주는 RecyclerView 를 사용하는 MainActivity를 만들어보겠습니다.

다음 글에서 보겠습니다.

반응형
반응형

프로그램을 개발하다 보면 기존에 만들었던 class 에 새로운 기능을 추가해야 하는 경우가 있습니다.

만약 기존 class 를 여러분이 직접 설계하여 .kt 파일을 소유한 상태라면 파일을 열어 직접 새로운 기능메소드를 추가하였을 겁니다.

 

근데. 만약 여러분이 제작한 class 가 아니어서 .kt 파일을 보유하지 않았거나, 다른 모듈에서 새로운 기능메소드 없이 기존의 클래스를 사용하고 있어서 원본 class 를 수정하지 못하는 상황이 있을 수도 있습니다. 또는 외부 라이브러리를 통해 제공된 class 인데 새로운 기능을 추가해서 사용해야 할 수 도 있습니다. 하지만 외부에서 제작된 class 이기에 역시 원본 class .kt 파일에 메소드를 추가로 작성하는 것이 불가능 합니다. 이런 경우에는 어떻게 새로운 기능메소드를 추가해야 하는 걸까요?

 

그럴때는 보통 상속을 통해 기존 class 를 가져온 후 새로운 기능 메소드를 추가하는 방법을 사용합니다. 

하지만 코틀린의 class 는 기본적으로 상속이 불가능한 final class 입니다. 상속을 허용하려면 open 키워드를 이용해야 하죠. 즉, 기존의 클래스가 상속이 불가능한 클래스 일 수 도 있습니다. 

 

이를 위해 코틀린은 별도의 상속없이 기존 클래스에 새로운 기능메소드를 추가할 수 있도록 확장(extension) 기능을 제공합니다.

 

기존 클래스의 .kt 원본 파일에 추가되는 것이 아니고 메모리 상에 정적으로 연결되는 기능이기에 실행 중에만 확장이 되어 사용되며 원본에는 영향을 주지 않아 부담없이 그 앱에서만 필요한 기능을 추가해 사용할 수 있는 특징이 있습니다. 

 

실제로 제가 AI 앱 개발을 할 때 촬영된 이미지를 분석하여 object detecting 을 하고 ImageView 가 보여주는 Bitmap 이미지에 오버레이된 도형과 글씨를 표시할 때 기존 ImageView 클래스에 확장함수로 drawBitmapWithBounce() 라는 확장 함수를 추가하여 기능을 구현한 적이 있습니다. 


 

실습을 위해 새로운 Test11ExtensionFunction.kt 코틀린 파일을 만들겠습니다.

프로그램의 시작 함수 main() 까지 작성하고 시작하겠습니다.

 

 

1. 확장 함수 Extenstion Function

기존 클래스에 새로운 기능을 추가하는 대표적인 방법은 상속 Inheritace 입니다. 하지만 상속이 불가능한 final class 는 상속이 불가능 하여 새로운 기능을 추가하지 못합니다. 

상속이 불가능한 클래스에 기능을 추가할 수 있는 아주 손쉬운 방법인 확장 함수를 알아보기 위해 간단한 로봇캐릭터를 이용한 게임을 제작한다고 가정해 보겠습니다.

게임의 메인 캐릭터인 로봇을 설계해야 겠지요. 실습을 위한 것인 만큼 아주 간결하게 멤버변수 1개와 멤버함수 1개로 설계하겠습니다.

 

클래스 명 : Robot

멤버변수(property) :  캐릭터 이름 [  name : String? ]

멤버함수 (method) :  이동 기능 [ move() ]

 

먼저, main() 함수 밖에 Robot class 를 설계하겠습니다.

//시작 함수
fun main(){


}//main 함수 종료..

// [이름, 이동기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------

 

클래스가 잘 설계 되었는지 확인하기 위해 main()함수에서 Robot 객체를 생성하고 [이동기능]을 사용해 보겠습니다.

//시작 함수
fun main(){

    // Robot 객체 생성
    val robot= Robot()
    // 원래 있는 메소드 호출
    robot.move()
    
}//main 함수 종료..

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


//**출력**
아장 아장 이동.

 

잘 동작 되는 군요. 근데 로봇이 그냥 이동만 하면 게임이 재미 없을 것 같네요. 로봇끼리 서로 싸울 수 있도록 [공격기능]을 추가해 보겠습니다. 통상적으로 특정 클래스에 기능을 추가하려면 원본 클래스 파일을 수정하지만, 이 로봇클래스의 원본은 수정하는 않는 것을 가정하여 제작해 보겠습니다. 원본을 수정할 수 없다면 기능을 상속받아 새로운 기능을 추가하는 것이 보편적입니다. 시도해 볼까요?

// Robot 에게 attack(공격) 가능 추가 하기
// [상속을 통한 기능 추가 시도]
class MyRobot : Robot(){ //error -  final class Robot 은 상속 불가.

}

 

주석으로 설명했듯이 Robot 클래스는 상속이 불가능한 클래스입니다. 프로그램을 개발하다보면 이런 경우가 꽤 존재합니다. 특정 기능을 추가하고 싶은데 final class 여서 상속이 안되기에 별도의 기능함수에 파라미터로 전달하여 대신 처리하는 등 피동적인 기법을 사용할 수 밖에 없습니다. 

이번에 이 글의 주제인 확장 함수를 이용하여 Robot 클래스에 [공격]기능을 추가해 보겠습니다.

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


// Robot 에게 attack(공격) 가능 추가 하기

// [확장 함수 만들기]  -- 클래스명.함수명  으로 새로운 함수 제작
fun Robot.attack(){
    println("주먹 발사!!")
}

 

원본 클래스인 Robot 클래스의 밖이면 어디든 상관없습니다. 함수를 만드는 문법은 그대로 사용하되 함수의 이름앞에 어떤 클래스의 확장함수 인지만 명시해 주시면 됩니다.  [클래스명.함수명]

 

자. 이제 Robot 클래스에 attack() 기능 함수가 추가되었습니다. 이 함수는 메모리에 정적 바인딩되어 있기에 앱이 종료될 때 까지, 즉, 앱이 실행 중에는 Robot 클래스의 멤버메소드 인것 처럼 사용이 가능합니다.

main() 함수에서 생성한 robot 객체에게 확장함수로 만든 [공격 attack 기능]을 사용해 보겠습니다.

//시작 함수
fun main(){

    // Robot 객체 생성
    val robot= Robot()
    // 원래 있는 메소드 호출
    robot.move()

    // 원래 class 작성할 때는 없던 확장 함수 호출. -- 호출의 문법적 차이는 없음.
    robot.attack()

}//main 함수 종료..

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


// Robot 에게 attack(공격) 가능 추가 하기

// [확장 함수 만들기]  -- 클래스명.함수명  으로 새로운 함수 제작
fun Robot.attack(){
    println("주먹 발사!!")
}


//**출력**
아장 아장 이동.
주먹 발사!!

 

너무 쉽고 간편하게 기능을 추가하였습니다.

자바에는 없는 문법입니다.[참고로. kotlin 파일은 컴파일 되면 java 로 변환됩니다. 이 확장함수는 자바에서는 static 함수로 대체됩니다.]

 

이번에는 확장함수에 파라미터를 전달해볼까요? 더불어 클래스의 중괄호 영역 밖에서 만든 확정함수안에서 원본 클래스의 영역안에 선언된 멤버변수를 인식할 수 있을 까요? 뭐. 미리 정답을 말하면 당연히 가능합니다. 원래 있는 기능처럼 사용하는 것인 만큼 당연히 허용됩니다.

아! 조심하세요. 위에 참고사항으로 설명했듯이 자바로 변환될때 static 함수를 만들고 파라미터로 객체를 전달받아 해당 기능을 동작하도록 하기에 외부에서 접근이 불가능한 private 접근 제한자가 있는 멤버변수는 사용이 불가능합니다. 그러니 엄밀하게는 기존 메소드와 완전히 같지는 않습니다.

 

일단, 코틀린에서는 대부분 private 으로 프로퍼티를 제작하지 않으니 이는 잠시 뒤로 하고 멤버변수를 사용하는 확정함수를 만들어 보겠습니다.

// [확장 함수 만들기2 - 멤버 변수 사용해 보기 (이름을 매개변수로 전달받아 멤버변수에 대입하기)]
fun Robot.fly(name: String){
    this.name= name
    println("${this.name} 난다~~ 난다~~~")
}

 

잘 동작하는 지 main()함수에서 로봇객체의 이름을 파라미터에 전달하면서 확장함수 fly 기능을 사용해 보겠습니다.

//시작 함수
fun main(){

    // Robot 객체 생성
    val robot= Robot()
    // 원래 있는 메소드 호출
    robot.move()

    // 원래 class 작성할 때는 없던 확장 함수 호출. -- 호출의 문법적 차이는 없음.
    robot.attack()

    // 파라미터 값 전달 및 멤버변수를 사용하는 확장함수 호출
    robot.fly("옵티머스 프라임")

}//main 함수 종료..

// [이름, 이동 기능]이 설계된 클래스. [ open class 가 아님. 더이상 상속 불가한 클래스 ]
class Robot{

    var name:String?= null

    fun move(){
        println("아장 아장 이동.")
    }

}//------------------------------


// Robot 에게 attack(공격) 가능 추가 하기

// [확장 함수 만들기]  -- 클래스명.함수명  으로 새로운 함수 제작
fun Robot.attack(){
    println("주먹 발사!!")
}

// [확장 함수 만들기2 - 멤버 변수 사용해 보기 (이름을 매개변수로 전달받아 멤버변수에 대입하기)]
fun Robot.fly(name: String){
    this.name= name
    println("${this.name} 난다~~ 난다~~~")
}

//**출력**
아장 아장 이동.
주먹 발사!!
옵티머스 프라임 난다~~ 난다~~~

 

 

확장함수는 내가 만든 클래스에만 적용할 수 있는 것이 아닙니다.

이번에는 Kotlin APIs 클래스 중에서 상속이 불가능한 String 클래스에 새로운 기능 추가해 보겠습니다.

//시작 함수
fun main(){

    // Kotlin APIs 로 제공되는 final class String 에 새로운 확장 함수 사용하기!
    "안녕하세요.".twicePrint()

}//main 함수 종료..

// String 클래스에 새로운 기능 추가!!
fun String.twicePrint(){
    println("$this $this")
}

 

 

제네릭이 있는 컬렉션 List 에도 확장함수 적용이 가능합니다. 

//시작 함수
fun main(){

    // 제네릭 사용 클래스에 확장 함수 사용하기!
    val scores= listOf(90,88,75,68,95,100)
    val total= scores.total()
    println("성적 총합 : $total")    

}//main 함수 종료..

// 제네릭 타입에 확장함수 추가하기 - 정수값들을 가진 리스트의 총합을 구해서 리턴해주는 기능 추가!!
fun List<Int>.total():Int{
    var total:Int=0
    this.forEach { total += it }
    return total
}


//**출력***
성적 총합 : 516

 

단, 같은 리스트여도 제네릭 타입이 다르면 확장함수를 사용할 수 없습니다.

// 같은 리스트여도 제네릭 타입이 다르면 확장함수 사용 불가
val scores2= listOf(60.5, 43.5, 66.7)
val total2= scores2.total() //error - 확장함수 안됨..

 

 

확장 함수 기타 특성

- 확장 프로퍼티(멤버변수)도 가능함.

- 오버로딩 Overloading 가능함.

- 오버라이드 Override 는 불가능함.

- 기존에 클래스에 있는 메소드명과 같은 이름의 확장함수를 만들어도 에러는 아니지만 기존의 메소드가 우선시 됨.

- package 가 다른 곳에서 만든 확장 함수를 사용하려면 import 해야 함.

 

 

반응형
반응형

Kotlin은 자바에는 없는 Scope(스코프 : 범위) Function 이라는 문법이 존재합니다. 그 이름 처럼 특정 객체의 범위(scope)를 만들어주는 함수로서 객체의 멤버 여러개를 접근해야 할 때 그 객체만의 범위를 만들어주는 함수로서 코드의 가독성이 아주 좋아집니다.

 

♣ Scope function :

- 객체의 멤버를 실수없이 편하게 사용할 수 있는 범위(영역)을 만들어내는 함수 - 람다식을 이용하여 영역처리, 가독성 향상

 

스코프 함수에 대한 문법을 소개하기 전에 이 함수의 효용성을 느낄 수 있도록 클래스를 하나 만들고 그 멤버들을 사용하는 상황은 살펴보겠습니다.

 


 

실습을 위해 Test10ScopeFunction.kt 코틀린 파일을 만들겠습니다.

프로그램의 시작함수 main()를 작성하겠습니다.

 

1. 스코프 함수의 등장

자바에는 없었던 스코프 함수가 왜 등장하게 되었는지를 느껴보기 위해 멤버변수가 여러개인 클래스를 하나 설계해 보겠습니다.

 

어떤 그룹에 소속한 사람들(크루원)의 [이름, 나이, 주소] 정보를 관리하는 앱을 만들어야 한다고 예를 들어보겠습니다.

사람 한명당 [이름, 나이, 주소] 3개의 데이터를 저장해야 하기에 그룹화 하여 관리하도록 Crew 라는 이름으로 클래스를 설계해 보겠습니다. 편의상 별도의 .kt 파일보다는 main()함수 아래에 작성하겠습니다.

//시작함수
fun main(){


}//main 함수 종료...

//어떤 그룹의 크루원 정보 저장 클래스 
class Crew{
    
    //property [ 이름, 나이, 주소 ]
    var name:String?= null
    var age:Int?= null
    var address:String?= null

    //method - property 출력 기능
    fun show(){
        println("$name : $age , $address")
    }
}

 

이제, Crew 클래스를 사용하기 위해 객체로 생성 한 후 멤버변수에 값을 대입하고 출력해보는 코드를 작성해 보겠습니다. 

//시작함수
fun main(){

    //Crew클래스 객체 생성 및 멤버 여러개를 사용해보기
    val crew= Crew()    
    crew.name="sam"
    crew.age= 20
    crew.address="seoul"
    crew.show()

}//main 함수 종료...

//어떤 그룹의 크루원 정보 저장 클래스 
class Crew{
    
    //property [ 이름, 나이, 주소 ]
    var name:String?= null
    var age:Int?= null
    var address:String?= null

    //method - property 출력 기능
    fun show(){
        println("$name : $age , $address")
    }
}

//**출력**
sam : 20 , seoul

 

이미 객체에 대한 학습을 진행했다면 어렵지 않게 객체를 생성하고 멤버를 사용하기 위해 . 연산자로 멤버에 접근하여 사용했을 겁니다. 여기까지는 특별한 내용이 없습니다. 자바에서도 많이 했던 작업입니다.

 

근데. 조금 생각해 볼까요? 위 Crew 객체의 멤버는 총 4개 입니다. property 3개, method 1개 입니다.

이 멤버 4개(변수3,메소드1)를 사용할때 마다 객체명.xxx 라고 쓰는게 생각보다 번거롭고 코드의 실수가능성도  많으며 가독성도 떨어집니다. 위 코드를 보면 멤버를 사용할 때 마다 crew.neme , crew.age, crew.xxx 형태로 매번 crew 라고 쓰는거 좀 짜증입니다. 다행히 지금은 객체명이 다소 짧은 crew 였습니다. 4글자에 불과하죠. 근데 만약 더 길다면? 예를 들어 sharedPreferences 처럼 긴 이름를 사용해야 한다면 매번 작성하기 너무 번거롭습니다. 복사붙이기로도 해소하기 곤란한 내용이며 타자 실수도 나올 가능성이 많습니다. 이를 위해 등장한 문법이 Scope(스코프:범위) functioin(함수) 입니다.

 

스코프 함수의 종류는 5개 정도 있는데 일단, 이 중에 가장 많이 사용되는 apply 를 이용하여 간략하게 소개하겠습니다.

코틀린의 모든 객체는 기본적으로 스코프함수를 사용할 수 있으며 함수이지만 통상 범위 영역을 만들기위해 람다식으로 { } 를 작성합니다.

//시작함수
fun main(){

    //Crew클래스 객체 생성 및 멤버 여러개를 사용해보기
    val crew= Crew()    
    crew.name="sam"
    crew.age= 20
    crew.address="seoul"
    crew.show()
    
    //스코프 함수로 멤버 여러개 사용해보기
    var crew2= Crew()
    crew2.apply {
        //이 apply{} 영역 안에서는 this가 crew2를 의미함. 또한 this는 생략이 가능함
        this.name= "robin"
        age= 25
        address="busan"
        show()
    }

}//main 함수 종료...

 

코드에서 볼 수 있듯이 특정 객체(crew2)의 멤버 범위(scope) 영역을 만들어주는 스코프함수 apply의 중괄호 {} 영역 안에서는 객체명을 사용할 필요 없이 마치 class 내부에서 처럼 this 키워드를 사용할 수 있습니다. 이 this 참조변수가 바로 crew2를 의미하게 되는 문법입니다. 즉, crew2.name 대신에 this.name 으로 작성이 가능하며 나아가 class의 중괄호 {} 안에서 그랬듯이 this 키워드는 생략이 가능하기에 this.age 를 범위 안에서는 this 없이 그냥 age만 작성할 수 있어 코드가 매우 간결해 지는 특징이 있는 문법입니다.

위처럼 영역을 묶었기에 참조변수명을 잘못 기입하는 실수를 줄일 수 있고 다른 개발자가 볼때도 crew2에 대한 설정들을 하나의 영역에 묶어서 가독성이 좋아집니다.

 

※ 원래는 아래처럼 객체생성후 apply 라는 이름의 함수를 추가하는 것입니다. 일종의 확장함수(extension) 같습니다.  이를 람다식처럼 줄여서 ()를 생략한 문법인 것 입니다. 거의 대부분 람다식으로 작성합니다.

// 원래는 이런식으로 객체생성후 apply 라는 이름의 함수를 추가하는 것임( 일종의 확장함수 ) -- 이를 람다식처럼 줄여서 ()를 생략한 문법임
crew2.apply(){
}

 

 

2. 스코프 함수의 종류

스코프 함수는 크게 2가지 분류로 구분할 수 있습니다.

 

1) 영역안에서 this 키워드로 본인을 참조하는 scope function : apply, run
2) 영역안에서 this 키워드 대신 it으로 본인객체를 참조하는 scope function : also, let   [ 마치 람다식 처럼 ]

 

두 분류의 차이를 대략적으로 알아보고 자제한 내용을 이어서 소개하겠습니다.

영역안에서 this 키워드로 본인을 참조하는 것은 위에서 살펴보았으니 it 키워드로 본인을 참조하는 스코프 함수 중 also 를 이용하여 crew의 멤버값을 사용해 보겠습니다.

//시작함수
fun main(){

    //Crew클래스 객체 생성 및 멤버 여러개를 사용해보기
    val crew= Crew()    
    crew.name="sam"
    crew.age= 20
    crew.address="seoul"
    crew.show()
    
    //스코프 함수로 멤버 여러개 사용해보기
    var crew2= Crew()
    crew2.apply {
        //이 apply{} 영역 안에서는 this가 crew2를 의미함. 또한 this는 생략이 가능함
        this.name= "robin"
        age= 25
        address="busan"
        show()
    }
    
    //영역안에서 it을 사용하는 스코프 함수
    val crew3= Crew()
    crew3.also {
        // also{} 영역 안에서는 it이 crew3을 의미함. it은 생략 불가
        it.name= "hong"
        it.age= 30
        it.address="paris"
        it.show()
    }

}//main 함수 종료...

 

코드에서 확인 가능하듯이 영역안에서 this 를 it 으로 변경한 것만 차이가 있고 기능적인 부분은 똑같기에 상황에 따라 어떤 스코프 함수를 사용하든 상관없습니다. 경우에 따라 더 적합한 것이 다르기에 지금 당장 어떤 것을 써야할 지 확실히 구분하려 할 필요는 없습니다. 경험이 쌓이면 자연스럽게 선택하여 사용이 가능해 집니다. 조금해 하지 마세요.

 

그런데. 이렇게 2개의 분류가 있으니 스코프 함수도 2개면 될 것 같은데 각 분류마다 2개씩 존재하는 이유가 뭘까요?

if 표현식을 기억하시나요? if문의 마지막 실행문의 결과 값을 리턴하여 사용할 수 있습니다.

스코프 함수도 리턴이 있습니다. 단지 함수 축약표현에 의해 생략된 것입니다. 

이 리턴값이 다릅니다. 한 종류는 객체 본인의 참조값을 리턴하고, 또 다른 종류는 마지막 실행문의 값이 리턴됩니다.

 

정리하면.

  리턴값이 본인객체 리턴값이 마지막 실행문의 값
영역안에서 this 키워드 사용 apply run
영역안에서 it 키워드 사용 also let

 

 

각각의 사용 모습을 알아보겠습니다.

 

1) 영역안에서 this 키워드 사용

 

1.1) apply - 본인객체를 return

// 1.1) apply - 본인객체를 return
val crew4:Crew= Crew()
val crew5:Crew= crew4.apply {
    name="kim"   //this. 키워드 생략 가능
    age= 40
    address="newyork"
    //별도의 리턴 명시가 없지만 무조건 return this 인것임
}
crew5.show()

 

1.2) run - 마지막 실행문이 return 값

// 1.2) run - 마지막 실행문이 return 값
val crew6:Crew= Crew()
val len= crew6.run {
    name="park"
    age= 45
    address="tokyo"
    name?.length //마지막 실행문이 리턴값  - 글자수 4 리턴됨
}
println("이름의 글자수 : $len")

 

 

2) 영역안에서 it 키워드 사용

 

2.1) also -  본인객체를 return

//2.1) also - it 키워드를 사용하며 본인객체를 return
val crew7:Crew= Crew()
val crew8:Crew= crew7.also {
    it.name="lee"
    it.age= 50
    it.address="incheon"
    //별도의 리턴 명시가 없지만 무조건 return this 인것임
}
crew8.show()

 

2.2) let - 마지막 실행문이 return 값

//2.2) let - it 키워드를 사용하며 마지막 실행문이 return 값
val crew9:Crew= Crew()
val n:String?= crew9.let {
    it.name="choi"
    it.age=20
    it.address="LA"
    it.name?.uppercase()  //마지막 실행문의 결과가 리턴   -- 이름을 대문자로 변경한 결과값 "CHOI" 리턴됨
}
println("이름의 대문자 출력 : $n")

 

 

 

추가로. 위 4개의 scope 함수의 문법적 표기법이 다소 다른 scope 함수가 존재합니다. 

영역을 만들려는 객체에게 스코프함수를 적용하는 것이 아니라 스코프함수의 영역에 객체를 전달하는 형태의 모습입니다. 

 

3) with 

스코프함수 run과 비슷한 동작을 하지만 문법적 사용이 다른 스코프 함수 입니다.

//** scope function 중 run과 비슷한 동작을 하는 with가 있음. 사용문법이 약간 다름.... **
val crew10:Crew= Crew()
with(crew10){      //범위 안에 객체를 파라미터로 전달받는 형태
    name="aaa"
    age=20
    address="HANOI"
    show()
    name?.lowercase()  //리턴값을 마지막 실행문의 결과
}

 

 

5가지 스코프 함수를 알아봤는데 앞서 설명했듯이 스코프 함수를 많이 사용하다 보면 자연스럽게 상황에 맞는 종류의 스코프함수를 선택하여 사용하게 됩니다. 지금부터 완벽하게 효율적으로 구분하여 사용하려고 노력하지 않아도 됩니다.


 

스코프 함수는 앱 개발에서 같은 객체에게 여러작업을 할때 유용하게 사용할 수  있습니다.

[ex. AlertDialog.Builder, Notification.Builder, SharedPreference.Editor ....]

 

마지막으로, 안드로이드 앱 개발에서 스코프 함수를 사용하는 코드의 예를 보여드리겠습니다.

현재 파일은 일반 코틀린 파일이기에 따라해도 에러가 발생합니다. MainActivity.kt 액티비티 안에서 사용하는 것을 가정하여 코드만 소개하는 것이니 눈으로만 보시며 스코프함수의 효용성을 느껴보시길 기대합니다.

 

- 스코프 함수를 사용하지 않고 다이얼로그를 생성하는 코드  ( 스코프 함수 X )

val builder:AlertDialog.Builder= AlertDialog.Builder(this)
builder.setTitle("aaa")
builder.setMessage("bbbb")
builder.setPositiveButton("OK", null)
builder.setNegativeButton("CANCEL", null)
val dialog=builder.create()
dialog.show()

 

- 스코프 함수를 사용하여 다이얼로그를 생성하는 코드  ( 스코프 함수 O )

val builder2:AlertDialog.Builder= AlertDialog.Builder(this)
val dialog2= builder2.run{
    setTitle("sss")
    setMessage("asdfasdfa")
    setPositiveButton("확인", null)
    setNegativeButton("취소", null)
    create()      // 빌더가 생성하는 dialog 객체가 리턴됨
}
dialog2.show()
반응형
반응형

이번 글은 대부분의 개발자들을 고통스럽게 했던 null 값에 대한 코틀린의 대응 문법에 대한 내용입니다.

 

참조변수가 객체를 참조하지 않은 상태에서 객체의 기능을 사용하려 했을 때 많이 만나는 NullPointerException[NPE]은 코드를 작성할 때 발견되지 않고 앱이 실행중에 발생하기에 코딩 중 문제를 발견하기 어렵고 초보자들의 경우 예외처리에 대한 부분에 미흡하여 앱이 다운되는 주요 원인 중 하나가 되어 개발자들 사이에 아주 짜증나는 문제 중 하나입니다. 

NPE는 이 문제의 원인을 제공한 null 이라는 값을 만든 토니호어(Tony Hoare) 조차도 2009년 강연에서 이 문제에 대해 본인을 자책하는 말을 남겨 더욱 유명해 졌습니다.

 

null값 : 토니 호어'(Tony Hoare)가 1965년에 처음 발명, -- 퀵정렬 이론을 만든 사람

 

2009년 강연 중..

 

 (널 포인터는) 내 10억 달러짜리 실수였다. 1965년 당시, 나는 ALGOL W라는 객체 지향 언어에 쓰기 위해 포괄적인 타입 시스템을 설계하고 있었다.
    내 원래 목표는 어떤 데이터를 읽든 항상 안전하도록 컴파일러가 자동으로 확인해 주는 것이었다.
그러나 나는 널 포인터를 집어넣으려는 유혹을 이길 수가 없었다.

    그렇게 하는 게 훨씬 쉬웠기 때문이다. 이 결정은 셀 수도 없는 오류와 보안 버그, 시스템 다운을 낳았다.
지난 40년 동안 이러한 문제들 때문에 입은 고통과 손해는 10억 달러는 될 것이다.



 

 

코틀린은 NullPointerException[NPE]에 대한 앱의 버그를 문법에서부터 막아주기 위한 Null safety 관련 문법들을 제공합니다.

 

새로운 파일 Test09NullSafe.kt 을 만들고 문법들에 대해 알아봅시다.

 

프로그램의 시작함수 main() 함수를 만들고 문법들에 대해 알아보겠습니다.

 

 

1. null 값을 저장할 수 있는 nullable 변수

코틀린은 null값을 저장할 수 있는 타입을 명시적으로 구분하여 사용하도록 하고 있습니다. 이를 통해 코드를 작성하는 시점에 이 변수가 널값을 가지고 있을 수 있다는 것을 개발자에게 인식하게 하며 문법적으로 강제로 null 값에 대한 처리를 하도록 하였습니다.

 

1.1) non nullable 변수 - null 값을 저장할 수 없는 변수 

var s1:String  = null //ERROR - non nullable 변수

 

1.2) nullable 변수 - null 값을 저장할 수 있는 변수 [ 자료형(type)뒤에 ? 추가]

var s2:String? = null //OK  - nullable 변수

 

 

두 타입의 변수가 어떻게 다른지 알아보면서 nullable 변수를 안전하게 사용하기 위한 몇가지 특별한 연산자 들을 소개하도록 하겠습니다.

fun main(){

    //null 값을 저장할 수 없는 변수와 있는 변수.
    var str1:String = "Hello"   // non nullable type
    var str2:String? = "Nice"   // nullable type

}

 

두 참조변수 모두 실제 문자열객체를 참조하고 있습니다. 이제 각각의 참조변수를 이용하여 문자열 객체의 글자수(길이)를 화면에 표시해보겠습니다. 이미 잘 알고 있듯이 String 객체는 본인이 가진 문자열의 길이값을 length 라는 프로퍼티(멤버변수)에 저장하고 있습니다.

 

먼저 null 값을 저장할 수 없는 non nullable variable 인 str1 변수의 length를 사용해 보겠습니다. 자바와 특별히 다른 점이 없습니다.

fun main(){

    //null 값을 저장할 수 없는 변수와 있는 변수.
    var str1:String = "Hello"   // non nullable type
    var str2:String? = "Nice"   // nullable type
    
    //non nullable 변수가 참조하는 문자열 객체의 글자수(길이) 출력하기
    println("글자수 : " + str1.length)    // OK :   . 연산자를 통해 String객체의 멤버 접근하여 사용

}

//**출력**
글자수 : 5

 

이번에는 null 값을 저장할 수 있는 nullable variable 인 str2 변수의 length를 사용해 보겠습니다. ERROR가 발생하는 것을 확인하세요.

fun main(){

    //null 값을 저장할 수 없는 변수와 있는 변수.
    var str1:String = "Hello"   // non nullable type
    var str2:String? = "Nice"   // nullable type
    
    //non nullable 변수가 참조하는 문자열 객체의 글자수(길이) 출력하기
    println("글자수 : " + str1.length)    // OK :   . 연산자를 통해 String객체의 멤버 접근하여 사용
    
    //nullable 변수가 참조하는 문자열 객체의 글자수(길이) 출력시도하기
    println("글자수 : " + str2.length)  // ERROR :  String? 타입은 null 일 수도 있다는 것이기에 . 연산자가 언제나 안전하게 멤버를 가져올 수 있다고 확신할 수 없음 - 실제 null 이 아니어도 사용불가 [ 객체가 없을때 NullPointException 발생 ]

}

 

str1 과 다르게 문법적 에러를 발생합니다. str2 참조변수가 실제 문자열 객체를 참조하는 것과는 상관없이 nullable 타입은 언제든 객체를 참조하지 않을 수도 있기에 컴파일 단계에서부터 개발자에게 null 값인지를 체크하는 코드를 추가하여 런타임(실행 중) 예외가 발생하지 않도록 코드를 작성하도록 강제하는 것입니다.

 

그럼 해결해볼까요? 일단, 자바에서 처럼 if 조건문을 통해 null 체크 코드로 해결해 보겠습니다.

fun main(){

    //null 값을 저장할 수 없는 변수와 있는 변수.
    var str1:String = "Hello"   // non nullable type
    var str2:String? = "Nice"   // nullable type
    
    //non nullable 변수가 참조하는 문자열 객체의 글자수(길이) 출력하기
    println("글자수 : " + str1.length)    // OK :   . 연산자를 통해 String객체의 멤버 접근하여 사용
    
    //nullable 변수가 참조하는 문자열 객체의 글자수(길이) 출력시도하기
    //println("글자수 : " + str2.length)  // ERROR :  String? 타입은 null 일 수도 있다는 것이기에 . 연산자가 언제나 안전하게 멤버를 가져올 수 있다고 확신할 수 없음 - 실제 null 이 아니어도 사용불가 [ 객체가 없을때 NullPointException 발생 ]
    
    //해결방법 - null인지 확인하는 if문 코드 필요
    if(str2!=null) println("글자수 : " + str2.length) //if문이 참일때만 실행되는 영역이어서 String?도 사용가능함.

}

//**출력**
글자수 : 5
글자수 : 4

 

if() 조건문이 참(true)인 영역에 작성하기에 str2는 무조건 객체를 참조하고 있다는 것이 확실하기에 컴파일 단계에서 에러로 표시하지 않도록 합니다. 자바에서 많이 사용하던 null check 방법입니다. 

근데 위 해결방법이 좀 코드가 번거로운 것 같습니다. 그래서 등장한 코틀린의 null 안정성 문법을 소개하고자 합니다.

 

♣ null을 안전하게 사용하기 위한 몇가지 연산자

 

1) ?. 연산자  : null safe 연산자

코틀린은 if 처리 코드를 조금 더 간결하게 표시하기 위해 null 이 아닐때만 . 연산자로 멤버를 접근하는 널 안전 연산자를 제공합니다.

println("글자수 : " + str2?.length)  // null이 아니면 멤버에 접근, null이면 그냥 null 을 결과로 줌

//**출력**
글자수 : 4

 

만약 str2 가 정말로 null 이라면 null 값이 출력됩니다.

str2= null;
println("글자수 : " + str2?.length)  // null 출력

//**출력**
글자수 : null

 

근데 객체가 null일때 그냥 null로 값이 전달되는 것이 싫고 내가 원하는 값으로 나왔으면 할 수 도 있습니다.
만약 객체가 null이면 길이값을 -1 로 주고 싶다면 아래처럼 if - else 를 사용해야 할겁니다.

val len= if(str2!=null) str2.length else -1
println(len)

//**출력**
-1

 

근데 if 표현식이 조금 지저분해 보이네요. 이를 간결하게 하기 위한 2번째 연산자를 소개하겠습니다.

 

 

2) ?: 연산자 -  엘비스[Elvis] 연산자

연산자에 사용된 ? 의 모양이 엘비스 프레슬리의 머리부터 구렛나루 모양과 흡사하다고 해서 엘비스 연산자 라고 부릅니다. 좀 어이없지만요. ?: 연산자의 앞에 작성한 참조변수가 null 이면 ?: 연산자 뒤에 작성된 값이 결과값이 되는 연산자 입니다. 

val len2= str2?.length ?: -1   // ?: 연산자 뒤에 null일때 원하는 값을 지정
println(len2)

//**출력**
-1

 

위 if - else 코드에 비해 훨씬 간결해 진 코드를 볼 수 있습니다. 아주 맘에 드는 연산자 입니다. 연산자 이름도 맘에 듭니다.^^

 

위 2가지 null 안전 연산자를 이용하여 코드를 작성하기 하지만 ? 를 표시하는 것이 귀찮거나 굳이 nullable variable 로 결과값을 받아 사용하는 것이 필요치 않을수도 있습니다.

 

이런 NPE에 안전한 연산자를 쓰지 않고 그냥 자바에서처럼 실수로 null 참조변수를 사용하면 Exception이 발생하여 앱이 종료되로록 하고싶다면?
즉, 개발자가 의도하지 않게 null이면 앱이 꺼지도록 하여 앱을 더이상 실행되게 하고 싶지 않다면 이 참조변수가 null 이 아니라고 주장하면 됩니다. 

 

 

3) non-null asserted(주장된,단언된) call !! 연산자

이 참조변수는 null 이 아니니 컴파일단계에서 에러로 표시하지 말라고 주장하는 연산자 입니다.

var ss:String?= "Hello"
//println( ss.length )  //ERROR - String? 타입은 NPE 발생이 안되도록 문법적으로 못쓰도록 하고 있음
println( "글자수 : " + ss!!.length  )

//**출력**
글자수 : 5

 

만약 실제 참조변수가 null 을 참조하고 있었다면 컴파일 단계에서는 에러는 아니지만 런타임(실행 중)에 예외가 발생하여 앱이 다운됩니다.

//null참조객체
val sss:String?= null
println( "글자수 : " + sss!!.length  ) //Exception -- 실행 중 예외발생

//**출력**
Exception in thread "main" java.lang.NullPointerException

 

 

마지막으로 null에 대한 대응은 아니지만 안전하고 간결한 예외처리를 제공하는 연산자를 소개하고 마무리 하겠습니다.

 

4) 안전한 casting 연산자 as?  - 자료형이 맞지 않는 타입을 억지로 형변환 하는 경우

코틀린은 참조형 타입의 형변환을 위한 연산자로 as( ~로서) 를 사용합니다. 그러다 만약, 형변환이 불가능한 타입으로 변환을 시도하면 런타임(실행 중) 예외가 발생하여 앱이 다운됩니다. 컴파일 단계에서 에러를 표시하지 않기에 개발자가 발견하기 어려운 예외입니다.

이를 위해 형변환 중 예외가 발생하면 그냥 null 값으로 결과를 주는 안전한 형변환(type casting) 연산자가 as? 입니다.

 

참조형 타입의 형변환을 실습해 보기 위해 main()함수 아래에 MMM 과 ZZZ 라는 이름의 클래스를 설계하겠습니다. 

멤버변수는 둘다 a 변수 1개씩 가진 비슷한 모양이지만 둘은 전혀 상관없는 class 입니다. 서로 연관이 없는 클래스이기에 형변환이 불가능한 클래스입니다.

//시작 함수..
fun main(){

}// main() 함수 종료..

//4) 안전한 캐스팅 실습용 클래스들
class MMM{
    var a=10
}

class ZZZ{
    val a=20
}

 

main 함수에 MMM 객체를 생성한 후 zzz 참조변수에 대입해 보겠습니다.

fun main(){

    val mmm:MMM= MMM()

    //전혀 다른 타입 참조변수에 대입
    val zzz:ZZZ= mmm       //문법적으로 error - 타입이 전혀 다르므로.
    
}

//4) 안전한 캐스팅 실습용 클래스들
class MMM{
    var a=10
}

class ZZZ{
    val a=20
}

 

당연히 컴파일 단계에서 문법적으로 에러를 발생합니다. 전혀 다른 타입을 대입하려 했으니 당연합니다.

이때, 억지로 MMM객체를 ZZZ 타입으로 형변환 하여 대입해보겠습니다.

fun main(){

    val mmm:MMM= MMM()

    //전혀 다른 타입 참조변수에 대입
    //val zzz:ZZZ= mmm       //문법적으로 error - 타입이 전혀 다르므로.
    
    //억지로 as 연산자를 통해 형변환
    val zzz:ZZZ = mmm as ZZZ  //문법적으로는 error가 없지만 Class cast Exception 발생함!
    
}

//4) 안전한 캐스팅 실습용 클래스들
class MMM{
    var a=10
}

class ZZZ{
    val a=20
}

 

명시적인 형변환 연산자 as 를 사용함으로서 컴파일 단계에서는 에러로 인지하지 않습니다. 하지만 실제 런타임(실행 중)에 형변환이 불가능하여 예외가 발생하며 앱이 다운됩니다. 

 

이때 혹시 잘못된 casting을 해도 예외로 인해 앱이 다운되지 않고 그냥 null을 리턴하도록 하고 싶다면 안전한 캐스팅 연산자 as? 를 사용합니다.

fun main(){

    val mmm:MMM= MMM()

    //전혀 다른 타입 참조변수에 대입
    //val zzz:ZZZ= mmm       //문법적으로 error - 타입이 전혀 다르므로.
    
    //억지로 as 연산자를 통해 형변환
    //val zzz:ZZZ = mmm as ZZZ  //문법적으로는 error가 없지만 Class cast Exception 발생함!
    
    //이때 혹시 잘못된 casting을 해도 예외로 인해 앱이 다운되지 않고 그냥 null을 리턴하는 안전한 캐스팅 연산자 as?
    val zzz:ZZZ? = mmm as? ZZZ?
    println(zzz) // null출력   - 객체가 실제로 캐스팅이 되었다면 [클래스명@해시코드] 가 출력됨
    
}

//4) 안전한 캐스팅 실습용 클래스들
class MMM{
    var a=10
}

class ZZZ{
    val a=20
}

 

 

반응형
반응형

이번 글에서는 안드로이드 앱 개발 과정에 매주 자주 사용되는 몇가지 문법들에 대해 간략하게 소개하고자 합니다. 

 

1)이너클래스

2)인터페이스

3)익명클래스
4)static 의 기능인 companion object[동반객체]
5)늦은초기화 - lateinit, by lazy

 

1)~3) 까지는 자바에도 존재하는 문법이기에 코틀린 언어의 문법적 차이점을 위주로 살펴보시기 바랍니다.

4)~5) 는 자바에는 없던 새로운 문법이니 다소 생소하겠지만 앱 개발과정에 자주 사용되고 중요하니 주의깊게 보시길 바랍니다.

 

그럼, 위 5가지의 문법을 알아보도록 하겠습니다.


 

새로운 코틀린 파일 Test08OOP8.kt 를 만들고 문법을 알아보겠습니다.

코틀린 프로그램의 시작은 언제나 main() 함수이니 만들고 시작하겠습니다.

 

1. 이너 클래스 Inner class

클래스 안에 설계하는 클래스를 이너 클래스(inner classs)라고 부릅니다. 이너 클래스는 다음의 몇가지 특징을 가지고 있습니다.

- 이너클래스는 감싸고 있는 아웃터 클래스 밖에서는 인식되지 않는다. 인식하려면 아웃터클래스명.이너클래스명 으로 작성함.

- 이너클래스는 아웃터 클래스 밖에서 객체를 생성할 수 없음.

- 이너클래스는 아웃터 클래스의 멤버를 곧바로 사용할 수 있음.

 

코틀린의 이너클래스가 자바와 다른 점은 inner 키워드를 사용해야 한다는 겁니다. 안써도 에러는 아니지만 아웃터의 멤버를 사용할 수 없기에 이너클래스라고 볼 수 없는 클래스가 됩니다.

AAA 클래스 안에 BBB 라는 이름의 이너클래스를 만들어 보겠습니다.

//시작 함수
fun main(){

}//main 함수 종료..

//1. 클래스안에 inner 클래스만들기
class AAA{
    var a:Int=0

    fun show(){
        println("AAA클래스이 show")
        println()
    }

    //이너클래스 [ 자바와 다르게 inner키워드가 없으면 아웃터클래스의 멤버를 마음대로 사용할 수 없음 ] ////////////////
    inner class BBB{

        fun show(){
            println("BBB 클래스의 show") 
        }
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////////
}

 

 

이너클래스의 특징을 하나씩 확인해 보겠습니다.

 

1.1) 이너클래스는 감싸고 있는 아웃터 클래스 밖에서는 인식되지 않는다. 인식하려면 아웃터클래스명.이너클래스명 으로 작성함.

당연하게도 이너 클래스는 아웃터 클래스 안에 숨어 있는 설계도면 같은 것 이기에 그냥은 인식될 수 없겠죠. 아웃터 클래스 안에 있는 멤버를 사용하듯이 . 연산자로 안에 있다는 것을 명시적으로 표시해야 합니다.

//시작 함수
fun main(){

    //1. 이너클래스
    val bbb: BBB       //ERROR - BBB 이너클래스를 인식하지 못함
    val bbb: AAA.BBB   //OK - 아웃터 클래스명 AAA를 통해 인식 가능

}//main 함수 종료..

 

1.2) 이너클래스는 아웃터 클래스 밖에서 객체를 생성할 수 없음.

아웃터 클래스명을 통해 인식은 가능하더라도 아웃터 클래스 밖에서는 객체를 생성할 수 없습니다.

//시작 함수
fun main(){

    //1. 이너클래스
    //val bbb: BBB               //ERROR - BBB 이너클래스를 인식하지 못함
    //val bbb: AAA.BBB           //OK - 아웃터 클래스명 AAA를 통해 인식 가능
    val bbb: AAA.BBB = AAA.BBB() //ERROR - 아웃터 클래스 밖에서는 객체 생성 불가

}//main 함수 종료..

 

그럼 이너클래스는 어떻게 객체를 생성할 수 있을 까요? 이너 클래스는 아웃터 클래스가 존재할 때만 만들도록 강제하고자 하는 목적이 있기에 아웃터 클래스에서만 만들 수 있도록 하였습니다. 즉, 아웃터 클래스 안에서는 이너클래스를 생성할 수 있다는 것 입니다. 그래서 보통 아웃터 클래스 안에 이너 클래스 객체를 생성하여 리턴 해주는 기능 메소드를 정의하고 이를 호출하여 이너클래스 객체를 얻어옵니다. 앱 개발에 사용되는 APIs 중에도 이런 식으로 객체를 얻어오는 기능이 아주 많습니다. 

 

AAA 클래스 안에 BBB 이너클래스 객체를 생성하여 리턴해주는 기능 메소드 getBBBInstance() 를 직접 정의해 호출해 보겠습니다.

//시작 함수
fun main(){

    //1. 이너클래스
    //val bbb: BBB                 //ERROR - BBB 이너클래스를 인식하지 못함
    //val bbb: AAA.BBB             //OK - 아웃터 클래스명 AAA를 통해 인식 가능
    //val bbb: AAA.BBB = AAA.BBB() //ERROR - 아웃터 클래스 밖에서는 객체 생성 불가
    
    val obj: AAA = AAA() // 아웃터 객체 생성
    val obj2: AAA.BBB = obj.getBBBInstance()  //이너클래스 객체를 생성하여 리턴해주는 메소드 이용
    obj2.show()

}//main 함수 종료..

//1. 클래스안에 inner 클래스만들기
class AAA{
    var a:Int=0

    fun show(){
        println("AAA클래스이 show")
        println()
    }

    // BBB 이너클래스를 객체로 생성하여 리턴해주는 메소드
    fun getBBBInstance() : BBB{
        return BBB()
    }

    //이너클래스 [ 자바와 다르게 inner키워드가 없으면 아웃터클래스의 멤버를 마음대로 사용할 수 없음 ] ////////////////
    inner class BBB{

        fun show(){
            println("BBB 클래스의 show") 
        }
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////////
}

//**출력**
BBB 클래스의 show

 

이 특징은 이너클래스를 아웃터 클래스의 종속관계로 함으로서 개발자가 실수로 아웃터 없이 이너객체를 생성하는 실수를 방지 해 줍니다.

예를 들어 다이얼로그 안에 있는 전용 버튼처럼 다이얼로그가 없는데 전용 버튼만 따로 만들어 지게 하지 않고 싶을 때 전용 버튼을 다이얼로그 이너 클래스로 만들어 사용합니다.

 

1.3) 이너클래스는 아웃터 클래스의 멤버를 곧바로 사용할 수 있음.

이너 클래스를 사용할 때 개발자에게 아주 매력적인 특징으로서 아웃터의 멤버를 내 것인양 사용할 수 있다는 것은 프로그래밍 과정에서 주입코드를 작성할 필요 없이 아웃터의 멤버를 제어할 수 있어 종속기능을 수행하기 용이합니다. 

잘 생각해보면 이너클래스를 작성하는 위치가 아웃터 클래스 영역안에 있는 멤버변수, 멤버함수와 같은 위치에 있기에 멤버라고 볼 수 있습니다. 그렇기에 다른 멤버를 사용할 수 있는 것은 어찌보면 당연한 문법적 허용입니다.

또한, 아웃터 객체없이는 이너클래스 객체가 생성될 수 없는 1.2) 특징이 있었기에 이너 안에서 사용하는 멤버는 반드시 존재할 수 밖에 없기에 다른 클래스의 멤버지만 안전하게 사용이 가능합니다.

 

BBB 이너클래스의 show() 기능 메소드를 수정하여 아웃터의 멤버변수(프로퍼티)를 사용해 보겠습니다.

//시작 함수
fun main(){

    //1. 이너클래스
    //val bbb: BBB                 //ERROR - BBB 이너클래스를 인식하지 못함
    //val bbb: AAA.BBB             //OK - 아웃터 클래스명 AAA를 통해 인식 가능
    //val bbb: AAA.BBB = AAA.BBB() //ERROR - 아웃터 클래스 밖에서는 객체 생성 불가
    
    val obj: AAA = AAA() // 아웃터 객체 생성
    val obj2: AAA.BBB = obj.getBBBInstance()  //이너클래스 객체를 생성하여 리턴해주는 메소드 이용
    obj2.show()

}//main 함수 종료..

//1. 클래스안에 inner 클래스만들기
class AAA{
    var a:Int=0

    fun show(){
        println("AAA클래스이 show")
        println()
    }

    // BBB 이너클래스를 객체로 생성하여 리턴해주는 메소드
    fun getBBBInstance() : BBB{
        return BBB()
    }

    //이너클래스 [ 자바와 다르게 inner키워드가 없으면 아웃터클래스의 멤버를 마음대로 사용할 수 없음 ] ////////////////
    inner class BBB{

        fun show(){
            println( "부모클래스의 프로퍼티 a : $a ") //부모의 멤버를 마음대로

            //아웃터 클래스의 this사용법이 Java와 다름!! [ this@부모클래스명 ]
            //아웃터클래스의 show를 호출하고 싶다면..
            this@AAA.show();
        }
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////////
}

//**출력**
부모클래스의 프로퍼티 a : 10
AAA클래스이 show

 

이너 클래스 안에서 아웃터 클래스와 같은 이름의 show()메소드가 존재하기에 이너 클래스 안에서 부모클래스를 명시적으로 구분하여 호출하고 싶다면 부모클래스.this 가 필요합니다. 다만, 코틀린에서는 이 부모 클래스의 this 참조변수를 this@부모클래스명 으로 Label 문법으로 작성하는 것이 차이가 있습니다.

앱 개발 할 때 버튼 이벤트 처리에 사용되는 익명클래스 안에서 Toast 같은 작업에 사용되었던 MainActivity.this 를 코틀린에서는 라벨 문법을 사용하여 this@MainActivity 를 쓰는 것도 이와 같습니다. 

 

 

2. 인터페이스 interface

인터페이스는 자바와 다른 특징은 없습니다. 

잘 알다시피 인터페이스는 규격을 만들기 위한 목적이기에 기능 코드가 작성되지 않은 이름만 있는 추상 메소드만 가지는 클래스 입니다. 

// 인터페이스는 특별할 것 없음
interface Clickable{
    //추상메소드
    fun onClick()
}

 

코틀린이라고 다를 것 없이 인터페이스는 곧바로 객체를 생성할 수 없습니다.

//시작 함수
fun main(){

    //2. 인터페이스
    val c= Clickable() //ERROR :인터페이스는 곧바로 생성(new)할 수 없어서..

}//main 함수 종료..

//2. 인터페이스는 특별할 것 없음
interface Clickable{
    //추상메소드
    fun onClick()
}

 

인터페이스를 사용하려면 이름만 있는 추상메소드를 구현하는 class 를 만들어서 기능을 완성한 후 객체를 생성하여 사용합니다.

인터페이스를 사용함으로서 이 인터페이스를 구현한 클래스의 객체들은 반드시 추상메소드를 구현해야만 하기에 같은 이름의 기능메소드를 가질 수 밖에 없도록 강제합니다. 

자바 앱 개발을 해보셨다면 이미 익숙하셨을 Button 의 클릭 이벤트 처리에 반응하는 리스너(Listener)가 인터페이스로 되어 있지요. 안드로이드에서 리스너를 인터페이스로 설계하여 제공하였기에 어떤 개발자든 버튼이 클릭되면 onClick() 메소드가 발동 하도록 코드를 작성할 수 밖에 없기에 유지관리가 용이합니다. 어떤 버튼은 onPress, 어떤 버튼은 onDown, 어떤 버튼은 onClick.. 등으로 매번 다르게 만들면 유지보수 하는 입장에서 해당 이벤트 처리 코드를 찾기 어려웠을 겁니다. 그렇기에 onClick()이라는 이름의 추상메소드를 가진 리스너 인터페이스를 구현하는 클래스를 만들고 onClick() 추상메소드의 기능을 해당 버튼에 맞게 만들면 규격화된 프로그래밍이 가능합니다.

그래픽적으로 화면을 구현하는 GUI 프로그래밍에 아주 많이 사용되는 문법입니다.

 

그럼. 위에서 안드로이드의 클릭 리스너와 비슷한 느낌으로 설계한 Clickable 이라는 인터페이스를 구현(implement)하는 Test 라는 이름의 클래스를 만들고 추상메소드 onClick()를 구현하고 사용해 보겠습니다.

 

자바와 문법적으로 다른 점

- 인터페이스를 구현하는 키워드 implement 대신에 코틀린의 상속처럼 콜론 : 키워드 사용.

- 상속때와 다르게 부모클래스의 위치에 있는 인터페이스 뒤에 생성자() 호출을 하지 않음. (인터페이스는 객체생성이 안되니 당연하겠죠.)

//시작 함수
fun main(){

    //2. 인터페이스
    //val c= Clickable() //ERROR :인터페이스는 곧바로 생성(new)할 수 없어서..
    
    //clickable을 구현한 Test클래스 객체 생성
    val t= Test()
    t.onClick()

}//main 함수 종료..

//2. 인터페이스는 특별할 것 없음
interface Clickable{
    //추상메소드
    fun onClick()
}

//인터페이스를 구현하는 클래스 [ 클래스 상속과 문법적으로 표현법이 비슷해보이지만 주생성자 호출()문이 없음 :  (다중 구현할때는 , 를 사용) ]
class Test : Clickable{

    //추상메소드 구현  - 기능구현도 이미 Clickable에 있는 함수명과 같은 함수를 만드는 만큼 override 키워드 필요
    override fun onClick() {
        println("click!!!!")
        println()
    }
}


//**출력**
click!!!!

 

자바의 경우 상속할 때는 extends, 구현할 때는 implement 를 사용해야 해서 구별이 명확하지만 코드가 번거로운 반면에 코틀린의 경우에는 클래스 상속이나 인터페이스 구현이나 모두 콜론 : 키워드로 같기에 구별이 확 되지는 않지만 코드가 간결합니다. 그래서 코틀린의 경우 상속인지 구현인지를 코드 상으로 구별할 때 부모클래스 옆에 생성자 호출문이 있느냐 없느냐로 1차 구분합니다. 물론, 클래스 상속이어도 보조 생성자 사용으로 인해 생성자 호출문이 생략될 수도 있지만 대부분의 경우 구별이 되는 편입니다.

 

결국, 인터페이스를 사용하려면 추상메소드를 구현하는 클래스를 매번 설계하고 객체를 생성하여 사용해야 합니다.

근데. 이런식이면 만약 버튼이 10개 있다면 버튼 리스너 인터페이스를 구현하는 10개의 클래스를 설계해야 합니다. 물론, 그래도 되기는 하지만 클래스 이름 10가지를 만드는 것도 꽤 애를 먹게 되고 코드도 지저분해 집니다. 그래서 앱 개발에 아주 많이 필요한 문법이 이름이 없는 클래스를 만드는 익명클래스 입니다.

 

 

3. 익명클래스 Anonymous class

익명클래스는 객체를 생성할 때 설계하는 클래스를 말합니다. 그러다 보니 별도의 클래스명을 가지지 않습니다. 그래서 익명 클래스 라고 부릅니다.

 

코틀린에서 익명클래스를 만드는 문법은 자바와 많이 다릅니다. 객체를 영어로 표시한 object 키워드를 통해 객체를 생성합니다. 그 옆에 인터페이스 구현 기호인 콜론 : 을 통해 인터페이스를 구현합니다. 위에서 소개했던 Clickable 인터페이스를 구현하는 Test 클래스를 만들 때 하는 방법과 비슷하되 다른 점은 클래스명 대신에 object 키워드를 사용하는 겁니다. 

정리하면 Clickable 인터페이스를 구현하는 클래스를 만들면서 객체까지 생성하는 것이 익명클래스입니다.

//시작 함수
fun main(){

    //2. 인터페이스
    //val c= Clickable() //ERROR :인터페이스는 곧바로 생성(new)할 수 없어서..
    
    //3. 익명클래스 [ Java와 문법이 많이 다름 : object키워드를 사용해야만 함 ]
    val a= object : Clickable{ //[ ()없는 것 주의!!!] -- Clickable 인터페이스 구현
        override fun onClick() {
            println("Anonymous class onClick!!!!!")
            println()
        }
    }
    a.onClick()

}//main 함수 종료..

//2. 인터페이스는 특별할 것 없음
interface Clickable{
    //추상메소드
    fun onClick()
}


//**출력**
Anonymous class onClick!!!!!

 

익명클래스는 안드로이드 앱 개발에서 매우 많이 사용하게 되니 연습은 앱 개발에서 충분히 해보도록 하고 이 글에서는 문법적 소개만 하고 넘어 가겠습니다.

 

 

4. 동반 객체 companion object

혹시 자바의 static 키워드를 기억하시나요? 미리 메모리에 만들어 놓기에 정적 멤버를 만들 때 사용하는 키워드 였습니다.

클래스의 멤버들은 객체를 생성하지 않으면 사용할 수 없습니다. 이 때 static 키워드가 붙은 정적 멤버는 객체 생성 없이 사용이 가능했습니다. 다만, 멤버이기 때문에 바로 인식은 안되고 클래스명을 통해 정적멤버를 인식하도록 하였습니다.

public class Hello{
    public static void main(String[] args){
    
        System.out.println("인스턴스 변수 a : " + a);  //ERROR - a 변수 인식안됨, 또한 Sample 객체를 생성해야만 사용가능
        System.out.println("static 변수 b : " + b);  //ERROR - static 변수 b는 Sample 안에 있기에 인식안됨
        
        //Sample 객체를 생성하지 않고 정적 멤버 사용하기
        System.out.println("static 변수 b : " + Sample.b);  //OK - 클래스명.정적멤버  로 사용가능
    
    }
}


class Sample{
    int a= 10;           //일반 멤버변수 - 인스턴스 변수
    static int b= 20;    //정적 멤버변수
}

 

코틀린언어에는 static 키워드가 없습니다. 대신 등장한 문법이 동반객체 companion object 입니다. object(객체)가 클래스(설계도면)에 동반되었다는 의미에서 붙여진 이름입니다. 

 

동반객체안에 선언한 변수와 함수(메소드)들은 객체를 생성하지 않고도 사용이 가능합니다. 

//시작 함수
fun main(){

    //4. 동반객체 [ companion object -  java의 static 키워드와 유사한 기능 : 객체 생성없이 사용가능한 멤버들 ]
    println( Sample.title )   

    // 동반객체의 멤버변수 값 변경
    Sample.title="robin"
    println( Sample.title)

    //당연히 static method 같은 기능의 메소드도 만들수 있음
    Sample.showTitle()
    println()

}//main 함수 종료..

//4. companion object [ companion: 동반자, 동행 ] - JAVA의 static 키워드를 대신하는 문법
class Sample{
    var a:Int=10

    companion object{
        var title:String="sam"  //java static variable 같은 역할

        fun showTitle():Unit{   //java static method 같은 역할
            println("제목 : $title")
            //println("Sample클래스 객체의 프로퍼티 a : $a ") //당연히 객체의 프로퍼티[자바에서는 인스턴스변수]는 사용할 수 없음
        }
    }
}

//**출력**
sam
robin
제목 : robin

 

자바의 static 과 비슷한 역할 이기에 객체생성 해야만 사용가능한 프로퍼티인 Int형 값 a 변수는 동반객체의 메소드안에서는 사용이 불가능합니다.

 

 

5. 늦은 초기화 

코틀린의 클래스 문법에서는 프로퍼티(멤버변수)를 만들 때 반드시 초기화를 해야 만 합니다. 자바처럼 Default 초기화를 수행해 주지 않습니다. 하지만 프로그램을 구현하다 보면 클래스를 설계할 때 초기값을 주기에 적합하지 않은 경우도 존재합니다.

 

안드로이드 앱 개발과정에서의 대표적인 사례가 View 참조변수를 만들 때 입니다.

보통 액티비티안에 보여지는 뷰들을 제어하는 참조변수는 액티비티 전체에서 사용되는 경우가 많아 멤버변수로 만드는 경우가 많습니다. 하지만 실제 참조할 뷰는 별도의 xml 문서에 작성되어 객체 생성 후 자동 호출되는 onCreate() 메소드에서 객체로 생성되기에 이 때 찾아와 참조해야 합니다. 즉, 이미 참조변수를 선언할 때에는 아직 해당 뷰 객체가 만들어지기 전이어서 참조가 되지 않습니다. 그렇기에 뷰 참조변수를 만들 때 초기화를 하지 못하고 나중에 해야 할 필요가 있습니다. 자바에서는 null 값으로 자동 초기화 되지만 코틀린은 null 값을 저장하기 위해서 nullable 변수로 만들어야 하고 사용할 때마다 null 관련 연산자를 사용해야 하는 등의 추가 작업이 필요하여 불편해 집니다.

class MainActivity : AppCompatActivity() {

    //뷰 참조변수
    var tv: TextView     //1. ERROR - 클래스의 프로퍼티는 반드시 초기화 해야 함.
    
    var tv2: TextView= findViewById(R.id.tv2)  //2. Exception - 문법에러는 아니지만 런타임(실행 중) 오류 발생함
    
    //3. null값으로 초기화 하기 위해 TextView? nullable 변수로 선언
    var tv3: TextView?= null  
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 화면에 보여줄 뷰를 설정
        setContentView(R.layout.activity_main)
        
        tv3= findViewById(R.id.tv3)
        tv3?.text="aaa".   // nullable 참조변수의 멤버를 사용할 때 마다 ?. null 안전 연산자를 사용하는 불편함이 있음.
    }
    
}

 

위 코드처럼 뷰 참조용 프로퍼티는 초기화를 나중에 해야 할 필요가 있습니다. nullabal 변수를 사용하는 방법도 있지만 추후 코드가 불편해 질 수 있기에 더 좋은 방법을 권해드립니다. 이때 사용하는 문법이 늦은 초기화 문법 입니다.

 

코틀린의 늦은 초기화 문법에는 2가지 종류가 있습니다.

1) lateinit    :  var 변수에 사용하는 늦은 초기화

2) by lazy   :  val 변수에 사용하는 늦은 초기화

 

 

5.1) lateinit

class Hello{

    var name:String  //ERROR : 프로퍼티는 선언하면서 값을 초기화 하지 않으면 에러
    
}

 

앞서 설명했듯이 프로퍼티를 선언하면서 초기화를 하지 않으면 문법적 에러 입니다.

만약 나중에 초기화를 하고 싶다면 늦은 초기화를 해야 합니다. var 변수에 적용하는 lateinit( late:늦은 + initialize:초기화) 키워드를 적용해 보겠습니다. var 키워드 앞에 lateinit 을 추가하면 됩니다.

class Hello{

    //var name:String  //ERROR : 프로퍼티는 선언하면서 값을 초기화 하지 않으면 에러
    
    lateinit var name:String  //lateinit 키워드를 통해 나중에 초기화한다고 명시!
    
    fun show(){
        println("property name : $name") //프로퍼티이므로 메소드에서 사용가능
    }
    
}

 

당연히 초기화가 되어 있지 않기에 초기화 없이 바로 name변수를 사용하면 Exception(런타임 중 에러)이 발생합니다.

//5.1) 프로퍼티(멤버변수)의 늦은 초기화문법 [ lateinit ]
val h:Hello= Hello()

// 초기화 없이 사용해 보기
println(h.name) //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생
h.show()        //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생 - 늦은 초기화확인 문법을 통해 예외발생 막을 수 있음.

 

그런 name 변수값을 초기화 해주는 코드를 작성할 기능메소드를 추가해 보겠습니다. 이름은 onCreate()라고 해보겠습니다.

class Hello{

    //var name:String  //ERROR : 프로퍼티는 선언하면서 값을 초기화 하지 않으면 에러

    lateinit var name:String  //lateinit 키워드를 통해 나중에 초기화한다고 명시!

    //초기화 코드를 가진 메소드!!    
    fun onCreate(){
        name= "sam"
    }

    fun show(){
        println("property name : $name") //프로퍼티이므로 다른 메소드에서 사용가능
    }

 

이제 main()함수에서 초기화 후에 프로퍼티를 사용해보겠습니다.

fun main(){

    val h:Hello= Hello()

    //println(h.name) //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생
    //h.show()        //Exception : lateinit 프로퍼티의 초기화를 하기 전에 사용하면 Exception 발생 - 늦은 초기화확인 문법을 통해 예외발생 막을 수 있음.

    h.onCreate() //이 메소드에서 초기화
    println(h.name)
    h.show()

}

//**출력**
sam
property name : sam

 

 

♣ lateinit 키워드 사용할때 주의할 점.

 

- lateinit은 null초기화는 불가능[즉, String? 타입은 불가능함]

lateinit var title:String?  //ERROR

 

- 기본형 자료형[primitive type : Int, Double, Boolean ..... ]들에도 사용불가

lateinit var age:Int //ERROR


- lateinit은 var 에만 사용가능 [ val 에는 사용불가 ]

lateinit val address:String //ERROR

 

- 늦은 초기화확인 문법이 등장함 [ 코틀린 버전 1.2부터 사용가능 ]
 :: 을 붙인 lateinit 프로퍼티에 사용가능함 .isInitialized  -- show()메소드에서 소개

fun show(){
    //println("property name : $name") //프로퍼티이므로 다른 메소드에서 사용가능

    //lateinit 프로퍼티의 초기화여부에 확인하여 안전하게 사용하기 [ 코틀린 버전 1.2부터 사용가능 ]
    if( ::name.isInitialized  ) println("property name : $name")
}


- lateinit 변수에 대한 setter/getter properties 정의가 불가능
- 클래스 주 생성자의 파라미터에서 사용 불가능
- 로컬 변수에서 사용 불가능

 

 

5.2) by lazy

class Hello{

    //5.2) val의 늦은 초기화는 lazy [ by lazy ]  - 작성 위치와 상관없이 사용될 때 lazy 블럭{}의 내용이 실행되고 마지막에 있는 값이 대입됨
    val address:String by lazy { "seoul" }

}

 

코틀린에서 by 키워드는 특정 처리를 다른 객체에게 위임(Delegate) 할 때 사용합니다. by 에 대한 소개는 추후 소개하도록 하고 지금은 by 옆의 중괄호 { } 영역의 값을 대입해주는 정도로 인식하시면 됩니다. 단, 늦은 초기화를 위해 lazy(게으른) 키워드를 적용하여 중괄호 {} 영역의 내용이 작성 위치와는 상관없이 이 변수가 처음 사용되는 곳에서 대입되는 특징을 가집니다.

즉, lazy 옆의 {} 내용을 지금 당장 변수에 대입하지 않고 좀 게으르게 수행하라고 명령을 내렸다고 보시면 됩니다.

 

by lazy의 중요한 특징은 val 변수에만 적용할 수 있다는 것입니다.

 

lazy 블럭{}안에는 일반적인 실행문도 구현할 수 있습니다. 다만 title 변수는 String 타입을 값을 원하기에 중괄호 {} 의 마지막 실행문은 대입되는 문자열 값 이어야만 합니다.

class Hello{

    //5.2) val의 늦은 초기화는 lazy [ by lazy ]  - 작성 위치와 상관없이 사용될 때 lazy 블럭{}의 내용이 실행되고 마지막에 있는 값이 대입됨
    val address:String by lazy { "seoul" }
    
    // lazy 블럭{}안에 일반적인 실행문도 구현가능함 
    val title:String by lazy {
        println("by lazy 초기화블럭")
        "Hello title"
    }

}

 

즉, title 에 나중에 대입 될 값은 "Hello title" 이라는 글씨입니다. {}영역의 실행이 나중에 된다는 점을 조금 더 확인해 보기 위해 객체가 생성될 때 실행되는 {초기화 블럭} init 를 추가하고 by lazy의 중괄호 {} 안의 println()실행문이 호출되는 순서를 살펴보겠습니다.

fun main(){

    //5.2)lazy 초기화
    val h:Hello= Hello() //이때 init{} 초기화 블럭 실행됨
    
    println(h.address) //이때 address 변수가 사용되면서 lazy 블럭이 실행되며 초기화 됨 - "seoul"값이 대입됨

    //by lazy{}블럭안에 실행문 넣고 실행되는 시점 확인
    println(h.title) //이때 title이 사용되면서 lazy 블럭이 실행되며 초기화 됨  -- android에서 활용하기 좋음

}


class Hello {

    //5.2) val의 늦은 초기화는 lazy [ by lazy ]  - 작성 위치와 상관없이 사용될 때 lazy 블럭{}의 내용이 실행되고 마지막에 있는 값이 대입됨
    val address:String by lazy { "seoul" }

    // lazy 블럭{}안에 일반적인 실행문도 구현가능함 - 아래 만들어 본 초기화블럭인 init{} 과 다르게 이 프로퍼티를 사용하기 전에는 발동하지 않음.
    val title:String by lazy {
        println("by lazy 초기화블럭")
        "Hello title"
    }

    //init 초기화블럭
    init {
        println("이 초기화 블럭은 객체생성시에 자동 호출됨")
    }
    
}


//**출력**
이 초기화 블럭은 객체생성시에 자동 호출됨
seoul
by lazy 초기화블럭
Hello title

 

이 by lazy {} 의 초기화시점을 통해 안드로이드에서 아래와 같은 코드 작성이 가능합니다.

class MainActivity : AppCompatActivity() {

    val tv: TextView = findViewById() //ERROR - onCreate()에서 setContentVeiw() 다은에 실행해야 하기에 에러
    val tv:TextView by lazy { findViewById(R.id.tv) } <--이런식으로 사용할 수 있음. 그럼.. onCreate() 에서 findViewById()를 안해도 됨.
    
}

 

 

♣ by lazy 키워드를 사용할 때의 특징 ( 대부분 var 변수에 사용했던 lateinit 의 특징에 반대되는 특징들 입니다. )

 

- by lazy는 primitive 자료형도 가능함

val age:Int by lazy { 20 }

 

- by lazy는 nullable type에서도 사용가능함

val message:String? by lazy { "Hello by lazy....." }
val message2:String? by lazy { null }

 

- 이런식의 연산에 의한 초기화도 가능함

val message3:String? by lazy {
    if(age<20) "미성년자"
    else "성인"
    //참고로 만약 lazy 초기화 하는 age가 이전에 사용된 적이 없다면 바로 이때 초기화됨
}

 

- by lazy는 var에는 사용할 수 없음

var sss:String by lazy { "Nice" }  //ERROR

 

- 클래스 주 생성자에서는 사용 불가능
- 로컬 변수에서 사용 가능

- 당연히 사용자정의 클래스의 객체 늦은 초기화도 가능함

val nnn:MyKotlinClass by lazy { MyKotlinClass() }

 

반응형
반응형

이번 글에서는 객체지향 프로그래밍의 가장 유용한 문법 중 하나인 상속 inheritance 입니다.

지난번 글에서 소개했듯이 객체란 변수와 기능을 가진 것이라고 했습니다. 그리고 그 객체가 어떤 변수와 기능을 가질 것인지를 설계하는 것을 class 라고 했습니다.

개발자가 필요한 모든 데이터와 기능들을  매번 직접 class로 설계하기에는 번거로울 겁니다. 나에게 필요한 기능과 비슷한 기능을 가진 class가 이미 다른 누군가에 의해 만들어져 있을 수 있다면 처음 부터 설계하기 보다는 기존에 존재하는 클래스 설계를 그대로 가져와서 나에게 필요한 몇가지 기능만 수정하거나 추가하면 프로그래밍 노고가 상당부분 개선되고 생산성도 높아질 겁니다. 이렇게 이미 존재하는 클래스의 변수와 함수를 그대로 물려받아 재사용할 수 있도록 하는 문법이 상속입니다.

 

그럼. 코틀린에서 상속을 다루는 문법에 대해 알아보도록 하겠습니다. 역시, 자바와는 약간 차이가 있으니 주의깊게 살펴보시기 바랍니다.

 


 

새로운 코틀린 파일 Test07OOP2.kt 를 만들고 문법을 알아보겠습니다.

코틀린 프로그램의 시작은 언제나 main() 함수이니 만들고 시작하겠습니다.

 

1. 상속 inheritance

먼저 상속을 해줄 부모 클래스를 만들어 보겠습니다. [ 클래스명 First ]

코틀린 class는 자바와 다르게 기본적으로 final 클래스처럼 상속이 불가능한 클래스로 만들어집니다. 그래서 부모클래스로서 상속을 해주는 클래스는 반드시 class 앞에 open 키워드로 열어줘야 합니다. 

메소드 역시 자바와 다르게 기본적으로 final 메소드처럼 기능을 개선하는 오버라이드 Override 가 불가능한 메소드로 만들어 집니다. 그래서 부모클래스의 메소드 중 오버라이드를 허용하는 메소드는 fun 앞에 open 키워드로 오버라이드를 허용해야 합니다.

//시작함수
fun main(){

}//main 함수 종료...

//상속을 해줄 부모클래스는 반드시 open 키워드가 추가되어야 함. 없으면 Java에서의 final 클래스와 같은 것임
open class First{
    var a:Int=10

    //override 해줄 메소드라고 명시하기위해 open키워드 추가 [없으면 final method ]
    open fun show(){
        println(" a : $a ")
    }
}

 

멤버변수(프로퍼티) a 와 멤버함수(메소드) show()를 가진 First 클래스를 상속하는 Second 클래스를 만들어 보겠습니다.

자바에서는 상속을 위해 extends 라는 키워드를 사용하였다면 코틀린에서는 C계열 언어처럼 콜론 : 으로 extends 키워드를 대체합니다.

또한 상속을 위해 부모클래스명을 작성할 때 반드시 명시적으로 생성자 호출을 위한 소괄호()가 있어야 합니다.

가끔 초보자들 중에 상속이 부모의 멤버들만 쏙 뽑아오는 것처럼 인식하는 분들이 있더군요. 하지만 실제로 상속이라는 것은 자식객체 안에 부모객체가 존재하는 겁니다. 마치 인형안에 인형이 들어 있는 러시안 인형(마트료시카)처럼 말이죠. 다만 자식객체에서 부모객체의 멤버를 마치 내것인양 사용하는 문법이 상속인 겁니다. 

코틀린의 상속은 콜론 : 다음에 부모 클래스의 생성자 호출문을 직접 작성하기에 부모 객체를 명시적으로 생성하는 표기문법이서 개인적으로는 더 직관적이고 좋다고 생각합니다. 더불어 부모생성자에 파라미터를 전달하는 코드가 매우 간결해지는 장점도 있습니다. 코드를 보면서 차차 소개하도록 하겠습니다.

//시작함수
fun main(){

    //1) 자식 클래스의 객체 생성하기 - 코틀린의 상속은 부모클래스에 open키워드를 추가해야만 함.
    var obj= Second()
    obj.a=10    //부모클래스 First의 멤버변수를 내것인양 사용
    obj.b=20    //자식클래스 Second의 멤버변수
    obj.show()  //부모클래스 First의 멤버함수를 내것인양 호출

}//main 함수 종료...

//상속을 해줄 부모클래스는 반드시 open 키워드가 추가되어야 함. 없으면 Java에서의 final 클래스와 같은 것임
open class First{
    var a:Int=10

    //override 해줄 메소드라고 명시하기위해 open키워드 추가 [없으면 final method ]
    open fun show(){
        println(" a : $a ")
    }
}

//상속을 받은 자식클래스
//상속의 문법 [ 클래스명 뒤에 : 후 부모클래스명() 작성 <- 부모클래스명 뒤에 주 생성자호출()를 주의할것!! ]
class Second : First(){
    var b:Int=20
}

 

Second 클래스는 명시적으로는 멤버변수 b 만 직접 만들었지만 First 클래스를 상속받았기 때문에 이미 a, show() 멤버를 보유한 상태입니다. 다만 부모클래스 First의 출력 기능 show()를 사용한 것이어서 자식 클래스 Second의 멤버변수 b는 출력되지 않습니다. 이렇게 부모로 부터 받은 특정 기능의 메소드를 개선해야 할 필요가 있을 때 사용하는 것이 오버라이드 override 입니다. 이미 First 클래스의 show메소드는 open 키워드를 통해 오버라이드를 허용하였기에 Second에서 개선이 가능합니다. 단, 자바와 다르게 이 메소드가 오버라이드 메소드라는 것을 반드시 명식적으로 표기해야 합니다. 자바의 @Override 와 같이 어노테이션을 사용하는 것이 아니고 fun 앞에 override 키워드를 붙여줘야 합니다. 또한 자식클래스에서 부모클래스의 show()를 호출하기 위해 super 키워드를 사용하는 것은 자바와 동일합니다.

//시작함수
fun main(){

    //1) 자식 클래스의 객체 생성하기 - 코틀린의 상속은 부모클래스에 open키워드를 추가해야만 함.
    var obj= Second()
    obj.a=10    //부모클래스 First의 멤버변수를 내것인양 사용
    obj.b=20    //자식클래스 Second의 멤버변수
    obj.show()  //**오버라이드에 의해 Second의 show()메소드 호출**

}//main 함수 종료...

//상속을 해줄 부모클래스는 반드시 open 키워드가 추가되어야 함. 없으면 Java에서의 final 클래스와 같은 것임
open class First{
    var a:Int=10

    //override 해줄 메소드라고 명시하기위해 open키워드 추가 [없으면 final method ]
    open fun show(){
        println(" a : $a ")
    }
}

//상속의 문법 [ 클래스명 뒤에 : 후 부모클래스명() 작성 <- 부모클래스명 뒤에 주 생성자호출()를 주의할것!! ]
class Second : First(){
    var b:Int=20

    //같은 이름의 메소드를 부모로부터 상속받았기에 그냥 쓰면 에러!!
    //오버라이드라고 명시적으로 표기를 해야함.[ 이때, 오버라이드가 되는 메소드도 역시 open키워드가 있어야함. 없으며 final 메소드로 인식함 ]
    override fun show(){
        super.show()         //First 부모클래스의 show() 호출 - a 변수 출력
        println(" b : $b")
    }
}

//**출력**
 a : 10
 b : 20

 

 

2. 업 캐스팅, 다운 캐스팅

· UP casting : 부모참조변수가 자식객체를 참조하는 것

· DOWN casting : 자식참조변수가 부모를 참조하는 것 - 단, 부모는 업 캐스팅 상태여야 함

 

다형성을 위한 업 캐스팅과 다운 캐스팅은 자바와 특징이 동일합니다. 다만, 형변환[type cast] 문법에 차이가 있습니다.

 

위 상속 클래스 소개에 사용했던 First - Second 클래스를 사용하겠습니다.

부모클래스 : First 

자식클래스 : Second

//시작함수
fun main(){

    var f:First= Second() // up casting   - 부모참조변수로 자식 객체 참조
    f.show()  // 실제 가리키는 대상객체인 Second의 show() 메소드 호출

}//main 함수 종료...

//**출력**
 a : 10
 b : 20

 

부모참조변수인 f는 First참조변수이지만 실제로는 Second 객체를 참조하고 있기에 Second객체의 show()메소드가 실행됩니다.

즉, 부모참조변수로 자식객체의 기능 메소드를 사용할 수 있다는 점에 그 의미가 있습니다.

다만, f 참조변수는 본인의 자료형이 First 로 지정되어 있기에 실제 참조하는 있는 대상이 Second 객체인지 알지 못합니다. 그럼에도 show()메소드를 호출할 수 있었던 이유는 First클래스에도 show()메소드가 있었기 때문입니다. 즉, f 참조변수는 본인의 멤버메소드인 show()메소드를 호출한 겁니다. 다만, 자식 객체에 의해 show()메소드가 오버라이드 되어 있어서 Second의 show()를 호출된 것 입니다.

그렇기에 업 캐스팅을 통해 부모참조변수가 자식객체를 참조할 수는 있더라도 부모의 멤버나 오버라이드 된 메소드만 호출 할 수 있고 자식객체만의 고유한 멤버는 사용할 수 없습니다.

확인을 위해 Second 클래스에 aaa() 라는 이름의 메소드를 하나 추가하고 First로 호출하려 하면 에러가 발생되는 것을 확인할 수 있습니다.

//시작함수
fun main(){

    var f:First= Second() // up casting
    f.show()    // 실제 가리키는 대상객체인 Second의 show() 메소드 호출
    f.aaa()     // ERRPR : 부모참조변수인 f 가 실제 Second를 참조하고 있더라도 Second클래스의 고유 멤버는 사용불가
    
}//main 함수 종료...

//부모 클래스
open class First{
    var a:Int=10

    //override 해줄 메소드
    open fun show(){
        println(" a : $a ")
    }
}

class Second : First(){
    var b:Int=20

    //오버라이드 한 show() 메소드
    override fun show(){
        super.show()
        println(" b : $b")
    }

    //Second 클래스만의 고유한 메소드
    fun aaa(){
        println("Second의 고유 메소드")
    }
}

 

결국 업 캐스팅을 통해 부모가 자식을 참조하여 제어할 수 있더라도 자식만의 고유기능은 사용할 수 없다는 것입니다.

만약, 자식만의 고유한 기능을 사용하고 싶다면, 자식 참조변수를 새로 만들어서 부모참조변수의 참조값을 대입해 주어야 합니다.

이를 다운캐스팅 이라고 부릅니다.

//시작함수
fun main(){

    var f:First= Second() // up casting
    
    //down casting.
    val s:Second = f   //ERROR - 형변환 없이 대입하려하면 자식이 부모를 참조한다고 판단하여 에러문법처러됨
    
    
}//main 함수 종료...

 

코드에서 확인했듯이 자식은 부모를 참조할 수 없습니다. 왜 못 하게 했을 까요?

자식의 멤버는 부모의 멤버들을 상속받아 온 후 추가로 새로운 멤버들을 추가해서 만든 클래스 입니다. 그러니 부모보다 멤버의 개수가 같거나 많을 수 밖에 없습니다. 그렇기에 자식 참조변수가 사용하려는 멤버를 실제 부모객체가 보유하지 않는 문제로 실행이 불가능한 상황이 생길 수 있습니다. 하여. 이렇게 자식이 부모를 참조하는 것 자체를 문법적으로 막아 놓았습니다.

그런데. 위 코드 처럼 부모참조변수 f 가 참조하는 실제 객체는 Second 객체 입니다. 그렇기에 f 의 참조값을 Second 참조변수 s 에 대입하면 결국 s 는 Second 객체를 온전히 참조하는 상황이 되니 문제 될 것이 없습니다.

그래서 참조값을 대입할 때 명시적으로 Second 임을 알려주기 위해 형변환 하면서 대입 해주면 문제없이 대입이 됩니다. 코드적으로 보면 자식 참조변수 s 가 부모인 f 를 대입하여 참조하는 모습이어서 down casting 이라고 부릅니다. 즉, 다운 캐스팅은 형변환이 필수입니다.

참고로, 코틀린의 형변환 연산자는 as 입니다. '~로서' 라는 의미로 읽으시면 쉽게 기억될 겁니다. 

//시작함수
fun main(){

    var f:First= Second() // up casting
    
    //down casting.
    val s:Second = f   //ERROR - 형변환 없이 대입하려하면 자식이 부모를 참조한다고 판단하여 에러문법처러됨
    
    //val  s:Second = (Second)f   // ERROR - 자바에서의 형변환 연산자 (Type) 은 코틀린에 없는 문법임
    val  s:Second = f as Second   // 형변환 연산자 as
    s.show()
    s.aaa()
    
}//main 함수 종료...

//**출력**
 a : 10
 b : 20 
 Second의 고유 메소드

 

 

♣상속 문법 마무리 예제

지금껏 학습한 상속에 대해 문법적으로 정리해 보는 마무리 예제를 만들어 보겠습니다. 자바 학습 자료들에 상속관련 예제 중 가장 유명한 예제이며 이를 통해 class, 객체, 주 생성자, 보조 생성자, 상속 들에 대해 정리 해보겠습니다. 코드의 효율성 보다는 학습내용을 정리하기 위해 일부러 주 생성자, 보조 생성자를 섞어가며 만들어 볼 겁니다.

 

[예제]

어느 대학의 학사정보를 제공하는 앱을 만들고자 합니다. 이 앱을 사용하는 사용자에 따라 접근할 수 있는 권한을 제한하고자 합니다.

그렇기에 이 앱의 사용자 종류는 크게 4 종류로 구분하였습니다. 일반회원, 학생회원, 교수회원, 근로장학생회원 으로 구분하고자 합니다.

일반회원은 전공이 있을 수 없으니 회원마다 저장할 정보가 다르겠지요. 그래서 회원별 저장할 대표 데이터를 정리해 보겠습니다.

 

* 대학교 학사정보 앱 회원 데이터*

일       반 : 이름, 나이 

학       생 : 이름, 나이, 전공

교       수 : 이름, 나이, 연구과제

근로학생 : 이름, 나이, 전공, 업무

 

각 각의 회원들은 저장 정보가 다르기에 같은 멤버변수의 개수와 종류가 다릅니다. 하여 각 회원별로 별도의 class 를 만들어야 합니다.

다들 느끼셨겠지만, 4개의 회원에 공통적으로 저장되는 데이터가 보이네요. 상속을 활용하기에 적합해 보입니다. 코드를 보면서 확인해보겠습니다.

 

1) 먼저, 일반 회원 부터 class 를 만들어 보겠습니다. 별도의 Person.kt 파일을 만들어 클래스를 설계하겠습니다. [ 주 생성자 활용 ]

//일반회원 : [이름, 나이] 정보를 프로퍼티(멤버변수)로 가지는 클래스 - 주 생성자 활용 [ 클래스 명 옆에 constructor 키워드]
open class Person constructor(var name:String, var age:Int){ // 상속을 허용하기 위해 open 키워드

    init {
        println("create Person instance")
    }

    //멤버변수 값을 출력해주는 기능 메소드 - 자식클래스에서 override 하는 것을 허용하기 위해 open 키워드
    open fun show(){
        println("name : $name    age: $age ")
    }
}

 

주 생성자의 파라미터를 만들 때 var 키워드를 적용함으로 파라미터면서 멤버변수를 만들어서 코드가 간결해 진 것을 확인할 수 있습니다. 또한, [이름, 나이] 정보는 이후 만들 학생회원에서도 사용하는 멤버이기에 상속을 해주고자 open 키워드를 적용한 부분이 특별히 보셔야 할 부분입니다.

 

잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 Person 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.

//시작 함수
fun main(){

    //상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
    
    //1) 일반회원    
    var p= Person("sam", 20)  //이름, 나이
    p.show()
    println()

}//main 함수 죵료..


//**출력**
create Person instance
name : sam    age: 20

 

 

2) 다음으로, [이름, 나이, 전공] 정보를 가지는 학생회원 클래스로 별도의 Student.kt 파일을 만들어 보겠습니다. [이름, 나이] 정보는 이미 설계된 일반 회원 클래스를 상속하고 추가로 [전공] 정보만 직접 설계하겠습니다. 즉, Person 클래스를 상속하여 Student 클래스를 만들고자 합니다. Person 클래스를 만들 때 처럼 주 생성자를 이용하여 만들어 보겠습니다. 상속받을 Person클래스에 이미 [name, age] 프로퍼티들이 있으므로 Student에서는 주 생성자의 파라미터로 [name, age]를 받을 때는 멤버변수를 만들어주는 var키워드를 사용하면 안됩니다. 단지 매개변수로만 만들어야 하며 Student 클래스에만 존재하는 전공 정보를 저장할 프로퍼티를 만들기 위해 major만 var 키워드를 추가해 줘야 합니다. 또한, 상속하는 Person 클래스의 생성자를 호출하면서 파라미터로 받은 name, age 값을 전달 해야 합니다. 코드로 확인해 보겠습니다.

//학생회원 : [이름, 나이, 전공]을 저장하는 클래스 - 일반회원을 상속받아 제작 
//상속받을 Person클래스에 이미 name, age 프로퍼티들이 있으므로 Student에서는 name, age를 받을 때 var키워드를 사용하면 안됨!!! 변수 오버라이드가 됨.
open class Student constructor(name:String, age:Int, var major:String) : Person(name, age) {
    init {
        println("create Student instance")
    }

    //override 키워드가 추가되면 open키워드가 이미 적용된 것임.
    override fun show(){
        //super.show()
        println("name : $name    age: $age    major : $major ")
    }
}

 

상속받은 Person의 show()메소드는 name, age 값만 출력해주는 기능이기에 이를 개선하기 위해 오버라이드를 통해 name, age, major 값을 출력해 주도록 설계하였습니다. 참고로 override 키워드는 open 키워드가 이미 적용된 것이기에 오버라이드를 허용합니다.

 

잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 Student 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.

//시작 함수
fun main(){

    //상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
    
    //1) 일반회원    
    var p= Person("sam", 20)  //이름, 나이
    p.show()
    println()
    
    //2) 학생회원
    var stu= Student("robin", 25, "kotlin android")
    stu.show()
    println()

}//main 함수 죵료..


//**출력**
create Person instance
name : sam    age: 20

create Student instance
name : robin    age: 25    major : kotlin android

 

 

3) 다음으로, [이름, 나이, 전공, 업무] 정보를 가지는 근로장학생 회원 클래스로 AlbaStudent.kt 파일을 만들어 보겠습니다. [이름, 나이, 전공] 정보는 Student 학생 회원 클래스에 있는 정보이니 굳이 다시 설계하지 않고 Student 클래스를 상속하여 만들고 [업무] 프로퍼티 만 직접 추가하여 클래스를 설계하겠습니다. 주 생성자를 이용하되 constructor 키워드를 생략하여 코드를 보다 간결하게 작성하겠습니다.

//근로장학생 : [이름, 나이, 전공, 업무] property(멤버변수) 를 가지는 클래스 - Student 클래스 상속
//constructor 키워드 생략해보기
class AlbaStudent(name:String, age:Int, major:String, var task:String) : Student(name, age, major){
    init {
        println("create AlbaStudent instance")
    }

    override fun show() {
        //super.show()
        println("name : $name    age: $age    major : $major    task : $task ")
    }
}

 

constructor 키워드를 생략한 것 말고는 Student 클래스를 만들 때와 동일한 방식이어서 크게 어렵지 않을 겁니다. 더 이상 상속을 해줄 필요가 없기에 class 앞에 open 키워드를 추가하지 않았습니다. 새로 추가된 [ 업무 task ] 변수의 값을 출력해주도록 상속받은 show() 메소드를 오버라이드 하였습니다.

 

잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 AlbaStudent 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.

//시작 함수
fun main(){

    //상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
    
    //1) 일반회원    
    var p= Person("sam", 20)  //이름, 나이
    p.show()
    println()
    
    //2) 학생회원
    var stu= Student("robin", 25, "kotlin android")
    stu.show()
    println()
    
    //3) 근로장학생 회원
    val alba= AlbaStudent("tom", 27, "java android", "PC Management")
    alba.show()
    println()

}//main 함수 죵료..


//**출력**
create Person instance
name : sam    age: 20

create Student instance
name : robin    age: 25    major : kotlin android

create AlbaStudent instance
name : tom    age: 27    major : java android    task : PC Management

 

 

4) 마지막으로, [이름, 나이, 연구과제] 정보를 가지는 교수 회원 클래스로 Professor.kt 파일을 만들어 보겠습니다. 앞서 만든 Student 처럼 일반 회원 클래스 Person을 상속하여 [이름, 나이] 정보는 파라미터로만 받고 [연구과제] 정보만 멤버변수로 직접 만들겠습니다. 다양한 실습을 위해 보조 생성자를 이용하여 설계해 보겠습니다. 보조 생성자를 사용하여 상속할 때는 부모생성자 클래스명 뒤에 ()를 쓰지 않고 자식클래스의 보조 생성자에서 super() 생성자로 호출해야 합니다.

//교수회원 : [이름, 나이, 연구과제] 프로퍼티를 가지는 클래스 - Person 클래스 상속
//보조 생성자를 사용하여 상속할 때 부모생성자 클래스명 뒤에 ()를 쓰지 않고.. 자식클래스의 보조 생성자에서 super()로 호출하도록 함.
class Professor : Person{

    //Professor 클래스의 property(멤버변수) : 보조생성자는 직접 프로퍼티를 만들 수 없기에....
    var subject:String?= null

    //보조 생성자
    constructor(name:String, age:Int, subject: String) : super(name, age){ //super()키워드로 부모생성자 호출
        //멤버변수(프로퍼티)에 매개변수 전달!!
        this.subject= subject
        println("create Professor instance")
    }

    override fun show() {
        //super.show()
        println("name : $name    age: $age    subject: $subject ")
    }
}

 

보조 생성자에서 name, age 값을 전달 받기에 클래스명 옆에 : Person()의 주 생성자로 전달 할 수 없기에 보조생성자 옆에 : super 생성자를 통해 값을 전달하는 것을 주의 깊게 보시기 바랍니다.

 

잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 Professor 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.

//시작 함수
fun main(){

    //상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
    
    //1) 일반회원    
    var p= Person("sam", 20)  //이름, 나이
    p.show()
    println()
    
    //2) 학생회원
    var stu= Student("robin", 25, "kotlin android")
    stu.show()
    println()
    
    //3) 근로장학생 회원
    val alba= AlbaStudent("tom", 27, "java android", "PC Management")
    alba.show()
    println()
    
    //4) 교수회원 - 보조생성자를 이용하는 Professor 클래스(Person을 상속하는) 연습
    val t= Professor("son", 45, "mobile optimization")
    t.show()
    println()

}//main 함수 죵료..


//**출력**
create Person instance
name : sam    age: 20

create Student instance
name : robin    age: 25    major : kotlin android

create AlbaStudent instance
name : tom    age: 27    major : java android    task : PC Management

create Professor instance
name : son    age: 45    subject: mobile optimization

 

반응형
반응형

코틀린의 기본 문법까지 알아보았으니 이제 코틀린에서의 객체지향프로그래밍 문법을 알아보도록 하겠습니다.

자바와 문법적 차이가 많이 발생하기에 다소 어색할 수 있지만 하면 할수록 보다 간결하고 좋다고 느낄겁니다. 이해가 다소 힘든 부분이 있더라도 끝까지 잘 따라와 주시기 바랍니다.

 

♣ OOP : Object Oriented Programming 객체 지향 프로그래밍

서로 연관있는 데이터와 이를 제어하는 기능을 묶어서 만든 것을 객체라고 부릅니다.

잠시 복습하는 의미에서 객체지향 프로그래밍을 간단하게 이해해 보도록 하겠습니다.

 

아래와 같이 학생들의 기본정보를 보여주는 데이터가 있습니다.

이름 나이 주소
sam 20 seoul
robin 25 busan
hong 30 newyork

 

이 데이터들을 앱에서 사용하기 위해 변수를 만들어 저장하도록 하겠습니다.

첫번째 학생의 이름, 나이, 주소 정보를 저장하기 위해 name, age, address 변수 3개를 만들어 각각의 값을 대입해주면 될겁니다.

근데 문제는 학생이 한명이 아니라는 거죠. 두번째 학생의 정보를 저장하려면 또 다시 변수 3개가 필요합니다. 기존에 만든 name, age, address 변수를 사용하면 첫번째 학생의 데이터가 없어지니 어쩔 수 없이 name2, age2, address2 라는 식으로 변수명을 조금 다르게 하여 3개를 또 만들어야합니다. 세번째 학생의 데이터도 마찬가지로 변수 3개를 만들어야 겠죠. 그럼 3명의 학생 데이터를 저장하기 위해 변수를 9개나 만들어서 관리해야 합니다. 9개 정도면 할만합니다. 근데 만약 학생이 10명, 20명, 100명이 되면 만들어야 하는 변수가 너무 많습니다. 관리가 너무 어렵습니다. 그래서 이 3개의 변수를 묶어서 새로운 큰 박스로 묶어서 하나의 변수로 만들면 관리가 더 용이하겠죠. 이렇게 한 학생의 정보인 이름, 나이, 주소 정보를 하나의 그룹으로 묶어주는 문법class 라는 문법입니다.

또한 이 변수들의 값을 출력하거나 제어하는 기능(함수, 메소드)을 변수들이 있는 class에 같이 묶어서 설계하면 관리가 훨씬 용이합니다. 

이렇게 설계된 class 를 실제 프로그램에서 사용하기 위해서 객체 라는 것으로 만들어 사용하게 됩니다.

정리하면. class 는 어떤 변수들과 기능(함수)을 묶을 것인가를 설계해놓은 코드이고 이 코드를 사용하려면 객체 라는 것으로 실체화 하여 안에 있는 변수와 기능(함수)를 사용하는 겁니다. 그래서 class를 설계도, 객체를 설계도로 만든 제품으로 비유하기도 합니다.

대략적인 개념적 복습을 해봤으니 실습을 해보면서 객체지향프로그래밍에 대해 학습하면서 코틀린에서 OOP를 다루는 문법을 알아보도록 하겠습니다.


 

새로운 코틀린 파일 Test06OOP.kt 를 만들고 문법을 알아보겠습니다.

코틀린 프로그램의 시작은 언제나 main() 함수이니 만들고 시작하겠습니다.

 

 

1. 클래스와 객체 생성

먼저 새로운 class 를 만들어 보겠습니다. 특별한 의미는 없이 그냥 문법적 표기법을 알아보기 위한 class입니다.

class 를 작성하는 위치는 어디든 상관없는 데 우선은 main()함수 밖에 정의해 보겠습니다.

//시작함수
fun main(){

}//main 함수 종료...

//클래스 선언 - 자바나 C++과 기본 모습은 비슷함. 단, 멤버변수(Field)를 Property(프로퍼티) 라고 부름
class MyClass{
    //멤버변수[ Property:프로퍼티] -반드시 초기화 해야함
    var a:Int= 10

    //메소드 : Method
    fun show(){
        println(" show : $a ")
        println()
    }
}

 

주석으로 설명했듯이. class 정의 문법은 자바와 동일합니다.

멤버변수를 만드는 문법은 코틀린의 변수 선언 문법인 var, val 키워드를 이용하여 만듭니다.

다만, 멤버변수를 Field(필드)라고 부르지 않고 Property(프로퍼티) 라고 부릅니다. 또한 반드시 초기화를 해야 합니다. 초기화를 하지 않으면 문법적 에러입니다.

또한, 자바와 마찬가지로 멤버함수를 Method(메소드)라고 부르면 작성 방식은 함수를 만드는 문법과 동일하게 fun 키워드로 만들어야 합니다.

 

앞에서 설명했듯이 클래스는 어떤 변수와 기능메소드를 묶을것인가를 설계한 설계도를 만든 것이기에 이 상태에서 실행을 해도 main()함수 안에 아무것도 작성한 것이 없어서 아무 동작을 하지 않습니다.

 

설계한 class를 사용하려면 객체로 만들어서 사용해야 합니다. main()함수 안에 위에서 설계한 MyClass 클래스를 객체로 생성해 보겠습니다. 자바에서는 객체를 생성하기 위해 new 라는 키워드를 사용했습니다. 하지만 코틀린은 new 키워드 없이 클래스명과 생성자 호출로 객체를 생성합니다. 파이썬 언어의 객체 생성과 같은 모습입니다.

//시작함수
fun main(){

    //1. 클래스 정의 및 생성 - main()함수 밑에 클래스 선언
    //객체 생성 [매우 특이함!!! new키워드가 존재하지 않음 ]
    var obj= MyClass()
    obj.show()   
    
 }//main 함수 종료...

//클래스 선언 - 자바나 C++과 기본 모습은 비슷함. 단, 멤버변수(Field)를 Property(프로퍼티) 라고 부름
class MyClass{
    //멤버변수[ Property:프로퍼티] -반드시 초기화 해야함
    var a:Int= 10

    //메소드 : Method
    fun show(){
        println(" show : $a ")
        println()
    }
}

//**출력**
 show : 10

 

 

클래스를 작성하는 위치는 어디서든 가능합니다.

이번에 별도의 파일로 class를 만들어 보겠습니다. 별도의 코틀린 파일을 만들 듯이 만들면 되지만 파일의 종류를 class로 선택하면 기본 class 구조까지 작성된 상태로 파일이 만들어 집니다. 

 

파일명 : MyKotlinClass.kt

만들어진 KotlinClass 파일을 볼 수 있습니다. 그동안 만들었던 일반 코틀린 파일과 다르게 class 구조까지는 미리 작성된 상태로 만들어 줍니다. 

 

이전에 소개했듯이 코틀린은 자바와 다르게 class 없이 변수와 함수를 만들고 사용할 수 있습니다. 그러다 보니 만들어진 코틀린 파일명 만으로는 클래스인지 일반 파일인지 구별되지 않습니다. 

그래서 Android Studio IDE 는 개발자들의 혼동을 방지하기 위해 class 파일과 일반 파일을 목록에서 다르게 보이도록 했습니다. 이 파일이 Class라는 것을 인식하도록 C 모양의 아이콘이 있으면 클래스 파일이고 코틀린의 K 모양 아이콘이면 일반 파일입니다. 또 한가지 파일명 뒤에 .kt 확장자를 클래스는 보이지 않도록 하였습니다. 오해하지 마세요. 실제 클래스파일도 .kt 확장자 인것은 똑같습니다. 단지, 개발자다 편집기에서 파일을 쉽게 구분하여 사용하도록 해주는 편의 기능입니다.

코틀린 일반파일과 클래스파일의 아이콘 및 확장자표시 차이

 

자, 이제 새로 만든 MyKotlinClass 라는 클래스를 완성해 보겠습니다. 이름을 보시면 알겠지만 아무 의미없는 클래스 입니다. 단지 문법적인 소개를 위한 클래스이니 변수나 메소드에 특별한 의미를 두실 필요없습니다.

 

참고로 java언어와는 다르게 클래스 이름이 파일명과 반드시 같을 필요는 없습니다.

MyKotlinClass.kt
//java언어와는 다르게 클래스 이름이 파일명과 반드시 같을 필요는 없음.
class MyKotlinClass {
    //property  -- 멤버변수
    var a=10
    val b=20

    //method    -- 멤버함수
    fun show(){
        println(a)
        println(b)
        println()
    }
}

 

이제 main()함수가 있는 Test06OOP.kt 파일로 와서 MyKotlinClass 클래스를 객체로 생성하여 사용해 보겠습니다.

//시작함수
fun main(){

    //1-1. 별도의 파일로 만든 MyKotlinClass 클래스를 객체로 만들기 [당연히 클래스파일의 확장자는 .kt]
    var obj2= MyKotlinClass()  //객체생성
    obj2.show()    //메소드 호출
    
 }//main 함수 종료...
 
 //**출력**
 10
 20

 

 

2. 생성자 Constructor

생성자가 무엇인지는 잘 알고 있으시겠죠? 아주 간단히 정리해 보겠습니다.

생성자(Constructor)란, 객체를 생성할 때 무조건 자동으로 실행되는 아주 특별한 메소드(함수)를 말합니다. 보통 객체의 멤버변수들에 대한 초기값을 설정하는 등의 초기화 코드를 작성하는데 많이 사용됩니다.

 

코틀린의 생성자 문법은 자바와 완전히 다릅니다. 너무 달라서 살짝 당혹스러울 정도 입니다. 하지만, 역시 익숙해지면 자바보다 훨씬 간결하고 효과적인 코딩이 가능합니다. 잘 익혀보시기 바랍니다.

 

코틀린의 생성자는 2가지 종류가 있습니다. 주 생성자보조 생성자 입니다.

 

2.1) 주 생성자 ( Primary Constructor )

주 생성자는 클래스명 뒤에 생성자를 의미하는 constructor()라는 키워드로 만들 수 있습니다.

//주 생성자 [클래스명 옆에 constructor()키워드로 정의
class Simple constructor(){
	
}

 

자바처럼 클래스의 영역 중괄호{} 안에 클래스명과 같은 이름의 함수를 만드는 자바 생성자 문법과는 차이가 아주 큽니다. 생성자라는 의미의 영어표현으로 constructor 라는 키워드를 사용함으로서 개인적으로 가독성도 좋다고 생각합니다.

다만, 자바와 다르게 별도의 생성자함수 영역 중괄호{} 가 없기에 초기화 코드를 작성할 수 있는 영역이 필요하다면 초기화 블럭을 만들어주는 init{ ..} 영역을 통해 처리합니다. 이 init 초기화 블럭은 사실 주 생성자가 있을 때만 사용되는 것은 아니고 객체가 생성되면 자동으로 실행되는 자바에도 존재했던 초기화블럭입니다. 다만, 주 생성자의 파라미터(매개변수)가 있다면 이 초기화 블럭안에서 인식이 된다는 특징이 있어 보통 주 생성자의 코드영역으로 활용되기도 합니다. 

잘 이해가 되지 않을 수 있으니 코드를 보며 확인해 보겠습니다.

 

별도의 파일을 만들어 새로운 클래스를 만들기는 다소 번거로우니 main()함수 아래 Simple 이라는 이름의 새로운 클래스를 설계해 보면서 주 생성자를 추가해 보겠습니다. 

//시작함수
fun main(){

    //2.1 주 생성자 [Primary Constructor]
    var s= Simple()
    
 }//main 함수 종료...

//2.1 주 생성자 [클래스명 옆에 constructor()키워드로 정의]
class Simple constructor(){
    //근데 주 생성자가 별도의 메소드가 아니어서.. 코드를 작성할 수 없음.
    //그래서 존재하는 초기화 블럭 키워드
    init {
        //주 생성자가 호출될 때 실행되는 영역
        println("Simple primary constructor!!")
        println()
    }
}

//**출력**
Simple primary constructor!!

 

· 주 생성자에 값 전달 - 파라미터가 있는 주 생성자

일반적으로 생성자의 존재 목적은 객체를 생성할때 객체안에 있는 멤버변수(프로퍼티)의 초기화를 위한 경우가 많습니다. 즉, 객체를 생성하면서 멤버변수에 대입할 값을 생성자 메소드의 파라미터(매개변수)를 통해 주입해 줍니다.

이번에는 주 생성자에 파라미터를 추가하고 값을 전달해 보도록 하겠습니다.

여기서, 파라미터를 만들때 아주 중요한 부분이 있습니다.

코틀린에서는 함수 파라미터를 만들 때 변수를 만드는 키워드인 var, val 키워드를 사용하면 안됩니다. 자동으로 무조건 val 변수로 만들어 집니다. 근데. 이 주 생성자의 파라미터를 만들때는 var, val 키워드의 사용이 가능합니다. 이렇게 var, val 키워드를 추가하면 매개변수이면서 클래스의 멤버변수가 됩니다.

즉, 코틀린에서 클래스를 설계할 때는 멤버변수를 중괄호{} 안에 만드는 것이 아니라 주 생성자의 파라미터안에 var, val 키워드를 명시하면서 만듭니다. 그렇기에 자바에서 처럼 생성자안에서 매개변수의 값을 멤버변수에 대입해주는 코드가 필요없습니다. 이거 생각보다 코딩의 간결성이 엄청 좋아집니다. 참고로. 주 생성자는 클래스명 옆에 추가하는 문법이어서 오버로딩은 불가능 합니다.

//시작함수
fun main(){

    //주 생성자에 파라미터 전달  [ 기본적으로 주 생성자는 오버로딩이 없음 ]
    var s2= Simple2(1000) 
    println(s2.num) //멤버변수 확인
    s2.show()       //멤버함수 호출
    
 }//main 함수 종료...

//주 생성자에 값 전달
//아주 특이하게 주 생성자의 파라미터를 만들 때 var, val을 사용하면 곧바로 프로퍼티, 즉, 멤버변수가 됨
class Simple2 constructor(var num:Int){

    init {
        println("Simple2 primary constructor!! : $num ")  //초기화블럭에서는 당연히 주 생성자의 파라미터인 num 변수 사용가능
    }

    fun show(){
        println("프로퍼티 num : $num ")  //주 생성자의 파라미터면서 멤버변수인 num 변수 사용가능
        println()
    }
    
}


//**출력**
Simple2 primary constructor!! : 1000
1000
프로퍼티 num : 1000

 

주 생성자의 파라미터를 만들 때 var, val 키워드를 사용하지 않으면 초기화에만 사용가능한 일반 매개변수가 됩니다. 즉, 클래스 영역 전체에서 인식가능한 멤버변수가 아닙니다.

//시작함수
fun main(){

    //주 생성자에 파라미터 전달 
    var s2= Simple2(1000, 20) //2개의 파라미터에 값 모두 전달
    println(s2.num) //멤버변수 확인
    //println(s2.num2) //ERROR- 멤버변수가 아니어서 인식 불가
    s2.show()
    
 }//main 함수 종료...

// var을 안써도 에러는 아니지만 그때의 파리미터는 단순 매개변수임. 즉, 멤버변수(property)가 아님
class Simple2 constructor(var num:Int, num2:Int){  //num2는 var 키워드가 없기에 일반 매개변수

    init {
        println("Simple2 primary constructor!! : $num ")
        println("Simple2 primary constructor!! : $num2 ") //초기화 블럭에서는 num2매개변수 인식가능
    }

    fun show(){
        println("프로퍼티 num : $num ")
        //println("프로퍼티 num : $num2 ") //ERROR - 일반 메소드에서는 num2인식 불가
        println()
    }

}

//**출력**
Simple2 primary constructor!! : 1000
Simple2 primary constructor!! : 20
1000
프로퍼티 num : 1000

 

참고로, 주 생성자의 파라미터를 이용하지 않고 자바에서 처럼 클래스 영역안에 멤버변수를 만들수 있으며 일반 매개변수를 통해 초기화를 할 수도 있습니다.

//시작함수
fun main(){

    //주 생성자에 파라미터 전달 
    var s2= Simple2(30) 
    println(s2.n)  //멤버변수 확인
    println(s2.n2) //멤버변수 확인
    
 }//main 함수 종료...

class Simple2 constructor(num:Int){  //num : var 키워드가 없기에 일반 매개변수

    //자바에서 처럼 멤버변수 만들기
    var n:Int= 10

    //프로퍼티에 주 생성자의 매개변수를 대입하여 사용하는 것은 가능함
    var n2:Int= num;
    
}

//**출력**
10
30

 

 

2.2) 보조 생성자 ( Secondary Constructor )

보조 생성자는 자바처럼 class 영역 안에 메소드처럼 존재하며 constructor 라는 이름으로 만든 생성자 입니다. 

//시작함수
fun main(){

    //보조 생성자 [Secondary Constructor]
    var s3= Simple3()
    
 }//main 함수 종료...

//보조 생성자 - 자바처럼 class안에 메소드처럼 존재하는 생성자
class Simple3{

    //보조 생성자
    constructor(){
        println("Simple3 Secondary 생성자")
        println()
    }

}//Simple3 class....


//**출력**
Simple3 Secondary 생성자

 

주 생성자가 있음에도 보조 생성자가 존재하는 이유가 무엇일까요? 바로 주 생성자로는 처리할 수 없는 오버로딩을 하기 위해서 입니다.

 

오버로딩 Overloading : 메소드의 이름은 같고 파라미터의 개수나 자료형이 다른 메소드

호출하려는 메소드의 이름이 같더라도 전달하는 값의 개수나 자료형이 다르면 해당 메소드를 식별할 수 있어서 자바에서도 많이 사용하는 문법입니다. 생성자도 메소드처럼 파라미터를 가질 수 있기에 당연히 오버로딩이 가능합니다. 즉, 객체를 생성할 때 전달하는 값을 여러형태로 만들어 사용할 수 있다는 것입니다. 이 글은 정식 문법수업이 아니기에 오버로딩의 사용사례 같은 내용은 생략하도록 하겠습니다. 

 

위 Simple3 클래스에 정수형 숫자 1개를 파라미터로 전달 받는 생성자 오버로딩을 만들어 사용해 보겠습니다.

보조 생성자의 파라미터는 주 생성자와 다르게 var, val 키워드를 사용할 수 없습니다. 즉, 멤버변수(프로퍼티)면서 매개변수로의 사용이 불가능 합니다. 이것이 주 생성자와의 결정적 차이 입니다.

//시작함수
fun main(){

    //보조 생성자 [Secondary Constructor]
    var s3= Simple3()
    
    //생성자 오버로딩
    var s4= Simple3(100)
    
 }//main 함수 종료...

//보조 생성자 - 자바처럼 class안에 메소드처럼 존재하는 생성자
class Simple3{
    
    //보조 생성자
    constructor(){
        println("Simple3 Secondary 생성자")
        println()
    }

    //보조 생성자는 Overloading 이 됨 [보조생성자의 파라미터에는 var로 곧바로 property를 만들 수 없음]
    constructor(num: Int){
        println("Simple3 Overloading Secondary 생성자 : $num ")
        println()
    }

}


//**출력**
Simple3 Secondary 생성자

Simple3 Overloading Secondary 생성자 : 100

 

참고로. 주 생성자가 없더라도 초기화 영역 init 은 사용할 수 있습니다.

//시작함수
fun main(){

    //보조 생성자 [Secondary Constructor]
    var s3= Simple3()
    
    //생성자 오버로딩
    var s4= Simple3(100)
    
 }//main 함수 종료...

class Simple3{
    //초기화 블럭 [물론 없어도 됨] -- 생성자보다 먼저 실행되는 영역
    init {
        println("이 영역은 항상 객체생성시에 초기화를 위해 처음으로 실행됨")
    }

    //보조 생성자
    constructor(){
        println("Simple3 Secondary 생성자")
        println()
    }

    //보조 생성자는 Overloading 이 됨 [보조생성자의 파라미터에는 var로 곧바로 property를 만들 수 없음
    constructor(num: Int){
        println("Simple3 Overloading Secondary 생성자 : $num ")
        println()
    }

}

//**출력**
이 영역은 항상 객체생성시에 초기화를 위해 처음으로 실행됨
Simple3 Secondary 생성자

이 영역은 항상 객체생성시에 초기화를 위해 처음으로 실행됨
Simple3 Overloading Secondary 생성자 : 100

 

· 주 생성자 + 보조생성자

주 생성자를 사용한 상태에서 오버로딩을 위해 보조 생성자를 함께 사용하는 경우도 필요한 경우가 있습니다.

위 보조 생성자만을 사용한 오버로딩처럼 그냥 여러개의 constructor() 를 사용하면 되지만 한가지 아주 중요한 강제 사항이 있습니다.

오버로딩을 위해 추가된 보조 생성자는 반드시 명시적으로 주 생성자를 호출해 줘야 한다는 것이고 이때 사용하는 것이 자바에서도 많이 사용해 보셨을 this() 생성자 호출문법입니다. this는 클래스안에서 본인을 지칭하는 특별한 키워드(정확히는 참조변수) 입니다. 본인 생성자를 다시 호출한다고 하여 this() 생성자라고 부릅니다. 

자바에도 존재하지만 다른점은 생성자의 중괄호 {..} 안에 작성하는 것이 아니고 constructor() 소괄호 다음에 콜론 : 후 위치해야 합니다. 일반 메소드의 리턴타입이 작성되는 위치입니다. 생성자에서 이 this()키워드를 중괄호 {..}보다 먼저 작성한 이유는 중괄호 안의 실행문보다 주 생성자의 호출이 더 먼저 된다는 것을 명시적으로 강제하게 함으로서 개발자의 실행 순서 이해를 실수하지 않게 함이라고 보여집니다.

//시작함수
fun main(){

    //주 생성자와 보조 생성자를 동시에 사용할 때.. [즉, 주 생성자를 오버로딩하고 싶다면...]
    var s5= Simple4()       //주 생성자 호출
    var s6= Simple4(1000)   //보조 생성자 호출
    
 }//main 함수 종료...

//주 생성자와 보조 생성자를 동시에 사용할 때..[즉, 주 생성자를 오버로딩하고 싶다면...]
class Simple4 constructor(){ //1. 주 생성자

    init {
        println("Simple4 init")
    }

    //2. 보조 생성자 - 주 생성자가 명시적으로 표기되어 있다면 반드시 주 생성자를 보조생성자에서 호출해야만 함. (Overloading)
    // [보조생성자 뒤에 : this()]
    constructor(num:Int) : this(){
        println("Simple4 secondary constructor!!!!!!!")
        println()
    }
}


//**출력**
Simple4 init
Simple4 secondary constructor!!!!!!!

 

 

♣ 주 생성자의 construcor 키워드 생략

참고로 주 생성자의 constructor키워드는 접근제한자나 어노테이션이 없다면 생략이 가능합니다.

//시작함수
fun main(){

    //참고. constructor키워드 생략가능
    var s7= Simple5()
    
 }//main 함수 종료...

class Simple5 (){   // 클래스명 Simple5 옆에 constructor 키워드 없이 소괄호 () 만 작성함

    init {
        //주 생성자가 호출될 때 실행되는 영역
        println("Simple5 primary constructor!!")
        println()
    }
    
}


//**출력**
Simple5 primary constructor!!

 


 

다음으로 객체지향의 주요 기능 중 하나인 상속에 대해 알아보도록 하겠습니다.

반응형
반응형

이번글은 프로그래밍의 꽃인 함수 입니다. 이미 알고 있으시겠지만 함수(function:기능)란 '특정 기능의 코드가 써있는 영역' 이라고 생각하시면 됩니다.

로그인기능 관련코드들이 써 있다면 'Login함수', 회원가입기능 관련 코드가 있다는 'Signup함수' 같은 식으로 만들어 필요할 때 원하는 기능의 함수들을 적절히 호출하여 전체 프로그램이 동작하도록 하는 것이 프로그래밍 이라고 보시면 됩니다.

 

이번에도 함수문법 자체를 수업하듯이 소개한다기 보다는, 자바와는 다른 코틀린의 함수 문법 위주로 소개를 하겠습니다. 이것도 사실 자바와 표기법의 차이가 많고 함수형프로그래밍 언어의 특징이 도입되어 다소 생소한 문법과 개념이 등장하니 한번에 이해한다기 보다 이런식으로 사용하는 구나 정도로 살펴보고 앞으로의 앱 개발 과정을 통해 익숙해지면 학습해보길 바랍니다.

 

새로운 코틀린 파일 Test05Basic5.kt 를 만들고 문법을 알아보겠습니다.

프로그램의 시작인 main()함수까지 작성하겠습니다.

 

 

7. 함수 function

1) 함수 정의 및 호출

특정 기능을 수행하는 코드를 작성하는 함수를 만드는 것을 함수를 정의한다고도 부르는데요. 이 별도의 함수는 일반적으로는 main()함수 영역의 바깥쪽에 만드는 것이 일반적입니다.

자바의 경우 완전체 객체지향언어를 원하는 언어여서 객체를 설계하는 class {..} 영역 밖에 무엇인가를 작성하는 것을 허용하지 않았습니다. 그래서 자바에서의 모든 기능(함수)은 class.. 객체안에만 존재할 수 있기에 함수라고 부르지 않고 메소드(method)라고 불렀습니다. 즉, 자바에서는 함수 라는 용어가 존재하지 않습니다.

코틀린은 객체지향언어가 아니고 함수형 언어의 특징을 가진 언어여서 객체 밖에 별도의 변수나 함수가 존재할 수 있습니다. 그래서 코틀린에서는 객체안에 있는 것을 메소드method 라고 부르고, 객체 밖에 있는 것을 함수function 이라고 구분하여 부릅니다. 이번글에서는 객체 밖에 있는 함수function만 소개하고자 합니다. 메소드는 향후 진행 될 코틀린의 객체지향 글에서 자세히 소개하겠습니다.

 

자. 그럼 console 화면에 "show function" 이라는 글씨를 출력하는 기능함수 show() 를 만들어 보겠습니다.

코틀린에서는 함수를 정의할 때 리턴타입의 위치에 fun 키워드를 사용합니다.

//시작 함수
fun main(){

}//main함수 .. 영역 끝

//1) 함수의 정의
//함수를 정의할 때 리턴타입의 위치에 fun 키워드를 사용  -- 파라미터와 리턴이 없는 함수
fun show(){
    println("show function")
    println()
}

 

함수를 만들었다고 해서 그 함수안에 작성한 코드가 자동으로 실행되는 것은 아닌것을 알고 있으시죠?

잘 알다시피 프로그램의 시작은 main()함수의 중괄호 {  .. 로부터  ..} 중괄호 닫을 때까지만 실행되기에  main함수{..} 안에서 show()함수를 불러야만 실행됩니다. 이를 함수의 호출이라고 합니다.

 

그럼 main함수 안에서 show()함수를 호출하여 실행되도록 코드를 추가하겠습니다.

//시작 함수
fun main(){

    //함수호출
    show()

}//main함수 .. 영역 끝

//1) 함수의 정의
//함수를 정의할 때 리턴타입의 위치에 fun 키워드를 사용  -- 파라미터와 리턴이 없는 함수
fun show(){
    println("show function")
    println()
}

//**출력**
show function

 

 

2) 파라미터 전달

함수를 호출할 때 특정 값을 전달하면, 함수에 파라미터(매개변수)를 만들어 받아서 사용할 수 있는데 이 매개변수를 만들 때는 변수를 만들때 사용하는 var, val 키워드를 사용하면 안됩니다. 코틀린은 기본적으로 모든 매개변수는 값 변경이 불가능한 val 변수로 만들어집니다.

//시작 함수
fun main(){

    //함수호출 - 파라미터에 값 전달  - 정수, 문자열 전달
    output(100, "Hello")

}//main함수 .. 영역 끝

//파라미터를 가진 함수 ( 파리미터명 : 자료형 ) - var, val 키워드 명시 X , 자동 val로 지정됨 (자료형 생략 불가)
fun output(a:Int, b:String){   //전달된 정수, 문자열을 받기위해 Int, String 변수로 지정
    println(a)
    println(b)    
    println()
}

//**출력**
100
Hello

 

파라미터는 값 변경이 불가능 한 val 변수로 만들어지기에 값을 대입하는 코드를 사용하면 문법적 에러가 발생합니다.

fun output(a:Int, b:String){
    println(a)
    println(b)
    
    //파라미터 값 변경 시도해보기
    a=50 //ERROR - 매개변수는 자동 val
    println()
}

 

 

3) 리턴하는 함수

자바와 다르게 함수를 만들 때 리턴타입을 먼저 작성하지 않고 함수 소괄호() 뒤에 : 후에 작성합니다.

두 정수를 파라미터로 받아 덧셈하여 그 연산 결과를 리턴해주는 함수를 만들고 이를 main()함수에서 호출하여 사용해보겠습니다.

//시작 함수
fun main(){

    //리턴을 하는 함수 호출   - 두 정수 50, 30을 sum함수에 전달하고 연산된 결과 리턴값을 num변수로 받기
    var num= sum(50, 30)
    println("sum함수의 결과값 : $num ")

}//main함수 .. 영역 끝


//리턴하는 함수 [ 리턴타입을 함수()뒤에 : 후에 작성 ]
fun sum(a:Int, b:Int) :Int{  
    return a+b
}

//**출력**
sum함수의 결과값 : 80

 

참고로. 자바와 다르게 함수의 리턴이 없으면 void 타입이 아니고 Unit이라는 타입의 객체를 리턴하게 됩니다. 즉, 리턴값이 없는 게 아니기에 참조변수로 참조하는 것이 가능합니다. 물론. 특별히 이 Unit을 사용하는 것은 아니니 그렇구나 정도로 알아 두시기 바랍니다.

//시작 함수
fun main(){

    //리턴이 없는 함수의 리턴을 받으면?? void가 아니라 Unit이라는 자료형이 됨.
    var x= display()
    println(x)

}//main함수 .. 영역 끝


//리턴타입이 없는 함수
fun display(){
    println("display!!");
}

//**출력**
display!!        //display()함수 안에 있는 println() 에 의해 출력된 글씨
kotlin.Unit      //main()함수의 x 변수의 출력에 의한 글씨.. Unit 타입이라는 것을 알 수 있음.

 

1) ~ 3) 까지, 함수정의 및 호출부터 파라미터, 리턴타입까지 자바와 다른 코틀린의 기본 함수 문법을 알아봤습니다. 

정리하면,

- 함수를 정의할 때는 fun 키워드를 사용 [ function(함수)의 약자 ]

- 자바와 다르게 class 밖에서도 함수를 정의 할 수 있음

- 파라미터(매개변수)를 만들 때 var, val 키워드를 명시하면 에러. [ default : val 변수 ]

- 리턴타입은 함수() 소괄호 다음에 : 콜론 표기 후 명시

 


♣ 코틀린 함수 문법의 특이점들

 

4) 함수선언의 단순화

함수를 정의할 때 return 키워드를 할당 연산자 [ = ] 로 대체하여 간단하게 표기하는 것이 가능합니다. 

 

4.1) 단순화 문법의 표기법을 알아보기 전에 먼저, 기본적인 return 을 가진 함수를 살펴보겠습니다.

//시작 함수
fun main(){

    // getData()함수의 return 값을 받기
    val data= getData()
    println(data)  // 출력 : Hello

}//main함수 .. 영역 끝

//기본적인 return을 가진 함수
fun getData(): String{
    return "Hello"
}

 

4.2) return 값을 할당연산자 = 로 바꾸어 함수 선언해보기

//시작 함수
fun main(){

    // 단순화된 함수의 리턴값 받기  -- 사용법은 일반 return 키워드 함수 사용과 다른 점 없음
    val data2= getData2()
    println(data2)  // 출력 : Hello

}//main함수 .. 영역 끝

fun getData2():String = "Hello"

 

getData()함수를 호출하면 Hello 값이 리턴되니, 호출하는 입장에서는 getData()함수가 Hello값을 가지고 있는 것으로 볼 수도 있으니 그냥 함수가 return 값을 가지고 있다는 형태의 표기법이 어색하지 않으니 중괄호{ .. }와 return 키워드를 생략하여 코드가 아주 간결해 지도록 하는 간편 문법입니다.

 

근데 함수의 리턴 코드가 다소 복잡한 경우도 있겠죠. 이를 알아보겠습니다.

 

4.3) 먼저, 조금 더 복잡한 리턴 코드가 있는 함수 만들어보기

//시작 함수
fun main(){

    //함수 호출하면서 정수값 5를 전달하여 결과 받기 
    val data3= getData3(5)
    println(data2)  // 출력 : Good    

}//main함수 .. 영역 끝

//좀더 복잡한 리턴 코드가 있는 함수  -- 파라미터로 값을 받아 조건에 따라 리턴값이 다른 함수
fun getData3(num:Int): String{
    if( num < 10 ) return "Good"  -- 전달받은 5가 10보다 작기에 Good 리턴
    else return "Bad"
}

 

4.4.) 할당연산자로 조건에 따른 리턴도 가능합니다. 실행문의 마지막 값을 리턴할 수 있는 if표현식을 사용하는 겁니다.

//시작 함수
fun main(){

    //함수 호출하면서 정수값 15를 전달하여 결과 받기 
    val data4= getData4(15)
    println(data4)  // 출력 : Bad    

}//main함수 .. 영역 끝

//할당연산자로 조건에 따른 리턴도 가능   -- if 표현식 이용
fun getData4(num:Int): String = if(num<10){
    "Good"
} else {
    "Bad"
}

 


 

5) 익명함수

함수의 이름이 없는 함수하고 하여 '익명함수' 라고 부릅니다. 함수를 변수에 대입하여 전달할 수 있다는 점에서 함수형 프로그래밍 언어의 특징을 가장 잘 보여주는 문법입니다.

자바의 익명클래스에 대해 익숙하다면 어느정도 받아들이기 어렵지 않겠지만 익숙치 않다면 다소 어렵게 느껴질겁니다. 이해한다기 보다 이런문법이 있다는 정도로 받아들이고 추후 앱개발 과정에서 이벤트 리스너 처리 코드를 작성하면서 익숙해 지도록 하겠습니다.

 

일반 함수와 익명함수의 문법적 차이를 비교하기 위해 먼저, 일반적인 함수 부터 만들어 보겠습니다.

 

5.1) 기본적인 함수

//시작 함수
fun main(){

    aaa()   //일반함수 호출

}//main함수 .. 영역 끝

//기본적인 함수
fun aaa(){
    println("aaa")
}

 

 

5.2) 익명함수

함수를 만들 때 함수명을 지정하지 않는 것이 익명함 수 입니다. 하지만 단순히 일반함수에서 이름만 없으면 호출할 방법이 없으니 문법적 에러입니다.

//익명함수 - 함수의 이름이 없는 함수 [ 당연히 그냥 이름만 지우면 에러 - why? 함수의 기능은 있지만 호출할 이름이 없으니까.
fun (){...}  //ERROR

 

그래서 익명함수는 반드시 어떤 변수에서 참조되고 있어야 합니다. 이렇게 함수를 가진 변수명을 이용하여 함수를 호출할 수 있습니다.

//bbb 변수에 이름없는 함수(익명함수)를 대입
val bbb= fun(){
    println("bbb")
}

 

 

이제 익명함수를 저장하고 있는 변수명 bbb 를 이용하여 익명함수를 호출해서 실행하겠습니다. 변수인 bbb 를 마치 함수이름 인양 호출하시면 됩니다.

//시작 함수
fun main(){

    bbb()   //익명함수를 참조하는 변수명을 이용하여 함수 호출

}//main함수 .. 영역 끝

//bbb 변수에 이름없는 함수(익명함수)를 대입
val bbb= fun(){
    println("bbb")
}

 

그러고 보니 bbb 변수의 자료형은 무엇일까요? bbb는 함수를 저장하고 있으니 함수의 자료형이겠네요. 아주 중요하게 보셔야할 함수의 자료형 표기법을 알아보겠습니다.

 

5.3) 익명함수를 가진 변수의 자료형

모든 변수는 자료형을 가집니다.[명시적이든 자동추론이 되든] , 그럼 함수를 가진 변수의 자료형은? 람다표기법(화살표)을 사용합니다.

[ 익명함수의 자료형 : () -> 리턴타입 ]  ** 리턴타입이 없으면 Unit [자바의 void역할]

//시작 함수
fun main(){

    ccc()   //명시적으로 익명함수의 자료형[()->리턴타입]이 지정된 참조변수.ccc 이름을 통해 익명함수 호출

}//main함수 .. 영역 끝

//[ 익명함수의 자료형 : () -> 리턴타입 ]  ** 리턴타입이 없으면 Unit [자바의 void역할]
val ccc:()->Unit = fun(){
    println("ccc")
}

 

5.4) 익명함수를 축약형으로 쓰고 싶다면?

즉, fun() 키워드가 굳이 없어도 익명함수임을 구별할 수 있을 듯 하여 {..}만으로 생략이 가능합니다.

//시작 함수
fun main(){

    ddd()   //fun()키워드 생략한 익명함수 호출 [ {} 까지는 생략불가 ]

}//main함수 .. 영역 끝

//익명함수 축약표기법
val ddd:()->Unit = {
    println("ddd")
}

 

당연하게도 {...} 까지 생략하는 축약형은 불가능 합니다. 함수의 기능 코드 작성임을 인식하기도 해야하고. {}까지 없애면 실행문이 함수일때 리턴값을 대입하라는 글씨로 오인될 수 있습니다.(함수선언의 단순화). 즉, 익명함수 {}는 생략불가!

val ddd2:()->Unit = println("ddd2") //ERROR

 

5.5) 축약된 익명함수를 참조하는 변수의 자료형을 생략하는 자동 추론도 가능합니다.

//시작 함수
fun main(){

    eee()   //익명함수를 가진 변수의 자료형을 명시하지 않고 추론하도록..하고 사용

}//main함수 .. 영역 끝

//변수의 자료형은 자동 추론이 되니까 익명함수를 참조하는 변수의 자료형은 생략가능
val eee= {
    println("eee")
}

 

 

♣파라미터 있는 익명함수

 

5.6) 파라미터를 전달받는 익명함수

문자열을 전달받아 그 글자의 글자수를 출력해주는 함수를 만들어 보겠습니다. 리턴값은 없는 함수입니다.

//시작 함수
fun main(){

    fff("Hello") //파라미터를 전달받는 익명함수 호출 - 문자열을 전달받아 그 글자의 글자수를 출력해주는 함수 [리턴값은 없는 함수 ]

}//main함수 .. 영역 끝

//파라미터를 전달받는 익명함수 [리턴값은 없는 함수 ]
val fff= fun(s:String){
    println("글자수:" + s.length)
}

//**출력**
글자수:5

 

5.7) 파라미터를 가진 익명함수의 자료형 [ (파라미터 자료형)->리턴타입 ]

//시작 함수
fun main(){

    ggg("Nice")  //익명함수의 자료형을 명시한 참조변수를 이용한 호출
    
}//main함수 .. 영역 끝

//이 익명함수를 가진 참조변수도 명시적으로 자료형을 명시할 수 있음. [ 파라미터:String 1개, 리턴타입 : Unit ]
val ggg:(String)->Unit = fun(s:String){
    println("글자수:" + s.length)
}

//**출력**
글자수:4

 

5.8) 파라미터를 가진 익명함수의 축약표현 

파라미터가 있는 익명함수도 fun()키워드를 생략할 수 있습니다. 다만, 파리미터를 만드는 소괄호()가 생략되었기에 {} 안에 참조변수 명을 쓰고 화살표 -> 로 실행문을 작성합니다. 

자바와 비슷하지만 참조변수명과 화살표-> 가 중괄호 {} 안에 있다는 것이 차이가 있습니다.

//시작 함수
fun main(){

    hhh("God")   //fun()키워드 생략한 익명함수 호출 [ { } 안에서 파라미터 작성하는 코드 필요 ]
    
}//main함수 .. 영역 끝

//파라미터를 전달받는 함수도 fun()키워드를 생략할 수 있음. 단, { }안에 익명함수의 자료형[()->리턴타입] 같은 형식으로 코딩 필요
val hhh:(String)->Unit = {
    s -> println("글자수:" + s.length)  // "s->" 의 s가 이 익명함수의 파라미터 변수이름 ( s:String 처럼 자료형명시 해도 됨 )
}

//**출력**
글자수:3

 

5.9) 파라미터 1개 일때는 파라미터명과 화살표도 생략가능합니다.

//시작 함수
fun main(){

    iii("He")    //파라미터가 1개라면 { }안에서 생략가능 - 단, 만약 그 파라미터를 사용하고자 한다면 익명함수의 특별한 키워드 "it" 사용
    
}//main함수 .. 영역 끝

//혹시 파라미터가 1개뿐이라면 "s->" 생략해도 됨.
val iii:(String)->Unit = {
    println("iii")
    //단, 만약 그 파라미터를 사용하려면.. 익명함수의 특별한 변수 "it" 키워드 사용 [ it변수의 자료형은 참조변수에 명시한 익명함수 자료형(String) 으로 자동 선언됨
    println("글자수:" + it.length)
}

//**출력**
iii
글자수:2

 

단, 익명함수 참조변수의 명시적 타입지정을 안하면 자동으로 파라미터가 없는 ()->Unit 으로 추론되기에 "it" 키워드의 사용이 불가능합니다.

val iii2= {
    println("글자수:"+it.lenght) //ERROR   - it 을 인식하지 못함
}

 

또한, {} 안에서 파라미터를 생략하지 않았더라도 익명함수의 참조변수 자료형을 자동 추론되게 할 수 없습니다. 즉, 파라미터가 있을때는 익명함수의 자료형을 참조변수에 명시거나 파라미터의 자료형을 명시적으로 표시해야 합니다.

//{} 안에서 파라미터를 생략하지 않았더라도 익명함수의 참조변수 자료형을 자동 추론되게 할 수 없음. 즉, 파라미터가 있을때는 익명함수의 자료형을 참조변수에 명시해야함.
val iii3= {
    //s -> println("글자수:" + s.length)  // ERROR
    s:String -> println("글자수:" + s.length)  // OK - 많이 활용되지는 않음.
}

 

5.10) 파라미터가 여러개인 익명함수의 자료형.  [ (자료형1, 자료형2, ...) -> 리턴타입 ]

파라미터가 여러개일때는 it변수만으로 파라미터들을 표현하는 것이 불가능 하기에 파라미터의 생략은 불가능 합니다. 

//시작 함수
fun main(){

    jjj("sam", 20)  //파라미터 여러개인 익명함수 호출 [ "it"키워드 불가 ]
    
}//main함수 .. 영역 끝

//파라미터 여러개도 당연히 가능 [ 파라미터가 여러개일때는 "it" 키워드는 사용 불가 ]
val jjj:(String, Int) -> Unit = {
        name, age -> println("name: $name    age: $age")
}

//**출력**
name: sam   age: 20

 

♣리턴타입이 있는 익명함수

 

5.11) 리턴타입이 있는 익명함수

익명함수의 리턴타입 지정도 일반함수와 마찬가지로 함수 소괄호 () 뒤에 : 콜론 후 지정합니다.

//시작 함수
fun main(){

    val number= kkk()  // 리턴타입이 있는 익명함수 호출  [ number의 타입은 자동추론 ]
    println( number )  // 출력 : 10  
    
}//main함수 .. 영역 끝

//리턴타입이 있는 익명함수 [파라미터 없고 리턴타입이 Int인 익명함수]
val kkk= fun():Int{
    return 10
}

 

5.12) 리턴타입이 있는 익명함수를 참조하는 변수의 자료형을 명시적으로 지정해보기

//시작 함수
fun main(){

    val number2:Int= lll()  // 익명함수의 자료형을 명시한 참조변수를 이용한 호출 [명시적으로 자료형 지정]
    println( number2 )  // 출력 : 20  
    
}//main함수 .. 영역 끝

//리턴타입이 있는 익명함수를 참조하는 변수의 자료형 명시적으로 지정
val lll: ()->Int = fun():Int{
    return 20
}

 

5.13) 리턴타입이 있는 익명함수의 fun()키워드 생략하는 축약 표기법 사용해보기

* 주의! : 람다식처럼 축약 표기법을 사용했다면 return 키워드는 생략 해야만 합니다.

//시작 함수
fun main(){

    val number3:Int= mmm()  // fun()키워드를 생략한 익명함수 호출 - [ return 키워드도 삭제해야 함 ]
    println( number3 )  // 출력 : 30  
    
}//main함수 .. 영역 끝

//리턴타입이 있는 익명함수의 fun()키워드 생략하는 축약형 
val mmm: ()->Int = {
    30     // - [ * 주의 : return 키워드를 제거해야 함 *]
}

 

5.14) 축약 표기법안에 실행문이 여러개 인 경우

만약 축약된 {}안에 실행문이 여러개 인 경우에는 마지막 실행문의 값이 return 되는 값이 됩니다. if표현식과 동일합니다.

//시작 함수
fun main(){

    val number4:Int= nnn()  // fun()키워드를 생략한 익명함수의 {}안에 실행문이 여러줄인 함수 호출 [ 가장 마지막 실행문이 리턴값 ]
    println( number4 )  // 출력 : 50  
    
}//main함수 .. 영역 끝

//만약 {}안에 실행문이 여러개 라면? - 마지막 실행문의 값이 return 값
val nnn: ()->Int = {
    30
    println("중간 글씨") //{}영역은 실행문 작성 영역이므로 당연히 중간에 이렇게 리턴값이 아닌 실행문이 있어도 됨. 단, 이 println()이 마지막 실행문이면 에러. [리턴타입이 안 맞아서]
    40
    50 //이 값이 리턴값
    //println("중간 글씨") //ERROR  -- 이 함수의 리턴타입이 Int여서 println()함수가 마지막 값일 수 없음
}

//**출력**
중간 글씨
50

 

5.15) 파라미터와 리턴타입이 모두 있는 익명함수

두 개의 정수값을 전달받아 덧셈결과 값을 리턴해주는 함수를 익명함수로 만들고 이 함수의 자료형를 명시해 보겠습니다.

//시작 함수
fun main(){

    val add= ooo(5,3)   //파라미터와 리턴타입이 모두 있는 익명함수 호출 [두수를 전달받아 덧셈결과를 리턴하는 함수]
    println(add)    //출력 : 8
    
}//main함수 .. 영역 끝

//파라미터와 리턴타입이 같이 있는 익명함수 [두 수를 전달받아 덧셈결과를 리턴하는 함수 ]
val ooo:(Int, Int) -> Int = fun(a:Int, b:Int):Int{
    return a+b
}

 

5.16) 파라미터와 리턴타입이 모두 있는 익명함수를 축약표현하기 - 람다식처럼

fun()키워드를 생략하였기에 파라미터는 {}안에 자료형없이 기입하고 화살표 -> 후 return 키워드를 생략한 후 결과값을 기입합니다.

//시작 함수
fun main(){

    val add2= ppp(4,6)  //fun()키워드 생략한 파라미터와 리턴타입이 같이 있는 익명함수 호출
    println(add2)   //출력: 10
    
}//main함수 .. 영역 끝

//fun()키워드 생략한 파라미터와 리턴타입이 같이 있는 익명함수 [두 수를 전달받아 덧셈결과를 리턴하는 함수 ] : {}안에 익명함수 타입(Int,Int)->Int 형태로 코딩 , return 키워드 역시 생략
val ppp:(Int, Int) -> Int = {
        a, b -> a+b      // (Int,Int)->Int 형태로 코딩 , return 키워드 역시 생략
}

 

5.17) 파리미터와 리턴타입의 자료형이 다른 익명함수 연습 - 4가지 형태로 연습

 

5.17-1) 파라미터와 리턴타입이 다른 일반함수

//기본적인 함수 [ String 입력을 받아 글자수를  Int 로 리턴해 주는 기능 함수]
fun stringLength(str:String): Int{
    return str.length
}

 

5.17-2) 파라미터와 리턴타입이 다른 익명함수

//익명함수 [함수를 stringLength2라는 변수에 넣는 것 (String) -> Int 는 String를 매개변수로 받아  Int를 리턴해 준다는 표기법 ]
val stringLength2: (String) -> Int = fun(str:String):Int{
    return str.length
}

 

5.17-3) 파라미터와 리턴타입이 다른 축약형 람다표기법  - fun(), return 키워드 생략

//익명함수 축약형 [ fun() , return 키워드 생략 ]
val stringLength3: (String) -> Int = {
        str -> str.length
}

 

5.17-4) 축약형 람다표기법의 파라미터가 1개인 경우 파라미터명 생략 - fun(), return 키워드, 파라미터 생략 [ 숨겨진 it 파라미터 사용 ]

//파라미터가 1개이므로 생략한 익명함수 축약형 [ fun() , return 키워드 및 파라미터 생략 ]
val stringLength4: (String) -> Int = {
    it.length      // 생략된 파라미터 1개를 대체하는 it 변수
}

 

위 4개의 함수를 호출해 사용해 보겠습니다.

//시작 함수
fun main(){

    //파라미터와 리턴타입이 다른 익명함수 사용의 마지막 연습
    val len= stringLength("android")
    println(len)       //출력: 7

    //익명함수로 만든 함수 호출
    val len2= stringLength2("kotlin")
    println(len2)      //출력: 6

    //축약표현 익명함수 호출
    println( stringLength3("nice") )   //출력: 4
    println( stringLength4("web") )    //출력: 3
    
}//main함수 .. 영역 끝

 

지금까지 살펴본 익명함수는 '고차함수'로 이용할 때 많이 사용됩니다. 다음으로 고차함수를 알아보겠습니다.


 

6) 고차함수

함수를 호출할 때 파라미터에 전달되는 값을 '인수 argument' 라고 부릅니다.

코틀린의 함수는 다른 함수를 인수로 전달 받을 수 있습니다. 이렇게 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 즉, 파라미터에 함수를 받아서 이 함수를 사용한다는 겁니다. 처음 보시는 분들은 다소 어렵게 느껴하는 문법입니다. 이름부터 다소 난이도가 있어 보이기도 합니다. 사실. 고차함수 라는 이름은 크게 중요하지 않습니다. 개발자들도 '고차함수 만들어 봐' 라는 식의 말은 거의 사용하지 않습니다. 그냥 함수를 파라미터로 받는 함수가 고차함수 라는 것을 알고 있을 뿐이니 굳이 고차함수라는 용어에 너무 집착하지 말고 그냥 함수를 받아 사용하는 함수의 문법적 모습만 경험해 보시기 바랍니다. 추후 앱 개발 과정에서 매우 자주 사용하시게 될 겁니다. 그때 익숙해 져 봅시다.

 

고차함수를 알아보기 전에 먼저 함수를 객체처럼 다른 변수에 대입할 수 있다는 것을 먼저 알아보겠습니다.

 

6.1) 익명함수를 참조하는 변수를 다른 변수에 대입하기

//정수를 전달받아 출력하는 익명함수를 저장하는 sss 변수
var sss= fun(a:Int){
    println("sss : $a")
}
var ttt= sss //위에서 만든 익명함수 sss 를 ttt변수가 참조하면 ttt()로 함수호출 가능함.

 

함수를 참조하는 변수 sss와 ttt 모두 같은 함수를 참조하고 있기에 두 변수명 중 어떤 것을 사용해도 호출이 가능합니다.

//시작 함수
fun main(){

    sss(100)
    ttt(200)
    
}//main함수 .. 영역 끝

//**출력**
sss : 100     // sss변수에 의해 호출된 출력
sss : 200     // ttt변수에 의해 호출된 출력

 

6.2) 익명함수를 축약한 람다표기법를 참조하는 변수도 다른 변수에 함수 전달 가능합니다. 즉, 코틀린은 모든 함수를 객체로 다룹니다.

//문자열을 파라미터로 받아 출력해 주는 람다 함수를 저장하는 xxx 변수
var xxx:(String)->Unit= { println(it) }

//xxx 변수가 참조하는 람다함수를 yyy변수에 대입
var yyy= xxx

 

함수를 참조하는 xxx화 yyy변수명을 이용하여 함수를 호출해 보겠습니다.

//시작 함수
fun main(){

    xxx("Hello world")
    yyy("Nice to meet you")
    
}//main함수 .. 영역 끝

//**출력**
Hello world         // xxx변수에 의해 호출된 출력
Nice to meet you    // yyy변수에 의해 호출된 출력

 

위 2가지 예제로 살펴봤듯이 코틀린은 함수를 마치 값처럼 다른 변수에 넘겨주면서 호출하는 것이 가능합니다.

이렇게 다른 변수에 함수를 넘겨줄 수 있다면, 어떤 함수의 파라미터(매개변수)에 다른 함수를 인수 값으로 넘겨주는 것도 가능합니다.

 

6.3) 고차함수 

함수를 다른 변수에 대입할 수 있듯이 함수의 파라미터로 다른 함수를 전달하여 사용하는 것을 고차함수 라고 부릅니다.

첫번째 파라미터로 문자열을 받고, 두번째 파라미터로 함수를 전달받는 고차함수 getLength()라는 함수를 만들어 보겠습니다.

이 함수는 첫번째 파라미터로 전달받은 문자열을 두번째 파라미터로 전달받은 함수의 인수로 전달하는 기능을 구현합니다. 조금 이해가 어렵죠? 주석과 코드를 보면서 이해해 보겠습니다.

//'고차함수' : 함수의 파라미터로 다른 함수를 사용하는 것을 고차함수라고 부름 [다른변수에 함수를 대입할 수 있듯이]
// 두번째 파라미터 aaa는 익명함수를 인자로 받겠다고 표시
fun getLength(str:String, aaa: (String)->Int ): Int{
    return aaa(str) //전달받은 함수(aaa)의 파라미터로 첫번째 파라미터인 str을 전달하면서 함수호출하고 결과 return
}

 

위 고차함수의 두번째 파라미터에 5.17-3)에서 사용했던 문자열의 길이값을 리턴해주는 함수를 전달하여 사용해 보겠습니다.

//시작 함수
fun main(){

    //함수는 다른 함수를 인수로 취할 수 있습니다. 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 이 패턴은 자바에서 콜백 인터페이스를 사용할 때와 동일한 방식으로 구성요소 간에 통신하는 데 유용합니다.
    val len3= getLength("android",stringLength3) //아래 만든 익명함수를 2번째 파라미터에 전달
    println(len3)  //출력: 7
    
}//main함수 .. 영역 끝


//5.17-3)익명함수 축약형 [ fun() , return 키워드 생략 ]
val stringLength3: (String) -> Int = {
        str -> str.length
}

//'고차함수' : 함수의 파라미터로 다른 함수를 사용하는 것을 고차함수라고 부름 [다른변수에 함수를 대입할 수 있듯이]
// 두번째 파라미터 aaa는 익명함수를 인자로 받겠다고 표시
fun getLength(str:String, aaa: (String)->Int ): Int{
    return aaa(str) //전달받은 함수(aaa)의 파라미터로 첫번째 파라미터인 str을 전달하면서 함수호출하고 결과 return
}

 

고차함수에 전달하는 함수를 이전에 만들었던 함수를 사용하는 것이 아니라 함수를 호출하면서 그 자리에서 바로 정의하여 전달할 수도 있습니다.

//시작 함수
fun main(){

    //함수는 다른 함수를 인수로 취할 수 있습니다. 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 이 패턴은 자바에서 콜백 인터페이스를 사용할 때와 동일한 방식으로 구성요소 간에 통신하는 데 유용합니다.
    val len3= getLength("android",stringLength3) //아래 만든 익명함수를 2번째 파라미터에 전달
    println(len3)  //출력: 7
    
    //2번째 파라미터 자리에서 익명함수[String받아 Int를 리턴해주는]를 그 자리에서 설계하여 전달해보기
    val len4= getLength("kotlin", { str -> str.length })
    println(len4)  //출럭: 6 
    
}//main함수 .. 영역 끝

 

당연히 위와 다른 기능의 함수를 정의하여 전달 할 수 도 있습니다.

//시작 함수
fun main(){

    //함수는 다른 함수를 인수로 취할 수 있습니다. 다른 함수를 인수로 사용하는 함수를 고차 함수라고 합니다. 이 패턴은 자바에서 콜백 인터페이스를 사용할 때와 동일한 방식으로 구성요소 간에 통신하는 데 유용합니다.
    val len3= getLength("android",stringLength3) //아래 만든 익명함수를 2번째 파라미터에 전달
    println(len3)  //출력: 7
    
    //2번째 파라미터 자리에서 익명함수[String받아 Int를 리턴해주는]를 그 자리에서 설계하여 전달해보기
    val len4= getLength("kotlin", { str -> str.length })
    println(len4)  //출럭: 6 
    
    //전달되니 문자열을 Int 타입으로 형변환하는 함수를 전달받아 기능 다르게 동작하도록 해보기
    val len5= getLength("450",{ str -> str.toInt() }) //이런식으로 원래 의도와 다른 함수를 전달하여 기능이 바뀌게 할 수도 있음.
    println(len5 + 3)  //출력: 453    ~ 450+3
    
}//main함수 .. 영역 끝

 

6.4) 특이한 고차함수 축약 람다표기법

고차함수의 파라미터안에 또 다른 함수를 쓰면 소괄호 () 안에 중괄호 {} 코드가 작성되기에 가독성이 나빠집니다. 이를 위해 람다식 {}를 소괄호 밖으로 빼서 작성하는 축약 표기법이 가능합니다. 처음에는 잘 읽어지지 않지만 익숙해지면 코딩이 너무 편해집니다.

// ** 코틀린문법에서는 람다식으로 표현된 함수의 파라미터를 () 밖에 작성하는 방식을 권장함. [마치 함수정의 하듯이]
val len6= getLength("ios"){ str -> str.length }
println(len6)

// ** 람다함수의 파라미터가 1개이므로 str-> 를 생략하고 it 이라는 특별한 변수명으로 대체 가능함.
val len7= getLength("mobile"){ it.length }
println(len7)

 

이 고차함수가 가장 많이 사용되는 곳은 안드로이드의 클릭리스너 작업에 많이 사용됩니다.

추후 SAM 변환 문법 소개시에 추가로 소개할 예정입니다.

 


 

7) 함수 파라미터의 default value

자바의 경우 함수를 호출할 때 파라미터의 개수만큼 인수를 전달하지 않으면 에러가 발생합니다. 그래서 함수 호출하면서 사용할 값이 아니더라도 반드시 값을 전달해야만 하는 불편함이 있었습니다. 그래서 코틀린은 혹시 값을 전달 받지 않으면 기본값을 지정할 수 있는 문법을 제공합니다.

//시작 함수
fun main(){

    //함수 파라미터의 default value
    zzz(5,3) 
    zzz() //a,b 가 모두 default 값으로 적용됨.
    zzz(10) //b는 default 값으로 적용됨.
    //혹시 특별하게 b에게 10을 적용하고 싶다면.. 함수 파라미터 선택 적용
    zzz(b=5) //호출할때 변수명 지정
    
}//main함수 .. 영역 끝


//함수 파라미터의 default value
fun zzz(a:Int=1000, b:Int=2000){   //a에 값이 전달되지 않으면 1000, b에 값이 전달되지 않으면 2000
    println("a: $a , b: $b")
}

//**출력**
a: 5 , b: 3
a: 1000 , b: 2000
a:10 , b: 2000
a:1000 , b: 5

 

 

함수 파라미터의 지정을 통하여 값 전달의 순서를 변경할 수도 있습니다.

//시작 함수
fun main(){

    //함수 파라미터의 지정을 통하여 값 전달 순서를 변경할 수도 있음.
    xxxxx("korea", "seoul")
    xxxxx(city = "newyork", nation = "usa")
    
}//main함수 .. 영역 끝

//첫번째 파라미터 nation만 default 값 지정
fun xxxxx(nation:String="korea", city:String){
    println(nation)
    println(city)
    println()
}

//**출력**
korea
seoul

usa
newyork

 

반응형

+ Recent posts