mini pms v22 - 상속
상속 구현 방법
상속(inheritance) 을 구현하는 방법에는 전문화와 일반화 두 가지가 있다.
1. 전문화
- 기존 클래스의 기능을 그대로 활용할 수 있도록 연결하고, 여기에 새 기능을 추가하거나 기존 기능을 변경하여 좀 더 특수 목적의 서브 클래스를 만드는 기법이다.
- 마치 부모로부터 무언가를 물려 받는 것과 같아 상속 이라는 문법의 대표적인 기법으로 알려져 있다. 그래서 객체지향 프로그래밍의 상속을 얘기할 때는 대부분 전문화 를 가르킨다.
2. 일반화
- 클래스들의 공통 분모를 추출하여 수퍼 클래스를 정의하는 기법이다.
- 새로 정의한 수퍼 클래스와 부모/자식 관계를 맺는다.
- 프로그래밍 처음부터 상속을 고려하여 수퍼 클래스를 정의하는 것이 아니라 코드를 리팩토링하는 과정에서 수퍼 클래스를 정의하는 것이기 때문에 초보 개발자에게 적합하다.
- 보통 일반화를 통해 추출된 수퍼 클래스는 서브 클래스에게 공통 분모를 상속해주는 것이 목적이다. 직접 인스턴스를 생성하고 사용하기 위해 만든 클래스가 아니다. 그래서 일반화를 통해 도출한 수퍼 클래스는 보통 추상 클래스로 정의한다.
실습 목표
- 상속의 기법에서 전문화 와 일반화 기법을 이해하고 구현하는 방법을 배운다.
- 추상 클래스 의 용도를 이해하고 활용법을 연습한다.
- 다형적 변수(ploymorphic variables) 를 이용하여 서브 클래스의 인스턴스를 다루는 것을 연습한다.
- 의존성 주입(dependency injection; DI) 의 의미를 이해한다.
실습 내용
ArrayList
,LinkedList
의 공통 분모를 추출하여 수퍼 클래스를 정의한다.- XxxHandler 에서 사용할 List 객체를 외부에서 주입 받는 방식으로 변경한다.
일반화: 수퍼클래스 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.eomcs.util;
public class List<E> {
protected int size;
public int size() {
return size;
}
// 아래 메서드들은
// ArrayList나 LinkedList에서 동작 방법이 다르기 때문에
// 여기서 구현할 필요가 없다.
public boolean add(E e) {
// 서브 클래스에서 메서드를 구현하는 방법이 다르기 때문에
// 수퍼 클래스에서는 구현하지 않고 오버라이딩하게 한다.
// 수퍼 클래스에서 실질적으로 구현하지 않는 이 메서드를 두는 이유
// => 서브 클래스들이 갖춰야 할 기능을 정의
// => 즉 서브 클래스들에게 반드시 갖춰야 할 기능을 강제
return false;
}
public void add(int index, E element) {}
public E get(int index) {
return null;
}
public E set(int index, E element) {
return null;
}
public E remove(int index) {
return null;
}
public Object[] toArray() {
return null;
}
public E[] toArray(E[] arr) {
return null;
}
}
일반화는 상속 구현 방법의 일종으로, 서브 클래스의 공통 분모를 추출하여 수퍼 클래스로 정의 하고 상속 관계를 맺는 기법이다. 이전 버전에서 구현한 ArrayList와 LinkedList의 메서드들의 시그니처와 변수는 변수명은 모두 같다. 따라서 두 클래스의 공통분모인 변수와 메서드를 추출하여 수퍼 클래스로 정의하고 상속 관계를 맺어주었다. 이렇게 상속 관계를 맺을 경우 다형성을 이용하여 서브객체의 교체가 쉬워진다.
상속에 대한 오해! 서브 클래스는 수퍼 클래스의 코드를 그대로 가져오는 것이 아니다. 다만 연결될 뿐이다. 컴파일 하면 그 바이트 코드에는 해당 클래스에 대한 것만 있다.
기존 ArrayList와 LinkedList는 List를 상속받게 하였으며 각 메서드를 오버라이딩하도록 하였다.
다형적 변수: 수퍼클래스의 레퍼런스로 선언
수퍼 클래스는 다형적 변수로 사용할 수 있다. 서브클래스의 인스턴스를 다룰 수 있다. Handler에서 사용할 목록 관리 객체를 수퍼 클래스의 레퍼런스로 선언해, 수퍼클래스(List)의 서브 객체(ArrayList, LinkedList)로 교체하기 쉽도록 만든다. 즉 다형적 변수의 특징을 이용해서 ArrayList
또는 LinkedList
객체를 모두 담을 수 있도록 list
타입의 필드로 선언한다.
1
2
3
4
public class BoardHandler {
List<Board> boardList;
//...
}
의존성 주입
Handler가 의존하는 객체를 내부에서 생성하지 않고 생성자를 통해 외부에서 주입받는 것을 말한다. 이를 통해서 의존 객체 교체가 쉬워진다. 생성자의 파라미터를 통해 List
클래스의 서브 클래스를 공급받도록 변경하였다.
1
2
3
4
5
6
7
8
9
10
public class BoardHandler {
List<Board> boardList;
// 생성자의 파라미터로 List 클래스의 서브 클래스를 공급받도록 변경한다.
// 외부 객체 주입을 강제한다.
public BoardHAndler(List<Board> list) {
this.boardList = boardList;
}
//...
}
Handler가 사용할 List 객체 (의존 객체; dependency)를 생성자에서 직접 만들지 않고, 이렇게 생성자가 호출될 때 파라미터로 받으면 필요에 따라 List 객체를 다른 객체로 대체하기 쉽다. 즉 다형적 변수 법칙에 따라 List의 하위 객체라면 어떤 객체든지 가능하다. 이런 식으로 의존 객체를 외부에서 주입받는 것을 “Dependency Injection(DI; 의존성주입)”이라 부른다. 즉 의존객체를 부품화하여 교체하기 쉽도록 만드는 방식이다.
1
2
3
4
5
6
public class App {
public static void main(String[] args) {
ArrayList<Board> boardList = new ArrayList<>();
BoardHandler boardHandler = new BoardHandler(boardList);
}
}
IOC (Inversion of Control: 제어의 역전)
- 보통 계획에 따라 작업을 하는데 예상하지 못하게 제어하는 경우가 있다.
이 중 하나가 Dependency Injection 이다.
컨테이너란? 작성한 코드의 처리과정을 위임받은 독립적인 존재이다. 적절한 설정만 있으면 누구의 도움 없이도 프로그래머가 작성한 코드를 스스로 참조한 뒤 알아서 객체의 생성과 소멸을 컨트롤해준다. 프로그래머가 작성한 코드는 컨테이너를 사용하게 됨으로서 프로그래머의 손을 떠나 컨테이너의 영역으로 떠나버리게 된다. 정확하게 이야기하면 컨테이너가 마음대로 객체를 생성하는 것이 아니라 프로그램을 이용하는 이용자의 호출에 의해 컨테이너가 동작하게 되는 구조이다. 컨테이너의 예로는 스프링과 톰캣(WAS) 이 있다. 작성한 서블릿을 쓸 지 말 지는 톰캣이 결정한다. 즉 작성한 코드가 언제 호출될 지 감도 안 오는 막막한 상태라면 올바르게 컨테이너를 이용하고 있는 것이다.
IOC란? 컨테이너다. 한글로 번역하자면 제어의 역전이다. 컨테이너는 작성한 코드 컨트롤(객체의 생성과 소멸)을 대신 해주는데 이 IOC 덕분에 언제 호출될지도 모르는 코드를 마음껏 칠 수 있게 되었다.
DI란? IOC의 일종이다. 받아먹기도 전에 미리 받아먹겠다고 선언하는 주는 놈은 생각도 않았는데 먼저 말부터 꺼내놓는 파렴치한 코딩방식 (ㅋㅋㅋ)이라고 할 수 있다. DI란 전략페턴을 일종의 프레임워크처럼 편리하게 사용할 수 있도록 만든 것이라고 할 수 있다. 스프링은 이런 전략 패턴을 편리화하였다. DI 방식으로 코드를 작성한다면 현재 사용하고 있는 객체의 메서드가 어떻게 작성되었는지 알 필요가 없으며 그 클래스가 기능을 잘 구현했는지 조차도 알 필요가 없다. 그저 클래스의 기능을 추상적으로 묶어둔 인터페이스만 갖다 쓰면 그만이다. 나머지는 스프링에서 찜한 걸 연결시켜주니 말이다.
- 인터페이스의 메서드만 이용하더라도 구현부는 나중에 주입을 통해 해결하므로 획기적인 분업과 동시에 구현 클래스를 쉽게 교체할 수 있다는 장점을 얻게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface StrategyPattern {
void dependency_injection();
}
class StrategyPatternImpl implements StrategyPattern {
@Override
public void dependency_injection() {
System.out.println("줄 놈은 생각도 안했던 그 메서드");
}
}
class StrategyPatternService {
public static void main(String[] args) {
StrategyPattern sp = new StrategyPatternImpl();
sp.dependency_injection();
}
}
// 결과
// 줄 놈은 생각도 안 했던 그 메서드
23 - 추상 클래스와 추상 메서드
추상 클래스
- 서브 클래스에 기본 기능 및 공통 분모를 상속해 주는 역할을 하는 클래스
- new 명령을 통해 인스턴스를 생성할 수 없다.
- 상속 의 기법 중에서 일반화 를 통해 수퍼 클래스를 정의한 경우 보통 추상 클래스로 선언한다.
- 추상 메서드를 가질 수 있다.
- 일반 클래스, 콘크리트 클래스(concrete class)는 이러한 추상 클래스와 반대 개념이다.
- 일반화를 할 때 보통 추상 클래스를 선언한다.
추상 메서드
- 서브 클래스에 따라 구현 방법이 다른 경우 보통 추상 메서드로 선언한다.
- 서브 클래스에서 반드시 구현해야 하는 메서드다. 따라서 서브 클래스를 정의할 때 반드시 해당 메서드를 구현하도록 강제하고 싶다면 추상 메서드로 선언한다.
- 일반 클래스는 추상 메서드를 가질 수 없다.
- 추상 클래스와 인터페이스 만이 추상 메서드를 가질 수 있다.
실습 목표
- 추상 클래스 의 용도를 이해하고 활용법을 연습한다.
- 추상 메서드 의 용도와 활용법을 연습한다.
실습 내용
List
클래스를 추상 클래스로 변경한다.List
클래스의 메서드를 추상 메서드로 변경한다.
추상 클래스 및 추상 메서드 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class List<E> {
// 서브 클래스가 직접 인스턴스 필드를 사용할 수 있도록 하기 위해
// 접근 범위를 protected 로 선언한다.
protected int size;
// 수퍼 클래스에서 구현해도 상관 없는 메서드는 일반 메서드로 구현한다.
public int size() {
return size;
}
// 서브 클래스에서 정의할 메서드라면 수퍼 클래스에서 정의하지 않고
// 서브 클래스가 반드시 구현하도록 강제한다.
// => 메서드 선언부에 abstract 키워드(modifier)를 붙인다.
public abstract boolean add(E e);
public abstract void add(int index, E value);
public abstract E get(int index);
public abstract E set(int index, E e);
public abstract E remove(int index);
public abstract Object[] toArray();
public abstract E[] toArray(E[] arr);
}
- 추상 클래스 문법의 목적: 추상 클래스는 인스턴스 생성 목적의 클래스가 아니라 공통점을 뽑아내는 용도이다. 따라서 추상 클래스 문법은 서브 클래스에게 공통 분모(멤버)를 물려주는 용도의 클래스를 직접 인스턴스를 생성하지 못하도록 막는다.
- 보통 추상클래스는 AbstractXxxx 형식으로 짓는다.
- 추상 메서드 문법의 목적: 서브클래스에서 해당 메서드를 오버라이딩하도록 강제한다.
- 서브 클래스가 반드시 재정의해야 할 메서드라면 수퍼 클래스에서 정의하지 않는다.
상속
상속의 종류
specialization
- 가장 많이 사용하는 방법으로 수퍼 클래스를 상속 받아 서브 클래스를 만드는 것이다. 수퍼클래스에 새 특징을 추가하거나 새 기능을 추가하여 더 특별한 일을 수행하는 서브클래스를 만든다.
- 그래서 이런 상속을 “특수화/전문화(specialization)”이라 부른다.
generalization
- 리팩토링 과정에 수행하는 방법이다.
- 서브클래스들의 공통 분모를 추출하여 수퍼클래스를 정의하는 방법을 말한다. 그래서 이런 상속을 “일반화/표준화(generalization)”이라 부른다.
추상 클래스
서브 클래스에 변수와 메서드를 상속해주는 용도로 사용한다. 인스턴스를 생성해 직접 사용할 수 없다.
1
public abstract class A {}
추상 메서드
수퍼 클래스에서 정의하지 않고 서브 클래스에 반드시 정의하도록 강제하는 문법이 추상 메서드이다.추상 메서드는 구현할 수 없다.
1
2
3
public abstract class A {
public abstract void m1();
}
추상 클래스와 추상 메서드
추상 메서드는 구현하지 않은 메서드이기 때문에 일반 클래스 (= 구현 클래스; concrete class)는 추상 메서드를 가질 수 없다. 오직 추상 클래스만이 추상 메서드를 가질 수 있다.
일반 클래스는 인스턴스를 생성할 수 있고, 인스턴스로 메서드를 호출한다. 그런데 만약 클래스가 구현하지 않은 메서드를 가지고 있다면 호출할 때 오류가 발생할 것이다. 원천적으로 이런 문제를 발생시키지 않게 하기 위해 일반 클래스는 추상 메서드를 갖지 않게 하였다.
추상 클래스의 용도는 서브 클래스들이 가져야 할 공통 변수나 메서드를 제공하는 것이다. 따라서 일반 변수나 메서드, 특히 추상 메서드를 가질 수 있다.
특별히 추상 메서드를 선언하는 경우는
첫째, 서브 클래스마다 구현이 달라 굳이 수퍼 클래스에서 정의할 필요가 없을 때이다.
- 둘째, 서브 클래스에게 메서드 오버라이딩을 강제해야 할 때이다.
- 서브 클래스가 상속 받은 추상 클래스를 구현하지 않는다면 추상 메서드를 그냥 보유하기 때문에 일반 클래스가 될 수 없다. 추상 클래스가 되어야 한다. (따라서 이 경우
abstract
키워드를 빼먹을 경우 컴파일러 오류를 발생시킨다.)
- 서브 클래스가 상속 받은 추상 클래스를 구현하지 않는다면 추상 메서드를 그냥 보유하기 때문에 일반 클래스가 될 수 없다. 추상 클래스가 되어야 한다. (따라서 이 경우
- 직접 인스턴스를 만들어 사용하기 위해 정의한 클래스가 아니라 서브 클래스에게 공통 기능을 상속해주는 목적으로 만든 클래스인 경우 추상클래스로 선언해 클래스 사용을 막자.
- 보통 일반화 과정에서 생성되는 클래스를 추상 클래스로 만든다.
디자인 패턴과 추상 클래스 - 템플릿 메서드 패턴
템플릿 메서드 패턴은 수퍼 클래스에서 추상적으로 표현하고, 서브 클래스에서 그 구체적인 내용을 결정하는 설계 방식을 말한다. 즉 수퍼 클래스에서 전체적인 논리 흐름을 정의하고, 서브 클래스에서 각 흐름에 따라 구체적인 동작을 정의한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Building {
// 일반 메서드: 전체적인 논리 흐름 정의
// 서브 클래스는 메서드를 그대로 상속받기 때문에
// 모든 서브 클래스들이 건물을 지을 때 동일한 방식으로 작업할 것이다.
public void build () {
System.out.println("건축하겠습니다!");
startEffect();
System.out.println("건축이 완료되었습니다!");
endEffect();
}
// 템플릿 메서드: 틀만 갖춘 상태
public abstract void startEffect();
public abstract void endEffect();
}
1
2
3
4
5
6
7
8
9
10
public class Farm extends Building {
@Override
public void startEffect() {
System.out.println("땅을 고른다.");
}
@Override
public void endEffect() {
System.out.println("씨앗을 뿌린다.");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Restaurant extends Building {
@Override
public void startEffect() {
System.out.println("뚝딱뚝딱");
}
@Override
public void endEffect() {
System.out.println("밥을 먹는다!");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exam01 {
public static void main(String[] args) {
Restaurant r = new Restaurant();
Farm f = new Farm();
work(r);
work(f);
}
public void work(Building obj) {
obj.build();
}
}
인터페이스
배경
인터페이스 문법 이전
인터페이스 문법을 사용하기 전 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BlueWorker {
public void doFight() {
System.out.println("육체 노동자가 일을 합니다.");
}
}
public class JubuWorker {
public void doSsingSsing() {
System.out.println("주부로 일합니다.");
}
}
public class WhiteWorker {
public void doZingZing() {
System.out.println("사무직 노동자가 일을 합니다.");
}
}
객체 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exam01 {
public static void main(String[] args) {
BlueWorker w1 = new BlueWorker();
JubuWorker w2 = new JubuWorker();
WhiteWorker w3 = new WhiteWorker();
}
// 메서드 호출 방법이 다르기 때문에
// 각 노동자에게 일을 시키는 방법이 다르다.
w1.doFight();
w2.doZingZing();
w3.doSsingSsing();
}
객체에 일을 시키는 방식은 비슷한데 메서드 시그니처가 다르기 때문에 호출 시 일관성이 없다. 즉 worker 객체를 사용하는 입장에서는 각 worker에 어떤 메서드가 있는지 확인해서 그 형식에 맞춰야 하기 때문에 객체를 사용하기가 불편하다. 또한, 앞으로 worker를 추가 제작할 경우 개발자의 입맛에 맞춰 메서드 이름과 파라미터 형식이 결정될 것이기에 더 worker 객체를 사용하기 어려워진다.
유사한 일을 하는 데 객체 사용법이 다르면 쓰기가 매우 불편하다.
따라서 유사한 일을 하는 객체에 대해 사용법을 통일하자. 이를 위해서는
- 객체의 사용 규칙을 정의한다.
- 클래스를 정의할 그 규칙에 따라 만든다.
- 규칙에 따라 만든 클래스를 사용할 때는 일관된 방법으로 메서드를 호출할 수 있다.
이렇게 객체의 사용 규칙을 정의하는 문법이 “인터페이스”이다.
호출 규칙으로서의 인터페이스
인터페이스는 caller와 callee 사이의 호출 규칙을 정의하는 문법이다.
사용 규칙: Worker
caller(호출자; 사용자): Exam01
callee(피호출차; 도구): BlueWorker, JubuWorker, WhiteWorker
호출 규칙이란? 메서드 형식을 의미한다.
메서드 몸체는 규칙에 따라 만드는 클래스에서 정의하는 것이다.
인터페이스 문법 이후
위의 코드를 다음과 같이 인터페이스 문법을 사용해 변경하자.
interface(호출 규칙): Worker
=> worker 객체의 사용 규칙을 정의한다
// 인터페이스 - caller와 callee 사이의 호출 규칙을 정의하는 문법
public interface Worker {
void execute();
}
callee(피호출차; 도구): BlueWorker, JubuWorker, WhiteWorker
=> 클래스를 정의할 때 Worker 규칙에 따라 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Worker 인터페이스를 이행하는 클래스: Worker "구현체"
public class BlueWorker implements Worker {
// 인터페이스 구현체(인터페이스를 구현하는 클래스)는
// 반드시 인터페이스에 선언된 모든 메서드를 구현해야 한다.
public void execute() {
System.out.println("육체 노동자가 일을 합니다.");
}
}
public class JubuWorker {
public void doSsingSsing() {
System.out.println("주부로 일합니다.");
}
}
public class WhiteWorker {
public void doZingZing() {
System.out.println("사무직 노동자가 일을 합니다.");
}
}
caller(호출자; 사용자): Exam01
=> worker를 사용하는 측에서는 Worker 인터페이스에 정의된 대로 메서드를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
// 인터페이스 레퍼런스
// => 인터페이스를 구현한 클래스의 객체 주소를 저장하는 변수
Worker w1 = new BlueWorker();
Worker w2 = new WhiteWorker();
Worker w3 = new JubuWorker();
// 사용하려는 객체가 같은 인터페이스를 구현(사용규칙에 따라 작성)하였다면
// 다음과 같이 동일한 사용규칙에 따라 메서드를 호출할 수 있다.
// 호출의 일관성 확보
w1.execute();
w2.execute();
w3.execute();
}
// 인터페이스 레퍼런스?
// => 해당 인터페이스에 따라 작성된(사용규칙을 준수하는) 클래스의 인스턴스 주소를 저장한다.
//
// 위 예제에서 w 레퍼런스를말로 표현하는 방법:
// => Worker 사용규칙에 따라 작성된 클래스의 인스턴스 주소를 저장하는 변수 w.
// => Worker 인터페이스를 구현한 클래스의 인스턴스 주소를 저장하는 변수 w.
// => Worker 구현체의 인스턴스 주소를 저장하는 변수 w.
// => Worker 구현체의 객체 주소를 저장하는 변수 w.
// => Worker 구현 객체를 저장하는 변수 w.
// => Worker 객체를 저장하는 (변수) w.
// => Worker 객체를 가리키는 (변수) w.
추상 클래스와 인터페이스
공통점과 차이점
인터페이스 구현을 돕는 추상 클래스
추상 클래스를 이용하여 인터페이스 구현을 도와줄 수 있다. 인터페이스에 메서드가 많지만, 그 중 특정 메서드만 주로 구현한다면 개발자가 인터페이스를 구현하기 편하도록 서브 클래스에서 오버라이딩할 메서드를 제외한 나머지 메서드를 추상클래스에서 구현한다.
1
2
3
4
5
6
7
8
9
10
public abstract class AbstractServlet implements Servlet {
public void init(){}
public void destroy(){}
public String getServlet() {
return null;
}
public String getServletConfig() {
return null;
}
}
- 인터페이스를 구현한다는 것은 인터페이스에 선언된 모든 메서드를 구현한다는 뜻이다.
- 인터페이스에 선언된 메서드를 한 개라도 빠뜨리면 추상 메서드를 갖고 있는 상태가 된다. 즉 일반 클래스가 될 수 없고, 추상 클래스가 되어야 한다.
- 직접 인터페이스를 구현하기보다 추상 클래스를 상속받는다.
- 인터페이스를 직접 구현하면 인터페이스에 선언된 모든 메서드를 구현해야 하지만, 미리 인터페이스의 몇몇 메서드를 구현한 추상 클래스를 상속받는다면 서브 클래스는 좀 더 쉽게 인터페이스를 구현할 수 있다(추상 클래스에서 구현하지 않은 메서드만 정의하면 된다)
- 즉 인터페이스 메서드가 많을 경우 일부 메서드를 미리 구현함으로써 개발자가 좀 더 쉽게 인터페이스를 구현할 수 있게 도와주는 용도로 “추상 클래스” 문법을 사용할 수 있다.
인터페이스는 객체의 사용 규칙을 정의하는 문법이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exam01 {
public static void main(String[] args) {
BlueWorker w1 = new BlueWorker();
WhiteWorker w2 = new WhiteWorker();
JubuWorker w3 = new JubuWorker();
// 각 노동자에게 일을 시키는 방법이 다르다.
// 왜?
// => 메서드 호출 방법이 다르기 때문에
w1.doFight();
w2.doZingZing();
w3.doSsingSsing();
}
}
위의 예제에서 객체의 일을 시키는 방식은 비슷한데, 메서드 시그너처가 다르기 때문에 호출할 때 일관성이 없다. 즉 객체를 사용하는 입장에서는 어떤 메서드가 있는 지 확인해서 그 형식에 맞춰야 하기 때문에 객체를 사용하기 불편하다. 또한 다른 worker 클래스를 추가 제작할 경우 메서드 이름과 파라미터 형식이 결정될 것이기 때문에 더 worker 객체를 사용하기 힘들어진다.
즉 유사한 일을 하는데 객체의 사용법이 다르다면 쓰기가 매우 불편하다. 유사한 일을 하는 객체에 대해 사용법을 통일함으로써 이를 해결할 수 있다.
인터페이스 사용 방법
- 객체 사용 규칙을 정의한다.
- 클래스를 정의할 때 그 규칙에 따라 만든다.
- 그러면 규칙에 따라 만든 클래스를 사용할 때는 일관된 방법으로 메서드를 호출할 수 있어 코딩하기가 훨씬 편해지고, 유지보수가 쉬워진다.
이렇게 객체의 사용 규칙을 정의하는 문법이 인터페이스이다.
인터페이스와 추상클래스
인터페이스와 추상클래스는 엄연히 다르다.
- 추상 클래스 = 변수 + 구현된 메서드 + 추상 메서드
- 인터페이스 = 추상 메서드
- 추가적으로 구현된 default 메서드, static 메서드와 final변수가 있다. 이는 다음 시간에 배울 것이다.
인터페이스 레퍼런스
- 인터페이스를 구현한 클래스의 객체 주소는 다 저장할 수 있다.
- 그 객체가 누구의 자식인 지는 상관하지 않는다.
인터페이스 정의
모든 메서드는 public이다.
- 인터페이스에 정의하는 메서드는 호출 규칙이다. 규칙은 공개되어야 한다. 그래서 인터페이스에 선언되는 모든 메서드는 public이다. 이것은 생략 가능하다.
- 구현체에서 메서드를 구현할 때 public 보다 공개 범위를 좁힐 수 없다. 따라서 반드시 public이어야 한다.
모든 메서드는 abstract다.
- 인터페이스에 선언하는 메서드는 호출 규칙을 정의한 것이다. 규칙은 클래스가 따라야 한다.
- 모든 메서드는 몸체를 구현하지 않는다. 즉, abstract 메서드이다.
1
2
public abstract void m1();
/*public abstract*/ void m2();
자바 8부터 인터페이스에서 default 메서드, static 메서드를 정의하는 것을 허락한다. 이는 특수한 경우로 기본은 abstract라 생각하면 된다. 다음 시간에 더 자세하게 배울 것이다.
인스턴스를 만들 수 없다.
- 인터페이스는 규칙으로 사용되기 때문에 new명령어로 인스턴스를 생성할 수 없다.
- 인터페이스에 선언되는 모든 변수는 public으로 공개되고, static으로 클래스 변수가 된다. 또한 규칙은 변경되어선느 안 되기 때문에 모든 변수는 final로 선언한다. final변수는 선언과 동시에 특정 값으로 초기화해야 한다.
- 단 인스턴스는 static 블록을 둘 수 없다.