티스토리 뷰
1. 추상클래스란 ?
클래스를 설계도에 비유한다면, 추상클래스는 미완성 설계도에 비유할 수 있다.
클래스가 미완성이라는 것은 멤버의 개수에 관계된 것이 아니라 미완성 메서드(추상메서드)를 포함하고 있다는 의미이다. 추상클래스는 인스턴스를 생성할 수 없으며, 상속을 통해 자손클래스에 의해서만 완성될 수 있다.
한마디로 일반클래스에서 추상메서드를 포함한 클래스를 추상클래스로 부른다.
추상클래스는 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로서의 중요한 의미를 갖는다.
아무것도 없는 상태에서 새로운 클래스를 만드는 것보다는 어느 정도 틀을 갖춘 상태에서 시작하는 것이 나을 것이다.
2. 추상 클래스 작성 방법
추상클래스는 abstract를 붙이기만 하면 된다. 이렇게 함으로써 이 클래스를 사용하려고 할 때, abstract를 보고 이 클래스에는 추상메서드가 있으니 상속을 통해 구현해줘야 한다는 것을 쉽게 알 수 있다.
absctract class 클래스이름 {
...
}
추상메서드를 포함하고 있다는 것을 제외하면 일반클래스와 전혀 다르지 않다.
그렇다면 추상메서드를 알아보자.
3. 추상메서드
메서드는 선언부와 구현부로 구성되어 있다고 했는데, 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨둔 것이 추상메서드이다.
왜 구현부는 작성하지 않을까 ?
메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문이다. 보통 조상 클래스에서는 선언부만 작성하고 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려준다. 그러면 실제 내용은 상속받는 클래스에서 구현하는 것이다.
아래와 같이 추상클래스로부터 상속받은 자손클래스는 오버라이딩을 통해 추상클래스의 추상메서드를 모두 구현해주어야 한다. 만일 상속받은 추상메서드 중 하나라도 구현하지 않는다면 자손클래스 역시 추상클래스로 지정해 주어야 한다.
void play(), void stop()메서드 구현은 생략하겠다.
abstract class Player {
abstract void play();
abstract void stop();
}
//추상메서드를 다 구현함
class AudioPlayer extends Player {
void play() {}
void stop() {}
}
//추상메서드를 다 구현하지 않음
abstract class AbstractPlayer extends Player {
void play() {}
}
실제 작업내용인 구현부가 없는 메서드가 무슨 의미가 있을까 ?
있다. 오히려 메서드를 작성할 때 실제 작업내용인 구현부보다 더 중요한 부분이 선언부이다.
메서드의 이름과 메서드의 작업에 필요한 매개변수, 그 결과로 어떤 타입의 값을 반환할 것인가를 결정하는 것은 게임 닉네임을 정할 때 보다 어려운 일이다. 그래서 선언부만 작성해도 메서드의 절반 이상이 완성된 것이라 해도 과언이 아니다.
4. 추상클래스의 작성
그럼 무슨 상황일 때 추상클래스를 사용할까 ?
추상클래스는 기존에 있는 클래스들의 공통부분을 뽑아내서 추상클래스로 만드는 것이다.
한번 예를 들어보겠다.
class Marine {
int x, y; //현재 위치
void move(int x, int y) { /*지정된 위치로 이동*/ }
void stop() { /*현재 위치에 정지*/ }
void stimPack() { /*스팀팩을 사용한다.*/ }
}
class Tank {
int x, y; //현재 위치
void move(int x, int y) { /*지정된 위치로 이동*/ }
void stop() { /*현재 위치에 정지*/ }
void changeMode() { /*공격모드를 변환한다.*/ }
}
class Dropship {
int x, y; //현재 위치
void move(int x, int y) { /*지정된 위치로 이동*/ }
void stop() { /*현재 위치에 정지*/ }
void load() { /*선택된 대상을 태운다.*/ }
void unload() { /*선택된 대상을 내린다.*/ }
}
게임에 나오는 유닛들을 클래스로 간단히 정의해보았다. 이 유닛들은 각자 기능을 가지고 있지만 공통부분을 뽑아내어 하나의 클래스로 만들고 상속받도록 변경해보자.
abstract class Unit {
int x, y;
abstract void move(int x, int y);
void stop { */현재 위치에 정지*/ }
}
class Marine extends Unit {
void move(int x, int y) { /*지정된 위치로 이동*/ }
void stimPack() { /*스팀팩을 사용한다.*/ }
}
class Tank extends Unit {
void move(int x, int y) { /*지정된 위치로 이동*/ }
void changeMode() { /*공격모드를 변환한다.*/ }
}
class Dropship extends Unit {
void move(int x, int y) { /*지정된 위치로 이동*/ }
void load() { /*선택된 대상을 태운다.*/ }
void unload() { /*선택된 대상을 내린다.*/ }
}
이 클래스에 대해서 stop메서드는 모두 공통적이지만, Marine, Tank는 지상유닛이고 Dropship은 공중유닛이기 때문에 이동하는 방법이 서로 달라서 move메서드의 실제 구현 내용이 다를 것이다.
그래도 move메서드 선언부는 같기 때문에 추상메서드로 정의할 수 있다.
5. 인터페이스란 ?
인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높다. 뭔 말이냐면 일반 메서드 또는 멤버변수를 가질 수 없고, 추상메서드와 상수만을 멤버로 가질 수 있다.
6. 인터페이스 작성
인터페이스를 작성할 때 클래스를 작성하는 것과 같지만 class 대신 interface를 사용한다는 것만 다르다.
interface 인터페이스이름 {
public static final 타입 이름 = 값;
public abstract 메서드이름(매개변수목록);
}
일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있다.
모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
원래는 인터페이스의 모든 메서드는 추상메서드이어야 하는데, JDK1.8부터 인터페이스에 static메서드와 디폴트 메서드를 추가하는 것으로 변경되었다.
7. 인터페이스의 상속
인터페이스는 인터페이스로부터만 상속받을 수 있으며 클래스와 달리 다중상속이 가능하다.
하지만 다중상속은 거의 쓰지 않는다고 ?
만약 두 조상으로 상속받는 멤버 중에 멤버변수의 이름이 같거나 메서드의 선언부가 일치하면 두 조상으로부터 상속받은 자손클래스는 어느 조상의 것을 상속받게 되는 것인지 알수 없다. 그래서 다중상속은 장점도 있지만 단점이 더 크다고 판단한 자바에서는 다중상속을 허용하지 않는다. 하지만 다중상속의 장점도 있기 때문에 그에 대한 대응으로 '자바도 인터페이스를 이용하면 다중상속이 가능하다.'라고 하는 것일 뿐 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다. 이러한 이유로 인터페이스가 다중상속을 위한 것으로 오해를 하는데 절대 아니다.
8. 인터페이스의 구현
구현하는 방법은 추상클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다. 다만 클래스는 확장한다는 의미의 키워드 'extends'를 사용하지만 인터페이스는 구현한다는 의미의 키워드 'implements'를 사용할 뿐이다.
interface boxer {
void move();
void attack();
}
class infighter implements boxer {
public void move() { /*내용 생략*/ }
public void attack() { /*내용 생략*/ }
}
9. 인터페이스를 이용한 다형성
다형성에 대해 학습할 때 자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능하다는 것을 배웠다.
인터페이스 역시 인터페이스 타입의 참조변수로 이를 구현한 인스턴스를 참조할 수 있다.
인터페이스 Fightable을 클래스 Fighter가 구현했을 때, 아래와 같이 하는 것이 가능하다.
Fightable f = new Fighter();
10. 인터페이스의 장점
인터페이스를 사용하는 이유와 그 장점을 정리해보면 다음과 같다.
개발시간을 단축시킬 수 있다.
표준화가 가능하다.
서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
10-1. 개발시간을 단축할 수 있다.
메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.
그리고 동시에 한쪽에서는 인터페이스 구현을, 한쪽은 상속받는 클래스를 구현하게 되면 양쪽에서 동시에 개발을 진행할 수 있다.
10-2. 표준화가 가능하다.
프로젝트에 사용되는 기본틀을 인터페이스로 작성한 다음 그 인터페이스를 토대로 구현하게 되면 보다 일관되고 정형화된 프로그램 개발이 가능하다.
10-3. 독립적인 프로그래밍이 가능하다.
클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적으로 바뀌면 다른 클래스가 변경되도 남은 클래스에는 영향을 미치지 않는다.
7. 인터페이스의 이해
지금까지 인터페이스의 특징과 구현하는 법, 장점등 일반적인 사항들에 대해서 모두 살펴보았다. 하지만 '그래서 인터페이스가 도대체 뭔데?'라는 의문은 여전히 남아있을 것이다. 그래서 인터페이스의 규칙이나 활용이 아닌, 본질적인 측면에 대해 살펴보자.
먼저 인터페이스를 이해하기 위해서는 다음의 두 가지 사항을 알고 있어야 한다.
클래스를 사용하는 쪽과 클래스를 제공하는 쪽이 있다.
메서드를 사용하는 쪽에서는 사용하려는 메서드의 선언부만 알면 된다.(내용은 몰라도 된다.)
이 두 가지 사항에 대한 예제가 있는데 여기에 풀어쓰려니 너무 길고 이해하기 어려울 것 같다
대신 내가 직접 떠올린 예제를 가져왔다. 내가 이해하고 떠올린 다른 예제를 가지고 설명함으로써 내 실력도 한층 성장할 수 있을 것 같다.
바로 스프링에서 사용하는 JpaRepository의 save(), findOne(), findAll(), delete()메서드 이다.
실제 JpaRepository의 CRUD 관계가 아닌, 내가 인터페이스 이해에 대해 배우면서 생각해낸 것이기 때문에 실제로 동작하는 관계보다 인터페이스를 이해하는데 중심으로 두고 봤으면 좋겠다.
(실제로 save()메서드는 엔티티를 매개변수로 받지만, 여기선 엔티티를 영속성 컨텍스트에 저장한다는 개념을 지우고 그냥 저장한다라는 의미만 가져왔다.)
interface JpaRepository {
public abstract void save();
}
class A {
public void methodA(JpaRepository i) {
i.save();
}
}
class B implements JpaRepository {
public void save() {
em.persist();
}
}
클래스 A는 클래스 B의 메서드를 호출하지만, 클래스 A는 인터페이스 JpaRepository하고만 직접적인 관계에 있기 때문에 클래스 B의 변경에 영향을 받지 않는다.
클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다. 존재하지 않아도 되는 것이 아닌, 그만큼 B의 영향을 받지 않는다는 얘기이다. 클래스 A는 오직 직접적인 관계에 있는 인터페이스 JpaRepository의 영향만 받는다.
인터페이스 JpaRepository는 실제구현 내용(클래스 B)을 감싸고 있는 껍데기이며, 클래스 A는 껍데기 안에 어떤 알맹이(클래스)가 들어 있는지 몰라도 된다.
실제로 이런식으로 구현되어있는 것은 클래스 Thread의 생성자인 Thread(Runnable target)이 이런 방식으로 되어 있다.
(Runnable은 인터페이스이다.)
'Java' 카테고리의 다른 글
null 대신 Optional 클래스를 사용하자 (0) | 2022.07.06 |
---|---|
예외처리 개념과 예외처리를 할 때 출력만 하면 안되는 이유 (0) | 2022.05.01 |
객체지향에서 중요한 개념인 다형성 (0) | 2022.04.30 |
개발할 때 많이 쓰는 오버로딩 개념과 사용법 (0) | 2022.04.29 |
객체지향을 처음 들어보는 분들을 위한 정리 (0) | 2022.04.13 |
- Total
- Today
- Yesterday