본문 바로가기

Java 길찾기/이것이 자바다

[Java] 인터페이스 (2)

인터페이스 구현

개발 코드가 인터페이스 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 객체는 인터페이스에서 정의된 추상 메소드와 동일한 메소드이름, 매개 타입, 리턴 타입을 가진 실체 메소드를 가지고 있어야 한다. 이러한 객체를 인터페이스의 구현(implement) 객체라고 하고, 구현 객체를 생성하는 클래스를 구현 클래스라고 한다.

 

구현 클래스

구현 클래스는 보통의 클래스와 동일한데, 인터페이스 타입으로 사용할 수 있음을 알려주기 위해 클래스 선언부에 implements 키워드를 추가하고 인터페이스명을 명시해야 한다.

 

public class 구현클래스명 implements 인터페이스명 {
    // 인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}

 

그리고 인터페이스에 선언된 추상 메소드의 실체 메소드를 선언해야 한다. 다음은 Television이라는 이름을 가지고 있는 RemoteControl 구현 클래스를 작성하는 방법을 보여준다. 클래스 선언부 끝에 implements RemoteControl이 붙어 있기 때문에 이 두 클래스는 RemoteControl 인터페이스 사용이 가능하다. RemoteControl 인터페이스에는 3개의 추상 메소드가 있기 때문에 Television에는 이 추상 메소드들에 대한 실체 메소드를 가지고 있어야 한다.

 

public class Television implements RemoteControl {
    // 필드
    private int volume;
    
    // turnOn() 추상 메소드의 실체 메소드
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }
    // turnOff() 추상 메소드의 실체 메소드
    public void turnOff() {
        System.out.println("TV를 끕니다.");
    }
    // setVolume() 추상 메소드의 실체 메소드
    public void setVolume(int volume) {
        if(volume > RemoteControl.MAX_VOLUME) {
            this.volume = RemoteControl.MAX_VOLUME
        }
        else if(volume < RemoteControl.MIN_VOLUME) {
            this.volume = RemoteControl.MIN_VOLUME
        }
        else {
            this.volume = volume;
        }
        System.out.println("현재 TV 볼륨 : " + this.volume);
    }
}

 

구현 클래스에서 인터페이스의 추상 메소드들에 대한 실체 메소드를 작성할 때 주의할 점은 인터페이스의 모든 메소드는 기본적으로 public 접근 제한을 갖기 때문에 public보다 더 낮은 접근 제한으로 작성할 수 없다. public을 생략하면 컴파일 에러를 만나게 된다. 만약 인터페이스에 선언된 추상 메소드에 대응하는 실체 메소드를 구현 클래스에 작성하지 않으면 구현 클래스는 자동적으로 추상 클래스가 된다. 그렇기 때문에 클래스 선언부에 abstract 키워드를 추가해야 한다.

 

public abstract class Television implements RemoteControl {
    public void turnOn() { ... }
    public void turnOff() { ... }
}

 

구현 클래스가 작성되면 new 연산자로 객체를 생성할 수 있다. 문제는 어떤 타입의 변수에 대입하느냐이다. 다음과 같이 Television 객체를 생성하고 Television 변수에 대입한다고 인터페이스를 사용하는 것이 아니다.

 

Television tv = new Television();

 

인터페이스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장한다.

 

인터페이스 변수;
변수 = 구현객체;
// ==========
인터페이스 변수 = 구현객체;

 

RemoteControl 인터페이스로 구현 객체인 Television을 사용하려면 다음과 같이 RemoteControl 타입 변수 rc를 선언하고 구현 객체를 대입해야한다.

 

public class RemoteControlExaple {
    public static void main(String[] args) {
        RemoteControl rc;
        rc = new Television();
    }
}

 

 

익명 구현 객체

구현 클래스를 만들어 사용하는 것이 일반적이고, 클래스를 재사용할 수 있기 때문에 편리하지만, 일회성의 구현 객체를 만들기 위해 소스 파일을 만들고 클래스를 선언하는 것은 비효율적이다. 자바는 소스 파일을 만들지 않고도 구현 객체를 만들 수 있는 방법을 제공하는데, 그것이 익명 구현 객체이다. 자바는 UI 프로그래밍에서 이벤트를 처리하기 위해, 그리고 임시 작업 쓰레드를 만들기 위해 익명 구현 객체를 많이 활용한다. 자바 8에서 지원하는 람다식은 인터페이스의 익명 구현 객체를 만들기 때문에 익명 구현 객체의 코드 패턴을 잘 익혀두길 바란다. 다음은 익명 구현 객체를 생성해서 인터페이스 변수에 대입하는 코드이다. 작성 시 주의할 점은 하나의 실행문으로 끝내는 세미콜론( ; )을 반드시 붙여야 한다.

 

인터페이스 변수 = new 인터페이스() {
    // 인터페이스에 선언된 추상 메소드의 실제 메소드 선언
};

 

new 연산자 뒤에는 클래스 이름이 와야 하는데, 이름이 없다. 인터페이스( ) { }는 인터페이스를 구현해서 중괄호 { }와 같이 클래스를 선언하라는 뜻이고, new 연산자는 이렇게 선언된 클래스를 객체로 생성한다. 중괄호 { }에는 인터페이스에 선언된 모든 추상 메소드들의 실체 메소드를 작성해야 한다. 그렇지 않으면 컴파일 에러가 발생한다. 추가적으로 필드와 메소드를 선언할 수 있지만, 익명 객체 안에서만 사용할 수 있고 인터페이스 변수로 접근할 수 없다. 다음은 RemoteControl의 익명 구현 객체를 만들어 본 것이다.

 

public class RemoteControlExample {
    public static void main(String[] args) {
        RemoteControl rc = new RemoteControl() {
            public void turnOn() { /*실행문*/}
            public void turnOff() { /*실행문*/}
            public void setVolume(int volume) { /*실행문*/}
        };
    }
}

 

모든 객체는 클래스로부터 생성되는데, 익명 구현 객체도 예외는 아니다. RemoteControlExample.java를 컴파일하면 자바 컴파일러에 의해 자동으로 클래스 파일이 만들어진다. RemoteControlExample 이름 뒤에 $가 붙고 생성 번호가 붙는데 생성 번호는 1번부터 시작한다. 만약 두 번째 익명 구현 객체를 만들었다면 클래스 파일명은 RemoteControlExample$2.class 가 된다.

 

 

다중 인터페이스 구현 클래스

객체는 다수의 인터페이스 타입으로 사용할 수 있다. 인터페이스 A와 인터페이스 B가 객체의 메소드를 호출할 수 있으려면 객체는 이 두 인터페이스를 모두 구현해야 한다. 따라서 구현 클래스는 다음과 같이 작성되어야 한다.

 

public class 구현클래스명 implements 인터페이스A, 인터페이스B {
    // 인터페이스 A에 선언된 추상 메소드의 실체 메소드 선언
    // 인터페이스 B에 선언된 추상 메소드의 실체 메소드 선언
}

 

다중 인터페이스를 구현할 경우, 구현 클래스는 모든 인터페이스의 추상 메소드에 대해 실체 메소드를 작성해야 한다. 만약 하나라도 없으면 추상 클래스로 선언해야 한다. 다음은 인터넷을 검색할 수 있는 Searchable 인터페이스이다. search() 추상 메소드는 매개값으로 URL을 받는다.

 

// Searchable.java
public interface Searchable {
    void search(String url);
}

 

만약 SmartTelevision이 인터넷 검색 기능도 제공한다면 RemoteControl과 Searchable을 모두 구현한 SmartTelevision 클래스를 다음과 같이 작성할 수 있다.

 

public class Television implements RemoteControl, Searchable {
    private int volume;

    // RemoteControl의 추상 메소드에 대한 실체 메소드
    public void turnOn() {
        System..out.println("TV를 켭니다.");
    }
    public void turnOff() {
        System..out.println("TV를 끕니다.");
    }
    public void setVolume(int volume) {
        if(volume > RemoteControl.MAX_VOLUME) {
            this.volume = RemoteControl.MAX_VOLUME
        }
        else if(volume < RemoteControl.MIN_VOLUME) {
            this.volume = RemoteControl.MIN_VOLUME
        }
        else {
            this.volume = volume;
        }
        System.out.println("현재 TV 볼륨 : " + this.volume);
    }
    
    // Searchable의 추상 메소드에 대한 실체 메소드
    public void search(String url) {
        System.out.println(url + "을 검색합니다.");
    }
}

 

인터페이스 사용

인터페이스로 구현 객체를 사용하려면 다음과 같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다. 인터페이스 변수는 참조 타입이기 때문에 구현 객체가 대입될 경우 구현 객체의 번지를 저장한다.

 

인터페이스 변수;
변수 = 구현객체;
// ----------------
인터페이스 변수 = 구현객체;

 

예를 들어 RemoteControl 인터페이스로 구현 객체인 Television과 Audio를 사용하려면 다음과 같이 RemoteControl 타입 변수를 rc를 선언하고 구현 객체를 대입해야 한다.

 

RemoteControl rc;
rc = new Television();
rc = new Audio();

 

개발 코드에서 인터페이스는 클래스의 필드, 생성자 또는 메소드의 매개 변수, 생성자 또는 메소드의 로컬 변수로 선언될 수 있다.

 

public class MyClass {
    // 필드
    RemoteControl rc = new Television();
    
    // 생성자
    MyClass(RemoteControl rc) {
        this.rc = rc;
    }
    
    // 메소드
    void methodA() {
        // 로컬 변수
        RemoteControl rc = new Audio();
    }
    
    void methodB(RemoteControl rc) { ... }
}

 

 

추상 메소드 사용

구현 객체가 인터페이스 타입에 대입되면 인터페이스에 선언된 추상 메소드를 개발 코드에서 호출할 수 있게 된다. 개발 코드에서 RemoteControl의 변수 rc로 turnOn() 또는 turnOff() 메소드를 호출하면 구현 객체의 turnOn()과 turnOff() 메소드가 자동 실행된다.

 

RemoteControl rc = new Television();
rc.turnOn(); // Television의 turnOn() 실행
rc.turnOff(); // Television의 turnOff() 실행

 

public class RemoteControlExample {
    public static void main(String[] args) {
    
        RemoteControl rc = null; // 인터페이스 변수 선언
        
        rc = new Television();  // Television 객체를 인터페이스 타입에 대입
        rc.turnOn(); // 인터페이스의 turnOn(), turnOff() 호출
        rc.turnOff();
        
        rc = new Audio();  // Audio 객체를 인터페이스 타입에 대입
        rc.turnOn();  // 인터페이스의 turnOn(), turnOff() 호출
        rc.turnOff();
    }
}

 

 

디폴트 메소드 사용

디폴트 메소드는 인터페이스에 선언되지만, 인터페이스에서 바로 사용될 수 없다. 디폴트 메소드는 추상 메소드가 아닌 인스턴스 메소드이므로 구현 객체가 있어야 사용할 수 있다. 예를 들어 RemoteControl 인터페이스는 setMute()라는 디폴트 메소드를 가지고 있지만, 이 메소드를 다음과 같이 호출할 수는 없다.

 

RemoteControl.setMute(true);

 

setMute() 메소드를 호출하려면 RemoteControl의 구현 객체가 필요한데, 다음과 같이 Television 객체를 인터페이스 변수에 대입하고 나서 setMute()를 호출할 수 있다. 비록 setMute()가 Television에 선언되지는 않았지만 Television 객체가 없다면 setMute()도 호출할 수 없다.

 

RemoteControl rc = new Television();
rc.setMute(true);

 

디폴트 메소드는 인터페이스의 모든 구현 객체가 가지고 있는 기본 메소드라고 생각하면 된다. 그러나 어떤 구현 객체는 디폴트 메소드의 내용이 맞지 않아 수정이 필요할 수도 있다. 구현 클래스를 작성할 때 디폴트 메소드를 오버라이딩해서 자신에게 맞게 수정하면 디폴트 메소드가 호출될 때 자신을 오버라이딩한 메소드가 호출된다. 다음 예제를 보면 Audio는 디폴트 메소드를 오버라이딩했다. Television과 Audio 중 어떤 객체가 인터페이스에 대입되느냐에 따라서 setMute() 디폴트 메소드의 실행결과는 달라진다.

 

// Audio.java
public class Audio implements RemoteControl {
    // 필드
    private int volume;
    private boolean mute;
    
    // turnOn() 추상 메소드의 실체 메소드
    public void turnOn() {
        System.out.println("Audio를 켭니다.");
    }
    // turnOff() 추상 메소드의 실체 메소드
    public void turnOff() {
        System.out.println("Audio를 끕니다.");
    }
    // setVolume() 추상 메소드의 실체 메소드
    public void setVolume(int volume) {
        if(volume > RemoteControl.MAX_VOLUME) {
            this.volume = RemoteControl.MAX_VOLUME;
        }
        else if(volume < RemoteControl.MIN_VOLUEM) {
            this.volume = RemoteControl.MIN_VOLUEM;
        }
        else {
            this.volume = volume;
        }
        System.out.println("현재 Audio 볼륨 : " + this.volume);
    }
    
    @Override
    public void setMute(boolean mute) {
        this.mute = mute;
        if(mute) {
            System.out.println("Audio 무음 처리합니다.");
        }
        else {
            System.out.println("Audio 무음 해제합니다.");
        }
    }
}

 

// RemoteControlExample.java -- 디폴트 메소드 사용
public class RemoteControlExample {
    public static void main(String[] args) {
        RemoteControl rc = null;
        
        rc = new Television();
        rc. turnOn();
        rc.setMute(true);
        
        rc = new Audio();
        rc.turnOn();
        rc.setMute(true);
    }
}

 

 

정적 메소드 사용

인터페이스의 정적 메소드는 인터페이스로 바로 호출이 가능하다. 다음 예제는 RemoteControl의 changeBattery() 정적 메소드를 호출한다.

 

// RemoteControlExample.java -- 정적 메소드 사용
public class RemoteControlExample {
    public static void main(String[] args) {
        RemoteControl.changeBattery();
    }
}