09.추상 클래스
Last updated -
2023년 03월 10일
Edit Source
해당 게시글은 Do it! 자바 프로그래밍 입문 교재를 정리한 내용입니다.
# Chapter 09 - 추상 클래스
# 추상 클래스
# 추상 클래스란?
추상 클래스는 항상 추상 메서드를 포함한다. 추상 메서드는 구현 코드가 없다. 즉, 함수 몸체가 없다. 아래와 같이 {}
안의 내용이 함수 몸체이다.
1
2
3
| int add(int x, int y) {
return x + y;
}
|
중괄호 {}
로 감싼 부분 = 함수의 구현부(implementation)
함수의 구현부가 없는 함수 = 추상 함수(abstract function)
자바에서는 이를 추상 메서드(abstract method) 라고 한다.
추상 메서드는 선언만 하며 abstract 예약어 사용
1
| abstract int add(int x, int y);
|
아래는 중괄호 {}
가 있으니까 추상 메서드 아님. {}
안에 코드만 없을 뿐 {}
를 구현은 한 것이다.
1
| int add(int x, int y) {}
|
메서드 선언의 의미
변수를 선언하고 제어문을 사용하여 로직을 만들고 기능을 구현하는 것도 물론 중요하지만, 로직을 구현하는 것보다 중요한 것이 어떻게 구현할지를 결정하는 것이다.
1
| int add(int num1, int num2);
|
위 코드만 봐도 두 개의 정수를 입력받고 더해서 반환한다는 것을 유추할 수 있다.
메서드의 선언부(declaration)만 봐도 어떤 일을 하는 메서드인지 유추할 수 있다.
선언부에 해당하는 반환 값(리턴), 함수 이름, 매개변수를 정의한다는 것이 함수의 역할이 무엇인지, 어떻게 구현해야 하는지를 정의한다는 의미다.
어떤 의미로는, 함수 몸체를 구현하는 것보다 함수 선언부 작성이 더 중요하다는 말이다.
# 추상 클래스 구현
- 클래스 다이어그램에서 일반 클래스는 똑바로 쓰는데 추상 클래스는 기울임 꼴로 작성
Computer 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| package chapter09.abstractex;
public abstract class Computer {
public abstract void display();
public abstract void typing();
public void turnOn() {
System.out.println("전원을 켭니다.");
}
public void turnOff() {
System.out.println("전원을 끕니다.");
}
}
|
추상화 하고자 하는 메서드(몸체, 구현부 {}
없는)에 abstract 안붙히면 오류
추상 메서드가 들어있는 클래스에 abstract 안붙히면 오류
Computer 클래스를 상속받는 클래스 중 turnOn()
과 turnOff()
구현 코드는 공통
하지만, 추상 메서드인 display()
와 typing()
은 하위 클래스에 따라 달라질 수 있음
추상 메서드는 이 메서드는 Computer 클래스에서 구현하지 않고, 구현에 대한 책임을 상속 받는 클래스에 위임한다라는 의미
최종 정리
DeskTop 클래스
1
2
3
4
5
| package chapter09.abstractex;
public class DeskTop extends Computer{
}
|
이렇게 하면 오류 발생
Computer 클래스가 추상 클래스니까, 추상 클래스를 상속받는 클래스는 추상 메서드를 상속받음
해결방법 01. DeskTop도 추상 클래스로 만들기
해결방법 02. DeskTop에서 Computer에서 구현되지 않았던 추상 메서드를 구현하기, 대신 모든 추상 메서드를 모두 구현해야함. 하나라도 추상 메서드 있으면 추상 클래스니까
아래에서 해결방법 02를 사용했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| package chapter09.abstractex;
public class DeskTop extends Computer{
@Override
public void display() {
System.out.println("DeskTop display()");
}
@Override
public void typing() {
System.out.println("DeskTop typing()");
}
}
|
NoteBook 클래스
1
2
3
4
5
6
7
8
| package chapter09.abstractex;
public abstract class NoteBook extends Computer{
@Override
public void display() {
System.out.println("NoteBook display()");
}
}
|
- 추상 메서드
display()
와 typing()
중 display()
만 구현했으니 추상 클래스라고 해주는 abstract
를 붙혀야함
MyNoteBook 클래스
1
2
3
4
5
6
7
8
| package chapter09.abstractex;
public class MyNoteBook extends NoteBook {
@Override
public void typing() {
System.out.println("MyNoteBook typing()");
}
}
|
모든 추상 메서드를 구현하고도 클래스에 abstract 사용하면?
1
2
3
4
5
6
7
8
9
| public abstract class AbstractTV {
public void turnOn() {
System.out.println("전원을 켭니다.");
}
public void turnOff() {
System.out.println("전원을 끕니다.");
}
}
|
모든 추상 메서드를 구현했어도, 완벽하게 TV의 기능이 구현된 것은 아니고 TV의 공통 기능만 구현해놓은 것이다.
이 클래스는 사용할 목적이 아닌 상속만을 위해 만든 추상 클래스이다.
이런 경우 new 예약어로 인스턴스 생성 불가
추상 클래스 연습한거는 Car, Bus, AutoCar, CarTest
# 추상 클래스를 만드는 이유
추상 클래스는 인스턴스로 생성할 수 없다.
추상 클래스에서 구현하는 메서드
생성할 수 없는 추상 클래스를 어디에 씀? -> 추상 클래스는 상속을 하기 위해 만든 클래스
추상 클래스에서 구현하는 메서드 : 하위 클래스에서도 사용할, 하위 클래스에서도 구현 내용을 공유할 메서드를 구현
하위 클래스에서 내용을 각각 다르게 구현해야 하면, 구현 내용을 추상 메서드로 남겨 두고 하위 클래스에 구현을 위임
메서드 | 특징 |
---|
구현된 메서드 | 하위 클래스에서 공통으로 사용할 구현 코드. 하위 클래스에서 재정의 할 수도 있음 |
추상 메서드 | 하위 클래스가 어떤 클래스냐에 따라 구현 코드가 달라짐 |
예를 들어, turnOn()
, turnOff()
의 구현은 하위 클래스에서 공유할 수 있다. 하지만, display()
와 typing()
의 구현내용은 NoteBook인지 DeskTop인지에 따라 달라지니까 Computer 클래스에서 구현 안했던 것
추상 클래스와 프레임워크
추상 클래스는 많은 프레임워크에서 사용하고 있는 구현 방식
ex) 안드로이드 앱 만들 때 안드로이드 라이브러리에서 제공하는 많은 클래스 사용. 모두 구현된 클래스도 있지만, 일부만 구현되어 있어서 상속 받고 그 뒤에 구현하는 경우도 많음
구현을 미루어 놓은 메서드(추상 메서드)는 앱을 어떻게 만드냐에 따라 다르게 구현해야할 내용으로 앱에서 구현하도록 선언만 해둔 것
# 템플릿 메서드
# 추상 클래스와 템플릿 메서드
Car 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package chapter09.template;
public abstract class Car {
public abstract void drive();
public abstract void stop();
public void startCar() {
System.out.println("시동을 켭니다.");
}
public void turnOff() {
System.out.println("시동을 끕니다.");
}
final public void run() {
startCar();
drive();
stop();
turnOff();
}
}
|
final
이 부분이 템플릿 메서드
시동을 켜고 끄는 방법은 비슷비슷 하니까 startCar()
, turnOff()
는 미리 구현해놈
drive()
, stop()
은 차종에 따라 다를 수 있으니까 추상 메서드로 선언
run()
은 템플릿 메서드인데, 자동차가 달리는 방법을 순서대로 구현해뒀음. 시동 키고 -> 달리고 -> 브레이크로 멈추고 -> 시동 끔
AICar 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| package chapter09.template;
public class AICar extends Car {
@Override
public void drive() {
System.out.println("자율 주행합니다.");
System.out.println("자동차가 알아서 방향을 전환합니다.");
}
@Override
public void stop() {
System.out.println("스스로 멈춥니다.");
}
}
|
ManualCar 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| package chapter09.template;
public class ManualCar extends Car {
@Override
public void drive() {
System.out.println("사람이 운전합니다.");
System.out.println("사람이 핸들을 조작합니다.");
}
@Override
public void stop() {
System.out.println("브레이크로 정지합니다.");
}
}
|
CarTest 클래스
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
| package chapter09.template;
public class CarTest {
public static void main(String[] args) {
System.out.println("--- 자율 주행하는 자동차 ---");
Car myCar = new AICar();
myCar.run();
System.out.println();
System.out.println("--- 사람이 운전하는 자동차 ---");
Car hisCar = new ManualCar();
hisCar.run();
}
}
// --- 자율 주행하는 자동차 ---
// 시동을 켭니다.
// 자율 주행합니다.
// 자동차가 알아서 방향을 전환합니다.
// 스스로 멈춥니다.
// 시동을 끕니다.
// --- 사람이 운전하는 자동차 ---
// 시동을 켭니다.
// 사람이 운전합니다.
// 사람이 핸들을 조작합니다.
// 브레이크로 정지합니다.
// 시동을 끕니다.
|
# 템플릿 메서드 역할
템플릿 메서드는 메서드 실행 순서와 시나리오를 정의하는 것
시나리오를 정의한 메서드라서 바꾸면 안됨
final
예약어를 사용하면 상속받은 하위 클래스가 메서드를 재정의 할 수 없음
상수를 선언할 때 final
썼잖음 그거
템플릿 메서드는 로직 흐름이 이미 정해져 있는 프레임워크에서 많이 사용하는 기본 구현 방법
# 템플릿 메서드 응용하기
예제 시나리오
Player가 게임을 한다. 게임에서 Player가 가지는 레벨에 따라 할 수 있는 세 가지 기능이 있다. 이 기능은 run(), jump(), turn()이다.
# 클래스 설계하기
Player 클래스와 PlayerLevel 클래스는 포함(HAS-A) 관계
게임에서 모든 플레이어는 레벨이 있으니까, 플레이어가 레벨을 멤버 변수로 갖는 것
모든 레벨에서 공통으로 수행하는 기능은 구현
레벨마다 달라지는 기능은 추상 메서드로 선언만
다이아몬드 표시는 포함 관계를 나타내는 것
Player 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package chapter09.gameLevel;
public class Player {
private PlayerLevel level;
public Player() {
level = new BeginnerLevel();
level.showLevelMessage();
}
public PlayerLevel getLevel() {
return level;
}
public void upgradeLevel(PlayerLevel level) {
this.level = level;
level.showLevelMessage();
}
public void play(int count) {
level.go(count);
}
}
|
Player가 가지는 level 변수 선언
디폴트 생성자로 처음 생성되었을 때 초보자 레벨로 시작하게
레벨 변경 메서드는 현재 자신의 level을 매개변수로 받은 level로 변경
이때, 매개변수 자료형을 모든 레벨로 변환 가능하게 PlayerLevel 형으로 선언
PlayerLevel의 템플릿 메서드 go 호출
PlayerLevel 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package chapter09.gameLevel;
public abstract class PlayerLevel {
public abstract void run();
public abstract void jump();
public abstract void turn();
public abstract void showLevelMessage();
final public void go(int count) {
run();
for(int i = 0; i < count; i++) {
jump();
}
turn();
}
}
|
레벨마다 다른 기능인 run(), jump(), turn(), showLevelMessage()
는 추상 메서드로 선언만
템플릿 메서드인 go()
는 예약어 final
써서 순서 마음대로 못바꾸게
count에 따라 점프 횟수 바뀌니까 반복문으로
BeginnerLevel 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package chapter09.gameLevel;
public class BeginnerLevel extends PlayerLevel {
@Override
public void run() {
System.out.println("천천히 달립니다.");
}
@Override
public void jump() {
System.out.println("Jump 할 줄 모르지롱. 레벨 올려오셈");
}
@Override
public void turn() {
System.out.println("Turn 할 줄 모르지롱. 레벨 올려오셈");
}
@Override
public void showLevelMessage() {
System.out.println("****** 초보자 레벨입니다. ******");
}
}
|
AdvancedLevel 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package chapter09.gameLevel;
public class AdvancedLevel extends PlayerLevel {
@Override
public void run() {
System.out.println("빨리 달립니다.");
}
@Override
public void jump() {
System.out.println("높이 점프합니다.");
}
@Override
public void turn() {
System.out.println("Turn 할 줄 모르지롱. 레벨 올려오셈");
}
@Override
public void showLevelMessage() {
System.out.println("****** 중급자 레벨입니다. ******");
}
}
|
SuperLevel 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package chapter09.gameLevel;
public class SuperLevel extends PlayerLevel {
@Override
public void run() {
System.out.println("엄청 빨리 달립니다.");
}
@Override
public void jump() {
System.out.println("아주 높이 점프합니다.");
}
@Override
public void turn() {
System.out.println("한 바퀴 돕니다.");
}
@Override
public void showLevelMessage() {
System.out.println("****** 고급자 레벨입니다. ******");
}
}
|
MainBoard 클래스
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
| package chapter09.gameLevel;
public class MainBoard {
public static void main(String[] args) {
Player player = new Player();
player.play(1);
AdvancedLevel aLevel = new AdvancedLevel();
player.upgradeLevel(aLevel);
player.play(2);
SuperLevel sLevel = new SuperLevel();
player.upgradeLevel(sLevel);
player.play(3);
}
}
//****** 초보자 레벨입니다. ******
//천천히 달립니다.
//Jump 할 줄 모르지롱. 레벨 올려오셈
//Turn 할 줄 모르지롱. 레벨 올려오셈
//****** 중급자 레벨입니다. ******
//빨리 달립니다.
//높이 점프합니다.
//높이 점프합니다.
//Turn 할 줄 모르지롱. 레벨 올려오셈
//****** 고급자 레벨입니다. ******
//엄청 빨리 달립니다.
//아주 높이 점프합니다.
//아주 높이 점프합니다.
//아주 높이 점프합니다.
//한 바퀴 돕니다.
|
당연히, 이번 실습에서도 다형성이 사용됨을 확인 가능
Player가 가질 수 있는 여러 레벨을 PlayerLevel로 관리함
레벨 변경하는 upgradeLevel() 메서드의 매개변수 자료형도 PlayerLevel
하나의 코드가 다양한 자료형을 대상으로 동작하는 다형성을 활용한 것
# final 예약어
사용 위치 | 설명 |
---|
변수 | final 변수는 상수를 의미 |
메서드 | final 메서드는 하위 클래스에서 재정의 불가 |
클래스 | final 클래스는 상속할 수 없음 |
# final 변수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package chapter09.finalex;
public class Constant {
int num = 10;
final int NUM = 100;
public static void main(String[] args) {
Constant cons = new Constant();
cons.num = 50;
cons.NUM = 200;
System.out.println(cons.num);
System.out.println(cons.NUM);
}
}
// 오류
|
여러 자바 파일에서 공유하는 상수 값 정의
하나의 자바 파일에서만 사용하는 상수 값은 그냥 파일 안에서 final 정의하고 쓰면 됨
ex) 최솟값(MIN), 최댓값(MAX), 과목 코드 값 등 이런 값들은 여러 파일에서 공유하면서 써야하는 상수
여러 파일에서 공유하는 상수 값은 한 파일에 모아 public static final
로 선언하여 사용하면 좋음
1
2
3
4
5
6
7
8
9
10
| package chapter09.finalex;
public class Define {
public static final int MIN = 1;
public static final int MAX = 99999;
public static final int ENG = 1001;
public static final int MATH = 2001;
public static final double PI = 3.14;
public static final String GOOD_MORNING = "Good Morning!";
}
|
1
2
3
4
5
6
7
8
9
10
11
| package chapter09.finalex;
public class UsingDefine {
public static void main(String[] args) {
System.out.println(Define.GOOD_MORNING);
System.out.println("최솟값은 " + Define.MIN + "입니다.");
System.out.println("최댓값은 " + Define.MAX + "입니다.");
System.out.println("수학 과목 코드 값은 " + Define.MATH + "입니다.");
System.out.println("영어 과목 코드 값은 " + Define.ENG + "입니다.");
}
}
|
# final 메서드
final 메서드는 위에서 배웠던 하위 클래스에서 재정의 불가능한 템플릿 메서드 참고
# final 클래스
클래스를 final로 선언하면 상속 불가능.
상속하면 변수나 메서드 재정의할 수 있게되니까 그러면 클래스가 가지고 있는 기능에 오류 생길 수 있음
보안과 관련, 기반 클래스가 변하면 안되는 경우 => final로 선언
대표적으로 JDK에서 제공하는 클래스인 문자열을 나타내는 String 클래스
, 정수 값을 나타내는 Integer 클래스
가 final로 선언되었음.
# 프로그램을 잘 구현하는 또 다른 방법
위에서는 MainBoard 클래스 -> 각 레벨 클래스 -> PlayerLevel 클래스 -> Player 클래스 순으로 개발했다. 이제는 반대로 MainBoard 클래스부터 구현해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| package chapter09.gameLevel;
public class MainBoard {
public static void main(String[] args) {
Player player = new Player(); // 오류 발생
player.play(1);
AdvancedLevel aLevel = new AdvancedLevel();
player.upgradeLevel(aLevel); // 오류 발생
player.play(2);
SuperLevel sLevel = new SuperLevel();
player.upgradeLevel(sLevel); // 오류 발생
player.play(3);
}
}
|
이렇게 테스트 코드를 먼저 개발하는 방법론을 테스트 주도 개발(Test Driven Development; TDD) 라고 한다. 테스트 코드를 만들 수 있다는 것은 이미 구현 코드가 머릿속에 있다는 의미이기도 하다.