[Java] 8주차 과제: 인터페이스
by Roel Downey
스터디 링크 : 링크
인터페이스(interface)란?
자식 클래스가 여러 부모 클래스를 상속받을 수 있다면, 다양한 동작을 수행할 수 있다는 장점을 가지게 된다.
하지만 자바에서는 클래스 다중 상속을 지원하지 않는다.
하지만 다중 상속의 이점을 버릴 수는 없기에 자바에서는 인터페이스라는 것을 통해 다중 상속 지원하고 있다.
인터페이스(interface)란 다른 클래스를 작성할 때 기본이 되는 틀을 제공하면서, 다른 클래스 사이의 중간 매개 역할까지 담당하는 일종의 추상 클래스를 의미한다.
자바에서 추상 클래스는 추상 메소드뿐만 아니라 생성자, 필드, 일반 메소드도 포함할 수 있다.
하지만 인터페이스(interface)는 오로지 추상 메소드와 상수만을 포함할 수 있다.
- 기존에는 인터페이스에 일반 메소드를 구현할 수 없었지만, 자바 8버전부터 default 예약어를 통해 일반 메소드 구현이 가능하다.
인터페이스 정의하는 방법
자바에서 인터페이스를 선언하는 방법은 클래스를 작성하는 방법과 같다.
인터페이스를 선언할 때에는 접근 제어자와 함께 interface 키워드를 사용하면 된다.
자바에서 인터페이스는 다음과 같이 선언한다.
접근제어자 interface 인터페이스이름 { // 상수 (static final) ... public abstract 메소드이름(매개변수목록); ... } |
인터페이스 구현하는 방법
인터페이스는 추상 클래스와 마찬가지로 자신이 직접 인스턴스를 생성할 수는 없다.
따라서 인터페이스가 포함하고 있는 추상 메소드를 구현해 줄 클래스를 작성해야만 한다.
자바에서 인터페이스는 다음과 같은 문법을 통해 구현한다
문법 class 클래스이름 implements 인터페이스이름 { ... } |
만약 모든 추상 메소드를 구현하지 않는다면, abstract 키워드를 사용하여 추상 클래스로 선언해야 한다.
interface Animal { public abstract void cry(); }
class Cat implements Animal {
public void cry() {
System.out.println("냐옹냐옹!");
}
}
class Dog implements Animal {
public void cry() {
System.out.println("멍멍!");
}
}
public class Polymorphism03 {
public static void main(String[] args) {
Cat c = new Cat();
Dog d = new Dog();
c.cry();
d.cry();
}
}
인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
다형성을 공부하면 자손클래스의 인스턴스를 부모타입의 참조변수로 참조하는 것이 가능하다는 것을 알 수 있다.
인터페이스도 이를 구현한 클래스의 부모라 할 수 있으므로 해당 인터페이스 타입의 참조변수로클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.
다형성의 특징을 활용하여 인터페이스를 타입으로써 사용할 수 있다.
interface Flyable{
void fly();
}
interface Jumpable {
void jump();
}
class Bird implements Flyable, Jumpable {
@Override
public void fly() {
System.out.println("Bird's Flying");
}
@Override
public void jump() {
System.out.println("Bird's Jumping");
}
}
이제 Bird 객체를 활용해보겠다.
Flyable bird = new Bird();
bird.fly();
bird.jump(); // Cannot resolve method 'jump' in 'Flyable'
flyable을 통해서 bird를 선언하게되면, Flyable 가 가질 수 있는 메소드만 사용할 수 있다.
자바의 다형성의 대표적인 예시로 활용된다.
List<Flyable> list = ArrayList<Flyable>();
list.add(new Bird());
list.add(new Airplane());
list.add(new SuperMan());
for (Flyable element : list) {
element.fly(); // fly 메소드로 통일하여 호출 할 수 있다.
}
인터페이스 상속
인터페이스의 상속 구조에서 자식 인터페이스는 부모 인터페이스의 메서드까지 모두 구현해야 한다.인터페이스 레퍼런스는 인터페이스를 구현한 클래스의 인스턴스를 가리킬 수 있고, 해당 인터페이스에 선언된 메서드만 호출 할 수 있다.
자바에서는 다음과 같이 상속이 가능하다.
문법 interface [인터페이스 이름] extends [부모 인터페이스명 ...] { } |
인터페이스는 인터페이스로부터만 상속을 받을 수 있으며, 여러 인터페이스를 상속 받을 수 있다.
interface Drawble {
void draw();
}
interface Printable extends Drawble {
void print();
}
class Circle implements Printable {
@Override
public void draw() {
}
@Override
public void print() {
}
}
- 인터페이스를 사용한 다중 상속의 예제
추상메소드의 경우 추상메소드 선언 시점에 결정되는것이 아니라, 구현시점에 결정되기 때문에, 인터페이스의 상속에서도 역시 다중상속 문제가 없다.
/** 인터페이스의 다중 상속 예제 */
interface Dancable {
void perform();
}
interface Flyable {
void perform();
}
interface Perfomable extends Dancable, Flyable {
}
class Superman implements Perfomable {
@Override
public void perform() {
}
}
- 클래스를 이용한 다중 상속의 문제점
클래스를 이용하여 다중 상속을 하면 다음 예제와 같은 메소드 출처의 모호성 등의 문제가 발생할 수 있다.
class Animal {
public void cry() {
System.out.println("짖기!");
}
}
class Cat extends Animal {
public void cry() {
System.out.println("냐옹냐옹!");
}
}
class Dog extends Animal {
public void cry() {
System.out.println("멍멍!");
}
}
① class MyPet extends Cat, Dog {}
public class Polymorphism {
public static void main(String[] args) {
MyPet p = new MyPet();
② p.cry();
}
}
위의 예제에서 Cat 클래스와 Dog 클래스는 각각 Animal 클래스를 상속받아 cry() 메소드를 오버라이딩하고 있다.
여기까지는 문제가 없지만, ①번 라인에서 MyPet 클래스가 Cat 클래스와 Dog 클래스를 동시에 상속받게 되면 문제가 발생한다.
②번 라인에서 MyPet 인스턴스인 p가 cry() 메소드를 호출하면, 이 메소드가 Cat 클래스에서 상속받은 cry() 메소드인지 Dog 클래스에서 상속받은 cry() 메소드인지를 구분할 수 없는 모호성을 지니게 된다.
이와 같은 이유로 자바에서는 클래스를 이용한 다중 상속을 지원하지 않는다.
하지만 다음 예제처럼 인터페이스를 이용하여 다중 상속을 하게되면, 위와 같은 메소드 호출의 모호성을 방지할 수 있다.
interface Animal { public abstract void cry(); }
interface Cat extends Animal { public abstract void cry(); }
interface Dog extends Animal { public abstract void cry(); }
class MyPet implements Cat, Dog {
public void cry() {
System.out.println("멍멍! 냐옹냐옹!");
}
}
public class Polymorphism05 {
public static void main(String[] args) {
MyPet p = new MyPet();
p.cry();
}
}
실행 결과
멍멍! 냐옹냐옹!
위의 예제에서는 Cat 인터페이스와 Dog 인터페이스를 동시에 구현한 MyPet 클래스에서만 cry() 메소드를 정의하므로, 앞선 예제에서 발생한 메소드 호출의 모호성이 없다.
인터페이스의 장점
인터페이스를 사용하면 다중 상속이 가능할 뿐만 아니라 다음과 같은 장점을 가질 수 있다.
1. 대규모 프로젝트 개발 시 일관되고 정형화된 개발을 위한 표준화가 가능하다.
2. 클래스의 작성과 인터페이스의 구현을 동시에 진행할 수 있으므로, 개발 시간을 단축할 수 있다.
3. 클래스와 클래스 간의 관계를 인터페이스로 연결하면, 클래스마다 독립적인 프로그래밍이 가능하다.
인터페이스의 기본 메소드 (default Method), 자바 8
인터페이스는 기능에 대한 선언만 가능하기 때문에, 실제 코드를 구현한 로직은 포함될 수 없다.
하지만 자바8에서 이러한 룰을 깨트리는 기능이 나오게 되었는데, 그것이 default method이다.
메소드 선언시에 default를 명시하게 되면 인터페이스 내부에서도 코드가 포함된 메소드를 선언할 수 있다.
접근제어자에서 사용하는 default와 같은 키워드이지만, 접근제어자는 아무것도 명시하지 않은 접근제어자를 default라 하며
인터페이스의 default method는 'default'라는 키워드를 명시해야 한다.
interface MyInterface {
default void printHello() {
System.out.println("Hello World!");
}
}
default라는 키워드를 메소드에 명시하게 되면 인터페이스 내부라도 코드를 작성 할 수 있다.
왜 사용할까?
사실 인터페이스는 기능에 대한 구현보다는, 기능에 대한 '선언'에 초점을 맞추어서 사용 하는데, 디폴트 메소드는 왜 등장했을까?
...(중략)... 바로 "하위 호환성"때문이다. 예를 들어 설명하자면, 여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다. (자바의 신 2권)
기존에 존재하던 인터페이스를 이용하여서 구현된 클래스를 만들고 사용하고 있는데,
인터페이스를 보완하는 과정에서 추가적으로 구현해야 할, 혹은 필수적으로 존재해야 할 메소드가 있다면,
이미 이 인터페이스를 구현한 클래스와의 호환성이 떨어지게 된다. 이러한 경우 default 메소드를 추가하게 된다면
하위 호환성은 유지되고 인터페이스의 보완을 진행 할 수 있다.
interface MyInterface {
default void printHello() {
System.out.println("Hello World");
}
}
//구현체 생성
class MyClass implements MyInterface {
}
public class DefaultMethod {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.printHello(); //실행결과 Hello World 출력
}
}
[정리]
-interface에서도 메소드 구현이 가능하다.
-참조 변수로 함수를 호출할 수 있다.
-implements한 클래스에서 재정의가 가능하다.
인터페이스 static 메소드, 자바 8
해당 인터페이스를 구현한 모든 인스턴스, 헬퍼, 유틸리티 메소드를 제공할 수 있습니다.
static 메소드는 클래스의 static 메소드처럼..
default나 abstract메소드는 모두 instance 메소드처럼 처리 됩니다.
따라서 정리를 해보자면,
- Static 후에 → Instance 메소드는 가능하지만 (instance실행)
- Instance 메소드 후에 → Static은 불가합니다. (오류, static멤버를 오버라이드 할 수 없음)
Static이 default에 밀리는 경우 #1
밀리는게 아니라, 인스턴스로 접근하는것과 스태틱으로 접근하냐의 차이. 아래의 예제는 모두 인스턴스로 접근하기 때문에 default가 불림.
interface Dancable {
static void fly(){
System.out.println("call from static");
}
}
interface Flyable extends Dancable {
default void fly() {
System.out.println("call from default");
}
}
class Print implements Flyable {
}
(new Print()).fly(); // "call from default"
Print.fly(); // "call from static"
Static이 default에 밀리는 경우 #2
interface Dancable {
static void fly(){
System.out.println("call from static");
}
}
interface Flyable {
default void fly() {
System.out.println("call from default");
}
}
class Print implements Flyable, Dancable {
}
(new Print()).fly(); // "call from default"
Print.fly(); // "call from static"
default를 상속을 통해 static으로 덮으려고 하면 컴파일 에러가 나옴.
interface Dancable {
void fly();
// default void fly(){
// System.out.println("call from default");
// }
}
interface Flyable extends Dancable {
static void fly() {
System.out.println("call from static");
}
}
// error : Static method 'fly()' in 'Flyable' cannot override instance method 'fly()' in 'Dancable'
class Print implements Flyable {
}
인스턴스 -> 스태틱 OK
스태틱 -> 인스턴스 Not OK
인터페이스의 private 메소드, 자바 9
Java9은 인터페이스를 더더욱 클래스처럼 쓸 수 있게 만들어주는 키워드들을 추가하였다.
인터페이스의 모든 메소드들은 무조건 public이어야 하는데, java9부터는 interface 내부에 private 을 만듦으로써, 외부에 공개하지 않으면서도 코드중복을 피할 수 있게 된다.
사용 용례는 역시 클래스의 private , private static 메소드와 같다.
아래의 예제에서 에러가 나는부분을 확인해보자.
interface Dancable {
private int mySecretLogic(int a, int b) {
return a + b;
}
private static int mySecreteLogic2(int a, int b) {
return a * b;
}
default int doSomething() {
return mySecreteLogic2(1,2) + mySecretLogic(1,2);
}
}
// 상속 케이스
interface Flyable extends Dancable {
default void doSomething2() {
mySecretLogic(1,2); // error : 'mySecretLogic(int, int)' has private access in 'Dancable'
}
}
// 구현 케이스
class Print implements Dancable {
Print() {
System.out.println(doSomething()); // 5 ( 1* 2 + 1 +2)
mySecretLogic(1,2); // error : 'mySecretLogic(int, int)' has private access in 'Dancable'
}
}
두 에러 모두 private, private static을 쓴 인터페이스가 아닌 외부에서 접근하려고 했으므로, 에러가 난다.
스터디 라이브 피드백 정리
추상 메소드
- handlerInterceptor (서블릿필터)
1. prehandler, Posthandler... 안쓰는데도 인터페이스를 구현해야한다?
2. 어댑터를 만들어서, interface를 굳이 구현하지 않도록 한다.
- 어댑터패턴과는 다름.
- abstract class에서 interface를 받고, 이런경우 interface를 필수적으로 구현할 필요가 없다.
- 구현을 필요할 때만 할 수 있도록..
- 스프링같은경우도 Configurer, or Adapter에서 interface로 바꿈
- 장점 : 라이브러리에서 상속을 쓰면, 개인 개발자가 개발할 때 상속을 못받음.
class A extends JoinAdapter {
}
------
class A implements Join {
}
interface Join {
something();
}
=>
interface Join {
default something() {}
}
- default가 두개가 동시에 나오는경우? → 오버라이드로해결
public void afterJoin() {
JoinMember.super.afterjoin();
JoinGroup.super.afterjoin();
// static이 아니어서, 참조할 방법으로 super가 있음.
}
- 인터페이스는 Static은 해당 인터페이스에서만 접근 가능하다.
- 디폴트가 덮어쓰는게 아니라 인스턴스로 접근이냐 클래스로 접근이냐 차이 이다.
- 인스턴스형태의 메소드를 통해서 접근했기 때문에, default가 실행됨.
- private 메소드는 인터페이스 내에서 사용하기 위해...구현하는 클래스에서 오버라이딩 할 수 없음
- default 탄생 배경
-
오래된 코드 베이스 + 기능들을 추가 하고싶음.
- forEach를 추가해본다고 가정.
- List에 추가 된것처럼 보이지만,
- 하위 호환성이 깨지지 않음.
- 인터페이스에다가 기능을 추가해도, 컴파일에러가 나지 않음!
- 옛날에 만들어져있는것에 기반한 것들이 깨지지 않도록...
- constant interface = 안티패턴
- 인터페이스와 클래스의 관계
- 클래스 상속 is-a
- 인터페이스 has-a
- 롱폴링, 웹소켓, 웹푸시.
- default를 상속으로 덮으려고 하면 static 으로 컴파일 에러가 나옴
- forEach를 추가해본다고 가정.
- 추상클래스는 더이상 의미가 없나요?
- 인터페이스에서 정말 다 구현할 수 있을까요?
- 추상 '클래스'는 하나의 mutable한 상태를 가질 수 있습니다.
- 인터페이스는 상태를 가질 수 없습니다.
-
Spring JDBC , JdbcTemplate ⇒ 익명구현체를 쓰는 경우가 많음.
- 인터페이스에 메소드가 하나만 있으면, 람다로 대체할 수 있다. (함수형인터페이스)
-
강한결합과 느슨한결합에 대한 이야기.
- 인터페이스를 쓰는 주요한 이유 → 느슨한 결합을 유도할 수 있음.
- 느슨한결합 : 느리지만 유연하고 변경에 유리
- 강한결합 : 빠르지만 변경에 불리.
-
응집도와 같이 볼것.
'Java' 카테고리의 다른 글
[Java] 10주차 과제: 멀티쓰레드 프로그래밍 (0) | 2021.01.23 |
---|---|
[Java] 9주차 과제: 예외 처리 (0) | 2021.01.18 |
[Java] 7주차 과제: 패키지 (0) | 2020.12.28 |
[Java] 6주차 과제: 상속 (0) | 2020.12.22 |
[Java] Binary Tree (0) | 2020.12.16 |
블로그의 정보
What doing?
Roel Downey