[Java Semina] 8회

Jiwon-Woo·2021년 7월 18일
0

Java Semina

목록 보기
9/9

1. 인터페이스

인터페이스는 본래 추상 메서드와 상수만으로 구성되어 클래스나 프로그램이 제공하는 기능을 명시적으로 선언해주는 역할을 한다. 일종의 설계도라고 볼 수도 있다.

인터페이스의 구현 즉, 추상 메서드의 구현은 클래스에서 이루어지는데, 클래스마다 다르게 구현했다고 하더라도 인터페이스에서 선언한 메서드의 형태만 파악할 수 있기 때문에 메서드의 구현부를 몰라도 사용할 수 있다. 이런 특성에 의해 인터페이스를 설계도로서 이용할 수 있게 된다.

1.1 인터페이스 예제

package calculator;

public interface Calc {

	/* final 예약어를 쓰지 않아도 컴파일 과정에서 자동으로 상수가 됨 */
	double PI = 3.14;
	int ERROR = -999999999;
	
	/* abstract를 쓰지 않아도 컴파일 과정에서 자동으로 추상메서드가 됨 */
	int add(int num1, int num2);
	int sub(int num1, int num2);
	int times(int num1, int num2);
	int divide(int num1, int num2);

}

인터페이스는 기본적으로 상수와 함수의 선언으로만 이루어져있기 때문에 final 이나 abstract 를 쓰지 않아도 컴파일 과정에서 자동으로 상수와 추상 메서드로 변환되며, 아무것도 적지 않으면 메서드의 접근지정자는 public 이 된다.

구현부가 없는 추상 메서드로만 이루어져 있기 때문에 인스턴스 생성이 불가능 할 뿐만 아니라, 클래스와 달리 생성자의 선언조차 불가능하다.


1.2 인터페이스의 구현

인스턴스 생성이 불가능한 인터페이스를 사용하려면, 클래스에서 인터페이스를 구현하는 과정이 반드시 필요하다. 인터페이스를 구현한다는 것은 인터페이스에서 선언되었던 추상 메서드를 구현한다는 것을 뜻한다.

public class Calculator implements Calc {
	
	@Override
	public int add(int num1, int num2) {
		return num1 + num2;
	}

	@Override
	public int sub(int num1, int num2) {
		return num1 - num2;
	}
    
	@Override
	public int times(int num1, int num2) {
		return num1 * num2;
	}

	@Override
	public int divide(int num1, int num2) {
		if (num2 != 0) {			
			return num1 / num2;
		}
		else {
			return Calc.ERROR;
		}
	}
}

상속할 때 extends 예약어를 사용했듯 구현하고자하는 인터페이스를 implements 예약어 뒤에 적어주면 된다. 인터페이스에 선언된 상수를 사용하고 싶다면 그 상수의 이름을 그대로 가져다 쓰거나, 인터페이스 명으로 참조(Calc.Error)하여 사용한다.

만약 인터페이스의 모든 추상 메서드를 구현하고 싶지 않다면, 해당 클래스를 추상 클래스로 선언하고, 나머지는 하위 클래스에서 구현하면 된다. 하위 클래스에서만 사용하고 싶은 메서드를 인터페이스의 선언없이 따로 구현하여 사용하는 것 또한 가능하다.


Calculator 추상 클래스

package calculator;

public abstract class Calculator implements Calc {

	@Override
	public int add(int num1, int num2) {
		return num1 + num2;
	}

	@Override
	public int sub(int num1, int num2) {
		return num1 - num2;
	}

}

MyCalculator 클래스

package calculator;

public class MyCalculator extends Calculator {

	@Override
	public int times(int num1, int num2) {
		return num1 * num2;
	}

	@Override
	public int divide(int num1, int num2) {
		if (num2 != 0) {			
			return num1 / num2;
		}
		else
			return ERROR;
	}
    
	public void showInfo() {
		System.out.println("This is MyCalculator.");
	}

}

calculator 패키지 내 관계


1.3 인터페이스와 형 변환

상속 관계에서 상위 클래스로의 묵시적 형 변환이 가능했던 것처럼, MyCalcultor 자료형은 Calculator 자료형이기도 하면서, Calc 자료형이기도 하므로, 상위 인터페이스로의 묵시적 형 변환이 가능하다. 묵시적 형 변환과 가상메서드의 원리에 의해 인터페이스에서도 다형성을 구현할 수 있다.

Calc calc = new MyCalculator();
MyCalculator myCalc = new MyCalculator();

변수의 자료형에 의하여, calc 는 add, sub, times, divide 함수만, myCalcshowInfo 함수까지도 추가로 사용이 가능하다.



2. 인터페이스의 요소

자바7 까지는 인터페이스에서 오직 상수와 추상메서드만 사용할 수 있었다. 하지만 자바8 로 넘어오면서 인터페이스 내에서 구현까지 할 수 있는 default 메서드와 static 메서드가 추가되었고, 자바9 에서는 privat 메서드까지 활용할 수 있게 되었다.

2.1 default 메서드

원래 인터페이스에서는 함수의 구현이 아예 불가능했기 때문에 인터페이스를 구현한 클래스들이 공통된 기능을 사용한다고 하더라도 각각의 클래스에서 구현해야했다. 하지만 디폴트 메서드가 추가되면서 인터페이스에서도 함수 구현이 가능해졌고, 인터페이스를 구현할 클래스들이 공통적으로 사용할 기능을 디폴트 메서드로 구현할 수 있게 되었다. 원한다면 디폴트 메서드도 클래스에서 오버라이딩해서 사용할 수 있다.

package calculator;

public interface Calc {

	double PI = 3.14;
	int ERROR = -999999999;
	
	int add(int num1, int num2);
	int sub(int num1, int num2);
	int times(int num1, int num2);
	int divide(int num1, int num2);
	
	public default void description() {
		System.out.println("정수 계산기를 구현합니다.");
	}

}

디폴트 메서드는 함수명 앞에 default 예약어를 붙이고 함수를 구현하면 된다.


2.2 static 메서드

인터페이스의 메서드를 사용하고 싶다면 일반적으로 클래스로 인터페이스를 구현하고, 클래스의 인스턴스를 생성한 다음 접근할 수 있었다. 그러나 정적 메서드가 추가되면서 클래스의 인스턴스 생성 여부와 상관없이 인터페이스 명으로 참조하여 사용할 수 있게 되었다.

package calculator;

public interface Calc {

	double PI = 3.14;
	int ERROR = -999999999;
	
	int add(int num1, int num2);
	int sub(int num1, int num2);
	int times(int num1, int num2);
	int divide(int num1, int num2);
	
	public default void description() {
		System.out.println("정수 계산기를 구현합니다.");
	}
    
	static int square(int num) {
		return num * num;
	}

}

static 예약어를 함수명 앞에 붙이고, 함수를 구현한다. 메소드를 사용할 때는 인스턴스명으로 참조하므로 Calc.square(10) 과 같이 사용하면 된다.


2.3 private 메서드

private 메서드는 인터페이스 내에서만 사용 가능한 메서드로 주로 인터페이스에서 함수를 구현할 때 코드 재사용성을 높이기 위해 선언한다. 인터페이스 메서드의 구현부가 반복되는 부분이 많고, 그 부분을 외부에서 사용할 일이 없을 때 private 예약어와 함께 함수를 구현하면 된다.

private 메서드는 구현되어야 하므로, abstract 와 쓸 수는 없지만 static 과는 사용할 수 있다. privat static 메서드는 주로 static 메서드에서 호출하게 된다.

package calculator;

public interface Calc {

	double PI = 3.14;
	int ERROR = -999999999;
	
	int add(int num1, int num2);
	int sub(int num1, int num2);
	int times(int num1, int num2);
	int divide(int num1, int num2);
	
	public default void description() {
		privateMethod();
		privateStaticMethod();
		System.out.println("정수 계산기를 구현합니다.");
	}
	
	static int square(int num) {
		// 정적 메서드에서는 private 메서드를 호출하면 오류 발생
		privateStaticMethod();
		return num * num;
	}
	
	private void privateMethod() {
		System.out.println("private 메소드 입니다.");
	}
	
	private static void privateStaticMethod() {
		System.out.println("private 정적 메소드 입니다.");
	}

}



3. 인터페이스의 다중 구현

상속에서는 다중 상속이 불가능 했었다. 하지만 인터페이스의 경우, 한 클래스가 여러개의 인터페이스를 구현하는 것이 가능 하다.

위와 같은 그림을 코드로 표현하면 다음과 같다

public class C implements A, B {

}

이 경우, C 클래스를 추상 클래스로 사용하는 것이 아니라면, A 인터페이스와 B 인터페이스에 있는 모든 추상 메서드를 구현해야 한다.


C 클래스의 인스턴스가 A나 B로 형변환 되었다면

A 인터페이스에는 methodA() 메서드가, B 인터페이스에는 methodB() 메서드가 각각 선언되어있다고 가정해보자.

A cToA = new C();
B cToB = new C();

만약 이렇게 상위 인터페이스로 형 변환이 되었다면, cToA 변수는 methodA 에만 접근이 가능하고, cToB 변수는 methodB 에만 접근이 가능하다.


A와 B에 동일한 메서드가 존재한다면

이 경우 같은 이름의 메서드가 추상 메서드인 경우, 정적 메서드인 경우, 디폴트 메서드인 경우, private 메서드인 경우로 나눠 볼 수 있다.

1. 추상 메서드

추상 메서드의 경우 어차피 구현부가 C 클래스에 존재하기 때문에 C 클래스에서 재정의 된 메서드가 호출된다.

2. 정적 메서드

정적 메서드는 인터페이스명으로 참조해서 사용할 수 있으므로 이름이 같아도 크게 상관 없다. 이때 C 클래스에서 오버라이딩한다면 C 클래스 명으로도 참조할 수 있게 되지만, 그렇지 않다면 C 클래스 명으로 참조하면 오류가 발생한다.

3. 디폴트 메서드

무조건 C 클래스에서 재정의를 해주어야 오류가 나지 않는다. 재정의하여 사용할 경우 가상 메서드에 의해 C 클래스에서 재정의 된 메소드만 호출된다.

4. private 메서드

어차피 인터페이스 내부에서만 사용하는 메서드이고, 외부에서는 호출할 수도 없으므로 동일한 메서드가 존재해도 상관없다.



4. 인터페이스의 상속

클래스 간의 상속이 가능하듯 인터페이스 간에도 상속이 가능하다. 다만 구현한 코드를 상속하는 개념과는 다르기 때문에 기능 상속이 아닌 형 상속이라고 표현한다.
클래스 간의 상속과 다르게 다중 상속도 가능하며, 이 때 하위 인터페이스는 모든 상위 인터페이스의 추상 메서드를 갖게 된다.

물론 하위 인터페이스를 사용하고 싶다면 하위 인터페이스를 구현한 클래스도 있어야하며, 구현한 클래스가 추상 클래스가 아닌 이상 모든 상위 인터페이스의 추상메서드를 구현하는 것은 필수다.

이들의 관계를 간단하게 그림과 코드로 표현해보면 아래와 같다.

인터페이스 A와 B를 상속받은 X 인터페이스

public interface X extends A, B {

}

X 인터페이스를 구현한 C 클래스

public class C implements X {

}



5. 인터페이스 구현 + 클래스 상속

한 클래스에서 인터페이스를 구현하고 클래스를 상속 받는 것 모두 가능하다. 이와 같은 상황을 다이어그램과 코드로 간단하게 표현하면 다음과 같다.

public class C extends Y implements X {

}

Shelf을 상속하고 Queue를 구현한 BookShelf

Shelf 클래스

package bookshelf;
import java.util.ArrayList;

public class Shelf {
	
	protected ArrayList<String> shelf;
	
	public Shelf() {
		shelf = new ArrayList<String>();
	}
	
	public ArrayList<String> getShelf(){
		return shelf;
	}
	
	public int getShelfSize() {
		return shelf.size();
	}

}

Queue 인터페이스

package bookshelf;

public interface Queue {

	void addBack(String title);
	String popFront();
	int getSize();

}

BookShelf 클래스

package bookshelf;

public class BookShelf extends Shelf implements Queue {

	@Override
	public void addBack(String title) {
		shelf.add(title);
	}

	@Override
	public String popFront() {
		// 비어있는 예외 경우도 생각해줘야 실행 오류가 안남
		if (shelf.isEmpty()) {
			return null;
		}
		return shelf.remove(0);
	}

	@Override
	public int getSize() {
		return shelf.size();
	}

}



6. 인터페이스를 쓰는 이유

우선 인터페이스는 클래스가 제공하는 기능을 명시하여 설계하는 역할을 한다.

인터페이스에는 일반적으로 메서드 선언부만 존재하고, 구현부는 클래스에서 이루어진다. 인터페이스의 선언부를 통해 각각의 클래스를 직접 확인하지 않아도 메서드를 어떻게 사용할지 알 수 있다. 이는 개발 설계에 중요한 특성이 될 것이다.

그리고 상속과 마찬가지로 오버라이딩과 가상메서드에 의해 다형성을 활용할 수 있다는 장점이 있다.

다형성 구현이라는 부분에서 인터페이스의 구현은 상속과 유사한 점도 있지만 다른 점도 존재한다. 상속을 통해 구현한 다형성은 부모로부터 원치 않는 기능까지 물려받기 때문에 클래스간의 결합도가 높아지기 때문에 상속은 IS-A 관계에 있는 클래스에서 사용하는 것을 권장한다. 그러나 인터페이스의 경우 원하는 기능만 구현하여 사용하면 되기 때문에 유연성이 높은 편이다.

인터페이스와 다형성

Animal 추상 클래스

package animal;

public abstract class Animal {

	public abstract void description();
	
	public void wakeUp() {
		System.out.println("잠에서 깨어납니다.");
	}
	
	public void sleep() {
		System.out.println("잠을 잡니다.");
	}

}

LandAnimal 인터페이스

package animal;

public interface LandAnimal {

	void walk();
}

MarineAnimal 인터페이스

package animal;

public interface MarineAnimal {

	void swim();
}

Cat 클래스

package animal;

public class Cat extends Animal implements LandAnimal {

	@Override
	public void description() {
		System.out.println("이 동물은 고양이입니다.");
	}
	
	@Override
	public void walk() {
		System.out.println("고양이가 살금살금 걷습니다.");
	}

}

Whale 클래스

package animal;

public class Whale extends Animal implements MarineAnimal {

	@Override
	public void description() {
		System.out.println("이 동물은 고래입니다.");
	}
	
	@Override
	public void swim() {
		System.out.println("고래가 바다를 헤엄칩니다.");
	}

}

Duck 클래스

package animal;

public class Duck extends Animal implements LandAnimal, MarineAnimal {

	@Override
	public void description() {
		System.out.println("이 동물은 오리입니다.");
	}
	
	@Override
	public void walk() {
		System.out.println("오리가 뒤뚱뒤뚱 걷습니다.");
	}
	
	@Override
	public void swim() {
		System.out.println("오리가 연못에서 헤엄칩니다.");
	}

}

출력 테스트

package animal;
import java.util.ArrayList;

public class AnimalTest {

	public static void main(String args[]) {
		
		ArrayList<Animal> animalList = new ArrayList<Animal>();
		
		animalList.add(new Cat());
		animalList.add(new Duck());
		animalList.add(new Whale());
		
		for (Animal animal : animalList) {
			System.out.println("====================");
			animal.description();
			animal.wakeUp();
			if (animal instanceof LandAnimal) {
				LandAnimal land = (LandAnimal)animal;
				land.walk();
			}
			if (animal instanceof MarineAnimal) {
				MarineAnimal marine = (MarineAnimal)animal;
				marine.swim();
			}
			animal.sleep();
		}
		System.out.println("====================");
	}
}
====================
이 동물은 고양이입니다.
잠에서 깨어납니다.
고양이가 살금살금 걷습니다.
잠을 잡니다.
====================
이 동물은 오리입니다.
잠에서 깨어납니다.
오리가 뒤뚱뒤뚱 걷습니다.
오리가 연못에서 헤엄칩니다.
잠을 잡니다.
====================
이 동물은 고래입니다.
잠에서 깨어납니다.
고래가 바다를 헤엄칩니다.
잠을 잡니다.
====================

0개의 댓글