자바의 추상(Abstract) 클래스와 메서드 사용 방법
추상화(Abstraction)는 캡슐화(encapsulation)와 상속(inheritance)과 함께 객체 지향 프로그래밍(OOP)의 세 가지 핵심 원칙 중 하나입니다.
추상화는 프로그램의 복잡도를 줄이고 코드를 더 읽기 쉽게 만드는 방법입니다. 자바에서 추상화를 활용하면 다음과 같은 작업이 가능합니다.
- 클래스의 특정 정보만 외부에 노출하고
- 나머지 모든 세부 사항은 숨길 수 있습니다.
추상화는 자바에서 클래스와 메서드 양쪽 모두에 적용할 수 있습니다.
이 글에서는 자바에서 추상화의 기본 개념, 추상 클래스 사용법, 추상 메서드 사용법을 살펴보고, 이 중요한 개념을 더 잘 이해할 수 있도록 몇 가지 예제도 함께 다룹니다.
자바에서의 추상화
객체 지향 프로그래밍은 객체와 클래스를 사용해 데이터와 메서드를 표현하는 프로그래밍 방식입니다. 객체와 클래스를 활용하면 코드의 반복을 줄이고 프로그램의 효율성을 높일 수 있습니다.
자바는 객체 지향 프로그래밍 언어이며, 객체와 클래스를 다루기 위한 다양한 기능을 제공합니다. 그중 하나가 바로 추상화입니다. 추상화를 통해 특정 클래스에 노출되는 정보를 필요한 부분으로 제한할 수 있습니다.
자바에서 추상화가 사용되는 경우는 크게 두 가지입니다. 바로 클래스와 메서드입니다.
자바 추상 클래스란?
자바에서 추상 클래스(abstract class)는 객체를 생성할 수 없는 클래스입니다. 즉, 추상 클래스는 인스턴스화(instantiate)될 수 없습니다.
자바에서 추상 클래스를 만들려면 abstract 키워드를 사용합니다. 다음은 자바 추상 클래스의 예시입니다.
abstract class BankAccount {
// 코드 작성
}
추상 클래스이기 때문에, 이 클래스로 객체를 생성하려고 하면 프로그램은 오류를 반환합니다.
예를 들어, BankAccount라는 클래스의 인스턴스를 생성하려고 한다고 가정해 보겠습니다. 고객인 Lucy의 은행 정보를 저장하기 위해 lucyAccount라는 이름의 인스턴스를 만들고 싶습니다. 다음과 같은 코드를 사용해 인스턴스를 생성하려고 시도합니다.
BankAccount lucyAccount = new BankAccount();
하지만 이 코드를 실행하면 프로그램은 다음과 같은 결과를 반환합니다.
BankAccount is abstract; cannot be instantiated
위 결과에서 확인할 수 있듯이, 추상 클래스는 객체에 할당할 수 없습니다.
다만, 추상 클래스로부터 서브클래스(subclass)를 만드는 것은 가능합니다. 다음 섹션에서 추상 클래스로부터 서브클래스를 생성하는 방법을 알아보겠습니다.
추상 클래스의 상속
앞서 말했듯이 추상 클래스에는 객체를 할당할 수 없습니다. 따라서 추상 클래스의 내용, 즉 해당 클래스에 속한 속성과 메서드에 접근하려면 클래스를 상속(inherit)받아야 합니다.
은행 계좌 정보를 저장하는 프로그램을 만든다고 가정해 보겠습니다. 이 프로그램에는 BankAccount와 CheckingAccount 두 개의 클래스가 포함됩니다. BankAccount는 은행 계좌를 다루기 위한 속성과 메서드를 저장하는 부모 클래스이고, CheckingAccount는 그 메서드들을 상속받는 자식 클래스입니다.
이 예제에서 BankAccount는 추상 클래스입니다. 우리는 이 클래스를 오직 당좌계좌(checking account)를 생성하는 용도로만 사용하려 합니다.
BankAccount와 CheckingAccount 클래스를 활용하는 코드는 다음과 같습니다.
abstract class BankAccount {
public void viewAccountNumber() {
System.out.println("Account number: #1932042");
}
}
class CheckingAccount extends BankAccount {
}
class Main {
public static void main(String[] args) {
CheckingAccount lucyAccount = new CheckingAccount();
lucyAccount.viewAccountNumber();
}
}
이 코드를 실행하면 다음과 같은 결과가 출력됩니다.
Account number: #1932042
이 예제에서는 세 개의 클래스를 생성했습니다.
- BankAccount: 추상 클래스입니다.
viewAccountNumber()라는 메서드를 포함하고 있습니다. BankAccount는 추상 클래스이므로 이 클래스의 객체는 생성할 수 없습니다. - CheckingAccount: 자식 클래스입니다. 자식 클래스이기 때문에 BankAccount 클래스의 속성과 메서드를 상속받습니다.
- Main: 메인 프로그램의 코드를 담고 있는 클래스입니다.
메인 프로그램에서는 CheckingAccount 클래스의 인스턴스를 생성하여 lucyAccount라고 명명했습니다. lucyAccount는 객체입니다. 세 개의 클래스를 생성한 후, lucyAccount 객체를 통해 viewAccountNumber() 메서드를 호출했습니다.
아래 표는 이 예제의 핵심 요소들을 정리한 것입니다.
| 이름 | 유형 | 설명 |
| BankAccount | 추상/부모 클래스 | 은행 계좌를 다루기 위한 속성과 메서드를 저장합니다. |
| CheckingAccount | 자식 클래스 | 부모 클래스의 메서드를 상속받습니다. |
| Main | 클래스 | 메인 프로그램의 코드를 포함합니다. |
| lucyAccount | 객체 | 프로그램의 실제 구현체입니다. |
자바 추상 메서드
클래스뿐만 아니라 메서드도 추상적으로 선언할 수 있습니다.
추상 메서드를 선언할 때도 앞서 사용한 것과 동일한 abstract 키워드를 사용합니다. 추상 메서드는 내용(구현부)을 지정하지 않으며, 오직 추상 클래스 안에서만 선언할 수 있습니다.
다음은 자바에서 추상 메서드를 선언한 예시입니다.
abstract void checkBalance();
이 예제에서는 checkBalance()라는 추상 메서드를 생성했습니다. 이 메서드는 본문(body)이 없습니다. 즉, 메서드와 연관된 코드가 없다는 뜻입니다.
앞서 살펴본 은행 예제로 돌아가 보겠습니다. 그 프로그램에는 lucyAccount.viewAccountNumber();라는 메서드 호출이 있었습니다.
이전 섹션의 예제에서 BankAccount 부모 클래스는 일종의 템플릿 역할을 했습니다.
이 경우 자식 클래스들은 서로 다른 종류의 은행 계좌들입니다. 고객은 한 가지 이상의 계좌 유형을 가질 수 있고, 각 계좌 유형마다 동일한 기능을 수행하고 싶을 수 있기 때문입니다. 앞서 생성한 자식 클래스는 CheckingAccount였습니다.
예제의 고객 Lucy에게 다시 주목해 보겠습니다. Lucy는 당좌계좌를 가지고 있지만, 저축계좌(savings account)나 브로커리지 계좌(brokerage account)도 가지고 있을 수 있습니다. 우리는 Lucy의 각 계좌 번호를 모두 조회할 수 있기를 원합니다. 이를 위해 추가적인 두 계좌 유형에 대한 자식 클래스를 만들 수 있습니다. 각각 SavingsAccount, BrokerageAccount라고 부르겠습니다.
BankAccount 부모 클래스를 이미 만들어 두었기 때문에, 최소한의 코드 반복으로 세 가지 계좌 유형 모두의 계좌 번호를 효율적으로 조회할 수 있습니다.
아래 코드에서는 abstract 키워드를 두 번 사용합니다. 먼저 추상 클래스(BankAccount)를 선언합니다. 그다음, 나중에 프로그램이 고객의 계좌 번호를 표시하도록 도울 추상 메서드를 선언합니다. 그 추상 메서드 선언이 바로 abstract void viewAccountNumber();입니다. 이후 CheckingAccount 자식 클래스에서 viewAccountNumber() 메서드를 구현(초기화)합니다. 코드는 다음과 같습니다.
abstract class BankAccount {
abstract void viewAccountNumber();
}
class CheckingAccount extends BankAccount {
public void viewAccountNumber() {
System.out.println("Checking account number: #1932042");
}
}
class Main {
public static void main(String[] args) {
CheckingAccount lucyAccount = new CheckingAccount();
lucyAccount.viewAccountNumber();
}
}
이 코드를 실행하면 다음과 같은 결과가 출력됩니다.
Checking account number: #1932042
이 예제는 앞선 예제와 동일한 출력을 반환하지만, 코드상에서 주목할 만한 차이점이 몇 가지 있습니다.
- 추상 메서드의 선언: 이 예제에서는 BankAccount라는 추상 클래스를 선언했습니다. 이 클래스는
viewAccountNumber()라는 추상 메서드를 포함합니다. - 추상 메서드의 확장: CheckingAccount 클래스를 통해 BankAccount 클래스를 상속(extends)합니다. CheckingAccount 클래스는 BankAccount에 정의된 값을 상속받습니다.
CheckingAccount 클래스 안에서는 CheckingAccount 객체만 접근할 수 있는 viewAccountNumber() 메서드를 정의합니다. 그런 다음, 고객의 계좌 번호를 확인하기 위해 CheckingAccount 객체를 생성합니다.
자바 추상화 종합 예제
지금까지 자바의 추상 클래스와 추상 메서드를 모두 살펴보았으니, 이제 자바에서 추상화가 실제로 어떻게 동작하는지 전체 예제를 통해 확인해 보겠습니다. 앞서 다룬 은행 계좌 예제들의 내용을 모두 합쳐서 진행하겠습니다.
당좌계좌와 저축계좌, 두 가지 유형의 은행 계좌를 운영하는 은행이 있다고 가정해 보겠습니다. 우리는 계좌 소유자가 자신의 계좌 번호를 확인할 수 있는 프로그램을 작성하기로 했습니다.
이 은행에서는 계좌가 당좌계좌라면 계좌 번호가 고객 ID 번호로 시작하고 555로 끝납니다. 저축계좌라면 계좌 번호가 777로 끝납니다.
이 프로그램을 만들기 위해 다음과 같은 코드를 사용할 수 있습니다.
abstract class BankAccount {
abstract void viewAccountNumber();
}
class CheckingAccount extends BankAccount {
public void viewAccountNumber() {
System.out.println("Checking account number: #1932042555");
}
}
class SavingsAccount extends BankAccount {
public void viewAccountNumber() {
System.out.println("Savings account number: #1932042777");
}
}
class Main {
public static void main(String[] args) {
CheckingAccount ellieCheckingAccount = new CheckingAccount();
ellieCheckingAccount.viewAccountNumber();
SavingsAccount ellieSavingsAccount = new SavingsAccount();
ellieSavingsAccount.viewAccountNumber();
}
}
이 코드를 실행하면 다음과 같은 결과가 출력됩니다.
Checking account number: #1932042555 Savings account number: #1932042777
이 예제에서는 BankAccount라는 클래스를 생성했으며, 이 클래스에는 viewAccountNumber()라는 하나의 추상 메서드가 있었습니다.
각 계좌 유형마다 계좌 번호를 부여하는 방식이 다르기 때문에, BankAccount 클래스에서 viewAccountNumber() 메서드를 구현할 수 없습니다. 대신 BankAccount 클래스의 각 서브클래스가 각자 viewAccountNumber() 메서드를 구현합니다.
이 예제에서 CheckingAccount 클래스는 고객 ID로 시작하고 555로 끝나는 번호를 계좌 번호로 부여합니다. SavingsAccount 클래스는 고객 ID로 시작하고 777로 끝나는 번호를 계좌 번호로 부여합니다.
코드에서는 Ellie라는 고객을 위해 CheckingAccount 클래스의 인스턴스를 초기화했습니다. 이 인스턴스는 ellieCheckingAccount라는 이름이 할당되었으며, viewAccountNumber() 메서드를 호출해 계좌 번호를 조회합니다. 그 결과는 다음과 같습니다.
Checking account number: #1932042555
마찬가지로 SavingsAccount 클래스의 인스턴스인 ellieSavingsAccount를 초기화하고, viewAccountNumber() 메서드를 호출해 계좌 번호를 조회합니다. 그 결과는 다음과 같습니다.
Savings account number: #1932042777
마무리
추상화(Abstraction)는 객체 지향 프로그래밍의 중요한 기능입니다. 클래스나 메서드의 특정 세부 사항을 숨기고 필수적인 정보만 표시할 수 있게 해줍니다. 자바에서 추상 클래스와 추상 메서드를 만들 때는 abstract 키워드를 사용합니다.
이 글에서는 예제와 함께 추상화의 동작 원리, 추상화가 유용한 이유, 그리고 자바에서 추상 메서드와 추상 클래스를 생성하는 방법을 알아보았습니다. 이제 여러분도 전문 개발자처럼 자바의 추상 클래스와 추상 메서드를 활용할 준비가 되었습니다!