1. 추상화의 개념
한마디로 말해: 추상화는 복잡한 것을 단순하게 바라보는 기술입니다.
프로그래밍에서 추상화는 객체 집합의 공통 속성과 행동을 도출하고, 개별적인 세부(예를 들어 어떤 것이 “후드 아래”에서 정확히 어떻게 동작하는지)는 무시하는 과정입니다. 도시 지도를 그린다고 상상해 보세요. 도로, 건물, 강은 표시하지만, 각 창문의 커튼 색 같은 상세 정보는 없습니다. 지도는 도시의 추상화입니다.
OOP에서 추상화는 문제 해결에 중요한 속성과 동작만 드러내고 불필요한 세부를 감추는 클래스와 인터페이스를 만드는 것입니다.
생활 속 예
- 교통수단은 추상화입니다. 버스인지, 자전거인지, 우주선인지 중요하지 않습니다 — 교통수단에는 공통점이 있습니다. 이동할 수 있고, 승객과 운전자가 있다는 점입니다.
- 동물도 추상화입니다. 모든 동물은 숨 쉬고, 먹고, 움직일 수 있습니다 — 다만 그 방식은 종마다 다릅니다.
- 결제는 금융 소프트웨어에서의 추상화입니다. 카드 결제인지, PayPal인지, 비트코인인지가 항상 중요하지는 않습니다 — 중요한 것은 결제를 수행하고 결과를 얻을 수 있다는 점입니다.
왜 추상화가 중요할까요?
- 현재 과제를 푸는 데 중요하지 않은 세부에 덜 신경 쓸 수 있습니다.
- 확장과 유지보수가 쉬운 시스템을 설계할 수 있습니다.
- 구체 구현에 상관없이 공통 인터페이스를 통해 객체를 다룰 수 있습니다.
2. Java에서의 추상화
Java에서 추상화는 두 가지 주요 도구로 구현됩니다:
- 추상 클래스 (abstract class)
- 인터페이스 (interface)
이번 강의에서는 추상 클래스에 집중합니다. (인터페이스는 곧 다룹니다!)
추상 클래스는 객체를 직접 생성하기 위한 클래스가 아닙니다. 다른 클래스들을 위한 공통 토대(템플릿)를 제공합니다. 추상 클래스에는 구현된 메서드(본문이 있는 메서드)와 추상 메서드(본문이 없는 메서드) — 즉, 반드시 하위 클래스에서 구현해야 하는 메서드를 함께 정의할 수 있습니다.
추상 메서드는 구현 없이, 즉 본문 없이 선언된 메서드입니다. 이것은 “이 클래스의 모든 자식은 이 메서드를 각자 방식으로 구현해야 한다”라고 명시합니다.
예시: ‘도형(Shape)’ 추상화
public abstract class Shape {
public abstract void draw(); // 추상 메서드 — 본문이 없다!
}
여기서 우리는 이렇게 말합니다: “모든 도형은 그릴 수 있지만, 어떻게 그릴지는 모른다 — 각 하위 클래스가 스스로 결정하라.”
왜 항상 구현 세부를 알 필요가 없을까요?
추상화를 사용할 때는 객체의 “겉모습” — 즉, 객체가 제공해야 하는 메서드 집합을 통해 작업합니다. 그 메서드가 내부적으로 어떻게 작동하는지는 중요하지 않습니다.
예를 들어 payment.process()를 호출해 결제를 수행합니다. 구현이 어떻게 되어 있는지는 중요하지 않습니다 — 제대로 동작하기만 하면 됩니다. 이는 다음을 가능하게 합니다:
- 해당 구현을 사용하는 코드를 갈아엎지 않고도 한 구현을 다른 구현으로 교체할 수 있습니다.
- 테스트를 단순화합니다(“스텁” 같은 대체 구현으로 바꿔치기 가능).
- 코드를 더 유연하고 변경에 강하게 만듭니다.
3. 추상화의 장점
설계와 유지보수의 단순화
추상화는 불필요한 것들을 생각하지 않게 해 줍니다. 자동차의 엔진이 정확히 어떻게 동작하는지 몰라도 운전할 수 있듯 — 핸들, 페달, “앞으로 가라”라는 지시만 알면 됩니다. 코드도 마찬가지입니다. 추상 클래스나 인터페이스로 작업하면 필요한 것만 보이게 됩니다.
시스템 확장의 용이성
시스템이 추상화 위에 구축되어 있다면 새로운 객체 타입을 쉽게 추가할 수 있습니다. 예를 들어 추상 클래스 Shape가 있다면 기존 코드를 바꾸지 않고도 새로운 도형 타입 — Triangle — 을 추가할 수 있습니다.
구성 요소 간 결합도 감소
프로그램의 서로 다른 부분이 추상화를 통해서만 소통한다면, 서로 독립적으로 변경할 수 있습니다. 이것은 콘센트와 플러그와 같습니다. 표준만 맞으면 어떤 기기든 꽂을 수 있습니다.
4. 실전 예제
추상화가 실제로 어떻게 보이는지 살펴봅시다. 직접 만져 볼 수 있도록 학습용 애플리케이션에 바로 넣을 수 있는 예제들을 사용하겠습니다.
예시 1: ‘Shape’ 클래스(도형)
public abstract class Shape {
public abstract void draw();
}
이제 구체(구현) 도형 몇 가지를 만들어 봅시다:
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("원을 그립니다");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("직사각형을 그립니다");
}
}
추상화를 사용해 봅시다:
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 원을 그립니다
s2.draw(); // 직사각형을 그립니다
여기서는 Shape 타입 변수로 작업합니다 — 내부에 어떤 도형이 있든 상관없습니다. 이것이 바로 추상화의 힘입니다!
예시 2: ‘Payment’ 클래스
public abstract class Payment {
public abstract void process();
}
구체 구현:
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("신용카드 결제 처리");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("PayPal 결제 처리");
}
}
사용 예:
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment p : payments) {
p.process();
}
그 결과 각 결제는 각자 방식으로 처리되지만, 이를 호출하는 코드는 그런 세부를 고민하지 않습니다.
5. 추상화와 구현 세부: 헷갈리지 않는 법
추상화는 “무엇”이지 “어떻게”가 아닙니다
추상화를 설계할 때의 질문은 “객체가 무엇을 할 수 있어야 하는가?”입니다.
구현 세부는 “그것을 어떻게 하는가?”입니다.
예를 들어:
- 추상화: “모든 도형은 그릴 수 있어야 한다(draw()).”
- 구현 세부: “원은 원호로 그린다, 직사각형은 네 개의 선으로 그린다.”
생활 속 비유
TV 리모컨을 떠올려 보세요. 신호를 어떻게 보내는지는 신경 쓰지 않습니다 — 중요한 건 “켜기”, “채널 변경”, “볼륨 업” 버튼이 있다는 사실입니다. 이것이 추상화입니다 — 여러분이 누를 수 있는 버튼들의 집합. 반면 리모컨을 설계하는 엔지니어들은 구현 세부를 반드시 고민해야 합니다.
6. 프로그램에서 추상화를 뽑아내는 방법
1단계. 공통점을 찾기
여러분의 도메인 객체들을 살펴보세요. 무엇이 공통인가요? 예를 들어 모든 교통수단은 움직이고, 모든 동물은 먹을 수 있으며, 모든 결제는 수행될 수 있습니다.
2단계. 추상 클래스를 정의하기
모든 객체에 공통적인 것만 담은 추상 클래스를 만듭니다.
public abstract class Transport {
public abstract void move();
}
3단계. 하위 클래스에서 세부 구현하기
public class Car extends Transport {
@Override
public void move() {
System.out.println("자동차가 도로를 달립니다");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("자전거가 페달을 밟습니다");
}
}
4단계. 코드에서 추상화를 사용하기
Transport[] transports = {
new Car(),
new Bicycle()
};
for (Transport t : transports) {
t.move();
}
여러분의 코드는 추상화를 통해 동작하며 — 구현 세부에 의존하지 않습니다.
7. 추상화와 구체화의 균형
입문자의 전형적인 실수 — 추상화를 지나치게 일반적이거나 반대로 지나치게 상세하게 만들려는 것입니다.
- 추상화가 너무 일반적이라면(예: 메서드가 “doSomething”뿐인 “Object” 클래스) 아무런 이점을 주지 못합니다.
- 추상화가 너무 상세하다면(예: “빨간 테두리와 반지름 5의 원”), 의미가 사라집니다 — 차라리 바로 구체 클래스를 작성하는 편이 낫습니다.
황금률: 추상화는 여러분의 과제에서 진짜로 공통적이고 중요한 것만 반영해야 합니다.
8. 추상화를 다룰 때 흔한 실수
오류 №1: 추상 클래스의 인스턴스를 생성하려는 시도.
추상 클래스는 직접 객체를 생성하기 위한 것이 아닙니다. Shape s = new Shape();라고 작성하면 컴파일러는 Cannot instantiate the type Shape 오류를 냅니다. 이는 가게에서 그냥 “교통수단”을 사려는 것과 같습니다. 불가능합니다: 구체적인 자전거, 자동차, 버스 중에서 골라야 합니다.
오류 №2: 하위 클래스에서 추상 메서드 구현을 빼먹음.
클래스가 추상 클래스를 상속했는데 그 추상 메서드를 모두 구현하지 않으면 그 클래스도 추상 클래스가 되어 인스턴스를 생성할 수 없습니다. 필수 메서드를 모두 구현했는지 확인하세요.
오류 №3: 추상화와 구현 세부의 혼합.
특정 하위 클래스에만 필요한 세부를 추상 클래스에 넣기 시작한다면, 이는 나쁜 추상화의 신호입니다. 예를 들어 추상 클래스 Payment에 cardNumber 필드가 생겼는데, 모든 결제가 카드로 처리되는 것은 아닐 때입니다.
오류 №4: 추상화에 과도하게 집착함.
추상화를 위한 추상화를 만들 필요는 없습니다. 시스템에 객체 타입이 하나뿐이고 확장될 일이 없다면, 추상화는 코드를 오히려 복잡하게 만들 뿐입니다.
GO TO FULL VERSION