[Java] 인터페이스에 대해 알아보자
자바에서는 소스코드를 고쳐 쓰는 방식에는 두 가지가 존재합니다.
1. is a 상속(has a 상속이 아니다)을 통해 고쳐쓰는 방법
is a 상속 방식은 소스코드가 어떻게 고쳐쓰여질지 예측하지 못하여 자식 클래스에서 오버라이드하여 고쳐씁니다.
2. 인터페이스를 통해 고쳐쓰는 방법
인터페이스 방식은 소스코드가 어떻게 활용될지 예측할 수 있는 방식입니다.
이번 포스팅에서는 인터페이스에 대해 알아보겠습니다.
예전에는 인터페이스를 추상클래스와 비교하면서 다중 상속, 단일 상속, 메서드를 강제적으로 구현해야 하는 강제성이 누가 더 높은지를 중점적으로 공부했습니다.
하지만 이렇게 피상적으로만 알고 있다보니 막상 구현을 할 때는 건드릴 수가 없었습니다. 어떠한 경우에 추상클래스와 인터페이스를 사용해야 하는지도 모른채 암기식으로만 달달 외우고 있었던 거였죠.
사실상 인터페이스와 추상클래스는 사용 목적 자체가 완전 다르기 때문에 다중 상속, 단일 상속 등 이러한 방식으로 접근하는 것을 다소 방향이 빗나가지 않았나 생각합니다.
인터페이스는 내가 작성한 소스코드를 다른 사람이 사용할 때, 특정 시점에서 그 사람이 어떤 행위를 할 여지가 있다고 판단하여 미리 메서드를 정의해놓는 틀입니다. 즉, 다른 코드를 끼워 넣기 위한 접점으로 볼 수 있습니다.
만약 제가 우주선이 날아다니는 2D게임을 AWT(자바 추상 위젯 키트)로 개발하고 있다고 가정을 해보겠습니다.
저는 우주선의 방향이 바뀔 때마다 일정한 소리를 내도록 구현을 했습니다.
그런데 다른 사람들은 우주선의 방향이 바뀔 때마다 폭탄을 설치하고 싶을 것이란 생각이 문득 듭니다.
이러한 경우에 인터페이스를 사용합니다.
인터페이스를 제공하는 측(필자)은 사용자가 사용할 여지가 있는 메서드(폭탄을 설치해라)를 '정의'와 '호출'을 해야하고 사용자는 메서드를 '구현'만 하면 됩니다.
인터페이스를 만드는 방법은 총 세 가지가 존재합니다.
1. 새로운 클래스를 생성하여 만들기
2. 기존 클래스에 기어 들어가서 만들기
3. 익명 클래스 활용하기
1. 새로운 클래스를 생성하여 만들기
public class Enemy {
public void update() {
this.move(x, y);
// 우주선이 움직일 때 다른 사용자가 뭐라도 여기에서 코드를 넣고 싶지 않을까?
// 인터페이스를 만드는 사람은 메서드를 '정의'와 '호출'만 한다.
// 사용자가 메서드를 '구현'한다. 구현하는 위치는 캔버스의 생성자 내부.
?.onMove();
}
}
가령 제가 만드는 2D 게임에 우주선이 움직일 때마다 다른 사용자들이 어떠한 기능을 넣고 싶을 수도 있습니다.
이럴 때 인터페이스를 사용하는데 저는 사용자가 사용할 것 같은 메서드를 정의와 호출만 해줍니다. onMove();로 말이죠
자바는 어떠한 객체를 통해 메서드를 호출하는 방식입니다. 따라서 onMove 앞에서는 특정 객체가 와야 합니다.
어떤 객체가 올 수 있을까요? 인터페이스를 만들어야 합니다.
인터페이스를 만들 때는 ~~Listener라는 명칭을 많이 사용한다고 합니다. 저는 EnemyMoveListener로 만들어 보겠습니다.
public interface EnemyMoveListener {
// 여러분이 약속으로 정의하고 싶은 함수 목록을 쓰세요.
// 약속 목록이기 때문에 구현하지 않는다. 즉, 함수 블록이 있으면 안된다.
// 형식지정자는 사용하지 않는다. public을 의미한다.
void onMove();
}
인터페이스는 약속(메서드)를 정의하는 틀이기 때문에 구현부가 존재하지 않습니다. 왜냐하면 구현부는 직접 사용하고자 하는 사용자가 본인 입맞에 맞게 구현하면 되기 때문이죠.
인터페이스에 형식 지정자가 없는 이유는 public이 생략되어 있습니다. 외부 클래스에서 사용되기 때문입니다.
이제 인터페이스를 만들었으니 다시 Enemy 클래스로 돌아가 하나의 인스턴스를 만들어서 onMove를 호출해보겠습니다.
public class Enemy {
private EnemyMoveListener moveListener;
public void update() {
this.move(x, y);
moveListener.onMove();
}
}
인터페이스를 자료형으로 하는 변수(moveListener)를 만들고 이를 통해 onMove를 호출합니다.
그런데 이런 경우에는 moveListener가 null이기 때문에 실행하면 널 포인터 익셉션(NPE)가 발생합니다.
그래서 null이 아닌 경우에만 onMove를 실행할 수 있도록 조건을 걸어주어야 합니다.
그리고 추가적으로 사용자가 onMove 메서드를 구현하고 인터페이스 꽂기 위해 setter를 구현해야 합니다.
public class Enemy {
private EnemyMoveListener moveListener;
public void update() {
this.move(x, y);
if (moveListener != null)
moveListener.onMove();
}
// 사용자가 onMove 함수를 구현하고 꽂기 위함
public void setMoveListener(EnemyMoveListener moveListener) {
this.moveListener = moveListener;
}
}
이제 사용자가 메서드를 구현하는 과정을 살펴보겠습니다.
새로운 클래스가 필요합니다. 이 클래스는 인터페이스 메서드(onMove)를 구현하는 클래스입니다.
클래스명은 아무런 의미가 없습니다. 오히려 클래스명에 의미가 들어가는 순간 캡슐로 생각할 수 있기 때문에 조심해야 합니다.
어떤 분들은 I001, I002 이러한 방식으로 이름을 짓는데 저는 일단 막 지어보았습니다.
EnemyMoveListener를 사용하는 방법은 implements 키워드를 사용합니다.
public class Aadkjfhgbdgkjhbdag implements EnemyMoveListener {
@Override
public void onMove() {
System.out.println("Move!");
}
}
이후 현재 2D게임을 구현하고 있다는 가정이기 때문에 개체를 관리하는 ActionCanvas에서 onMove를 관리하겠습니다.
ActionCanvas는 Canvas를 상속하고 있습니다. 이 부분은 무시하셔도 됩니다.
public class ActionCanvas extends Canvas {
Enemy enemy;
public ActionCanvas() {
enemy = new Enemy();
enemy.setMoveListener(new Aadkjfhgbdgkjhbdag());
}
}
이렇게 사용자는 원하는 메서드를 구현하고 setter 함수에 꽂아줌으로써 우주선이 움직일 때마다 Move! 라는 문자열을 콘솔에서 확인할 수 있습니다.
2. 기존 클래스에 기어 들어가서 만들기(내부 클래스 활용)
ActionCanvas 클래스 내부에서 또 다른 클래스를 만들어 인터페이스 메서드를 구현할 수 있습니다.
내부 클래스명은 아무런 의미가 없기 때문에 아무거나 적어도 상관없습니다. 오히려 의미있는 이름을 지으면 헷갈릴 수도 있다고 합니다.
EnemyMoveListener를 사용하는 방법은 implements 키워드를 사용합니다.
public class ActionCanvas extends Canvas {
Enemy enemy;
public ActionCanvas() {
enemy = new Enemy();
}
class asjdhfbsd implements EnemyMoveListener {
@Override
public void onMove() {
System.out.println("Move!");
}
}
}
결과는 첫 번째 만드는 방식과 동일하게 Move!를 콘솔에 출력합니다.
3. 익명 클래스 활용하기
익명 클래스를 활용하는 방식은 최근 트랜드라고 합니다.
익명 클래스를 활용하면 따로 인터페이스 메서드를 구현하는 클래스를 만들지 않아 파일의 개수가 늘지 않는다는 장점과 내부 클래스를 사용하지 않는다는 장점이 있습니다.
public class ActionCanvas extends Canvas {
Enemy enemy;
public ActionCanvas() {
enemy = new Enemy();
enemy.setMoveListener(new EnemyMoveListener() {
@Override
public void onMove() {
System.out.println("Move!");
}
}
}
}
결과는 첫 번째 만드는 방식과 동일하게 Move!를 콘솔에 출력합니다.
다음 포스팅에서는 추상화와 추상 클래스에 대해 알아보겠습니다. 읽어주셔서 감사합니다.