서론
개인 프로젝트를 진행하던 와중, 메소드의 형식이 중복되며, 추후 확장성을 고려해야 할 클래스를 다루어야 될 일이 생겼습니다.
이러한 문제를 해결하기 위한 방법으로 생각하던것은 '추상 클래스', '인터페이스', '제네릭' 이지만, 이번에 다루어볼 내용은 추상클래스로 선택하였습니다.
먼저, 추상 클래스의 큰 특징으로는, 추상 클래스를 상속 받으면, 개발자는 프로젝트에서 필요하고 공통적으로 들어가야하는 필드 및 메소드를 오버라이딩을 하여 프로젝트의 큰 틀을 생각하지않고, 주어진 태스크만 집중하여 구현 하면 된다는 점이 있습니다.
Abstract Class ( 추상 클래스 )

추상클래스 개념
Abstract class는 Java의 추상클래스입니다.
말 그대로, 클래스의 기능을 추상화한것이라고 볼 수 있는데, class 앞에 abstract를 붙이면 추상 클래스가 만들어집니다
추상 클래스는 일반적인 클래스와는 다르게 단일로는 인스턴스화 할 수 없는 특징을 가졌습니다.
여기서, '추상'이라는 단어에 주목해 볼 필요가 있습니다.
객체 지향 언어의 핵심이 되는 단어 중 하나가 바로 '추상'이라는 개념입니다.
자바가 유명해진 이유로는 JVM의 등장도 있지만, 이러한 객체 지향 언어의 특징인 '추상화'도 한 몫을 하였습니다.
예시로, 우리가 자바에서 흔히 사용하는 Integer 이라는 클래스를 봅시다.

위 코드에서 '추상'이라는 단어를 생각해보았을 때 어떤점들이 보이나요?
사실, 이 코드에 보이는 모든것들이 다 '추상'이라고 볼 수 있습니다.
Integer, result, compareTo...
내부적으로 기계어로 되어있고, 어떻게 작성해놨는지도 모를 코드로 만들어진 '자바'라는 언어 내부에서 우리는 단지 'Integer'라는 단어를 통해 '정수'와 관련이 있는 클래스에 관련된 작업을 할 수 있고, 'compareTo'와 같은 글자를 작성함으로 "무언가 비교하는구나" 라는 '행동'을 유추해볼 수 있습니다.
자바는 이렇게 작성한 글자에 맞는 기능들을 자바에서 마법처럼 구현해주고있습니다.
이제 추상에 대한 감이 오셨나요?
네, 맞습니다. 내부에 대해서 생각을 하지 않아도 단지 눈 앞에 보이는것만으로 기능을 유추할 수 있는 점, 이것이 바로 프로그래밍에서 말하는 '추상'입니다.
근데, 이 '추상'이라는 단어가 abstract class와 어떤 연관이 있을까요?
바로, 이 abstract class가 이 '추상'을 접목시킨 클래스라는 것입니다.
추상클래스는 클래스 자체를 추상화시켜, 내부적으로는 어떤 메소드를 가질 지에대해 미리 선언해놓아 "앞으로 이 클래스가 어떤 기능을 하는 클래스겠구나" 하는것을 짐작할 수 있게끔 만들어주는 역할을 할 수 있습니다.
추상클래스 사용 방법
추상클래스는 위에서 설명했듯, 단독으로는 인스턴스화를 하여 사용할 수 없는 어떤 기능을 할 지 정의만 해놓은 빈 껍데기입니다.
따라서 상속을 통해서만 이 추상클래스를 사용할 수 있는데, 이러한 추상클래스를 사용하는 방법은 아래와 같습니다.
public abstract class Animal {
private String animal;
Animal(String animal){
this.animal = animal;
}
public void sound() {
System.out.println("Unknown Sound!");
}
public String getAnimal() {
return this.animal;
}
public abstract void run();
}
위는 'Animal' 클래스를 선언한 코드입니다.
이 Animal 클래스는'동물'에 관한 클래스이며, 'sound' 메소드를 통해 동물의 소리를 낼 수 있는 기능을 갖고있고, 'run'을 통해 달릴 수도 있는 클래스라는것을 우리는 알 수 있습니다.
혹시, 코드를 보면서 이상한 점을 알아차리셨나요?
맞습니다. run() 메소드가 중괄호도 붙지않았고, 구현이 되어있지 않은상태며, 접근 제한자 뒤에 'abstract'라는 키워드가 붙어있다는것입니다.
추상 메소드를 활용한 추상클래스 구현
Abstract Method야말로 이 추상클래스의 강력한 기능중의 하나입니다.
이러한 method는 추상클래스에서 직접 구현하지않고, 이를 상속받는 자식클래스에서의 구현을 강제하는 기능인데, 이를 통해 자식클래스들의 구조적인 통일성을 기대할 수 있게되었습니다.
바로 아래와 같이 말이죠.
public abstract class Animal {
private String animal;
Animal(String animal){
this.animal = animal;
}
public String getAnimal() {
return this.animal;
}
public abstract void sound(); // 자식클래스 메소드 구현 강제화
public abstract void run(); // 자식클래스 메소드 구현 강제화
}
public class Cat extends Animal{
public Cat(String name){
super(name);
}
@Override
public void sound() {
System.out.println("Meow Meow!");
}
@Override
public void run() {
System.out.println("Cat is Running!");
}
}
class Main{
public static void main(String[] args) {
Cat cat = new Cat("Cat");
Animal animal = new Animal("Animal"); // 컴파일 에러
cat.getType(); // 부모 클래스의 인스턴스 메소드 실행 - result : "Cat"
cat.run(); // 부모 클래스를 상속받은 Cat 클래스의 run 인스턴스 메소드 실행 - result : "Cat is Running!"
}
}
이 코드만으론 왜 구조적인 통일이 중요한지 잘 모르겠다구요?
그렇다면 'Dog' 클래스를 추가한다면 어떻게 될까요?
public class Dog extends Animal {
public Dog(String animal) {
super(animal);
}
@Override
public void sound() {
System.out.println("bow wow!");;
}
public void run() {
System.out.println("Dog is Running!");
}
}
class Main{
public static void main(String[] args) {
Animal cat = new Cat("Cat");
Animal dog = new Dog("Dog");
cat.getType(); // result : "Cat"
cat.run(); // result : "Cat is Running!"
dog.getType(); // result : "Dog"
dog.run(); // result : "Dog is Running!"
}
}
이렇게, Animal 클래스를 상속받고, 같은 동작(메소드)을 하지만, 내부적으로는 다른 기능을 구현하여야 하는 Dog클래스도 통일된 구조를 통해 Animal클래스로 업캐스팅을 하여 객체 지향 프로그래밍의 중요한 특징인 '다형성'을 통한 인스턴스화도 성공하였습니다.
추상 클래스 활용
이렇게, 'Animal'이라는 추상성을 덧붙인 클래스를 통해 이를 상속받는 클래스들은 대략적으로 어떤 기능들을 하겠구나 라는것을 알 수 있음을 배웠습니다.
나아가 우리는 프로젝트를 진행할 때, 같은 도메인이지만, 구조가 비슷한 클래스를 묶어야 할 때 이 abstract class를 사용하면 클래스 구조의 통일성을 챙길 수 있고, 두 클래스에서 동일하게 사용되는 메소드를 추상클래스단에서 구현해주어 중복 코드를 줄여 코드의 재사용성 또한 챙길 수 있습니다.
나아가, 추후 같은 도메인에 추가되는 기능을 구현할 때 기존의 코드들을 사용하여 유연하게 기능을 추가할 수도 있습니다.

예를 들면, 서비스를 운영하고있고, A 결제 시스템과, B 결제 시스템을 사용하고 있는 상태라고 생각해봅시다.
그런데, 유저들이 C 결제 시스템 도입을 고객센터에 문의함에 따라 서비스 기획이 추가가 되면서 C 결제 시스템을 도입하기로 합니다.
만약, 기존에 A와 B 결제 시스템을 별개의 클래스로 구현하여 결제 시스템API를 받는 코드, 잔액 확인 메소드, 결제 메소드 등을 따로 만든 상태라고 칩시다.
이런 상황이라면 C 결제시스템을 도입하려해도 하나의 클래스를 새로 만들어 처음부터 다시 만드는 상황이 펼쳐질것입니다.
이후, 추가적 결제시스템을 도입할 때에도 반복되겠지요.
하지만, 처음 기획단계에서부터 확장성을 위해 이러한 추상메소드를 사용하였다면, 추상클래스에서 결제시스템 전반에 공통적으로 사용되는 메소드는 추상클래스 단에서 선언을 하고, 메소드의 구현이 달라지는 부분은 구현을 강제하도록 한다면 코드의 구성은 통일성이 생기게 되고, 공통 코드는 재사용할 수 있게 되며, 필요한 기능을 누락할 걱정을 하지않아도 되는 등 여러 장점을 갖게됩니다.
후기
한 번의 포스팅으로 Interface와, Generic까지 추가적으로 설명을 하며 이들간의 차이를 근본적인 부분에서부터 작성하고싶었습니다.
허나, Abstract class도 작성하다 보니, 생각보다 텍스트가 너무 길어져 Interface와 Generic까지 담게 되면 가독성도 좋지않고, 글이 지루해질까 걱정되어 미루게 되었네요..
글이 길어지다보니 내용을 최대한 간소화 시켰습니다.
혹여 꼭 필요한 기능이 누락되었다던지, 설명해드린 부분 중에 틀린 부분이 있다면 감사히 수용하겠습니다.
긴 글 읽어주셔서 감사합니다.
'Language > Java' 카테고리의 다른 글
| [Java] Lambda, Stream API 심층분석 (0) | 2024.10.13 |
|---|---|
| [Java] 제네릭 (Generic) (3) | 2024.10.12 |
| [Java] 인터페이스 (Interface) + abstract class와의 차이 (1) | 2024.10.12 |