추상화 (Abstraction)
왜 쓰는지
구현의 세부사항은 자주 변경되지만, 역할(행동)은 상대적으로 안정적입니다: - "결제 수단"이 필요 (신용카드, 휴대폰, 계좌이체) - 각 결제 수단의 구현은 다르지만, "결제한다"는 행동은 같음 - 새로운 결제 수단이 추가되어도 기존 코드는 변경 없음
핵심: 추상화는 구현이 아닌 역할(행동)을 중심으로 설계하여, 변경에 강하고 확장에 유연한 구조를 만들기 위함입니다. OCP(Open–Closed Principle)의 핵심입니다.
어떻게 쓰는지
추상 클래스 (Abstract Class)
// 부모: 공통 인터페이스 + 일부 구현 제공
public abstract class PaymentMethod {
private String accountNumber;
// 역할만 정의 (구현 강제)
public abstract void pay(int amount);
public abstract boolean validate();
// 공통 구현 제공
public void logTransaction(int amount) {
System.out.println("거래 기록: " + amount + "원");
}
}
// 자식 1: CreditCard
public class CreditCard extends PaymentMethod {
@Override
public void pay(int amount) {
if (amount > 1000000) {
System.out.println("한도 초과");
return;
}
System.out.println("신용카드 결제: " + amount + "원");
}
@Override
public boolean validate() {
return true; // 신용카드 유효성 검사
}
}
// 자식 2: PhonePay
public class PhonePay extends PaymentMethod {
@Override
public void pay(int amount) {
System.out.println("휴대폰 결제: " + amount + "원");
}
@Override
public boolean validate() {
return true; // 휴대폰 유효성 검사
}
}
인터페이스 (Interface)
// 순수 역할 정의 (구현 없음)
public interface PaymentMethod {
void pay(int amount);
boolean validate();
}
// 구현 1
public class CreditCard implements PaymentMethod {
@Override
public void pay(int amount) {
System.out.println("신용카드 결제: " + amount + "원");
}
@Override
public boolean validate() {
return true;
}
}
// 구현 2
public class PhonePay implements PaymentMethod {
@Override
public void pay(int amount) {
System.out.println("휴대폰 결제: " + amount + "원");
}
@Override
public boolean validate() {
return true;
}
}
추상화의 활용 (OCP 준수)
// 추상화에 의존하므로 구현 변경에 영향 없음
public class ShoppingCart {
private PaymentMethod paymentMethod;
public ShoppingCart(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
public void checkout(int totalAmount) {
if (!paymentMethod.validate()) {
System.out.println("결제 수단 검증 실패");
return;
}
paymentMethod.pay(totalAmount);
}
}
// 사용: ShoppingCart는 어떤 결제 수단도 수용
ShoppingCart cart = new ShoppingCart(new CreditCard());
cart.checkout(50000);
// 나중에 새로운 결제 수단 추가 (ShoppingCart는 수정 없음)
ShoppingCart cart2 = new ShoppingCart(new Bitcoin());
cart2.checkout(50000);
언제 쓰는지
| 상황 | 선택 | 이유 |
|---|---|---|
| 역할이 명확함 | ✅ 추상화 | 인터페이스/추상클래스로 정의 |
| 구현이 자주 변함 | ✅ 추상화 | 기존 코드 수정 최소화 |
| 다양한 구현 필요 | ✅ 인터페이스 | 다중 구현 가능 |
| 공통 구현 많음 | ✅ 추상클래스 | 코드 중복 제거 |
| 단순한 경우 | ❌ 구체 클래스 | 과설계 방지 |
장점
| 장점 | 설명 |
|---|---|
| OCP 준수 | 확장에 열려있고, 변경에 닫혀있음 |
| 결합도 감소 | 구체 구현이 아닌 추상화에 의존 |
| 테스트 용이 | Mock 객체 주입으로 단위 테스트 가능 |
| 확장성 | 새로운 구현 추가 시 기존 코드 변경 불필요 |
| 유지보수 개선 | 변경 포인트 명확화 |
단점
| 단점 | 설명 |
|---|---|
| 과설계 | 단순한 경우까지 인터페이스 강제 |
| 복잡도 증가 | 계층이 많아지면 코드 추적 어려움 |
| 성능 오버헤드 | 다형성으로 인한 메서드 호출 비용 |
| 학습곡선 | SOLID 원칙 이해 필요 |
특징
1. 인터페이스 vs 추상클래스
| 항목 | 인터페이스 | 추상클래스 |
|---|---|---|
| 구현 | 제공 안 함 | 부분 제공 가능 |
| 상태 | 필드 불가 | 필드 가능 |
| 상속 | 다중 구현 가능 | 단일 상속 |
| 용도 | 역할 계약 | 공통 개념 + 구현 |
| 접근제어 | public만 | 다양함 |
2. 추상 메서드 vs 구체 메서드
public abstract class Shape {
// 추상 메서드: 자식이 반드시 구현해야 함
public abstract double getArea();
// 구체 메서드: 공통 구현
public void display() {
System.out.println("넓이: " + getArea());
}
}
public class Circle extends Shape {
private double radius;
@Override
public double getArea() {
return 3.14 * radius * radius;
}
// display()는 자동으로 상속됨
}
3. 템플릿 메서드 패턴
public abstract class DataProcessor {
// 템플릿: 알고리즘의 구조는 정의하고, 세부는 자식이 구현
public final void process(String data) {
String validated = validate(data);
String transformed = transform(validated);
save(transformed);
}
protected abstract String validate(String data);
protected abstract String transform(String data);
protected abstract void save(String data);
}
public class JsonProcessor extends DataProcessor {
@Override
protected String validate(String data) {
// JSON 검증
return data;
}
@Override
protected String transform(String data) {
// JSON 파싱
return data;
}
@Override
protected void save(String data) {
// DB에 저장
}
}
4. Java 8+ 인터페이스 default 메서드
public interface PaymentMethod {
// 추상 메서드
void pay(int amount);
// default 메서드 (구현 제공, 오버라이드 가능)
default boolean validate() {
return true; // 기본 구현
}
// static 메서드
static PaymentMethod createCreditCard() {
return new CreditCard();
}
}
주의할 점
❌ 추상 메서드 구현 안 함
public abstract class PaymentMethod {
public abstract void pay(int amount);
}
// ❌ 컴파일 에러: 추상 메서드 미구현
public class CreditCard extends PaymentMethod {
// pay() 구현 안 함
}
// 또는 CreditCard도 abstract 클래스여야 함
✅ 올바른 방식:
⚠️ Java 8+ default 메서드의 다이아몬드 문제
interface A {
default void method() { System.out.println("A"); }
}
interface B extends A {
default void method() { System.out.println("B"); }
}
interface C extends A {
default void method() { System.out.println("C"); }
}
// ❌ 어느 default 메서드를 사용할지 모호
class D implements B, C {
// 컴파일 에러: 명시적 해결 필요
}
// ✅ 명시적 해결
class D implements B, C {
@Override
public void method() {
B.super.method(); // B의 메서드 사용
}
}
⚠️ 추상 클래스는 인스턴스 생성 불가
베스트 프랙티스
| 권장 방식 | 이유 |
|---|---|
| 역할 이름으로 추상화 | PaymentMethod, NotificationSender처럼 사용하는 쪽의 관점이 드러남 |
| 구현이 2개 이상이거나 바뀔 가능성이 있을 때 도입 | 불필요한 인터페이스 남발을 줄일 수 있음 |
| 인터페이스는 작게 유지 | 구현 클래스가 필요 없는 메서드까지 강제로 구현하지 않게 함 |
| 호출 코드는 구체 클래스보다 추상 타입에 의존 | 구현체 교체와 테스트 대체가 쉬워짐 |
| 공통 상태가 필요하면 추상 클래스 검토 | 필드와 공통 알고리즘이 필요한 경우 인터페이스보다 자연스러움 |
| default 메서드는 호환성 보강용으로 제한 | 인터페이스가 비대한 구현 클래스처럼 변하는 것을 막음 |
추상화는 미리 많이 만드는 기술이 아니라, 변경 가능성이 보일 때 경계를 세우는 기술입니다.
처음부터 모든 클래스에 인터페이스를 만들기보다, 외부 시스템 연동, 정책 교체, 테스트 대체처럼 실제 이득이 있는 지점부터 적용합니다.
실무에서는?
| 위치 | 추상화 방식 |
|---|---|
| 결제/알림/파일 저장소 | 외부 구현이 바뀔 수 있으므로 인터페이스 뒤에 숨김 |
| Repository | 저장 방식 변경 가능성을 인터페이스로 분리 |
| 정책 객체 | 할인, 수수료, 권한 판단처럼 규칙이 여러 개인 경우 전략 인터페이스 사용 |
| Service 의존성 | 구현체를 직접 생성하지 않고 생성자 주입으로 받음 |
| 테스트 코드 | 인터페이스를 Fake/Mock 구현으로 바꿔 빠르게 검증 |
| 라이브러리 API | 외부에 공개되는 계약은 작고 안정적으로 유지 |
정리
| 항목 | 설명 |
|---|---|
| 추상화 | 구현 아닌 역할 중심 설계 |
| 인터페이스 | 순수 역할 정의 |
| 추상클래스 | 역할 + 공통 구현 |
| OCP | 확장에 열려있고 변경에 닫혀있음 |
| 주의 | 과설계 방지, 복잡도 관리 |
관련 파일: - OOP 개요 — 객체지향 4원칙 흐름 - 상속 — 추상화의 기반 - 다형성 — 추상화의 활용 - 캡슐화 — 정보 은닉