
오늘은 객체지향 프로그래밍(Object-Oriented Programming, OOP) 을 자바 관점에서 빠르고 명확하게 이해하는 시간을 가져보도록 할게요!
이 글을 읽고 나면 다음 내용을 알게 될 거에요!
- 객체와 클래스의 차이를 자신의 언어로 정리할 수 있다.
- OOP의 핵심 개념(추상화, 캡슐화, 상속, 다형성)을 예제 코드와 함께 이해한다.
- SOLID 원칙을 실제 코드 구조에 어떻게 적용할지 감을 잡는다.
- “클래스 중심”과 “객체 중심”이라는 표현의 차이를 명확히 구분한다.
객체 vs 클래스 — 왜 헷갈릴까?
객체와 클래스를 정의해보면 다음과 같아요
- 클래스(Class): 객체를 만들기 위한 설계도(틀) 로, 필드와 메서드를 정의한다.
- 객체(Object, 인스턴스): 클래스로부터 생성된 실제 실체로, 메모리에 존재하며 상태(필드 값) 와 행동(메서드 동작) 을 가진다.
public class Person {
private String name;
private int age;
private double height;
private double weight;
public Person(String name, int age, double height, double weight) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}
public void introduce() {
System.out.println("제 이름은 " + this.name + " 입니다.");
}
}
public class Main {
public static void main(String[] args) {
Person stoneHee = new Person("stoneHee", 25, 180.0, 60.0); // ← 객체 생성
stoneHee.introduce(); // "제 이름은 stoneHee 입니다."
}
}
이 코드에서 Person은 클래스(설계도),stoneHee는 그 클래스로부터 만들어진 객체(실제 인스턴스) 입니다.

클래스와 객체가 왜 헷갈릴까요?
“자바는 클래스 단위로 프로그래밍한다” → 프로그램 구조를 바라보는 관점에서의 표현
“자바는 객체 중심으로 프로그래밍한다” → 문제를 모델링하고 설계하는 관점에서의 표현
결국 둘 다 맞는 말이에요.
설계 단계에서는 객체의 관계와 역할을 고민하고,
구현 단계에서는 그 객체들을 표현할 클래스 단위로 코드를 작성합니다.
OOP의 4가지 핵심 특징 (with 자바 코드)
객체지향 프로그래밍(OOP)은 크게 추상화, 캡슐화, 상속, 다형성 네 가지 특징으로 요약됩니다.
이 네 가지를 이해하면 “왜 객체지향이 유지보수에 강한가”를 자연스럽게 체감할 수 있을거에요.
1. 추상화(Abstraction)
복잡한 개념에서 핵심만 드러내고, 나머지는 숨긴다.
public abstract class Car {
public abstract void drive();
public abstract void brake();
}
public class Sedan extends Car {
@Override public void drive() { System.out.println("세단을 운전합니다"); }
@Override public void brake() { System.out.println("세단이 정지합니다"); }
}
public class SportsCar extends Car {
@Override public void drive() { System.out.println("스포츠카를 운전합니다"); }
@Override public void brake() { System.out.println("스포츠카가 정지합니다"); }
}
Car 클래스는 “무엇을 할 수 있는가”라는 계약(Interface) 만 정의합니다.
실제 동작은 하위 클래스(Sedan, SportsCar)에서 구현하죠.
즉, 공통된 개념은 위로 구체적인 구현은 아래로 배치하는 것이 추상화의 핵심입니다.
2. 캡슐화(Encapsulation)
데이터를 외부로부터 보호하고, 공개된 메서드로만 접근하게 만든다.
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
if (initialBalance < 0) throw new IllegalArgumentException("잔액은 0보다 적을 수 없습니다");
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("입금액은 0보다 커야 합니다");
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0 || amount > balance)
throw new IllegalArgumentException("출금액은 0보다 크고 잔액 이하여야 합니다");
balance -= amount;
}
public double getBalance() { return balance; }
}
balance는 private으로 숨기고,deposit()과 withdraw()를 통해서만 변경할 수 있습니다.
이렇게 하면 객체의 무결성(integrity) 을 유지하면서도 필요한 조작만 허용할 수 있습니다.
3. 상속(Inheritance)
공통 속성과 동작을 재사용하고, 필요한 부분만 확장한다.
class Animal {
private String name;
public void setName(String name) { this.name = name; }
protected String getName() { return name; }
public void sound() { System.out.println("동물이 소리를 냅니다"); }
}
class Dog extends Animal {
public void wagTail() { System.out.println(getName() + "이(가) 꼬리를 흔듭니다"); }
@Override public void sound() { System.out.println(getName() + "이(가) 멍멍 짖습니다"); }
}
class Cat extends Animal {
public void purr() { System.out.println(getName() + "이(가) 그르릉합니다"); }
@Override public void sound() { System.out.println(getName() + "이(가) 야옹합니다"); }
}
Animal이 공통된 속성과 동작을 정의하고,Dog, Cat이 각자 필요한 기능을 확장합니다.
즉, 공통은 위로, 특수화는 아래로 구조를 나누면 중복을 최소화하고 유지보수를 쉽게 할 수 있습니다.
4. 다형성(Polymorphism)
같은 인터페이스로 서로 다른 구현을 동일하게 다루는 것
interface Shape { void draw(); }
class Circle implements Shape {
@Override public void draw() { System.out.println("원을 그립니다"); }
}
class Rectangle implements Shape {
@Override public void draw() { System.out.println("사각형을 그립니다"); }
}
class Triangle implements Shape {
@Override public void draw() { System.out.println("삼각형을 그립니다"); }
}
public class TestPolymorphism {
public static void main(String[] args) {
Shape[] shapes = { new Circle(), new Rectangle(), new Triangle() };
for (Shape s : shapes) drawShape(s);
}
private static void drawShape(Shape shape) { shape.draw(); } // ← 동일한 방식으로 호출
}
drawShape()는 Shape 타입만 알고 있지만,
실제 어떤 도형이 들어오든 각자의 방식으로 동작합니다.
이게 바로 다형성의 핵심인 확장에는 열려 있고, 수정에는 닫혀 있는 구조(Open–Closed Principle) 입니다.
좋은 객체 지향 설계의 5가지 원칙(SOLID)

SOLID 원칙은 객체지향 설계의 핵심 가이드라인이에요.
이 다섯 가지를 이해하면 코드가 훨씬 유연하고 변경에 강한 구조로 바뀝니다.
1. SRP (단일 책임 원칙, Single Responsibility Principle)
하나의 클래스는 단 하나의 책임(변경 이유)만 가져야 한다.
public class OrderProcessor {
private final OrderSaver saver;
private final EmailSender sender;
public OrderProcessor(OrderSaver saver, EmailSender sender) {
this.saver = saver;
this.sender = sender;
}
public void process(Order order) {
// ... 비즈니스 로직 ...
saver.save(order);
sender.sendConfirmation(order);
}
}
public class OrderSaver { public void save(Order order) { /* DB 저장 */ } }
public class EmailSender { public void sendConfirmation(Order order) { /* 이메일 전송 */ } }
OrderProcessor는 주문 처리만 담당하고,
저장(OrderSaver)과 통지(EmailSender)는 별도의 클래스로 분리했습니다.
→ 책임 분리로 변경 파급 범위를 최소화할 수 있습니다.
2. OCP (개방-폐쇄 원칙, Open–Closed Principle)
확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
interface Shape { double area(); }
class Rectangle implements Shape {
private final double w, h;
public Rectangle(double w, double h) { this.w = w; this.h = h; }
public double area() { return w * h; }
}
class Circle implements Shape {
private final double r;
public Circle(double r) { this.r = r; }
public double area() { return Math.PI * r * r; }
}
class AreaCalculator {
public double total(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}
}
// 새로운 도형 추가
class Triangle implements Shape {
private final double b, h;
public Triangle(double b, double h) { this.b = b; this.h = h; }
public double area() { return 0.5 * b * h; }
}
새로운 Shape 구현(Triangle)을 추가해도AreaCalculator는 코드 수정 없이 그대로 동작합니다.
→ 즉, 기존 코드는 닫혀 있고, 새로운 기능 추가에는 열려 있는 구조가 됩니다.
3. LSP (리스코프 치환 원칙, Liskov Substitution Principle)
하위 타입은 상위 타입으로 무리 없이 대체 가능해야 한다.
interface FlyingBird { void fly(); }
class Sparrow implements FlyingBird {
public void fly() { System.out.println("참새가 납니다"); }
}
class Penguin { // 날지 않음
public void swim() { System.out.println("펭귄이 수영합니다"); }
}
모든 새(Bird)가 fly()를 가진다고 가정하면,
펭귄처럼 날 수 없는 새는 설계상 모순이 생깁니다.
→ 불가능한 동작을 강요하지 않도록 올바른 추상화가 필요합니다.
4. ISP (인터페이스 분리 원칙, Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안 된다.
interface Workable { void work(); }
interface Eatable { void eat(); }
class Developer implements Workable, Eatable {
public void work() { System.out.println("개발자가 일합니다"); }
public void eat() { System.out.println("개발자가 식사합니다"); }
}
class Robot implements Workable {
public void work() { System.out.println("로봇이 일합니다"); }
}
일만 하는 Robot에게 eat()을 강요할 필요는 없습니다.
→ 인터페이스를 역할별로 나누면, 불필요한 구현 강제를 피할 수 있습니다.
5. DIP (의존관계 역전 원칙, Dependency Inversion Principle)
고수준과 저수준 모듈이 모두 추상화에 의존해야 한다.
interface SwitchableDevice {
void turnOn();
void turnOff();
}
class LightBulb implements SwitchableDevice {
public void turnOn() { System.out.println("전등 ON"); }
public void turnOff() { System.out.println("전등 OFF"); }
}
class Switch {
private final SwitchableDevice device;
public Switch(SwitchableDevice device) { this.device = device; }
public void operate() {
// ... 로직 ...
device.turnOn();
}
}
Switch는 LightBulb 같은 구체 클래스가 아닌 인터페이스(SwitchableDevice) 에 의존합니다.
그래서 나중에 Fan, Heater 등 다른 장치로 쉽게 교체할 수 있죠.
→ 의존 방향을 추상화로 역전시키면, 코드 확장이 훨씬 유연해집니다.
객체지향을 하면서 생기는 오해 바로잡기
객체지향을 배우다 보면 헷갈리기 쉬운 오해들이 있습니다.
아래 내용을 읽어보면서 같이 점검해보면 좋을 것 같아요 :)
- “상속으로 재사용만 늘리면 된다?”
→ ❌ 상속을 남용하면 결합도가 높아지고 유연성이 떨어집니다.
먼저 구성(Composition)으로 해결할 수 없는지 고려하세요. - 인터페이스는 나중에 추출해도 된다?
→ ❌ 역할 중심 인터페이스를 초기에 잡아야 합니다.
그래야 OCP, LSP, DIP 같은 원칙이 자연스럽게 녹아듭니다. - Getter/Setter는 다 열어두자?
→ ❌ 캡슐화가 무너집니다.
불변성이나 유효성 검사를 메서드 내부에서 보장하도록 설계하세요.
특히 Spring 기반으로 개발을 하다보면 자연스럽게 Lombok의 @getter / @setter 어노테이션을 사용하게 되는데 이를 점검해 볼 필요가 있어요
객체지향 프로그래밍(OOP)은 단순히 코드를 묶는 방식이 아니라,
서로 협력하는 객체들의 모델로 문제를 표현하는 사고법입니다.
추상화·캡슐화·상속·다형성을 역할 기반 설계로 엮고,
SOLID 원칙으로 변경에 강한 구조를 만든다면,
요구사항이 바뀌어도 확장으로 대응할 수 있는 코드를 만들 수 있습니다.
저는 객체지향 애용자(?)로서 객체지향적인 코드를 잘 짜는 개발자가 되도록 정진해보겠습니다 !
'Computer Science > 프로그래밍 이론 💬' 카테고리의 다른 글
| 일급 컬렉션이란? (0) | 2024.02.07 |
|---|---|
| 비동기 프로그래밍 이해하기 - 동기 방식과 뭐가 다를까? (0) | 2024.02.06 |
| Dependency Injection이란? (0) | 2024.02.06 |
| 제어의 역전(IOC) 톺아 보기 (0) | 2024.02.06 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆