“객체지향이 뭔가요?”라는 면접 질문 앞에서 머릿속이 하얘진 경험이 있으신가요? 자바 객체지향 4대 원칙은 단순히 외워야 하는 개념이 아닙니다. 왜 코드를 클래스로 나누는지, 왜 private을 쓰는지, 왜 인터페이스가 필요한지처럼 실제 코드 설계의 모든 판단 뒤에 이 네 가지 원칙이 자리하고 있습니다. 캡슐화·상속·다형성·추상화 각각의 의미와 동작 원리를 실전 코드와 함께 처음부터 끝까지 완벽하게 정리합니다.
목차
- 객체지향 프로그래밍이란 무엇인가: OOP의 탄생 배경
- 캡슐화(Encapsulation): 데이터와 동작을 하나로, 내부는 숨긴다
- 상속(Inheritance): 코드 재사용과 계층 구조의 설계
- 다형성(Polymorphism): 하나의 인터페이스, 다양한 구현
- 추상화(Abstraction): 복잡함을 감추고 핵심만 드러낸다
- 4대 원칙의 연결과 SOLID로의 확장: 실전 설계 관점
1. 객체지향 프로그래밍이란 무엇인가: OOP의 탄생 배경
객체지향 프로그래밍(OOP, Object-Oriented Programming)이 등장하기 전, 프로그램은 위에서 아래로 순서대로 실행되는 절차적(Procedural) 방식으로 작성되었습니다. 프로그램 규모가 커질수록 함수와 전역 변수가 뒤엉키고, 어디서 무엇이 바뀌는지 추적이 불가능해지는 문제가 반복됐습니다.
현실 세계를 코드로 모델링하다
OOP는 현실 세계의 사물을 객체(Object) 라는 단위로 모델링하는 아이디어에서 출발합니다. 현실에서 “자동차”는 색상·속도·연료량이라는 **속성(데이터)**과 가속·제동·주유라는 **행동(동작)**을 함께 가집니다. OOP는 이 구조를 그대로 코드에 옮깁니다.
현실 세계 → 자바 코드 매핑
사물(자동차) → 클래스(class Car)
속성(색상) → 필드(String color)
행동(가속하다) → 메서드(void accelerate())
실제 내 차 → 인스턴스(Car myCar = new Car())
자바는 처음 설계부터 OOP를 언어 차원에서 지원하도록 만들어졌으며, 그 핵심 뼈대를 이루는 네 가지 원칙이 바로 캡슐화·상속·다형성·추상화입니다.
4대 원칙이 해결하는 네 가지 문제
| 원칙 | 해결하는 문제 | 핵심 키워드 |
|---|---|---|
| 캡슐화 | 데이터가 외부에서 함부로 변경됨 | 정보 은닉, 접근 제어자 |
| 상속 | 비슷한 코드를 중복 작성해야 함 | 코드 재사용, 계층 구조 |
| 다형성 | 타입마다 다른 코드로 분기 처리 | 유연성, 확장성 |
| 추상화 | 구현 세부사항에 의존하는 코드 | 인터페이스, 계약 |
2. 캡슐화(Encapsulation): 데이터와 동작을 하나로, 내부는 숨긴다
캡슐화는 OOP의 4대 원칙 중 가장 먼저 체감되는 원칙입니다. 두 가지 의미를 동시에 담고 있습니다. 첫째, 관련된 데이터(필드)와 동작(메서드)을 하나의 클래스 안으로 묶는 것(Bundling). 둘째, 내부 구현을 외부에서 직접 접근하지 못하도록 숨기는 것(Information Hiding). 알약(캡슐)이 약 성분을 내부에 감싸고 있는 것처럼, 클래스가 데이터를 감싸고 보호합니다.
캡슐화 없이 발생하는 문제
java
// 캡슐화 적용 전: 데이터가 완전히 노출됨
class BankAccountBad {
public String owner;
public double balance; // 누구나 직접 접근·변경 가능
}
public class Main {
public static void main(String[] args) {
BankAccountBad account = new BankAccountBad();
account.owner = "홍길동";
account.balance = 1_000_000;
// 문제 ①: 유효성 검사 없이 잘못된 값 설정 가능
account.balance = -999_999; // 음수 잔액! 누구도 막지 않음
// 문제 ②: 비즈니스 규칙 우회 가능
account.balance = account.balance * 1000; // 입출금 기록 없이 잔액 조작
}
}
캡슐화 적용: 접근 제어자와 getter/setter
java
public class BankAccount {
private String owner; // private: 외부 직접 접근 차단
private double balance; // private: 검증된 방법으로만 변경 가능
private final List<String> transactionHistory = new ArrayList<>();
public BankAccount(String owner, double initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("초기 잔액은 0 이상이어야 합니다.");
}
this.owner = owner;
this.balance = initialBalance;
}
// 입금: 유효성 검사 + 기록 포함
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("입금액은 0보다 커야 합니다: " + amount);
}
balance += amount;
transactionHistory.add("입금: +" + amount + " → 잔액: " + balance);
}
// 출금: 잔액 부족 검사 + 기록 포함
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("출금액은 0보다 커야 합니다: " + amount);
}
if (amount > balance) {
throw new IllegalStateException("잔액 부족. 현재 잔액: " + balance);
}
balance -= amount;
transactionHistory.add("출금: -" + amount + " → 잔액: " + balance);
}
// getter: 읽기는 허용
public double getBalance() { return balance; }
public String getOwner() { return owner; }
// 거래 이력은 방어적 복사로 반환 (내부 리스트 노출 방지)
public List<String> getTransactionHistory() {
return Collections.unmodifiableList(transactionHistory);
}
}
자바 접근 제어자 4단계
캡슐화의 핵심 도구는 **접근 제어자(Access Modifier)**입니다.
접근 범위 (좁음 → 넓음):
private → default → protected → public
┌────────────┬──────┬─────────┬───────────┬──────────┐
│ 접근 가능 │ 같은 │ 같은 │ 하위 │ 모든 │
│ 범위 │ 클래스│ 패키지 │ 클래스 │ 클래스 │
├────────────┼──────┼─────────┼───────────┼──────────┤
│ private │ ✅ │ ❌ │ ❌ │ ❌ │
│ (default) │ ✅ │ ✅ │ ❌ │ ❌ │
│ protected │ ✅ │ ✅ │ ✅ │ ❌ │
│ public │ ✅ │ ✅ │ ✅ │ ✅ │
└────────────┴──────┴─────────┴───────────┴──────────┘
실무 설계 원칙은 “가능한 가장 좁은 접근 범위를 선택하라” 입니다. 필드는 거의 항상 private, 외부에 공개할 메서드만 public, 하위 클래스에만 허용할 내용은 protected로 선언합니다.
캡슐화가 주는 실질적 이점
캡슐화를 제대로 적용하면 BankAccount 내부 구현(예: balance를 BigDecimal로 교체)을 언제든 바꿔도 외부 코드(deposit(), withdraw() 호출 부분)는 전혀 수정할 필요가 없습니다. 이것이 바로 캡슐화가 만드는 변경에 강한 코드의 본질입니다.
3. 상속(Inheritance): 코드 재사용과 계층 구조의 설계
상속은 이미 존재하는 클래스(부모 클래스, Superclass)의 필드와 메서드를 새로운 클래스(자식 클래스, Subclass)가 물려받는 메커니즘입니다. extends 키워드로 표현하며 “A는 B의 일종이다(A is-a B)” 관계를 코드로 표현합니다.
상속의 기본 구조
java
// 부모 클래스 (공통 속성과 동작 정의)
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 공통 동작: 모든 동물이 공유
public void eat() {
System.out.println(name + "이(가) 먹이를 먹습니다.");
}
public void sleep() {
System.out.println(name + "이(가) 잠을 잡니다.");
}
// 자식 클래스에서 반드시 재정의할 동작
public void makeSound() {
System.out.println(name + "이(가) 소리를 냅니다.");
}
@Override
public String toString() {
return name + "(나이: " + age + ")";
}
}
// 자식 클래스: Animal의 모든 것을 물려받음
public class Dog extends Animal {
private String breed; // 강아지만의 추가 속성
public Dog(String name, int age, String breed) {
super(name, age); // 부모 생성자 호출 (첫 줄에 위치해야 함)
this.breed = breed;
}
// 오버라이딩: 부모 메서드를 자식에 맞게 재정의
@Override
public void makeSound() {
System.out.println(name + "이(가) 멍멍! 합니다.");
}
// 강아지만의 추가 동작
public void fetch() {
System.out.println(name + "이(가) 공을 가져옵니다.");
}
public String getBreed() { return breed; }
}
public class Cat extends Animal {
private boolean isIndoor;
public Cat(String name, int age, boolean isIndoor) {
super(name, age);
this.isIndoor = isIndoor;
}
@Override
public void makeSound() {
System.out.println(name + "이(가) 야옹~ 합니다.");
}
public void purr() {
System.out.println(name + "이(가) 그루밍합니다.");
}
}
super 키워드: 부모와 소통하는 방법
java
public class ElectricCar extends Car {
private int batteryLevel;
public ElectricCar(String model, int batteryLevel) {
super(model); // ① 부모 생성자 호출 (반드시 첫 줄)
this.batteryLevel = batteryLevel;
}
@Override
public String getInfo() {
// ② 부모 메서드 호출 후 추가 정보 덧붙이기
return super.getInfo() + " | 배터리: " + batteryLevel + "%";
}
}
상속의 주의점: 남용하면 독이 된다
상속은 강력하지만 잘못 사용하면 오히려 유지보수를 어렵게 만듭니다.
상속 적합 vs 부적합 판단 기준:
✅ 적합: "A는 B의 일종이다(is-a)" 관계
Dog is-a Animal → ✅ Dog extends Animal
ElectricCar is-a Car → ✅ ElectricCar extends Car
❌ 부적합: "A는 B를 가진다(has-a)" 관계
Car has-a Engine → ❌ Car extends Engine (잘못된 설계)
Car has-a Engine → ✅ class Car { Engine engine; } (올바른 구성)
자바는 **단일 상속(Single Inheritance)**만 허용합니다. 하나의 클래스는 하나의 부모 클래스만 extends할 수 있습니다. 이는 다중 상속에서 발생하는 **다이아몬드 문제(Diamond Problem)**를 원천 차단하기 위한 설계 결정입니다. 대신 자바는 인터페이스 다중 구현으로 이 한계를 보완합니다(추상화 섹션 참고).
4. 다형성(Polymorphism): 하나의 인터페이스, 다양한 구현
**다형성(Polymorphism)**은 “하나의 타입으로 여러 형태를 다룰 수 있다”는 원칙입니다. 그리스어로 “poly(많은) + morphe(형태)”를 의미합니다. 자바에서 다형성은 크게 두 가지 형태로 나타납니다.
- 컴파일 타임 다형성: 메서드 오버로딩(Overloading) — 같은 이름, 다른 매개변수
- 런타임 다형성: 메서드 오버라이딩(Overriding) — 참조 타입은 부모, 실제 객체는 자식
실무에서 “다형성”이라고 할 때는 대부분 런타임 다형성을 의미합니다.
오버로딩 vs 오버라이딩 비교
java
public class Calculator {
// 오버로딩(Overloading): 같은 이름, 다른 매개변수 → 컴파일 타임 결정
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int a, int b, int c) { return a + b + c; }
public String add(String a, String b) { return a + b; }
}
// 오버라이딩(Overriding): 부모 메서드를 자식에서 재정의 → 런타임 결정
class Shape {
public double area() { return 0; }
}
class Circle extends Shape {
private double radius;
Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; } // 런타임에 이 버전 호출
}
class Rectangle extends Shape {
private double width, height;
Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
public double area() { return width * height; } // 런타임에 이 버전 호출
}
런타임 다형성의 핵심: 업캐스팅과 동적 바인딩
java
public class PolymorphismDemo {
public static void main(String[] args) {
// 업캐스팅(Upcasting): 자식 → 부모 타입으로 자동 변환
Shape s1 = new Circle(5.0); // Circle 객체를 Shape 타입으로 참조
Shape s2 = new Rectangle(4.0, 6.0);
Shape s3 = new Triangle(3.0, 4.0);
// 동적 바인딩(Dynamic Binding): 런타임에 실제 객체의 메서드 호출
System.out.println(s1.area()); // Circle의 area() 호출: 78.53...
System.out.println(s2.area()); // Rectangle의 area() 호출: 24.0
System.out.println(s3.area()); // Triangle의 area() 호출: 6.0
// 가장 강력한 활용: 배열이나 컬렉션으로 통합 처리
List<Shape> shapes = List.of(
new Circle(3.0),
new Rectangle(4.0, 5.0),
new Circle(7.0),
new Rectangle(2.0, 8.0)
);
// 타입 분기 없이 모든 도형의 넓이를 동일한 코드로 계산
double totalArea = shapes.stream()
.mapToDouble(Shape::area) // 각각의 area() 자동 호출
.sum();
System.out.println("전체 넓이 합: " + totalArea);
}
}
다형성이 없다면 어떻게 됐을까요?
java
// 다형성이 없는 세계: 타입마다 분기 처리 필요 (최악의 코드)
for (Object shape : shapes) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
total += Math.PI * c.getRadius() * c.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
total += r.getWidth() * r.getHeight();
} else if (shape instanceof Triangle) {
// 새 도형이 추가될 때마다 이 분기를 수정해야 함 → OCP 위반
}
}
새 도형이 추가될 때마다 모든 분기문을 찾아 수정해야 합니다. 다형성을 활용하면 새 도형 클래스를 추가하고 area()만 구현하면 기존 코드를 전혀 건드리지 않아도 됩니다.
다운캐스팅: 자식 타입으로 되돌리기
java
Shape shape = new Circle(5.0); // 업캐스팅
// 다운캐스팅(Downcasting): 부모 → 자식 타입으로 명시적 변환
// 실제 객체가 Circle이 아니라면 ClassCastException 발생!
if (shape instanceof Circle circle) { // Java 16+ 패턴 매칭
System.out.println("반지름: " + circle.getRadius()); // Circle 전용 메서드 접근
}
// Java 15 이하 방식 (instanceof 후 캐스팅)
if (shape instanceof Circle) {
Circle c = (Circle) shape; // 명시적 캐스팅
System.out.println("반지름: " + c.getRadius());
}
5. 추상화(Abstraction): 복잡함을 감추고 핵심만 드러낸다
추상화는 복잡한 내부 구현을 숨기고 사용자가 알아야 할 핵심 인터페이스만 노출하는 원칙입니다. 리모컨으로 TV를 켤 때 TV 내부 회로를 알 필요가 없는 것처럼, 추상화는 “무엇을 하는가”를 정의하고 “어떻게 하는가”는 구현 클래스에 위임합니다.
자바에서 추상화는 두 가지 도구로 구현됩니다. **추상 클래스(abstract class)**와 **인터페이스(interface)**입니다.
추상 클래스(abstract class)
java
// 추상 클래스: 직접 인스턴스 생성 불가
public abstract class Vehicle {
protected String brand;
protected int speed;
public Vehicle(String brand) {
this.brand = brand;
this.speed = 0;
}
// 추상 메서드: 하위 클래스가 반드시 구현해야 함 (몸체 없음)
public abstract void accelerate(int amount);
public abstract void brake(int amount);
public abstract String getFuelType();
// 일반 메서드: 공통 구현 제공 (하위 클래스가 그대로 사용 가능)
public void displayInfo() {
System.out.printf("[%s] 현재 속도: %d km/h | 연료: %s%n",
brand, speed, getFuelType());
}
// final 메서드: 공통 안전 로직은 오버라이딩 금지
public final void emergencyStop() {
speed = 0;
System.out.println(brand + " 비상 정지!");
}
}
// 구체 클래스: 모든 추상 메서드를 반드시 구현
public class GasolineCar extends Vehicle {
public GasolineCar(String brand) {
super(brand);
}
@Override
public void accelerate(int amount) {
speed = Math.min(speed + amount, 200); // 최고 속도 200 제한
System.out.println(brand + " 가속: " + speed + "km/h");
}
@Override
public void brake(int amount) {
speed = Math.max(speed - amount, 0);
System.out.println(brand + " 감속: " + speed + "km/h");
}
@Override
public String getFuelType() { return "가솔린"; }
}
public class ElectricCar extends Vehicle {
private int batteryLevel;
public ElectricCar(String brand, int batteryLevel) {
super(brand);
this.batteryLevel = batteryLevel;
}
@Override
public void accelerate(int amount) {
if (batteryLevel < 10) {
System.out.println("배터리 부족! 충전 필요");
return;
}
speed = Math.min(speed + amount, 250); // 전기차는 최고 250
batteryLevel -= amount / 10;
System.out.println(brand + " 가속: " + speed + "km/h (배터리: " + batteryLevel + "%)");
}
@Override
public void brake(int amount) {
speed = Math.max(speed - amount, 0);
batteryLevel = Math.min(batteryLevel + amount / 20, 100); // 회생 제동
System.out.println(brand + " 감속: " + speed + "km/h (배터리 회생: " + batteryLevel + "%)");
}
@Override
public String getFuelType() { return "전기"; }
}
인터페이스(interface): 순수 계약의 정의
java
// 인터페이스: 구현 없이 "무엇을 할 수 있는가"만 정의
public interface Payable {
void pay(double amount); // 추상 메서드 (public abstract 생략)
double getBalance();
boolean isValid();
// Java 8+: default 메서드 (기본 구현 제공)
default String getPaymentSummary() {
return "결제 가능 여부: " + isValid() + " | 잔액: " + getBalance();
}
// Java 8+: static 메서드
static Payable ofCredit(String cardNumber) {
return new CreditCard(cardNumber);
}
}
public interface Rechargeable {
void recharge(double amount);
double getChargeLimit();
}
// 인터페이스 다중 구현: 자바 단일 상속의 한계를 보완
public class PrepaidCard implements Payable, Rechargeable {
private double balance;
private final String cardId;
private static final double CHARGE_LIMIT = 500_000;
public PrepaidCard(String cardId, double initialBalance) {
this.cardId = cardId;
this.balance = initialBalance;
}
// Payable 구현
@Override
public void pay(double amount) {
if (!isValid() || amount > balance) {
throw new IllegalStateException("결제 불가: 잔액 부족 또는 유효하지 않은 카드");
}
balance -= amount;
System.out.printf("결제 완료: %.0f원 | 남은 잔액: %.0f원%n", amount, balance);
}
@Override
public double getBalance() { return balance; }
@Override
public boolean isValid() { return balance > 0; }
// Rechargeable 구현
@Override
public void recharge(double amount) {
if (balance + amount > CHARGE_LIMIT) {
throw new IllegalArgumentException("충전 한도 초과: " + CHARGE_LIMIT + "원");
}
balance += amount;
System.out.printf("충전 완료: %.0f원 | 현재 잔액: %.0f원%n", amount, balance);
}
@Override
public double getChargeLimit() { return CHARGE_LIMIT; }
}
추상 클래스 vs 인터페이스: 언제 무엇을 쓸까
abstract class vs interface 선택 기준:
abstract class를 선택해야 할 때:
✅ "A는 B의 일종(is-a)" 관계
✅ 공통 상태(필드)를 공유해야 할 때
✅ 하위 클래스에 기본 구현(일반 메서드)을 제공해야 할 때
✅ 생성자 로직이 필요할 때
예: Vehicle(추상) → GasolineCar, ElectricCar(구체)
interface를 선택해야 할 때:
✅ "A는 B를 할 수 있다(can-do)" 관계
✅ 서로 무관한 클래스가 공통 계약을 구현해야 할 때
✅ 다중 구현이 필요할 때
✅ 완전한 분리와 유연한 교체가 필요할 때
예: Payable(인터페이스) → PrepaidCard, CreditCard, Wallet
결론: 공유할 상태(필드)가 있으면 abstract class,
순수한 행동(계약)만 정의한다면 interface
6. 4대 원칙의 연결과 SOLID로의 확장: 실전 설계 관점
4대 원칙은 각각 독립적이지 않습니다. 실전 코드에서는 항상 함께 사용되며, 더 나아가 SOLID 원칙이라는 실무 설계 지침으로 발전합니다.
4대 원칙이 함께 작동하는 예시
java
// 4대 원칙이 모두 적용된 결제 시스템 예제
// ① 추상화: 결제 방법의 계약 정의
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
boolean supports(PaymentMethod method);
}
// ② 상속 + 캡슐화: 공통 로직을 추상 클래스로 묶고 내부 보호
public abstract class BasePaymentProcessor implements PaymentProcessor {
private final String processorId; // 캡슐화: private 필드
private final List<String> processedIds // 캡슐화: 외부 직접 접근 차단
= new ArrayList<>();
protected BasePaymentProcessor(String processorId) {
this.processorId = processorId;
}
// 템플릿 메서드 패턴: 전체 흐름은 final로 고정
@Override
public final PaymentResult process(PaymentRequest request) {
validate(request); // 공통 검증
PaymentResult result = doProcess(request); // 하위 클래스가 구현
record(request, result); // 공통 기록
return result;
}
// 추상 메서드: 각 결제 방법만의 구체적 처리
protected abstract PaymentResult doProcess(PaymentRequest request);
private void validate(PaymentRequest request) {
if (request == null || request.getAmount() <= 0) {
throw new IllegalArgumentException("유효하지 않은 결제 요청");
}
}
private void record(PaymentRequest req, PaymentResult result) {
processedIds.add(req.getOrderId());
System.out.println("[" + processorId + "] 처리 완료: " + req.getOrderId());
}
}
// ③ 다형성: 같은 인터페이스로 다양한 결제 방법 구현
public class CardPaymentProcessor extends BasePaymentProcessor {
public CardPaymentProcessor() { super("CARD"); }
@Override
public PaymentResult doProcess(PaymentRequest request) {
// 카드 결제 전용 로직 (PG사 API 호출 등)
return PaymentResult.success(request.getOrderId(), "카드");
}
@Override
public boolean supports(PaymentMethod method) {
return method == PaymentMethod.CREDIT_CARD
|| method == PaymentMethod.DEBIT_CARD;
}
}
public class KakaoPayProcessor extends BasePaymentProcessor {
public KakaoPayProcessor() { super("KAKAO"); }
@Override
public PaymentResult doProcess(PaymentRequest request) {
// 카카오페이 전용 로직 (카카오 API 호출 등)
return PaymentResult.success(request.getOrderId(), "카카오페이");
}
@Override
public boolean supports(PaymentMethod method) {
return method == PaymentMethod.KAKAO_PAY;
}
}
// 클라이언트: 다형성으로 결제 방법 무관하게 동일한 코드 사용
public class PaymentService {
private final List<PaymentProcessor> processors; // 인터페이스 타입으로 관리
public PaymentService(List<PaymentProcessor> processors) {
this.processors = processors;
}
public PaymentResult pay(PaymentRequest request) {
return processors.stream()
.filter(p -> p.supports(request.getMethod()))
.findFirst()
.map(p -> p.process(request)) // 다형성: 실제 구현체의 process() 호출
.orElseThrow(() -> new UnsupportedOperationException(
"지원하지 않는 결제 방법: " + request.getMethod()));
}
}
OOP 4대 원칙 → SOLID 원칙으로의 확장
| OOP 원칙 | 연결되는 SOLID 원칙 | 핵심 메시지 |
|---|---|---|
| 캡슐화 | SRP (단일 책임 원칙) | 클래스는 하나의 책임만 가져야 한다 |
| 추상화 | OCP (개방-폐쇄 원칙) | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다 |
| 상속 | LSP (리스코프 치환 원칙) | 자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 한다 |
| 추상화 | ISP (인터페이스 분리 원칙) | 인터페이스는 클라이언트가 사용하는 메서드만 포함해야 한다 |
| 다형성 | DIP (의존성 역전 원칙) | 구체 구현이 아닌 추상화에 의존해야 한다 |
4대 원칙 한 줄 요약
캡슐화: "내 데이터는 내가 관리한다. 외부는 정해진 방법으로만 접근하라."
상속: "공통된 것은 부모에 두고, 고유한 것만 자식에서 정의한다."
다형성: "타입은 하나지만, 실제 동작은 객체에 따라 다르게 실행된다."
추상화: "무엇을 하는지 약속하되, 어떻게 하는지는 각자 결정한다."
결론
자바 객체지향 4대 원칙인 캡슐화·상속·다형성·추상화는 각각 독립된 규칙이 아니라 하나의 철학을 이루는 네 개의 기둥입니다. 캡슐화로 내부를 보호하고, 상속으로 공통 코드를 묶고, 다형성으로 다양한 구현을 하나의 타입으로 다루고, 추상화로 변경에 유연한 계약을 만들 때 비로소 유지보수하기 쉽고 확장하기 좋은 코드가 탄생합니다. 오늘 배운 4대 원칙을 다시 읽으며 지금 작성 중인 코드에 각각의 원칙이 올바르게 적용되고 있는지 점검해 보세요.
⚠️ 기술 적용 면책 고지: 본 포스트의 코드 예제는 Java 11–17 LTS 기준으로 작성된 교육용 예제입니다. 실제 결제 시스템 구현에는 보안·트랜잭션·예외 처리 등 추가적인 설계 요소가 필요하며, 프로덕션 코드 적용 전 공식 문서와 팀 컨벤션을 함께 참고하시기 바랍니다.
답글 남기기