안녕하세요. 명월입니다.
이 글은 Java에서 추상 클래스(abstract) 그리고 상속 금지(final)에 대한 글입니다.
제가 이전에 클래스 상속과 인터페이스(Interface)에 대해서 설명한 적이 있습니다.
링크 - [Java] 08. 클래스의 상속과 this, super 키워드 사용법
링크 - [Java] 12. 인터페이스(Interface)
이 추상 클래스는 인터페이스와 클래스의 중간 정도의 위치라고 생각하면 쉽습니다.
먼저 인터페이스는 클래스 할당(new)이 불가능합니다. 이유는 인터페이스 안에는 함수의 정의만 되어 있고 실제 실행 코드는 작성하지 않은 단계입니다. 그러나 인터페이스는 다중 상속을 가능합니다.
클래스는 맴버 변수와 함수의 실행 영역까지 작성되어 있습니다. 그러기 때문에 클래스 할당, 즉 인스턴스 생성이 가능합니다.
이 추상 클래스는 기본적인 형태는 클래스와 동일합니다. 그러나 클래스를 생성할 때, 직접 사용하는 것이 아니고 상속을 생각해서 클래스를 만드는 경우도 있습니다. 즉, 인터페이스 처럼 실행 영역은 상속하는 클래스에 맡기고 정의만 선언하고 작성할 수 있습니다. 실행 영역이 없는 함수가 존재하기 때문에 역시 클래스 할당은 불가능합니다. 또, 추상 클래스는 실행 영역이 작성되기 때문에 다중 상속이 불가능합니다.
추상 클래스의 상속은 extends를 사용합니다.
import java.util.ArrayList;
import java.util.List;
// 추상 클래스 AbstractClass
abstract class AbstractClass {
// 함수 설정
public void run() {
// print()함수를 호출한다.
print();
}
// print()는 함수명만 정의하고 실제 실행 영역은 상속받는 클래스에 맡긴다.
protected abstract void print();
}
// AbstractClass를 상속받았다.
class AClass extends AbstractClass {
// AbstractClass클래스의 print 함수는 함수명만 정의되어 있기 때문에 재정의를 해야한다.
@Override
protected void print() {
// 콘솔 출력
System.out.println("AClass print!");
}
}
//AbstractClass를 상속받았다.
class BClass extends AbstractClass {
// AbstractClass클래스의 print 함수는 함수명만 정의되어 있기 때문에 재정의를 해야한다.
@Override
protected void print() {
// 콘솔 출력
System.out.println("BClass print!");
}
}
// 실행 클래스
public class Example {
// 실행 함수
public static void main(String... args) {
// 리스트 선언
List<AbstractClass> list = new ArrayList<>();
// AClass 할당
list.add(new AClass());
// BClass 할당
list.add(new BClass());
// 리스트에서 클래스를 취득한다.
for (int i = 0; i < list.size(); i++) {
AbstractClass clz = list.get(i);
// run 함수 실행
clz.run();
}
}
}
위 소스를 보시면 AbstractClass에 print를 정의만 하고 실행 영역은 구현하지 않았습니다.
AbstractClass를 상속 받은 AClass나 BClass클래스에서 print 함수를 재정의됩니다.
위 소스만 봐도 클래스의 속성을 가지고 있으면서 인터페이스의 속성도 가지고 있는 것처럼 보입니다.
그러면 추상 클래스는 다중 상속도 되지 않으면서 하나의 완전체의 클래스를 작성하지 않았습니다. 이것만 보면, 굳이 추상 클래스를 작성할까 하는 의문이 생깁니다.
List에서 AbstractClass로 AClass와 BClass를 하나로 묶어서 사용했습니다만, 이건 인터페이스만으로도 충분히 구현이 가능한 영역이니 굳이 추상 클래스로 할 필요는 없을 듯 싶습니다.
하지만 이 추상 클래스는 오히려 실무에서 인터페이스보다 더 자주 사용됩니다. 캡슐화의 특성 때문입니다.
위 예제만 보더라도 main함수에서는 AClass의 print를 직접 호출하지 않습니다. AbstractClass 클래스의 run함수를 통해서 호출됩니다. 오히려 main함수에서는 접근제한자때문에 print()함수를 접근할 수 없습니다.
// 추상 클래스 AbstractClass
abstract class AbstractClass {
// 맴버 변수
private int data;
// 실행 함수
public void run() {
// init 함수를 호출하여 데이터를 받는다.
this.data = init();
// 맴버 변수에 10을 곱한다.
this.data = this.data * 10;
// execute 함수를 호출하여 데이터를 계산하여 받는다.
this.data = execute(this.data);
// 맴버 변수에 10을 곱한다.
this.data = this.data * 10;
// print 함수를 호출한다.
print(this.data);
}
// 추상 메소드 정의
protected abstract int init();
protected abstract int execute(int data);
protected abstract void print(int data);
}
// AbstractClass를 상속받았다.
class AClass extends AbstractClass {
// init 함수를 재정의 한다.
@Override
protected int init() {
// 1의 값을 리턴한다.
return 1;
}
// execute 함수를 재정의 한다.
@Override
protected int execute(int data) {
// 데이터를 5로 나눈 값을 리턴한다.
return data / 5;
}
// print 함수를 재정의 한다.
@Override
protected void print(int data) {
// 데이터를 출력한다.
System.out.println("Data - " + data);
}
}
// 실행 클래스
public class Example {
// 실행 함수
public static void main(String... args) {
// AClass 클래스를 선언
AClass clz = new AClass();
// run 함수를 실행
clz.run();
}
}
위처럼 AbstractClass를 캡슐화하여 클래스를 정의했습니다. 특히, data의 멤버 변수는 private로 설정되어 있기 때문에 상속받은 클래스나 main에서는 접근이 불가능합니다.
그러나 이 클래스를 완성하기 위해서는 이 data 변수의 대한 초기 정의와 중간 실행에 대한 정의가 필요할 때가 있습니다. 그러니깐 미완성 클래스입니다.
그렇다고 멤버 변수를 public이나 protected로 바꾸기에는 캡슐화의 특성이 사라지고 유저가 어떻게 사용할 지 알수가 없기 때문에 클래스 입장에서는 멤버 변수의 접근을 열 수는 없습니다.
여기서 해결책은 사용하는 유저로부터 AbstractClass를 사용하기 위해서는 상속하여 재정의를 받고 사용하게 하는 것이 가장 좋은 방법입니다.
실무에서 가장 가까운 예제로는 Database와 관련된 클래스입니다. Database와 관련된 클래스는 데이터 베이스에 접속해서 데이터를 검색하거나 데이터를 저장하기 위한 일련의 프로시져(실행 절차)가 있지만 초기에 Database 접속 정보가 없으면 실행할 수 없기 때문에 이렇게 추상 클래스로 정의해서 데이터 접속 정보를 받는 경우도 있습니다.
추상 클래스를 사용하기 위해서는 클래스 앞에 abstract가 있어야 합니다. 그리고 내부 함수에는 하나 이상의 abstract 함수가 필요합니다.(없어도 abstract 클래스를 작성하는데는 에러는 발생하지 않습니다만, 의미가 없어집니다.)
이렇게 상속된 AClass를 더이상 상속을 못하게 막을 수도 있습니다. 클래스의 앞에 final 키워드를 사용하면 됩니다.
위 이미지를 보시면 BClass는 AClass를 상속받으려고 하는데 AClass 앞에 final이 있기 때문에 상속을 받을 수 없다고 에러를 내보냅니다.
사실 이 final 키워드는 상속 금지만 사용하는 건 아닙니다.
변수를 상수 선언할 때도 사용할 수 있습니다.
링크 - [Java] 02. 변수와 상수 선언법, 그리고 원시 데이터형과 클래스 데이터형의 차이
여기까지 Java에서 추상 클래스(abstract) 그리고 상속 금지(final)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Study > Java' 카테고리의 다른 글
[Java] 17. 제네릭 타입(Generic type) (0) | 2020.05.15 |
---|---|
[Java] 16. 예외처리(try~catch~finally, throw)를 하는 방법 (0) | 2020.05.14 |
[Java] 15. 열거형(이진 데이터 비트 연산자 사용 예제) (0) | 2020.05.13 |
[Java] 14. 객체 지향(OOP) 프로그래밍의 4대 원칙(캡슐화, 추상화, 상속, 다형성) (0) | 2020.05.12 |
[Java] 12. 인터페이스(Interface) (0) | 2020.05.11 |
[Java] 11. String의 hashCode과 equals, 그리고 toString의 재정의(override) (0) | 2020.05.08 |
[Java] 10. 메모리 할당(stack 메모리와 heap 메모리 그리고 new)과 Call by reference(포인터에 의한 참조) (2) | 2020.05.08 |
[Java] 09. 접근 제한자와 static (0) | 2020.05.06 |