인터페이스 2앞에서 인터페이스의 기본 구조, 구현, 상속, 기본 다형성을 봤다면, 이번에는 한 단계 더 들어간다.
이번 파트의 핵심은 세 가지다.
- 인터페이스를 왜 쓰는가
- 인터페이스가 있으면 코드가 어떻게 덜 묶이게 되는가
default메서드와static메서드는 왜 필요하고, 충돌이 나면 왜 직접 재정의해야 하는가즉, 이번 정리는 인터페이스를 문법으로만 보는 것이 아니라,
왜 이런 구조를 만드는지까지 같이 이해하는 정리다.
1. 인터페이스를 왜 쓰는가인터페이스를 쓰는 이유는 단순히
implements를 배우기 위해서가 아니다.
핵심은 사용하는 쪽 코드가 구현 클래스 하나에 너무 강하게 묶이지 않게 만들기 위해서다.
예를 들어 TV를 사용하는 코드를 만든다고 해 보자.
처음에는 삼성 TV만 쓴다고 생각하면 아무 문제 없어 보인다.
그런데 나중에 LG TV로 바꾸려는 순간, 사용하는 쪽 코드까지 함께 흔들릴 수 있다.
이럴 때 필요한 것이 공통 규칙이다.
전원을 켜고, 끄고, 소리를 올리고, 내리는 기능이 필요하다면 그 기능 이름과 형태를 먼저 통일해 두면 된다.
즉, 인터페이스는
“어떤 회사 제품이냐”보다 “이 기능을 할 수 있느냐”를 기준으로 코드를 만들게 해 주는 도구다.
2. 규격이 없으면 무엇이 불편한가인터페이스가 왜 필요한지는 규격이 없을 때를 먼저 보면 더 잘 보인다.
예제:
case1-SamsungTVpackage day9.case1; public class SamsungTV { public void powerOn() { // 전원 켜기 System.out.println("SamsungTV --- 전원을 켠다."); } public void powerOff() { // 전원 끄기 System.out.println("SamsungTV --- 전원을 끈다."); } public void volumnUp() { // 소리 높이기 System.out.println("SamsungTV --- 소리를 높인다."); } public void volumnDown() { // 소리 낮추기 System.out.println("SamsungTV --- 소리를 낮춘다."); } }예제:
case1-LgTVpackage day9.case1; public class LgTV { public void turnOn() { // 전원 켜기 System.out.println("LgTV --- 전원을 켠다."); } public void turnOff() { // 전원 끄기 System.out.println("LgTV --- 전원을 끈다."); } public void soundUp() { // 소리 높이기 System.out.println("LgTV --- 소리를 높인다."); } public void soundDown() { // 소리 낮추기 System.out.println("LgTV --- 소리를 낮춘다."); } }예제:
case1-TVUserpackage day9.case1; public class TVUser { public static void main(String[] args) { SamsungTV tv = new SamsungTV(); // 삼성 TV 객체를 직접 생성 tv.powerOn(); tv.volumnUp(); tv.volumnDown(); tv.powerOff(); } } /* 출력결과 SamsungTV --- 전원을 켠다. SamsungTV --- 소리를 높인다. SamsungTV --- 소리를 낮춘다. SamsungTV --- 전원을 끈다. */이 상태에서는
SamsungTV를 쓸 때는 문제없다.
그런데 나중에LgTV로 바꾸려면 단순히 객체 생성 줄만 바꾸면 끝나지 않는다.
왜냐하면 메서드 이름부터 다르기 때문이다.
- 삼성 TV는
powerOn(),volumnUp()- LG TV는
turnOn(),soundUp()즉, 객체만 바꾸는 것이 아니라 사용하는 메서드 이름도 전부 바꿔야 한다.
이게 바로 규격이 없을 때의 문제다.
둘 다 TV인데, 사용하는 쪽에서는 같은 방식으로 다룰 수가 없다.
여기서 봐야 할 핵심이 문제는 삼성 TV가 틀렸거나 LG TV가 틀렸다는 뜻이 아니다.
문제는 같은 역할을 하는데 사용하는 방법이 통일되어 있지 않다는 점이다.
그래서 나중에 제품이 바뀌면 사용하는 쪽 코드도 같이 흔들리게 된다.
3. 인터페이스로 메서드 이름을 통일하면 무엇이 달라지는가이제 TV에 필요한 기능을 먼저 인터페이스로 정해 보자.
예제:
case2-TVpackage day9.case2; public interface TV { public void powerOn(); // 전원 켜기 public void powerOff(); // 전원 끄기 public void volumnUp(); // 소리 높이기 public void volumnDown(); // 소리 낮추기 }이제 삼성 TV와 LG TV는 이 규칙에 맞게 구현한다.
예제:
case2-SamsungTVpackage day9.case2; public class SamsungTV implements TV { public void powerOn() { System.out.println("SamsungTV --- 전원을 켠다."); } public void powerOff() { System.out.println("SamsungTV --- 전원을 끈다."); } public void volumnUp() { System.out.println("SamsungTV --- 소리를 높인다."); } public void volumnDown() { System.out.println("SamsungTV --- 소리를 낮춘다."); } }예제:
case2-LgTVpackage day9.case2; public class LgTV implements TV { public void powerOn() { System.out.println("LgTV --- 전원을 켠다."); } public void powerOff() { System.out.println("LgTV --- 전원을 끈다."); } public void volumnUp() { System.out.println("LgTV --- 소리를 높인다."); } public void volumnDown() { System.out.println("LgTV --- 소리를 낮춘다."); } }예제:
case2-TVUserpackage day9.case2; public class TVUser { public static void main(String[] args) { TV tv = new LgTV(); // 인터페이스 타입으로 객체를 받음 // 실제 객체가 SamsungTV든 LgTV든 // TV 규칙만 지키면 같은 메서드 이름으로 사용할 수 있다. tv.powerOn(); tv.volumnUp(); tv.volumnDown(); tv.powerOff(); } } /* 출력결과 LgTV --- 전원을 켠다. LgTV --- 소리를 높인다. LgTV --- 소리를 낮춘다. LgTV --- 전원을 끈다. */이제 구조가 달라진다.
TVUser는 더 이상SamsungTV만 안다고 생각하지 않는다.
대신TV규칙을 따르는 객체를 사용한다고 생각한다.
그래서new SamsungTV()를 넣든,new LgTV()를 넣든
사용하는 쪽에서는 같은 메서드를 호출하면 된다.
왜 중요한가이전에는 회사마다 메서드 이름이 달라서 사용하는 쪽도 같이 바뀌어야 했다.
지금은 인터페이스가 메서드 이름을 통일해 주기 때문에, 사용하는 쪽에서는 같은 방식으로 사용할 수 있다.
즉, 인터페이스는
구현 클래스가 제각각이어도 사용하는 방법은 하나로 맞춰 주는 규격이다.
4. 사용하는 쪽과 만드는 쪽을 분리하면 더 유연해진다
case2에서는 규격이 생겨서 사용이 편해졌다.
그런데 아직 한 가지가 남아 있다.
TVUser안에서 여전히 직접 객체를 만들고 있다는 점이다.TV tv = new LgTV();이 줄은 이전보다 나아졌지만, 여전히 어떤 제품을 쓸지
TVUser가 알고 있어야 한다.
즉, 인터페이스에 의존하고 있기는 하지만, 객체 생성은 아직 직접 하고 있다.
이걸 한 단계 더 분리한 것이case3다.
예제:
case3-TVpackage day9.case3; public interface TV { public void powerOn(); public void powerOff(); public void volumnUp(); public void volumnDown(); }예제:
case3-SamsungTVpackage day9.case3; public class SamsungTV implements TV { public void powerOn() { System.out.println("SamsungTV --- 전원을 켠다."); } public void powerOff() { System.out.println("SamsungTV --- 전원을 끈다."); } public void volumnUp() { System.out.println("SamsungTV --- 소리를 높인다."); } public void volumnDown() { System.out.println("SamsungTV --- 소리를 낮춘다."); } }예제:
case3-LgTVpackage day9.case3; public class LgTV implements TV { public void powerOn() { System.out.println("LgTV --- 전원을 켠다."); } public void powerOff() { System.out.println("LgTV --- 전원을 끈다."); } public void volumnUp() { System.out.println("LgTV --- 소리를 높인다."); } public void volumnDown() { System.out.println("LgTV --- 소리를 낮춘다."); } }예제:
case3-TVFactorypackage day9.case3; public class TVFactory { public static TV getTV(String beanName) { // 어떤 TV를 만들지 대신 결정 TV obj = null; if (beanName.equals("samsung")) { obj = new SamsungTV(); // samsung이면 삼성 TV 생성 } else if (beanName.equals("lg")) { obj = new LgTV(); // lg이면 LG TV 생성 } return obj; // 생성한 객체를 TV 타입으로 반환 } }예제:
case3-TVUserpackage day9.case3; public class TVUser { public static void main(String[] args) { // 객체 생성을 직접 하지 않고 공장 역할을 하는 TVFactory에 맡긴다. TV tv = TVFactory.getTV(args[0]); if (tv != null) { // 반환된 객체가 SamsungTV든 LgTV든 // TV 타입으로 받았기 때문에 같은 방식으로 사용 가능하다. tv.powerOn(); tv.volumnUp(); tv.volumnDown(); tv.powerOff(); } else { System.out.println("프로그램 아규먼트로 samsung 또는 lg 를 입력해 주세요.."); } } } /* 실행 예시 1 프로그램 아규먼트: samsung 출력결과 SamsungTV --- 전원을 켠다. SamsungTV --- 소리를 높인다. SamsungTV --- 소리를 낮춘다. SamsungTV --- 전원을 끈다. 실행 예시 2 프로그램 아규먼트: lg 출력결과 LgTV --- 전원을 켠다. LgTV --- 소리를 높인다. LgTV --- 소리를 낮춘다. LgTV --- 전원을 끈다. */이제
TVUser는 더 이상new SamsungTV()나new LgTV()를 직접 쓰지 않는다.
대신 “나는 TV가 필요하다”라고만 말하고, 실제로 어떤 객체를 만들지는TVFactory가 결정한다.
코드 흐름
TVUser는 프로그램 실행 시 받은 값(args[0])을TVFactory에 넘긴다.TVFactory는 값이samsung이면SamsungTV,lg이면LgTV를 만든다.- 반환 타입은 둘 다
TV이므로TVUser는 같은 방식으로 사용할 수 있다.
왜 좋은가이제 사용하는 쪽은
- 어떤 제품이 만들어지는지 세부 클래스를 몰라도 되고
TV규칙만 알면 되고- 객체 생성 책임도 직접 지지 않아도 된다
즉, 인터페이스를 사이에 두면
사용하는 쪽과 만드는 쪽을 분리할 수 있고, 코드가 훨씬 덜 묶이게 된다.
헷갈리기 쉬운 부분이 예제는 실행할 때 프로그램 인자를 넣는다는 전제에서 동작한다.
즉,samsung이나lg같은 값을 함께 실행해야 한다.
인자를 넣지 않으면args[0]에서 오류가 날 수 있다.
5. 직접 연결보다 간접 연결이 좋은 이유인터페이스를 이해할 때 가장 중요한 생각은 이것이다.
사용하는 쪽은 구현 클래스의 모든 내부 사정을 알 필요가 없다.
그 대신 “이 기능이 있다”는 선언만 알면 된다.
이걸 TV 예제로 다시 보면,
TVUser는 사용하는 쪽SamsungTV,LgTV는 실제 기능을 제공하는 쪽TV인터페이스는 둘 사이를 이어 주는 규칙이 된다.
즉, 구조를 이렇게 볼 수 있다.
- 직접 연결:
TVUser → SamsungTV- 간접 연결:
TVUser → TV → SamsungTV- 또는:
TVUser → TV → LgTV직접 연결이면 제품이 바뀔 때 사용하는 쪽도 함께 흔들리기 쉽다.
반대로 인터페이스를 사이에 두면, 사용하는 쪽은TV규칙만 알면 되기 때문에 구현체가 바뀌어도 덜 흔들린다.
이게 바로
선언과 구현을 분리한다는 뜻이고,
인터페이스가 두 객체 사이의 중간 역할을 한다는 뜻이다.
6. default 메서드와 static 메서드는 왜 생겼는가인터페이스는 원래 규칙을 정하는 역할이 강했다.
그래서 예전에는 보통 추상 메서드만 넣는다고 생각하면 됐다.
그런데 문제가 하나 있다.
이미 많은 클래스가 어떤 인터페이스를 구현하고 있는 상황에서,
인터페이스에 새 추상 메서드를 하나 추가하면 어떻게 될까?
그 인터페이스를 구현한 클래스들이 전부 그 새 메서드를 구현하도록 수정되어야 한다.
즉, 기존 코드에 영향이 크게 간다.
이 문제를 줄이기 위해 나온 것이default메서드다.
default메서드: 인터페이스 안에 기본 구현까지 같이 넣어 둔 메서드static메서드: 인터페이스 이름으로 바로 호출하는 메서드즉, 인터페이스가 완전히 선언만 있는 구조로만 머물지 않고,
필요한 경우에는 기본 동작도 함께 제공할 수 있게 바뀐 것이다.
7. default 메서드와 static 메서드 기본 구조이제 실제 코드로 보자.
예제:
InterfaceTest3package day9; interface InterCalculator { int add(int x, int y); // 구현 클래스가 반드시 만들어야 하는 추상메서드 int sub(int x, int y); // 구현 클래스가 반드시 만들어야 하는 추상메서드 default int mul(int x, int y) { // 기본 구현이 있는 default 메서드 return x * y; } static void print() { // 인터페이스 이름으로 바로 호출하는 static 메서드 System.out.println("사칙 연산 처리"); } } class Calculator implements InterCalculator { @Override public int add(int x, int y) { // add() 구현 return x + y; } @Override public int sub(int x, int y) { // sub() 구현 return x - y; } // mul()은 구현하지 않아도 됨 // 인터페이스에 default 구현이 이미 들어 있기 때문 public int div(int x, int y) { // 이 메서드는 인터페이스에 없는 Calculator 고유 기능 if (y != 0) return x / y; return 0; } } public class InterfaceTest3 { public static void main(String[] args) { InterCalculator cal = new Calculator(); // 인터페이스 타입으로 객체를 받음 System.out.println("5 + 3 = " + cal.add(5, 3)); System.out.println("5 - 3 = " + cal.sub(5, 3)); System.out.println("5 * 3 = " + cal.mul(5, 3)); // default 메서드 사용 System.out.println("5 / 3 = " + ((Calculator)cal).div(5, 3)); // 고유 기능은 형변환 후 사용 InterCalculator.print(); // static 메서드는 인터페이스 이름으로 호출 } } /* 출력결과 5 + 3 = 8 5 - 3 = 2 5 * 3 = 15 5 / 3 = 1 사칙 연산 처리 */이 코드에서 먼저 구분해야 할 것은 세 종류다.
1) 반드시 구현해야 하는 메서드int add(int x, int y); int sub(int x, int y);이 둘은 구현 클래스가 직접 내용을 채워야 한다.
2) 기본 구현이 이미 들어 있는 메서드default int mul(int x, int y) { return x * y; }이건 인터페이스 안에 기본 구현이 들어 있으므로,
구현 클래스가 꼭 다시 만들지 않아도 바로 쓸 수 있다.
3) 인터페이스 이름으로 바로 호출하는 메서드static void print() { System.out.println("사칙 연산 처리"); }이건 객체를 통해 부르는 메서드가 아니라
InterCalculator.print()처럼 인터페이스 이름으로 직접 호출하는 메서드다.
여기서div(5, 3)은 정수 나눗셈이라1이 나온다.
8. 인터페이스 타입으로 보면 보이는 기능이 다시 제한된다이 코드에서 또 하나 중요한 부분은 이 줄이다.
InterCalculator cal = new Calculator();실제 객체는
Calculator지만, 참조변수 타입은InterCalculator다.
그래서 이 상태에서는 인터페이스에 선언된 기능만 바로 사용할 수 있다.
즉, 아래는 바로 가능하다.cal.add(5, 3); cal.sub(5, 3); cal.mul(5, 3);왜냐하면 이 메서드들은 모두
InterCalculator안에 있기 때문이다.
그런데div()는 바로 안 된다.// cal.div(5, 3); // 에러왜냐하면
div()는Calculator에만 있는 고유 메서드이고,
인터페이스InterCalculator에는 선언되어 있지 않기 때문이다.
그래서 이렇게 형변환한다.((Calculator)cal).div(5, 3)즉,
인터페이스 타입으로 받으면 인터페이스에 있는 기능까지만 보이고, 구현 클래스의 고유 기능은 필요할 때 형변환해서 써야 한다.
9. default 메서드는 필요하면 다시 재정의할 수 있다
default메서드는 구현 클래스가 반드시 그대로만 써야 하는 것이 아니다.
필요하면 구현 클래스가 다시 재정의할 수도 있다.
예를 들면 이런 식이다.@Override public int mul(int x, int y) { System.out.println(x + "와 " + y + "를 곱합니다."); return x * y; }이 말은 곧,
- 기본 구현이 필요하면 인터페이스의
default를 그대로 쓰고- 더 구체적인 동작이 필요하면 구현 클래스에서 다시 바꿔 쓸 수 있다
는 뜻이다.
즉,default메서드는
구현 클래스에게 기본 동작을 제공하지만, 필요하면 재정의도 허용하는 메서드다.
10. default 메서드가 둘 이상 겹치면 왜 직접 재정의해야 하는가이번에는
default메서드 충돌 상황을 보자.
여기부터는 앞에서 본 두 가지가 한 번에 나온다.
하나는default메서드 충돌이고, 다른 하나는 참조 타입에 따라 바로 보이는 기능이 달라진다는 점이다.
그래서 앞부분보다 조금 더 어렵게 느껴질 수 있다. 그래도 순서대로 보면 충분히 이해할 수 있다.예제:
InterfaceTest4package day9; interface SmartAppliance { void turnOn(); void turnOff(); default void selfControl() { // 기본 점검 기능 1 System.out.println("시스템 상태를 점검합니다... 모든 장치가 정상입니다."); } } interface SmartAppliance2 { default void selfControl() { // 기본 점검 기능 2 System.out.println("시스템 상태를 점검합니다... 모든 장치가 정상입니다.2"); } } class AirConditioner implements SmartAppliance { @Override public void turnOn() { System.out.println("에어컨을 켭니다. 시원한 바람이 나옵니다."); } @Override public void turnOff() { System.out.println("에어컨을 끕니다."); } } class WashingMachine implements SmartAppliance, SmartAppliance2 { @Override public void turnOn() { System.out.println("세탁기를 작동합니다."); } @Override public void turnOff() { System.out.println("세탁이 종료되었습니다."); } @Override public void selfControl() { // 두 인터페이스의 default 메서드가 겹치므로 직접 재정의 System.out.println("세탁조 오염도를 체크합니다... 청소가 필요합니다!"); } } public class InterfaceTest4 { public static void main(String[] args) { SmartAppliance ac = new AirConditioner(); ac.turnOn(); ac.selfControl(); // SmartAppliance의 default 메서드 사용 System.out.println("--------------------"); SmartAppliance wm = new WashingMachine(); wm.turnOn(); wm.selfControl(); // WashingMachine이 재정의한 메서드 사용 SmartAppliance2 wm2 = new WashingMachine(); ((SmartAppliance)wm2).turnOn(); // wm2 타입에는 turnOn()이 없어서 형변환 후 호출 wm2.selfControl(); // 실제 객체가 WashingMachine이므로 재정의한 메서드 실행 Object wm3 = new WashingMachine(); ((SmartAppliance)wm3).turnOn(); // Object 타입에서는 바로 안 보여서 형변환 필요 ((SmartAppliance)wm3).selfControl(); ((SmartAppliance2)wm3).selfControl(); ((WashingMachine)wm3).selfControl(); } } /* 출력결과 에어컨을 켭니다. 시원한 바람이 나옵니다. 시스템 상태를 점검합니다... 모든 장치가 정상입니다. -------------------- 세탁기를 작동합니다. 세탁조 오염도를 체크합니다... 청소가 필요합니다! 세탁기를 작동합니다. 세탁조 오염도를 체크합니다... 청소가 필요합니다! 세탁기를 작동합니다. 세탁조 오염도를 체크합니다... 청소가 필요합니다! 세탁조 오염도를 체크합니다... 청소가 필요합니다! 세탁조 오염도를 체크합니다... 청소가 필요합니다! */여기서 먼저
AirConditioner는 하나의 인터페이스만 구현한다.class AirConditioner implements SmartAppliance그래서
selfControl()을 직접 만들지 않아도,
SmartAppliance에 들어 있는default selfControl()을 그대로 사용할 수 있다.
그런데WashingMachine은 다르다.class WashingMachine implements SmartAppliance, SmartAppliance2이 클래스는 두 인터페이스를 동시에 구현하는데,
두 인터페이스 모두selfControl()이라는 같은 이름의default메서드를 가지고 있다.
그러면 문제가 생긴다.
- 어느 쪽 기본 메서드를 따라야 할지 컴파일러가 결정할 수 없다.
그래서 이 상황에서는 구현 클래스가 직접 정리해 줘야 한다.
@Override public void selfControl() { System.out.println("세탁조 오염도를 체크합니다... 청소가 필요합니다!"); }즉,
default메서드가 충돌하면 자동으로 알아서 처리되는 것이 아니라,
구현 클래스가 직접 자기 기준의 메서드를 다시 만들어야 한다.
11. 같은 객체를 어떤 인터페이스 타입으로 보느냐에 따라 호출 방식이 달라진다
InterfaceTest4의 뒤쪽 코드는 이 점을 잘 보여준다.SmartAppliance wm = new WashingMachine(); SmartAppliance2 wm2 = new WashingMachine(); Object wm3 = new WashingMachine();실제 객체는 셋 다
WashingMachine이다.
그런데 참조변수 타입은 다르다.
wm은SmartAppliancewm2는SmartAppliance2wm3는Object그래서 바로 사용할 수 있는 기능도 달라진다.
예를 들어wm은SmartAppliance타입이므로 그쪽에서 보이는 메서드만 바로 사용할 수 있다.
wm2는SmartAppliance2기준으로 보이고,
wm3는Object기준이므로 인터페이스 메서드를 바로 쓸 수 없어서 형변환이 필요하다.((SmartAppliance)wm3).turnOn(); ((SmartAppliance)wm3).selfControl(); ((SmartAppliance2)wm3).selfControl(); ((WashingMachine)wm3).selfControl();
여기서
wm2도 한 번 더 봐야 한다.SmartAppliance2 wm2 = new WashingMachine(); ((SmartAppliance)wm2).turnOn(); wm2.selfControl();
wm2의 실제 객체는WashingMachine이지만, 참조 타입이SmartAppliance2라서
SmartAppliance2에 선언된 메서드만 바로 보인다.
그런데turnOn()은SmartAppliance2에 없고SmartAppliance에만 있다.
그래서turnOn()을 쓰려면SmartAppliance타입으로 형변환해야 한다.
즉, 여기서 핵심은
실제 객체는 하나인데, 어떤 타입으로 참조하느냐에 따라 바로 보이는 기능 범위가 달라진다는 점이다.
그리고WashingMachine이selfControl()을 직접 재정의했기 때문에,
어느 타입으로 보더라도 결국 실행되는 것은WashingMachine의selfControl()이다.
12. 이번 파트에서 꼭 잡아야 하는 핵심이번 인터페이스 2에서 꼭 이해해야 하는 것은 아래 다섯 가지다.
1) 인터페이스는 규격을 맞추기 위해 쓴다제품이 달라도 사용하는 방법을 통일하려고 인터페이스를 쓴다.
2) 인터페이스가 있으면 사용하는 쪽 코드가 덜 흔들린다구현 클래스가 바뀌어도, 사용하는 쪽은 인터페이스 기준으로 코드를 짜면 된다.
3) 객체 생성까지 분리하면 더 유연해진다
TVFactory처럼 객체를 대신 만들어 주는 쪽을 두면, 사용하는 쪽은 세부 클래스 이름을 덜 알아도 된다.
4) default 메서드는 기존 구현 클래스의 수정 부담을 줄이기 위해 나왔다인터페이스에 새 기능을 넣을 때, 기본 구현을 같이 제공할 수 있다.
5) default 메서드가 충돌하면 구현 클래스가 직접 정리해야 한다같은 이름의
default메서드가 둘 이상 겹치면, 어느 쪽을 따를지 구현 클래스가 직접 결정해야 한다.
13. 헷갈리기 쉬운 부분
1) 인터페이스가 있다고 해서 구현 클래스가 완전히 사라지는 것은 아니다인터페이스는 사용하는 기준을 통일해 주는 것이지, 실제 동작을 하는 구현 클래스가 없어지는 것은 아니다.
2) 인터페이스 타입으로 보면 인터페이스에 선언된 기능까지만 바로 보인다그래서 구현 클래스 고유 메서드는 필요할 때 형변환해서 써야 한다.
3) default 메서드는 자동으로 다 해결해 주는 마법 기능이 아니다기본 구현을 제공할 뿐이다.
필요하면 구현 클래스가 다시 재정의할 수 있고, 충돌이 나면 직접 정리해야 한다.
4) static 메서드는 객체로 부르는 것이 아니다인터페이스 안에 있어도
InterCalculator.print()처럼 인터페이스 이름으로 호출해야 한다.
핵심만 다시 정리하면
- 인터페이스는 공통 규칙을 만들어 사용하는 방법을 통일하는 도구다.
- 구현 클래스가 바뀌어도 사용하는 쪽 코드를 덜 바꾸게 해 준다.
- 객체 생성까지 분리하면 사용하는 쪽과 만드는 쪽을 더 깔끔하게 나눌 수 있다.
default메서드는 인터페이스에 기본 동작을 넣기 위해 나온 기능이다.static메서드는 인터페이스 이름으로 바로 호출한다.- 같은 이름의
default메서드가 둘 이상 겹치면, 구현 클래스가 직접 재정의해야 한다.- 인터페이스 타입으로 객체를 보면 인터페이스에 선언된 기능까지만 바로 사용할 수 있다.