인터페이스의 사용과 그 의미
🤔 인터페이스란?
- 인터페이스는 구현하는(상속받는)클래스의 "기본 설계도"의 역할을 합니다.
- 인터페이스 안에는 상수와 추상 메소드로 이루어져 있습니다.
인터페이스의 형태
interface Printable{
//추상 메소드
public void print(String doc);
}
- 인터페이스는 선언 시에 interface라는 키워드를 붙여 사용합니다.
- 또한 인터페이스 안에 있는 추상 메소드는 메소드의 몸체가 비어 있으며 세미콜론을 위와 같이 붙여 선언합니다.
- 인터페이스는 다른 클래스를 통해 구현됩니다.
- 이와 관련하여 다음의 코드를 살펴봅시다.
💻 예시 코드
class Printer implements Printable{
//Printable 인터페이스의 print 메소드를 구현합니다.
public void print(String doc) {
System.out.printlnn(doc);
}
}
👩🏻🏫 설명
- 위의 코드에서 Printer 클래스는 Printable 인터페이스의 print메소드를 구현하는 것을 확인할 수 있습니다.
- 이처럼 클래스가 인터페이스를 상속하는 행위를 ‘상속’이 아닌 ‘구현’이라고 말합니다.
🔎 클래스가 인터페이스를 구현하는데 같는 특징들
1. 구현하는 클래스는 키워드 implements를 사용하여 구현할 인터페이스를 표시합니다.
2. 하나의 클래스는 둘 이상의 인터페이스를 동시에 구현할 수 있습니다. (다중 구현이 가능합니다)
3. 하나의 클래스는 B라는 클래스를 상속하는 동시에 인터페이스 A를 동시에 구현할 수 있습니다.
(상속과 구현은 하나의 클래스에서 동시에 가능합니다)
- 이를 확인하기 위해 다음의 코드를 살펴보겠습니다.
ex) Robot이라는 클래스는 Machine 클래스를 상속화면서 인터페이스 Movable, Runnable을 구현하는 상황
class Robot extends Machine implements Movable, Ruunable{
....
}
🔎 인터페이스가 갖는 특징들
1.인터페이스를 대상으로 인스턴스의 생성은 불가능합니다.
2.그러나 인터페이스의 형을 대상으로 참조변수의 선언은 가능합니다.
3.인터페이스의 추상 메소드와 이를 구현하는 메소드 사이는 오버라이딩 관계가 성립합니다.
→ 따라서 어노테이션 @Override의 선언이 가능합니다.
- 인터페이스의 특징들을 확인하기위해 다음의 코드를 살펴봅시다.
💻 예시 코드
//인터페이스 Printable
interface Printable{
public void print(String doc);
}
// Printable을 구현하는 Printer 클래스
class Printer implements Printable{
//오버라이딩 관계 성립합니다.
@Override
public void print(String doc){
System.out.println(doc);
}
}
public class PrintableInerface {
public static void main(String []args){
// 인터페이스 Printable형 참조변수은 선언 가능합니다.
Printable prn = new Printer();
prn.print("Hello Java");
}
}
📸 출력 결과
Hello Java
👩🏻🏫 설명
- 위의 코드를 통해 앞서 언급한 인터페이스의 특징들을 다시 한번 확인해보겠습니다.
- 인터페이스를 대상으로 인스턴스의 생성은 불가능하나 인터페이스의 형을 대상으로는 참조변수의 선언이 가능합니다.
//아래와 같이 인터페이스를 대상으로 인스턴스 생성은 불가능합니다.
//Printable prn = new Printable();
//그러나 인터페이스이스의 형을 대상으로는 참조변수의 선언이 가능합니다.
Printable prn = new Printer();
- 위와 같이 Printable 인터페이스를 직접 혹은 간접적으로 선언되는 모든 클래스의 인스턴스를 참조할 수 있습니다.
- 인터페이스의 추상 메소드와 이를 구현하는 메소드 사이에는 오버라이딩 관계가 성립합니다.
- 참조 변수 prn의 추상 메소드 print를 호출할 때 인터페이스의 print메소드가 호출되는 것이 아니라, 이를 구현한 Printer 클래스의 print메소드가 호출됩니다.
prn.print("Hello Java");
호출되는 메소드
@Override
public void print(String doc){
System.out.println(doc);
}
인터페이스의 역할
- 이번에는 인터페이스 왜 이러한 의미를 갖게 되었으며 어떠한 역할을 하는지 다음의 상황에 빗대어 알아보겠습니다.
“마이크로 소프트의 윈도우는 삼성과 LG 프린터를 대상으로 출력을 진행할 수 있습니다.”
마이크로 소프트의 입장
- 프린터를 제작하는 업체는 여러가지가 있으므로 다음과 같은 결정을 내립니다.
“인터페이스를 하나 만들어서 모든 프린터 업체에게 제공해야 되겠다”
- 다음 인터페이스를 만들어서 모든 프린터 업체에게 제공하게 됩니다.
interface Printable{
public void print(String doc);
}
- 위의 코드에는 다음의 의미가 담겨져 있습니다.
“이 인터베이스를 바탕으로 프린터 회사별로 각자 구현해서 가져오기 바랍니다”
“그러면 저희는 print 메소드를 호출하면서 출력할 문서의 정보를 인자로 전달하겠습니다.”
- Printable 인터페이스를 전달받은 삼성과 LG의 프린터 파트는 자사의 프린터 사용에 필요한 클래스를 다음과 같이 각각 정의하여 제공합니다.
class SPrintDriver implements Printable {
@Override
public void print(String doc){
...
}
}
class LPrinterDriver implements Printable {
@Override
public void print(String doc){
...
}
}
- 이를 통해 마이크로 소프트는 위의 클래스 이름만 알면되며 내부적으로 어떻게 구현이 되어 있는지 알 필요가 없습니다.
- 이를 하나의 코드를 통해 살펴보면 다음과 같습니다.
💻 예제 코드
// 마이크로 소프트가 정의하고 제공한 인터페이스
interface Printable{
public void print(String doc);
}
//S사가 정의한 클래스
class SPrinterDriver implements Printable{
@Override
public void print(String doc){
System.out.println("From Samsung printer");
System.out.println(doc);
}
}
//L사가 정의한 클래스
class LPrinterDriver implements Printable{
@Override
public void print(String doc){
System.out.println("From LG printer");
System.out.println(doc);
}
}
public class PrinterDriver {
public static void main(String[]args){
String myDoc = " This is a report about ... ";
//삼성 프린터로 출력
Printable prn = new SPrinterDriver();
prn.print(myDoc);
System.out.println();
//LG 프린터로 출력
prn = new LPrinterDriver();
prn.print(myDoc);
}
}
📸 출력 결과
From Samsung printer
This is a report about ...
From LG printer
This is a report about ...
인터페이스의 문법 구성과 추상클래스
- 인터페이스에는 추상 메소드, 디폴트 메소드,static 메소드가 존재할 수 있습니다. 그리고 인터페이스 간 상속도 가능하며 인터페이스의 형(Type) 이름을 대상으로 instanceof 연산을 할 수도 있습니다.
- 이번에는 인터페이스의 문법에 대하여 자세히 살펴봅시다.
인터페이스에 선언되는 메소드와 변수
- 위의 코드에서 다음과 같은 Printable 인터페이스를 살펴봤었습니다.
interface Printable{
//추상 메소드
public void print(String doc);
}
- 인터페이스 내에 정의된 추상 메소드에는 다음과 같은 특징이 있습니다.
인터페이스의 모든 메소드는 public이 선언된 것으로 간주합니다.
즉, 인터페이스 내에 위치한 메소드에는 별도의 선언이 없어도 public이 됩니다.
이 때문에 Printable 인터페이스의 메소드 앞에는 public을 생략할 수 있습니다.
인터페이스에서는 다음과 같이 변수를 선언할 수 있습니다.
interface Printable{
int PAPER_WIDTH = 70;
int PAPER_HEIGHT = 120;
//추상메소드 print
void print(String doc);
}
- 이렇게 인터페이스 안에 선언된 변수는 다음과 같은 특징을 같습니다.
🔥 인터페이스 안에 선언된 변수의 특징
1. 반드시 선언과 동시에 값을 초기화해야 합니다.
2. 인터페이스 안의 모든 변수는 public,static, final이 선언된 것으로 간주합니다.(생략이 가능합니다)
-> 결론적으로 인터페이스 내에 선언된 변수는 상수입니다.
- 이 때문에 다음과 같은 코드를 작성할 수 있습니다.
//인터페이스 안에 있는 상수를 접근합니다.
System.out.println(Printable.PAPER_WIDTH);
3. 상수는 대문자로 이름을 짓는 관례에 따라서 인터페이스 안에 위치한 변수의 이름은 모두 대문자로 작성해야 합니다.
⚠️주의점
- 인터페이스를 구현하는 클래스는 인터페이스에 존재하는 모든 추상 클래스를 구현해야 합니다.
- 하나라도 구현하지 않으면, 해당 클래스를 대상으로 인스턴스의 생성이 불가능합니다.
인터페이스 간 상속
- 앞서 삼성과 LG 프린터를 가정으로한 코드를 살펴봤습니다. 이와 비슷하게 흑백을 출력할 수 있는 다음의 코드를 살펴봅시다.
💻 예시 코드
// MS가 정의하고 제공한 인터페이스
interface Printable{
public void print(String doc); // 흑백 출력을 위한 추상 메소드
}
// S사의 흑백 프린터 드라이버
class Prn204Drv implements Printable{
@Override
public void print(String doc) {
System.out.println("From MD-204 printer");
System.out.println(doc);
}
}
// L사의 흑백 프린터 드리이버
class Prn731Drv implements Printable{
@Override
public void print(String doc) {
System.out.println("From MD-731 printer");
System.out.println(doc);
}
}
public class PrinterDriver2 {
public static void main(String[]args){
String myDoc = "This is a report about...";
Printable prn = new Prn204Drv();
prn.print(myDoc);
System.out.println();
prn = new Prn731Drv();
prn.print(myDoc);
}
}
📸 출력 결과
From MD-204 printer
This is a report about...
From MD-731 printer
This is a report about...
👩🏻🏫 설명
- 이 상황을 말하자면 다음과 같습니다
“마이크로 소프트사에서 Printable 인터페이스를 디자인할 당시에는 모든 프린터가 흑백으로만 출력할 수 있었습니다.”
이후 몇년이 지난후 컬러 프린터가 등장하여 마이크로 소프트사에서는 컬러와 흑백을 출력하는 인터페이스를 제공해 하는 상황에 있습니다.
이 때 인터페이스의 상속을 사용하면 기존의 Printable 인터페이스를 수정하지 않고도 컬러 출력을 해결할 수 있습니다.
이와 관련하여 다음의 코드를 살펴봅시다.
💻 예시코드
interface Printable{
public void print(String doc); // 흑백 출력을 위한 추상 메소드
}
// Printable을 상속하는 인터페이스
interface ColorPrintable extends Printable{
void printCMK(String doc);
}
// S사의 컬러 프린터 드라이버
class Prn909Drv implements ColorPrintable{
@Override
public void printCMK(String doc) {
// 흑백 출력
System.out.println("From MD-909 black & white ver");
System.out.println(doc);
}
@Override
public void print(String doc) {
// 컬러 출력
System.out.println("From MD 909 CMK ver");
System.out.println(doc);
}
}
public class PrintDriver3 {
public static void main(String[]args){
String myDoc = "This is a report about ...";
ColorPrintable prn = new Prn909Drv();
prn.print(myDoc);
System.out.println();
prn.printCMK(myDoc);
}
}
📸 출력 결과
From MD 909 CMK ver
This is a report about ...
From MD-909 black & white ver
This is a report about ...
👩🏻🏫 설명
- 이처럼 기존에 제작 및 배포가 되어 사용중인 드라이버(흑백만 출력할 수 있는)를 수정할 필요가 없게 됩니다.
// Printable을 상속하는 인터페이스
interface ColorPrintable extends Printable{
void printCMK(String doc);
}
- 위의 코드를 통해 인터페이스 사이에 상속이 가능하고 이를 명시할 때는 extends를 사용합니다.
- 이를 정라하면 다음과 같습니다.
인터페이스간의 상속
- 클래스 간 상속에서 키워드 extends를 사용했듯이 인터페이스의 상속에서도 키워드 extends를 사용합니다.
인터페이스의 디폴트 메소드
- 자바 8에서 도입된 문법입니다.
- 인터페이스와 클래스 사이의 구현에서만 키워드 implements를 사용합니다.
- 다음의 상황을 가정합시다.
상황 가정
- 이미 정의되어 다양한 프로젝트에서 사용 중인 수십 개의 인터페이스가 있는데, 대대적으로 기능 보강을 위해 모든 인터페이스에 최소 한 개 이상의 추상 메소드를 추가해야하는 상황이 발생했습니다.
- 이때 인터페이스의 상속으로 문제를 해결하게 되면 인터페이스의 수는 두 배로 늘어납니다.
- 이로 인해 프로그램 개발에 불편이 생기게 됩니다.
문제 해결
- 이를 해결하기 위해 인터페이스의 디폴트 메소드(Default Method)가 등장하게 되었습니다.
- 앞서 봤었던 프린터 인터페이스를 살펴봅시다.
interface Printable {
void print(String doc);
}
- 이후 컬러 프린트의 등장으로 인해 다음의 추상 메소드를 추가해야 하는 상황이 발생했습니다.
void printCMYK(String doc);
- 이를 디폴트 메소드를 통해 해결하면 다음 코드와 같습니다.
💻 예시 코드
interface Printable {
void print(String doc);
//디폴트 메소드
default void printCMYK(String doc){ };
}
// S사의 흑백 프린터 드라이버
class Prn731Drv implements Printable{
@Override
public void print(String doc) {
System.out.println("From MD-731 printer");
System.out.println(doc);
}
}
// L사의 흑백 프린터 드리이버
class Prn99Drv implements Printable{
@Override
public void print(String doc) {
System.out.println("From MD-990 black & white ver");
System.out.println(doc);
}
@Override
public void printCMYK(String doc) {
System.out.println("From MD-909 CMYK ver");
System.out.println(doc);
}
}
public class PrinterDriver {
public static void main(String[]args){
String myDoc = "This is a report about ...";
Printable prn1 = new Prn731Drv();
prn1.print(myDoc);
System.out.println();
Printable prn2 = new Prn99Drv();
prn2.print(myDoc);
}
}
📸 출력 결과
From MD-731 printer
This is a report about ...
From MD-990 black & white ver
This is a report about ...
- 위의 인터페이스를 더 자세히 봅시다.
- 인터페이스의 메소드 중 default 선언이 붙은 메소드는 디폴트 메소드입니다.
interface Printable {
void print(String doc);
//디폴트 메소드
default void printCMYK(String doc){ };
}
디폴트 메소드는 다음과 같은 특징을 가지고 있습니다.
1. 비록 인터페이스 안에 정의되어 있지만 그 자체로 완전한 메소드입니다.
2. 따라서 이를 구현하는 클래스가 오버라이딩을 해도 되고 하지 않아도 됩니다.
🚨 또한 위의 Printable 인터페이스를 보았을 때 다음과 같이 이해할 수 있어야 합니다.
🔥 “처음에는 Printable 인터페이스에 print 추상 메소드만 있었는데 이후에 필요에 따라 printCMYK 메소드를 추가하였구나”
인터페이스의 static메소드(클래스 메소드)
- 자바 8버전 부터 인터페이스에 static 메소드를 둘 수 있게 되었습니다.
- 이와 관련하여 다음의 코드를 살펴봅시다.
💻 예시 코드
interface Printable{
//static 메소드 선언
static void printLine(String str){
System.out.println(str);
}
default void print(String doc){
// 인터페이스의 static 메소드 호출
printLine(doc);
}
}
// 인터페이스 Printable에는 구현해야 할 메소드가 존재하지 않는다.
class Printer implements Printable{}
public class SimplePrinter {
public static void main(String[]args){
String myDoc = "This is a report about...";
Printable prn = new Printer();
prn.print(myDoc);
// 인터페이스의 static 메소드 직접 호출
Printable.printLine("end of line");
}
}
📸 출력 결과
This is a report about...
end of line
- 위의 코드에서 정의된 Printable 인터페이스에는 구현해야 할 메소드가 존재하지 않습니다.
- 그러나 이를 대상으로는 인스턴스 생성이 불가능함으로 다음과 같이 이를 구현하는 클래스를 정의하였습니다.
class printer implements Printable{ ... }
- 보통 default 메소드를 오버라이딩하지만 static 메소드를 확인하는 것이 목적이라서 오버라이딩하지 않았습니다.
- 또한 인터페이스 내의 static 메소드는 인터페이스의 다른 메소드들과 마찬가지로 public으로 선언된 것으로 간주합니다.
- 이어 다음의 코드를 살펴봅시다.
Printable prn = new Printer();
prn.print(myDoc);
// 인터페이스의 static 메소드 직접 호출
Printable.printLine("end of line");
- 클래스 메소드 부분에서 배웠던 것 같이 static메소드를 사용합니다.
- 사실 직접 static 메소드를 정의하는 일은 굉장히 드물기 때문에 다음의 사실 정도만 알고 넘어갑시다.
”인터페이스에도 static 메소드를 정의할 수 있다”
“그리고 인터페이스이 static 메소드 호출 방법은 클래스의 static 메소드 호출 방법과 같다”
인터페이스 대상의 instanceof연산
- 앞의 포스팅에서 참조변수와 클래스의 이름을 피연산자로 하는 instanceof연산자에 대해서 살펴봤었습니다.
- 예를 들어 다음과 같이 문장을 작성하면
if(ca instanceof Case{
...
}
참조변수 ca가 참조하는 인스턴스가 “Cake의 인스턴스 이거나 Cake를 상속하는 클래스의 인스턴스”인 경우
true가 반환됩니다.
이와 비슷하게 인스턴스를 대상으로도 instanceof 연산자가 사용될 수 있습니다.
-> 인터페이스 Cake를 직접 혹은 간접적으로 구현한 클래스의 인스턴스의 경우 true를 반환합니다.
- 다음의 코드를 통해 살펴봅시다
💻 예시 코드
interface Printable3{
void printLine(String str);
}
// Printable을 직접 구현함
class SimplePrinter2 implements Printable3{
@Override
public void printLine(String str) {
System.out.println(str);
}
}
//Printable을 간접 구현함
class MultiPrinter extends SimplePrinter2{
public void printLine(String str){
super.printLine("stat of multil...");
super.printLine(str);
super.printLine("end of multi");
}
}
public class InstanceofInterface {
public static void main(String[]args){
Printable3 prn1 = new SimplePrinter2();
Printable3 prn2 = new MultiPrinter();
if(prn1 instanceof Printable3)
prn1.printLine("This is a simple printer.");
System.out.println();
if(prn2 instanceof Printable3)
prn2.printLine("This is a muliful printer.");
}
}
📸 출력 결과
This is a simple printer.
stat of multil...
This is a muliful printer.
end of multi
- 예제에서 SimplePrinter는 Printable 인터페이스를 직접, MultiPrinter는 Printable 인터페이스를 간접 구현하였다. 따라서 다음 두 연산의 그 결과로 true가 반환된다.
if(prn1 instanceof Printable3)
prn1.printLine("This is a simple printer.");
System.out.println();
if(prn2 instanceof Printable3)
prn2.printLine("This is a muliful printer.");
마커 인터페이스(Marker Interface)
인터페이스는 클래스에 특별한 표식을 사용하는 용도로도 사용이 됩니다.
-> 이렇게 사용되는 인터페이스를 가리켜 ‘마커 인터페이스(Marker Interface)’라고 하며 마커 인터페이스에는 아무런 메소드도 존재하지 않는 경우가 많습니다.
- 이를 코드를 통해 살펴봅시다.
💻 예시 코드
interface Upper{} // 마커 인터페이스
interface Lower{} // 마커 인터페이스
interface Printerable{
String getContents();
}
class Report implements Printerable,Upper{
String con;
//생성자
Report(String con) {
this.con = con;
}
@Override
public String getContents() {
return con;
}
}
class Printer{
public void printContents(Printerable doc){
// doc 참조 인스턴스가 Upper 구현한다면
if(doc instanceof Upper){
System.out.println((doc.getContents()).toUpperCase());
}
//doc 참조 인스턴스가 Lower 구현한다면
else if (doc instanceof Lower) {
System.out.println((doc.getContents()).toLowerCase());
}else{
System.out.println(doc.getContents());
}
}
}
public class MarkerInterface {
public static void main(String[]args){
Printer prn = new Printer();
Report doc = new Report("Simple Funny News!!");
prn.printContents(doc);
}
}
📸 출력 결과
SIMPLE FUNNY NEWS!!
👩🏻🏫 설명
1. 위의 코드에서 호출하는 toUpperCase와 toLowerCase 메소드는 String 클래스에 다음과 같이 정의되어 있습니다.
public String toUpperCase() //문자열의 모든 문자를 대문자로 바꾼다.
public String toLowerCase() //문자열의 모든 문자를 소문자로 바꾼다.
2. Printer 클래스를 살펴봅시다.
class Printer{
public void printContents(Printerable doc){
// doc 참조 인스턴스가 Upper 구현한다면
if(doc instanceof Upper){
System.out.println((doc.getContents()).toUpperCase());
}
//doc 참조 인스턴스가 Lower 구현한다면
else if (doc instanceof Lower) {
System.out.println((doc.getContents()).toLowerCase());
}else{
System.out.println(doc.getContents());
}
}
}
1) Printable을 구현한 클래스의 인스턴스만 printContents 메소드의 인자가 될 수 있습니다.
public void printContents(Printerable doc){
...
}
2)printContents에 전달된 인스턴스가 Upper 인터페이스를 구현하면 대문자로 출력됩니다.
if(doc instanceof Upper) {...}
3) printContents에 전달된 인스턴스가 Lower 인터페이스를 구현하면 소문자로 출력됩니다.
else if(doc instanceof Lower) {...}
4) 위의 클래스 Report는 Printable 인터페이스를 구현하면서 동시에 Upper 인터페이스를 구현합니다.
class Report implements Printable, Upper{...}
5) 따라서 다음의 코드를 실행할 때 대문자로 출력됩니다.
public class MarkerInterface {
public static void main(String[]args){
Printer prn = new Printer();
Report doc = new Report("Simple Funny News!!");
prn.printContents(doc);
}
}
6) 반면 클래스 Report를 Lower로 바꾸면 내용들이 소문자로 출력됩니다.
class Report implements Printable, Lower{...}
정리
인터페이스 Upper와 Lower는 Report클래스에 표식을 붙이는 형태로 사용하였습니다.
이처럼 사용되는 인터페이스를 ‘마커 인터페이스’라고 부릅니다.
추상 클래스: Abstract Class
❓ 추상 클래스란?
- 하나 이상의 추상 메소드를 갖는 클래스를 의미합니다.
- 다음의 코드를 통해 살펴봅시다.
💻 예시 코드
public abstract class House{ //추상 클래스
public void methodOne(){
System.out.println("method one");
}
//추상 메소드
public abstract void methodTwo();
}
이 처럼 하나의 추상 메소드를 갖고 있는 클래스를 ‘추상 클래스’라고 합니다.
클래스 선언부에 abstract 선언을 추가해야 합니다.
추상 메소드의 특징
1.추상 클래스를 대상으로 인스턴스의 생성이 불가합니다.
2.다른 클래스에 의해 추상 메소드가 구현되어야 합니다.
3. 추상 클래스도 클래스에 포함됨으로 상속이라고 하며 키워드 extends를 사용합니다.
- 따라서 추상 메소드는 다음과 같이 상속의 모습을 가집니다.
💻 예제 코드
public class MyHouse extends House{
@Override
public void methodTwo(){
System.out.println("method Two");
}
}
👨🏻🏫 정리
지금까지 알던 클래스들과 마찬가지로 인스턴스 변수와 인스턴스 메소드를 갖고 있지만, 이를 상속하는 하위 클래스에서 구현되어야 할 메소드가 하나 이상 있을 경우 이를 ‘추상 클래스’라고 합니다.
📒 Reference (참고 자료)
- 윤성우의 열혈 자바
'프로그래밍 언어 > Java문법 - 객체지향' 카테고리의 다른 글
14. Java 객체지향 문법(14) - 예외처리 (0) | 2022.11.19 |
---|---|
12. Java 객체지향 문법(12) - 상속의 목적 (0) | 2022.11.03 |
11. Java 객체지향 문법(11) - 상속과 오버라이딩 (0) | 2022.11.02 |
10. Java 객체지향 문법(10) - 상속의 기본 (0) | 2022.11.02 |
9. Java 객체지향 문법(9) - 배열 (0) | 2022.10.31 |