[Java] Interface
인터페이스란?
자바는 클래스를 하나만 상속이 가능한 특성이 있는데 이는 객체지향 프로그래밍에서 큰 제약이기 때문에 인터페이스(Interface)라는 개념을 도입하였다. 자바에서의 인터페이스(Interface)는 객체의 사용 방법을 정의한 타입이다.
인터페이스(Interface)는 객체의 교환성을 높여주기 때문에 다형성을 구현하는 매우 중요한 역할을 한다. 특히 자바 8에서 인터페이스의 중요성은 더욱 커졌다. 자바 8의 람다식은 함수적 인터페이스의 구현 객체를 생성하기 때문이다.
인터페이스(Interface)는 개발 코드와 객체가 서로 통신하는 접점 역활을 한다. 개발 코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출시킨다.
그럼 개발 코드가 직접 객체의 메소드를 호출하면 간단한데, 왜 중간에 인터페이스를 두었을까?
그 이유는 개발 코드를 수정하지 않고, 사용하는 객체를 변경할 수 있도록 하기 위해서이다. 인터페이스는 하나의 객체가 아니라 여러 객체들과 사용이 가능하므로 어떤 객체를 사용하느냐에 따라서 실행 내용과 리턴 값이 다를 수 있다. 따라서 실행 내용과 리턴 값을다양화할 수 있다는 장점이 있다.
인터페이스는 ~.java 형태의 소스 파일로 작성되고 컴파일러(javac.exe)를 통해 ~.class 형태로 컴파일되기 때문에 물리적 형태는 클래스와 동일하다. 차이점은 소스를 작성할 때 선언하는 방법이 다르다.
인터페이스 정의
public interface 인터페이스명 {
//상수
타입 상수명 = 값;
//추상 메소드
타입 메소드명(매개변수, ...);
//디폴트 메소드
default 타입 메소드명(매개변수, ...) { ... }
//정적 메소드
static 타입 메소드명(매개변수) { ... }
}
상수 필드
인터페이스는 객체 사용 설명서이므로 런타임 시 데이터를 저장할 수 있는 필드를 선언할 수 없다. 그러나 상수 필드는 선언이 가능하다. 상수는 인터페이스에 고정된 값으로 런타임 시에 데이터를 바꿀 수 없고 선언할 때에는 반드시 초기값을 대입해야 한다. 인터페이스에 선언된 필드는 모두 public static final의 특성을 갖으며 생략할 수 있다.
추상 메소드
추상 메소드는 객체가 가지고 있는 메소드를 설명한 것으로 호출할 때 어떤 매개값이 필요하고, 리턴 타입이 무엇인지 알려준다. 실제 실행부는 객체(구현 객체)가 가지고 있다. 인터페이스에 선언된 추상 메소드는 모두 public abstract의 특성을 갖으며 생략할 수 있다.
디폴트 메소드와 정적 메소드는 아래에서 좀 더 자세하게 다루겠다.
인터페이스 구현 방법
public interface RemoteControl {
void turnOn();
void turnOff();
}
클래스 선언부에 implements 키워드를 추가하고 인터페이스명을 명시해야 한다.
인터페이스를 통해 구현체를 사용 방법
RemoteControl 인터페이스를 구현한 Audio 클래스와 Television 클래스를 추가로 만든다.
public class Audio implements RemoteControl{
@Override
public void turnOn() {
System.out.println("오디오를 켭니다");
}
@Override
public void turnOff() {
System.out.println("오디오를 끕니다");
}
}
public class Television implements RemoteControl{
@Override
public void turnOn() {
System.out.println("티비를 켭니다");
}
@Override
public void turnOff() {
System.out.println("티비를 끕니다");
}
}
Audio, Television 클래스를 인스턴스화 하여 메소드를 호출한다. 두 클래스는 RemoteControl 타입으로도 생성이 가능하다.
public class Main {
public static void main(String[] args){
Television tv = new Television();
//RemoteControl tv = new Television();
Audio audio = new Audio();
//RemoteControl audio = new Audio();
tv.turnOn();
tv.turnOff();
audio.turnOn();
audio.turnOff();
}
}
결과는 다음과 같다.
티비를 켭니다
티비를 끕니다
오디오를 켭니다
오디오를 끕니다
인터페이스 상속
인터페이스도 다른 인터페이스를 상속할 수 있다. 인터페이스는 클래스와 달리 다중 상속을 허용한다.
하위 인터페이스를 구현하는 클래스는 하위 인터페이스의 메서드뿐만 아니라 상위 인터페이스의 모든 추상 메소드에 대한 실제 메소드를 가지고 있어야 한다. 그렇기 때문에 구현 클래스로부터 객체를 생성하고 하위 및 상위 인터페이스 타입으로 변환이 가능하다.
하위 인터페이스로 타입이 변환되면 상 하위 인터페이스에 선언된 모든 메소드를 사용할 수 있으나, 상위 인터페이스로 타입 변환되면 하위 인터페이스에 선언된 메소드는 사용할 수 없다.
아래의 코드는 위에서 말한 인터페이스 상속의 특징들을 보여준다.
public interface InterfaceA {
void methodA();
}
public interface InterfaceB extends InterfaceA{
void methodB();
}
public class Class implements InterfaceB{
//상 하위 인터페이스의 추상 메소드들을 모두 구현
@Override
public void methodB() {
}
@Override
public void methodA() {
}
}
//상위 인터페이스 -> methodA() 만 사용 가능
InterfaceA a = new Class();
//하위 인터페이스 -> methodA(), methodB() 사용 가능
InterfaceB b = new Class();
인터페이스 메소드
default 메소드
기본 메소드는 자바 8에서 추가된 인터페이스의 새로운 멤버이다. 형태는 클래스의 인스턴스 메소드와 동일한데, default 키워드가 리턴 타입 앞에 붙는다. 디폴트 메소드는 public 특성을 갖으며 생략이 가능하다.
기존 RemoteControl 인터페이스에 디폴트 메소드를 추가하였다.
public interface RemoteControl {
void turnOn();
void turnOff();
default void defaultMethod(){
System.out.println("디폴트 메소드입니다");
}
}
그럼 기본 메소드는 왜 사용할까?
...(중략)... 바로 "하위 호환성"때문이다. 예를 들어 설명하자면, 여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다. (자바의 신 2권)
인터페이스를 보완하는 과정에서 추가적으로 구현해야 할, 혹은 필수적으로 존재해야 할 메소드가 있다면, 이미 이 인터페이스를 구현한 클래스와의 호환성이 떨어지게 된다.
이러한 경우 default 메소드를 추가하여 하위 호환성은 유지되고 인터페이스의 보완을 지킬 수 있다. 새로 작성된 디폴트 메소드는 implements 한 클래스에서 재정의가 가능하다.
static 메소드
자바 8에서 추가되었다. 디폴트 메소드와는 달리 객체가 없어도 인터페이스만으로 호출이 가능하며, 상속이 불가능하다는 특징이 있다.
static 타입 메소드명(매개변수) { ... }
private 메소드
자바 9에서 추가되었다.
자바 8의 default method와 static method는 여전히 불편하게 만든다. 단지 특정 기능을 처리하는 내부 method일 뿐인데도, 외부에 공개되는 public method로 만들어야 하기 때문이다. interface를 구현하는 다른 interface 혹은 class가 해당 method에 액세스 하거나 상속할 수 있는 것을 원하지 않아도, 그렇게 될 수 있는 것이다.
자바 9에서는 위와 같은 사항으로 인해 private method와 private static method라는 새로운 기능을 제공해준다. 따라서 코드의 중복을 피하고 interface에 대한 캡슐화를 유지할 수 있게 되었다.
public interface RemoteControl {
void turnOn();
void turnOff();
default void defaultMethod(){
System.out.println("defaultMethod");
}
private void privateMethod() {
System.out.println("privateMethod");
}
private static void privateStaticMethod() {
System.out.println("privateStaticMethod");
}
}
인터페이스와 추상 클래스의 차이점
추상 클래스란?
추상 클래스는 일반 클래스와 별 다를 것이 없다. 단지, 추상 메서드를 선언하여 상속을 통해서 자손 클래스에서 완성하도록 유도하는 클래스라고 이해하면 된다. 그래서 미완성 설계도라고도 표현합니다. 상속을 위한 클래스이기 때문에 따로 객체를 생성할 수 없다.
클래스 앞에 abstract 키워드를 사용하여 상속을 통해서 구현해야 한다는 것을 알려주고, 선언 부만 작성하는 추상 메소드를 선언할 수 있다.
abstract class 클래스명 {
...
public abstract 타입 메소드명();
}
인터페이스와 추상 클래스 차이점
추상 클래스와 인터페이스의 공통점은 추상 메소드를 사용할 수 있다는 점이다. 그럼 왜 굳이 2가지로 나눠서 사용할까?
1. 사용 의도 차이점
추상 클래스는 "~이다"라고 표현할 수 있고 인터페이스는 "~를 할 수 있는"이라고 표현할 수 있다. 이렇게 구분하는 이유는 다중 상속의 가능 여부에 따라 용도를 정한 것이다.
자바의 특성상 한 개의 클래스만 상속이 가능하여 해당 클래스의 구분을 추상 클래스 상속을 통해 해결하고, 할 수 있는 기능들을 인터페이스로 구현하면 된다.
2. 공통된 기능 사용 여부
만약 모든 클래스가 인터페이스를 사용해서 기본 틀을 구성한다면 공통으로 필요한 기능들도 상속한 클래스에서 재정의 해야 하는 번거로움이 있다.
이렇게 공통된 기능이 필요하다면 추상 클래스를 이용해서 일반 메서드를 작성하여 자식 클래스에서 사용할 수 있도록 하면 된다. 만약 각각 다른 추상 클래스를 상속하는데 공통된 기능이 필요하면 해당 기능을 인터페이스로 작성해서 구현하면 된다.
글로는 이해하기 어려울 수 있으니 아래 예제를 통해 더 자세히 설명하겠다.
예제
위와 같은 관계{를 갖는 예제를 만들어 보겠다.
Creature 추상 클래스
public abstract class Creature {
private int x;
private int y;
private int age;
public Creature(int x, int y, int age) {
this.age = age;
this.x = x;
this.y = y;
}
public void age() {
age++;
}
public void move(int xDistance) {
x += xDistance;
}
public abstract void attack();
}
기본적으로 생명체가 갖는 요소로 위치와 나이가 필요하다고 생각해 선언했고 move() 메소드와 age() 메소드는 모든 생명체에게 공통적으로 필요한 기능이라 생각해 일반 메소드로 선언했다.
attack() 메소드의 경우에는 모든 생명체에게 공통적으로 필요한 기능이지만 각각 생명체에 따라 다르게 구현해야 하기 때문에 추상 메소드로 선언했다.
Human 추상 클래스
public abstract class Human extends Creature {
public Human(int x, int y, int age) {
super(x, y, age);
}
@Override
public void attack() {
System.out.println("도구를 사용!!");
}
@Override
public void talk() {
System.out.println("사람은 말을 할 수 있다.");
}
}
Human 추상 클래스는 Creature 추상 클래스를 상속하고, 사람은 도구를 사용해 공격할 수 있도록 attack() 메소드를 재정의하였다.
Swimable 인터페이스
public interface Swimable {
void swimDown(int yDistance);
}
수영을 할 수 있는 swimDown() 메소드를 가진 Swimable 인터페이스를 생성했다. swimDown 메소드를 Creature 추상 클래스 또는 Human 추상 클래스에 작성해도 되지 않을까? 하는 의문이 들 수도 있을 것이다. 만약 Human 추상 클래스를 상속할 사람들 중에 수영을 못하는 사람이 있을 수도 있다. 이런 경우를 대비해 따로 인터페이스로 선언해줘야 가독성이 좋고 유지보수하는 측면에서도 뛰어나다.
작성한 인터페이스들의 명명 규칙을 보면 알겠지만 ~able로 끝나는 인터페이스가 많은데, 모든 인터페이스가 그런 것은 아니지만 "~를 할 수 있는" 클래스라는 것을 명시해주기 위해 사용된다.
Kevin 클래스
public class Kevin extends Human implements Swimable{
public Kevin(int x, int y, int age) {
super(x, y, age);
}
@Override
public void swimDown(int yDistance) {
setY(getY() - yDistance);
if(getY() < -10) {
System.out.println("너무 깊이 들어가면 죽을수도 있어!!");
}
}
}
마지막으로 Kevin 클래스를 생성했고 Swimable 인터페이스의 swimDown() 메소드를 재정의 해주었다.
Main 클래스
public class Main {
public static void main(String[] args) {
Kevin kev = new Kevin(0, 0, 35);
kev.age();
kev.move(10);
kev.printInfo(); //예제에는 없지만 클래스의 필드를 출력하는 메소드이다.
kev.attack();
kev.swimDown(20);
}
}
Kevin 인스턴스를 생성하고 여러 메소드를 실행하면 아래와 같은 결과가 출력된다.
Kevin -> x : 10, y : 0, age : 36
도구를 사용!!
너무 깊이 들어가면 죽을 수도 있어!!
위 예제 코드는 마이자몽님의 예제 코드를 축약한 것이니 더 자세히 보고 싶으면 링크에 들어가 확인하면 된다.
참고
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java] 오버로딩과 오버라이딩 (0) | 2021.12.21 |
---|---|
[Java] Enum (0) | 2021.12.01 |
[Java] Generic (0) | 2021.11.24 |
[Java] Reflection (0) | 2021.11.21 |
[Java] PermGen과 Metaspace (0) | 2021.11.03 |
댓글