※ 본문은 혼자 공부한 내용을 기록한 글입니다. 오개념이 있다면 댓글로 알려주세요!
[ 1 ] 추상 클래스(abstract class)란?
abstract는 클래스나 메소드를 사용하려면 반드시 상속해서 사용하도록 강제하는 역할을 한다. 즉, abstract class 내부에 abstract method가 선언되어 있다면, 이를 상속받는 서브 클래스에서 abstract method를 완성하고 사용해야 한다.
"서브 클래스에서 완성하고 사용해야 한다"라는 말이 와닿지 않을 수 있다. 아래의 코드를 보자.
abstract class A {
public abstract void a();
}
class B extends A {
public void a() {
System.out.println("추상 클래스 A의 추상 메소드 a를 완성했습니다.");
}
}
추상 클래스 A의 추상 메소드 a를 보면 구현부 { } 가 없다. 즉, abstract라는 예약어를 통해 해당 메소드는 상속 후 서브 클래스에서 Overriding으로 완성(구현)해야 함을 알려주고 있는 것이다. 만약, 클래스 B가 추상 메소드 a를 구현하지 않으면 에러가 발생한다.
추상 클래스 내부에는 반드시 추상 메소드만 있어야 하는 것은 아니다. 추상 메소드가 없어도 추상 클래스를 선언할 수 있고, 추상 클래스 내부에는 필드, 생성자, 구현된 메소드도 존재할 수 있다.
다음은 추상 메소드 및 추상 클래스의 규칙을 정리해 보았다.
(1) 클래스 내부에 추상 메소드가 없어도 추상 클래스로 선언할 수 있다. 하지만, 클래스 내부에 추상 메소드가 하나라도 존재한다면 해당 클래스는 추상 클래스로 선언해야 한다. |
(2) 추상 클래스 내부에는 추상 메소드 뿐만 아니라 필드, 생성자, 구현된 메소드도 존재할 수 있다. 다만, 추상 메소드는 서브 클래스에서 반드시 구현한 후 사용할 수 있다. 구현된 메소드는 일반적인 클래스처럼 그대로 사용해도 되고, Overriding해도 된다. |
(3) 만약, 추상 클래스가 추상 클래스를 상속한다면 추상 메소드를 구현하지 않아도 된다. 단, 일반적인 클래스가 추상 클래스를 상속한다면 모든 추상 메소드를 구현해야 한다. |
(4) 추상 클래스의 인스턴스를 생성할 수 없다. |
(5) 다형성에 따라서 타입이 추상 클래스인 참조 변수로 추상 클래스를 상속받은 일반 클래스의 인스턴스를 참조하는 것은 가능하다. 즉, 위 소스코드에서 A a = new B(); 가 가능하다는 의미이다. |
[ 2 ] 인터페이스(interface)란?
어떤 객체가 있고 그 객체가 특정한 인터페이스를 사용한다면 그 객체는 반드시 인터페이스의 메소드들을 구현해야 한다(생활코딩). 이 내용만 보면 추상 클래스와 인터페이스의 역할에 큰 차이가 없어 보인다. 하지만, 인터페이스는 추상 클래스보다 더 많은 규제를 가지고 있다.
인터페이스는 "상수 필드"와 "추상 메소드"만 가질 수 있다. (단, Java8 이후 default 메소드와 static 메소드가 추가되었다)
- 인터페이스 내부에 필드 선언 시 명시적으로 작성하지 않아도 Compile Time에 자동으로 필드가 public static final로 선언된다.
- 인터페이스는 default 메소드와 static 메소드를 제외하면 "추상 메소드"만 가질 수 있기 때문에 default, static 메소드로 선언하지 않은 메소드는 Compile Time에 자동으로 public abstract로 선언된다.
interface Calculator {
public static final double PI = 3.14;
public abstract void add();
void mul(); // public abstract void mul() 과 같다.
}
인터페이스 선언은 위 소스코드처럼 interface + 인터페이스명 {} 으로 할 수 있다.
인터페이스 내부에는 상수 필드만 올 수 있으므로 PI라는 변수를 선언과 동시에 초기화했고, add() 와 mul()이라는 메소드를 선언했다. 위에서 언급했듯이 default, static 메소드로 선언하지 않은 메소드는 자동으로 public abstract로 선언되기 때문에 add()와 mul()은 모두 추상 메소드이다.
class Cal implements Calculator {
public int left;
public int right;
public void setOperands(int left, int right)
{
this.left = left;
this.right = right;
}
public void add()
{
System.out.println(this.left + this.right);
}
public void mul()
{
System.out.println(this.left * this.right);
}
public void circleArea(int r)
{
System.out.println(r * r * Calculator.PI);
}
}
클래스로 인터페이스를 구현할 때에는 implements라는 키워드를 사용한다. 클래스 내부에는 당연히 필드, 생성자, 구현된 메소드가 올 수 있지만, 반드시 내가 구현할 인터페이스의 추상 메소드를 구현해야 한다. 만약, 위 소스코드에서 Cal 클래스가 add()와 mul()을 구현하지 않는다면 에러가 발생한다.
Calculator 인터페이스 내부의 PI라는 상수는 클래스 멤버처럼 Calculator.PI로 접근하여 사용할 수 있다.
인터페이스도 인스턴스를 생성할 수 없으나 다형성에 따라서 타입이 인터페이스인 참조 변수로 인터페이스를 구현한 일반 클래스의 인스턴스를 참조하는 것은 가능하다.
즉, 위 소스코드에서 Calculator c = new Cal(); 가 가능하다는 의미이다. 단, 이러한 경우 인스턴스 c는 클래스 Cal의 모든 메소드에는 접근할 수 없는데, 이는 추후에 포스팅할 주제인 "다형성"에서 다루도록 하겠다.
[ 3 ] 왜 추상 클래스와 인터페이스를 사용할까?
이 의문에 대한 답을 찾기 위해 굉장히 많은 글들을 읽어보았는데, 대표적인 이유로는
(1) 추상 클래스
- 상속을 강제하기 위해서
- 상속받은 추상 클래스의 추상 메소드를 구현하고, 기능을 확장하기 위해서
(2) 인터페이스
- 추상 메소드의 구현을 강제하기 위해서
- 설계도의 역할(구현해야 할 추상 메소드들이 이미 정해져 있으므로)을 수행하며 협업 및 동시개발에 필수적이어서
등이 있었다. 하지만, 협업이란 것을 경험하지 못한 나에게 위의 답은 잘 와닿지 않았고, 스타크래프트 예제를 통해 추상 클래스와 인터페이스의 용도를 구분한 글이 더 명료하게 느껴져 이를 정리하려고 한다.
우선, Java는 다중 상속을 허용하지 않는다. 즉, 하나의 클래스는 하나의 클래스만 상속받을 수 있다. 이와 달리, 인터페이스는 다중 상속이 허용되며 하나의 클래스는 여러개의 인터페이스를 구현할 수 있다. 다중상속 가능의 여부가 추상 클래스와 인터페이스의 가장 큰 차이라고 생각한다.
추상 클래스는 다중 상속이 불가능하고, 상속을 강제하기 때문에 "명확한 계층구조"를 표현하는데 효과적이다. 예를 들어, 스타크래프트의 테란에는 scv, 마린, 탱크, 드랍쉽 등 많은 유닛이 존재한다. 우리는 이 유닛들을 '지상 유닛'과 '공중 유닛'으로 명확하게 분류할 수 있다.
지상 유닛과 공중 유닛으로 분류 후 scv, 마린, 탱크는 지상 유닛으로, 드랍쉽은 공중 유닛으로 분류할 수 있을 것이다.
이를 추상 클래스로 만들어보자.
abstract class Unit {
int MAX_HP;
int Present_HP;
public Unit(int MAX_HP) {
this.MAX_HP = MAX_HP;
}
public abstract void move();
}
우선, 모든 유닛이 공통적으로 가지는 최대 체력(Max_HP), 현재 체력(Present_HP) 필드를 선언했고, 모든 유닛은 움직일 수 있기에 각 유닛이 어느 정도의 속도로 이동하는지 나타낼 수 있는 move() 추상 메소드를 선언했다.
abstract class GroundUnit extends Unit {
public GroundUnit(int MAX_HP)
{
super(MAX_HP);
}
}
abstract class AirUnit extends Unit {
public AirUnit(int MAX_HP)
{
super(MAX_HP);
}
}
다음은 Unit을 상속받은 GroundUnit과 AirUnit 추상 클래스이다. 추상 클래스 규칙 "(3) 만약, 추상 클래스가 추상 클래스를 상속한다면 추상 메소드를 구현하지 않아도 된다." 으로 인해 move() 메소드를 구현하지 않아도 된다.
class Tank extends GroundUnit {
public Tank(int Present_HP) {
super(150);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("Tank는 10의 속도로 이동합니다.");
}
public String toString() {
return "Tank";
}
}
class Marine extends GroundUnit {
public Marine(int Present_HP) {
super(40);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("Marine은 5의 속도로 이동합니다.");
}
public String toString() {
return "Marine";
}
}
class DropShip extends AirUnit {
public DropShip(int Present_HP) {
super(150);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("DropShip은 15의 속도로 이동합니다.");
}
public String toString() {
return "DropShip";
}
}
그 후 Tank, Marine, DropShip 클래스를 선언한다. 이는 일반 클래스이므로 Unit의 move() 추상 메소드를 구현해야 한다.
SVC 클래스를 정의하기 전에, 테란의 SCV는 다른 기계 유닛(탱크, 드랍쉽 등)을 수리할 수 있는 기능이 있으므로 이를 repair() 메소드로 구현해보자.
class SCV extends GroundUnit {
...
... // 생성자 정의 및 move() 구현
...
public void repair(Tank tank) { ... }
public void repair(DropShip dropship) { ... }
}
로 구현할 수 있을 것이다. 하지만, 실제로 SCV가 수리할 수 있는 유닛은 예제에 나타난 탱크, 드랍쉽 뿐만 아닌 많은 유닛이 있기 때문에 위 방법은 비효율적이다. 이를 해결하기 위해 우리는
(1) 기계 유닛을 나타내는 추상 클래스를 새로 선언한다.
→ Tank는 이미 GroundUnit을, Dropship은 이미 AirUnit을 상속받았기에 기계 유닛 추상 클래스를 선언하더라도 상속받을 수 없다.
(2) 이미 선언된 GroundUnit과 AirUnit을 활용한다.
→ GroundUnit에는 수리할 수 있는 기계 유닛이 아닌 Marine이 포함되어 있기에 옳은 방법이 아니다.
등을 떠올릴 수 있지만, 모두 효과적이지 않다.
이때 우리는 인터페이스를 활용할 수 있다. 위에서 언급했듯이 인터페이스는 다중 상속, 다중 구현이 가능하기 때문에 한 객체가 "수행할 수 있는 기능들"을 표현하는데 효과적이다.
예를 들어, 탱크 유닛의 경우 SCV에게 수리받을 수 있고, 다른 유닛을 공격할 수도 있는 2개의 기능을 가지고 있다. 이를 인터페이스로 구현해보자.
interface Repairable {
}
interface Attack {
}
class Tank extends GroundUnit implements Repairable, Attack {
....
....
....
}
수리받을 수 있는 기능은 Repairable 인터페이스로, 공격 기능은 Attack 인터페이스로 선언하여 Tank 클래스가 두 인터페이스를 implements 하게 한다. 이를 통해 여러 기능을 가진 한 객체를 만들어낼 수 있다.
그렇다면 SCV의 repair() 메소드를 Repairable 인터페이스를 활용하여 다시 구현해보자.
우선, SCV가 수리할 수 있는 클래스가 Repairable 인터페이스를 구현하도록 한다.
class Tank extends GroundUnit implements Repairable {
}
class Marine extends GroundUnit {
// Marine은 수리받을 수 없다.
}
class DropShip extends AirUnit implements Repairable {
}
그 후 repair() 메소드를 완성하자.
class SCV extends GroundUnit implements Repairable {
// SCV도 수리받을 수 있다.
void repair(Repairable r) {
if(r instanceof Unit) {
Unit unit = (Unit)r;
while(unit.Present_HP != unit.MAX_HP) {
unit.Present_HP++;
}
System.out.println(unit.toString() + "의 수리가 완료되었습니다.");
}
}
}
(1) 위에서 언급했듯이 타입이 인터페이스인 참조 변수로 인터페이스를 구현한 일반 클래스의 인스턴스를 참조하는 것이 가능하므로 타입이 Repairable인 매개변수 r 이 Repairable을 구현한 Tank, DropShip, SCV 인스턴스를 받을 수 있다. 즉, 인터페이스를 활용하여 코드를 간결하게 작성할 수 있다.
(2) Repairable r 에는 Unit에 대한 정보가 저장되어 있지 않기 때문에 r 이 Unit을 상속받았다면 Unit으로 형변환을 해준다.
(3) 수리를 진행한다.
전체 소스코드는 다음과 같다.
interface Repairable {
}
abstract class Unit {
int MAX_HP;
int Present_HP;
public Unit(int MAX_HP) {
this.MAX_HP = MAX_HP;
}
public abstract void move();
}
abstract class GroundUnit extends Unit {
public GroundUnit(int MAX_HP)
{
super(MAX_HP);
}
}
abstract class AirUnit extends Unit {
public AirUnit(int MAX_HP)
{
super(MAX_HP);
}
}
class Tank extends GroundUnit implements Repairable {
public Tank(int Present_HP) {
super(150);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("Tank는 10의 속도로 이동합니다.");
}
public String toString() {
return "Tank";
}
}
class Marine extends GroundUnit {
public Marine(int Present_HP) {
super(40);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("Marine은 5의 속도로 이동합니다.");
}
public String toString() {
return "Marine";
}
}
class DropShip extends AirUnit implements Repairable {
public DropShip(int Present_HP) {
super(150);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("DropShip은 15의 속도로 이동합니다.");
}
public String toString() {
return "DropShip";
}
}
class SCV extends GroundUnit implements Repairable {
public SCV(int Present_HP) {
super(50);
this.Present_HP = Present_HP;
}
public void move() {
System.out.println("scv는 5의 속도로 이동합니다.");
}
public String toString() {
return "SCV";
}
void repair(Repairable r) {
if(r instanceof Unit) {
Unit unit = (Unit)r;
while(unit.Present_HP != unit.MAX_HP) {
unit.Present_HP++;
}
System.out.println(unit.toString() + "의 수리가 완료되었습니다.");
}
}
}
public class Main {
public static void main(String[] args) {
SCV scv1 = new SCV(50);
SCV scv2 = new SCV(40);
Tank tank1 = new Tank(100);
DropShip dropship1 = new DropShip(100);
Marine marine1 = new Marine(30);
scv1.repair(scv2);
scv1.repair(tank1);
scv1.repair(dropship1);
scv1.repair(marine1); // Marine은 수리할 수 없으므로 에러 발생
}
}
main 함수 실행 결과는 다음과 같다.
본문에는 오개념이 있을 수 있습니다. 오개념이 있다면 댓글로 꼭 알려주시길 바랍니다!
아래의 참고자료에 더 많은 내용이 포함되어 있습니다.
[ 참고자료 ]
https://myjamong.tistory.com/150
[JAVA] 추상클래스 VS 인터페이스 왜 사용할까? 차이점, 예제로 확인 :: 마이자몽
추상클래스 인터페이스 왜... 사용할까? 우리는 추상클래스와 인터페이스에 대해서 알고 있냐고 누가 물어본다면 알고 있다고 대답을 하고있습니다. 그런데 이론적인 내용 말고 정작 "왜 사용하
myjamong.tistory.com
https://doublesprogramming.tistory.com/157
자바 - 인터페이스(Interface)
7_interface.md 본 내용은 자바의 정석 3rd Edition을 참고하여 작성되었습니다. 개인적으로 학습한 내용을 복습하기 목적이기 때문에 내용상 오류가 있을 수 있습니다. 1. 인터페이스(interface) 인터페이
doublesprogramming.tistory.com
https://interconnection.tistory.com/129
자바 인터페이스(Java Interface)는 무엇인가?
개요 "자바 인터페이스(Java Interface)는 무엇인가?" 이런 궁금점을 가지고 있는 Java Programmer가 많습니다. 저는 "객체 지향 개발 5대 원칙 - SOLID"을 만족시켜줄 수 있어서라고 생각합니다. 그러면 "왜
interconnection.tistory.com
https://www.youtube.com/watch?v=Yv5Uw_vS3Uo&t=3s
'Programming > Java' 카테고리의 다른 글
[Java] 타입 변환 (type conversion) - 자동 타입 변환, 강제 타입 변환 (0) | 2022.08.18 |
---|---|
[Java] 다형성(Polymorphism) (0) | 2022.08.16 |
[Java] 패키지(Package) (0) | 2022.08.05 |
[Java] Overriding, Overloading (0) | 2022.08.04 |
[Java] 생성자 및 상속에서의 생성자 (0) | 2022.08.02 |