Before diving into interface

초보 자바 개발자 분들의 단골로 하는 질문이 있습니다.

“리스너(콜백)가 뭐에요?” 또는 “인터페이스가 뭔지 모르겠어요!!”

자바의 인터페이스에 대한 개념없이 리스너를 이해하려 하는것은 헤어나올 수 없는 늪에 빠진것과 같습니다. 초보자 분들을 위해 인터페이스의 개념도 알아보고 실제 용례도 알아보고자 이 포스팅을 작성합니다.

What is Interface?

포스팅을 작성하기 위해 인터페이스에 대한 정의부터 찾아 보았습니다.

An interface in the Java programming language is an abstract type that is used to specify a behavior that classes must implement.
(
자바프로그래밍의 인터페이스는 클래스가 반드시 구현해야할 행동을 지정하는 데 사용되는 추상타입이다. )

출처 : https://en.wikipedia.org/wiki/Interface_(Java)

개발에 막 입문한 초보자가 이 정의를 보고 이해가 될 것이라고는 생각 안합니다. 너무 어려운 이야기 입니다.

“뭘 구현한다는거지? 행동은 뭐고 추상은 뭘까…”

하나씩 풀어서 보겠습니다.

행동(Behavior)

보통 다른 언어에서는 함수라는 것이 별도로 존재하지만, 자바에서는 클래스밖에서 함수가 따로 존재하지 않습니다. 그래서 클래스 내에 있는 어떤 함수를 우리는 메소드라 부릅니다. 사실 기능적인 측면에서 봤을때는 메소드나 함수나 같은것이죠. 하지만 자바에서는 메소드라 부릅니다. 이 메소드가 바로 객체지향프로그래밍(OOP)에서의 행동입니다.

public int sum(int a, int b) {
    return a+b;
}

다음과 같은 코드가 있을때 sum이란 메소드가 있고 a와 b인자를 입력받아 a+b의 결과값을 리턴하고 있습니다. a와 b를 입력받아 더하는 행동을 하고 있습니다. 결국 sum이라는 메소드가 인터페이스의 정의에서 말한 행동을 뜻합니다. 인터페이스에서는 메소드(행동)를 정의 합니다.

구현 (Implementation)

자바의 interface는 아래의 예제와 같은 형태를 가지고 있습니다.

public interface Calculator {
    int sum(int a , int b);
}

interface를 정의 하기 위해 class대신 interface라는 키워드를 사용하였습니다.
Calculator는 제가 정의한 인터페이스명입니다. 인터페이스 Calculator{…} 내부에는 sum이라는 메소드를 정의하였지만 sum은 괄호{}가 없습니다. sum이란 메소드를 정의했지만 구현은 하지 않은 것입니다.   interface를 구현할때는 어떤 클래스에 implements라는 키워드를 사용합니다.

public class CalculatorImpl implements Calculator {
    @Override
    public int sum(int a, int b) {
        return a+b;
    }
}
Note : 인터페이스를 구현하고자 한다면, 인터페이스내의 정의된 메소드들은 반드시 구현해야합니다.

위와 같이 CalculatorImpl이라는 클래스에 Calculator 인터페이스를 구현하였다면 CalculatorImpl로 만들어진 객체는 CalculatorImpl타입이기도 하지만 Caculator이기도 합니다.

Calculator calc = new CalculatorImpl();

추상(abstraction)

위의 예제처럼 Calculator에서 메소드를 정의하고 CalculatorImpl에서 구현하는거처럼, 어떠한 행동에 대한 구체적인 구현을 나중으로 미루는것을  추상이라고 할 수 있겠습니다.

다시 정리하자면 추상이런것은 구체적이거나 실체는 없고, 행동(메서드) 정의만 있는 것입니다.

추상화를 통해 자바의 특징중 하나인 다형성(Polymorphism)을 구현할 수 있습니다.

추상화된 interface 구현을 어떻게 하느냐에 따라서 메소드내의 코드 또는 반환값을 다르게 만들 수 있습니다. 결국 같은 interface를 구현하더라도 다른 형태를 가질수 있기 때문에 이를 다형성이라 합니다. 다형성에 대한 예제를 보겠습니다

public class CalculatorPlus3 implements Calculator {
    @Override
    public int sum(int a, int b) {
        return a+b+3;
    }
}

CalculatorPlus3라는 클래스를 만들었고 마찬가지로 Calculator를 구현하였습니다. 하지만 아까와는 다르게 반환값에 항상 3을 더하고 있습니다.  CalculatorPlus3로 생성된 객체는 여전히 Calculator라는 타입이고 sum이라는 메소드를 가지고 있지만 그에 대한 반환값 등은 다르게 구현이 되어있습니다. 이렇게 형태가 달라지는것을 다형성이라 합니다.

다형성이 가지는 장점으로는 확장성이 좋은 코드를 작성 할 수 있고, 결합도가 강하지 않은 프로그래밍을 할 수 있다는 점이 있습니다.

콜백 vs 리스너 vs 옵저버

사실 코드로보면 이 셋은 다 똑같아 보입니다. 하지만 개념적으로 조금 다르게 해석됩니다.

  • 콜백(Callback) : 이벤트가 발생하면 특정 메소드를 호출해 알려준다. (1개)
  • 리스너(Listener) : 이벤트가 발생하면 연결된 리스너(핸들러)들에게 이벤트를 전달한다 (n개)
  • 옵저버(Observer) : 데이터나 속성의 변경을 감지하여 구독자에게 변경사항을 전달한다.

위 셋은 인터페이스를 이용하여 이벤트를 통지한다는 점에 있어서는 기능이 같고, 안드로이드 프레임워크의 리스너는 사실 n개가 아닌 1개밖에 등록을 못한다는 점에서 콜백이랑 개념이 크게 차이가 없습니다.

간단 예제, 직접 리스너를 만들어보자

이벤트를 전달하고자 하는 인터페이스를 정의합니다.

public interface MyEventListener {
    void onEvent();
}

 

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listener.onEvent();
    }
    
    MyEventListener listener = new MyEventListener() {
        @Override
        public void onEvent() {
            Toast.makeText(MainActivity.this, "onEvent",Toast.LENGTH_SHORT).show();
        }
    }; 
}

 

listener라는 MyEventListener객체를 만듭니다. 클래스 없이 바로 interface만으로도 객체를 생성할 수 있고, 이름이 없는 클래스이기 때문에 이를 익명클래스 또는 무명클래스라고 합니다.

보통 View.setOnclickListener(new OnClickListener(){…}) 이런식으로 View에 리스너를 다는데, 이것도 대표적인 무명클래스를 이용하는 방법입니다.

listener객체를 통해 onEvent()를 호출하여 무명클래스가 이벤트를 잘 전달받는지 확인합니다.

RecyclerView의 아이템 클릭 리스너 만들기

public interface MyItemClickListner {
    void onClickMeClick(int position);
    void onItemClick(int position);
}
package com.charlezz.interfacestudy;

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    public static final String TAG = MyAdapter.class.getSimpleName();

    private MyItemClickListner listener;

    public void setOnItemClickListener(MyItemClickListner listener){
        this.listener = listener;
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_view, viewGroup, false);
        final MyViewHolder holder = new MyViewHolder(view);
        holder.rootView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(listener!=null){
                    listener.onItemClick(holder.getAdapterPosition());
                }
            }
        });

        holder.clickMe.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(listener!=null){
                    listener.onClickMeClick(holder.getAdapterPosition());
                }
            }
        });
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {
        myViewHolder.textView.setText(String.format("%d번째 아이템", i));
        myViewHolder.rootView.setBackgroundResource(i%2==0?R.color.bg01:R.color.bg02);

    }

    @Override
    public int getItemCount() {
        return 100;
    }

    static class MyViewHolder extends RecyclerView.ViewHolder{
        Button clickMe;
        TextView textView;
        View rootView;

        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            rootView = itemView;
            clickMe = itemView.findViewById(R.id.click_me);
            textView = itemView.findViewById(R.id.text_view);
        }
    }

}
public class MainActivity extends AppCompatActivity implements MyItemClickListner{

    RecyclerView recyclerView;
    MyAdapter adapter = new MyAdapter();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setAdapter(adapter);

        adapter.setOnItemClickListener(this);


    }

    @Override
    public void onClickMeClick(int position) {
        Toast.makeText(MainActivity.this, String.format("ClickMe:%d", position),Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onItemClick(int position) {
        Toast.makeText(MainActivity.this, String.format("ItemClick:%d", position),Toast.LENGTH_SHORT).show();
    }
}

 

Conclusion

최대한 쉽게 풀어서 설명하고자 했는데 도움이 되셨을지 모르겠습니다.
궁금한점이나 틀린내용이 있다면 댓글로 남겨주시기 바랍니다.

본문의 예제는 github에서 확인가능합니다.

카테고리: 미분류

18개의 댓글

초보개발자 · 2018년 12월 13일 8:27 오후

너무너무 이해가 잘 되게 설명해주셨네요 정말 감사합니다!!!

    Charlezz · 2020년 7월 18일 12:34 오후

    감사합니다 🙂

익명 · 2020년 2월 18일 10:56 오전

어떤 프로그래머들은 설명을 하지면 딱딱하고 무슨 소리를 하는지 알아듣질 못하겠는데….
님께서는 참 쉽게 잘 전달하는 능력이 있으시네요. 잘 이해가 됐습니다. 고맙습니다.

    Charlezz · 2020년 2월 19일 5:04 오후

    과찬이십니다. 감사합니다!

익명 · 2020년 2월 27일 3:08 오후

깔끔하게 정리 잘하셨네요 감사합니다.

    Charlezz · 2020년 7월 18일 12:34 오후

    감사합니다 🙂

sjp · 2020년 4월 3일 8:52 오후

책보다가 너무 어려워서 찾아보다가 좋은 글을 발견했네요. 감사합니다.

    Charlezz · 2020년 7월 18일 12:34 오후

    감사합니다 🙂

익명 · 2020년 7월 17일 11:51 오후

감사합시다

    Charlezz · 2020년 7월 18일 12:34 오후

    감사합니다 🙂

Charlezz · 2020년 7월 18일 12:34 오후

Thank U. and bye

Astin · 2020년 12월 22일 8:47 오후

개념 설명 짱입니다.

    Charlezz · 2021년 2월 19일 3:34 오후

    감사합니다 🙂

확찐자 · 2021년 2월 19일 12:18 오후

초본데, 콜백구현 문제로 이틀간 고생하다 이 글보고 해결했네요.
감사합니다.

    Charlezz · 2021년 2월 19일 3:34 오후

    감사합니다 🙂

무야호 · 2021년 3월 17일 11:20 오후

와. 찰스님 블로그 몇 번 봤지만 이렇게 쉽게 설명한 글 처음봅니다.

    Charlezz · 2021년 3월 21일 3:43 오후

    감사합니다 ㅎㅎ

yinzhen · 2022년 12월 5일 10:04 오후

추상과 인터페이스라는 단어들을 때마다 애매했었는데, 오늘로써 개념을 확실히 했습니다! 귀한 글 감사합니다!!

Charlezz 에 답글 남기기 응답 취소

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.