7.4 추상화와 인터페이스 (Abstraction & Interface)
객체지향 설계에서 추상화(Abstraction) 란 복잡한 현실 세계를 프로그램으로 모델링할 때, 불필요한 세부 사항은 숨기고 핵심적인 공통 속성과 기능만을 추출 하여 뼈대를 만드는 작업입니다.
자바에서는 추상화를 두 가지 도구로 구현합니다.
- 추상 클래스 (Abstract Class)
- 인터페이스 (Interface)
1. 추상 클래스 (Abstract Class)
추상 클래스는 미완성 설계도 입니다. 구체적인 구현이 아직 결정되지 않은 메서드(추상 메서드)를 포함하는 클래스입니다.
추상 클래스 선언
// abstract 키워드로 추상 클래스 선언
public abstract class Shape {
protected String color;
// 일반 생성자 가질 수 있음
public Shape(String color) {
this.color = color;
}
// 추상 메서드: 선언부만, 구현부({}) 없음
// → 자식 클래스가 반드시 구현해야 함
public abstract double area();
public abstract double perimeter();
// 일반 메서드: 구현이 완료된 공통 기능
public void displayInfo() {
System.out.printf("도형: %s, 색상: %s, 넓이: %.2f, 둘레: %.2f%n",
getClass().getSimpleName(), color, area(), perimeter());
}
public String getColor() { return color; }
}
추상 클래스 구현 (자식 클래스)
// 원
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color); // 부모 생성자 호출
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
// 직사각형
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}
// 삼각형
public class Triangle extends Shape {
private double a, b, c; // 세 변의 길이
public Triangle(String color, double a, double b, double c) {
super(color);
this.a = a; this.b = b; this.c = c;
}
@Override
public double area() {
// 헤론의 공식
double s = (a + b + c) / 2;
return Math.sqrt(s * (s-a) * (s-b) * (s-c));
}
@Override
public double perimeter() {
return a + b + c;
}
}
추상 클래스의 특징
public class AbstractTest {
public static void main(String[] args) {
// Shape shape = new Shape("빨강"); // 컴파일 에러! 추상 클래스 인스턴스화 불가
// 자식 클래스로 인스턴스 생성
Shape circle = new Circle("빨강", 5.0);
Shape rectangle = new Rectangle("파랑", 4.0, 6.0);
Shape triangle = new Triangle("초록", 3.0, 4.0, 5.0);
// 다형성: 부모 타입 배열로 관리
Shape[] shapes = { circle, rectangle, triangle };
for (Shape s : shapes) {
s.displayInfo(); // 각 자식의 area(), perimeter() 호출
}
}
}
출력:
도형: Circle, 색상: 빨강, 넓이: 78.54, 둘레: 31.42
도형: Rectangle, 색상: 파랑, 넓이: 24.00, 둘레: 20.00
도형: Triangle, 색상: 초록, 넓이: 6.00, 둘레: 12.00
추상 클래스의 목적
추상 클래스는 자식 클래스들에게 공통 틀(템플릿) 을 제공합니다. 공통 필드와 구현된 메서드를 재사용하면서, 구체적인 구현이 필요한 메서드는 자식에게 강제합니다. "IS-A 관계"를 표현할 때 사용합니다.
2. 인터페이스 (Interface)
인터페이스는 추상 클래스보다 더 강력한 추상화를 제공합니다. 클래스들이 반드시 구현해야 할 동작의 규격(계약) 을 정의합니다.
기본 인터페이스 선언
public interface Printable {
// 모든 상수는 자동으로 public static final
int MAX_SIZE = 1000; // == public static final int MAX_SIZE = 1000
// 모든 메서드는 자동으로 public abstract
void print(); // == public abstract void print()
String getContent(); // == public abstract String getContent()
}
// 인터페이스 구현: implements 키워드
public class Document implements Printable {
private String content;
public Document(String content) {
this.content = content;
}
@Override
public void print() {
System.out.println("[문서 출력]: " + content);
}
@Override
public String getContent() {
return content;
}
}
다중 구현 (Multiple Implementation)
클래스는 단일 상속만 가능하지만, 인터페이스는 여러 개를 동시에 구현할 수 있습니다.
public interface Flyable {
void fly();
default String getTransportType() { return "공중"; }
}
public interface Swimmable {
void swim();
default String getTransportType() { return "수상"; }
}
public interface Drivable {
void drive();
}
// 여러 인터페이스를 동시에 구현
public class AmphibiousVehicle implements Flyable, Swimmable, Drivable {
@Override
public void fly() { System.out.println("하늘을 납니다!"); }
@Override
public void swim() { System.out.println("물 위를 달립니다!"); }
@Override
public void drive() { System.out.println("도로를 달립니다!"); }
// 두 인터페이스에 동일한 default 메서드가 있으면 반드시 오버라이딩
@Override
public String getTransportType() {
return "육·해·공";
}
}
3. Java 8/9+ 인터페이스 신기능
Java 8부터 인터페이스에 구현을 포함할 수 있게 되었습니다.
default 메서드 (Java 8+)
public interface Collection<E> {
// abstract 메서드: 구현 클래스가 반드시 구현
void add(E element);
void remove(E element);
int size();
// default 메서드: 기본 구현 제공 (오버라이딩 선택)
default boolean isEmpty() {
return size() == 0;
}
default void printAll() {
System.out.println("Collection with " + size() + " elements");
}
}
static 메서드 (Java 8+)
public interface Validator<T> {
boolean validate(T value);
// static 메서드: 인터페이스 이름으로 직접 호출
static <T> boolean validateNotNull(T value) {
return value != null;
}
}
// 호출
boolean result = Validator.validateNotNull("hello"); // true
private 메서드 (Java 9+)
public interface Logger {
default void info(String message) { log("INFO", message); }
default void error(String message) { log("ERROR", message); }
default void warn(String message) { log("WARN", message); }
// private 메서드: default/static 메서드에서만 사용 가능 (외부 노출 없음)
private void log(String level, String message) {
System.out.printf("[%s] %s%n", level, message);
}
}
4. 함수형 인터페이스와 람다
함수형 인터페이스(Functional Interface) 는 추상 메서드가 정확히 하나 인 인터페이스입니다. @FunctionalInterface 어노테이션으로 명시합니다.
// 함수형 인터페이스 선언
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
// 추상 메서드는 반드시 1개만! (default/static은 여러 개 가능)
}
public class LambdaExample {
public static void main(String[] args) {
// 기존 방식: 익명 클래스
Calculator add = new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
// 람다 방식: 훨씬 간결
Calculator addLambda = (a, b) -> a + b;
Calculator subtractLambda = (a, b) -> a - b;
Calculator multiplyLambda = (a, b) -> a * b;
System.out.println(addLambda.calculate(10, 5)); // 15
System.out.println(subtractLambda.calculate(10, 5)); // 5
System.out.println(multiplyLambda.calculate(10, 5)); // 50
// Java 표준 함수형 인터페이스 활용
java.util.function.Predicate<String> isLong = s -> s.length() > 5;
java.util.function.Function<String, Integer> length = String::length;
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.test("Hello World!")); // true
System.out.println(length.apply("Java")); // 4
}
}
5. 추상 클래스 vs 인터페이스 비교
| 구분 | 추상 클래스 (abstract class) | 인터페이스 (interface) |
|---|---|---|
| 상속/구현 | extends (단일 상속) | implements (다중 구현) |
| 변수 | 일반 멤버 변수 선언 가능 | public static final 상수만 |
| 메서드 | 추상 + 일반 메서드 모두 가능 | 기본적으로 추상, default/static 가능 |
| 생성자 | 있음 | 없음 |
| 접근 제어자 | 모든 접근 제어자 사용 가능 | public 또는 private (Java 9+) |
| 목적 | IS-A 관계, 공통 코드 공유 | CAN-DO 관계, 동작 규격 정의 |
| 사용 시기 | 관련 클래스의 공통 부모 | 서로 다른 클래스에 공통 기능 부여 |
6. 실전 예제: 결제 시스템
인터페이스를 활용한 실제적인 결제 시스템을 구현합니다.
// 결제 처리 인터페이스
public interface PaymentProcessor {
boolean processPayment(double amount);
void refund(double amount);
String getPaymentMethod();
default void printReceipt(double amount) {
System.out.printf("[영수증] 결제 수단: %s, 금액: %.0f원%n",
getPaymentMethod(), amount);
}
}
// 신용카드 결제
public class CreditCardProcessor implements PaymentProcessor {
private String cardNumber;
private double limit;
private double balance;
public CreditCardProcessor(String cardNumber, double limit) {
this.cardNumber = cardNumber;
this.limit = limit;
this.balance = 0;
}
@Override
public boolean processPayment(double amount) {
if (balance + amount > limit) {
System.out.println("[신용카드] 한도 초과! 결제 실패");
return false;
}
balance += amount;
System.out.printf("[신용카드] 카드 %s으로 %.0f원 결제 완료%n",
cardNumber.substring(cardNumber.length() - 4), amount);
printReceipt(amount);
return true;
}
@Override
public void refund(double amount) {
balance -= amount;
System.out.printf("[신용카드] %.0f원 환불 완료. 현재 사용액: %.0f원%n",
amount, balance);
}
@Override
public String getPaymentMethod() { return "신용카드"; }
}
// 카카오페이 결제
public class KakaoPayProcessor implements PaymentProcessor {
private String userId;
private double walletBalance;
public KakaoPayProcessor(String userId, double walletBalance) {
this.userId = userId;
this.walletBalance = walletBalance;
}
@Override
public boolean processPayment(double amount) {
if (walletBalance < amount) {
System.out.println("[카카오페이] 잔액 부족! 결제 실패");
return false;
}
walletBalance -= amount;
System.out.printf("[카카오페이] %s님 %.0f원 결제 완료. 잔액: %.0f원%n",
userId, amount, walletBalance);
printReceipt(amount);
return true;
}
@Override
public void refund(double amount) {
walletBalance += amount;
System.out.printf("[카카오페이] %.0f원 환불. 잔액: %.0f원%n",
amount, walletBalance);
}
@Override
public String getPaymentMethod() { return "카카오페이"; }
}
// 네이버페이 결제
public class NaverPayProcessor implements PaymentProcessor {
private double points;
public NaverPayProcessor(double points) {
this.points = points;
}
@Override
public boolean processPayment(double amount) {
if (points < amount) {
System.out.println("[네이버페이] 포인트 부족! 결제 실패");
return false;
}
points -= amount;
System.out.printf("[네이버페이] %.0f 포인트 사용. 잔여 포인트: %.0f%n",
amount, points);
printReceipt(amount);
return true;
}
@Override
public void refund(double amount) {
points += amount;
System.out.printf("[네이버페이] %.0f 포인트 환불. 잔여 포인트: %.0f%n",
amount, points);
}
@Override
public String getPaymentMethod() { return "네이버페이"; }
}
// 결제 서비스 (어떤 결제 수단이든 동일한 코드로 처리 — 다형성!)
public class PaymentService {
private PaymentProcessor processor;
public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
// 결제 수단을 교체 가능 (OCP: 확장에 열려 있음)
public void setProcessor(PaymentProcessor processor) {
this.processor = processor;
}
public void checkout(double amount) {
System.out.println("\n=== 결제 진행 ===");
boolean success = processor.processPayment(amount);
if (success) {
System.out.println("결제가 성공적으로 완료되었습니다.");
} else {
System.out.println("결제에 실패하였습니다. 다른 결제 수단을 이용해주세요.");
}
}
}
// 실행 테스트
public class PaymentTest {
public static void main(String[] args) {
PaymentProcessor creditCard = new CreditCardProcessor("1234-5678-9012-3456", 500000);
PaymentProcessor kakaoPay = new KakaoPayProcessor("user123", 50000);
PaymentProcessor naverPay = new NaverPayProcessor(30000);
PaymentService service = new PaymentService(creditCard);
service.checkout(150000); // 신용카드 결제
service.setProcessor(kakaoPay);
service.checkout(30000); // 카카오페이 결제
service.setProcessor(naverPay);
service.checkout(50000); // 네이버페이 결제 실패 (잔액 부족)
}
}
출력:
=== 결제 진행 ===
[신용카드] 카드 3456으로 150000원 결제 완료
[영수증] 결제 수단: 신용카드, 금액: 150000원
결제가 성공적으로 완료되었습니다.
=== 결제 진행 ===
[카카오페이] user123님 30000원 결제 완료. 잔액: 20000원
[영수증] 결제 수단: 카카오페이, 금액: 30000원
결제가 성공적으로 완료되었습니다.
=== 결제 진행 ===
[네이버페이] 포인트 부족! 결제 실패
결제에 실패하였습니다. 다른 결제 수단을 이용해주세요.
요약
| 개념 | 핵심 내용 |
|---|---|
| 추상화 | 공통 속성/기능 추출, 불필요한 세부사항 은닉 |
| 추상 클래스 | abstract 선언, 인스턴스화 불가, 추상 메서드 강제 구현 |
| 인터페이스 | 완전한 추상화, 다중 구현, 동작 규격 정의 |
| default 메서드 | Java 8+, 인터페이스에 기본 구현 추가 |
| 함수형 인터페이스 | 추상 메서드 1개, @FunctionalInterface, 람다와 사용 |
| 추상 클래스 vs 인터페이스 | IS-A 관계 vs CAN-DO 관계 |