
“기계가 이해할 수 있는 코드는 누구나 쓸 수 있다.
하지만 사람이 이해할 수 있는 코드를 쓰는 것은 훈련된 개발자만이 할 수 있다.”
깨끗한 코드는 단순히 작동하는 코드가 아니라
읽히고 유지보수 가능한 코드를 의미한다.
즉, 나뿐만 아니라 동료 개발자나 미래의 내가 봐도
무엇을 의도했는지 쉽게 이해할 수 있어야 한다.
→ 클린 코드란 ‘의도를 전달하는 코드’이며, 이는 개발자의 책임이다.
객체지향 프로그래밍(Object-Oriented Programming, OOP)은
프로그램을 객체(Object)라는 독립적인 단위로 나누어 설계하는 방법이다.
각 객체는 자신만의 상태(state)와 행동(behavior)을 가지며,
서로 메시지를 주고받으며 협력한다.
즉, “무엇을 할 것인가”보다
“누가 그 일을 해야 하는가”에 초점을 맞춘 사고방식이다.

절차지향(Procedural) 프로그래밍은 데이터를 중심으로 함수가 동작한다.
반면 객체지향은 데이터를 다루는 함수를 객체 내부로 숨기고,
객체 간의 협력을 통해 기능을 완성한다.
| 구분 | 절차지향 | 객체지향 |
|---|---|---|
| 중심 | 함수(로직) | 객체(역할과 책임) |
| 데이터 | 외부에서 직접 조작 | 객체 내부에서 스스로 관리 |
| 결합도 | 높음 | 낮음 (캡슐화로 분리) |
절차지향은 “순서” 중심의 설계라면,
객체지향은 “관계와 협력” 중심의 설계이다.
객체는 스스로 책임을 지는 존재다
즉, 객체는 단순히 데이터를 담는 구조체가 아니라,
하나의 역할(Role)을 수행하고 책임(Responsibility)을 다하는 주체다.
예를 들어, 주문 시스템을 생각해보자.
class Customer {
private Order order;
public void requestOrder() {
order.place();
}
}
class Order {
public void place() {
System.out.println("주문이 완료되었습니다.");
}
}
Customer는 주문을 요청하는 역할,
Order는 실제 주문을 처리하는 책임을 가진다.
두 객체는 서로의 내부 구현을 모르고, 오직 메시지를 통해 협력한다.
→ 이런 설계는 결합도를 낮추고, 객체 간 책임이 명확해진다.
캡슐화는 데이터와 기능을 하나로 묶고,
외부에서 객체의 내부 상태를 직접 접근하지 못하도록 숨기는 것이다.
이 원칙이 깨지면, 내부 제약이 쉽게 무너진다.
class Speaker {
int volume;
void volumeUp() {
volume += 10;
}
}
외부에서 speaker.volume = 200;처럼 직접 수정이 가능하다.
이 경우, 내부 제약(100 이하 제한 등)이 쉽게 깨진다.
class Speaker {
private int volume;
void volumeUp() {
if (volume >= 100) {
System.out.println("최대 음량입니다.");
return;
}
volume += 10;
}
void showVolume() {
System.out.println("현재 음량: " + volume);
}
}
필드는 private으로 감추고,
외부는 메서드를 통해서만 객체에 요청을 보낼 수 있다.
→ 내부 데이터는 보호되고, 외부는 불필요한 정보를 알 필요가 없다.
“데이터를 꺼내서 조작하지 말고, 객체에게 시켜라.”
객체지향의 핵심은 데이터를 직접 조작하는 것이 아니라,
객체에게 메시지를 던져 일을 맡기는 것이다.
// 외부에서 데이터 직접 사용
if (order.getTotalPrice() > 10000) {
order.discount(1000);
}
// 객체에게 책임을 위임
order.applyDiscountIfEligible();
외부는 단지 “무엇을 할지”를 요청하고,
“어떻게 할지”는 객체 스스로 판단한다.
→ 이는 객체의 자율성을 높이고, 변경에 강한 구조를 만든다.
추상화는 공통된 특징을 정의하고 세부 구현을 감추는 것이다.
다형성은 같은 메시지에 서로 다른 방식으로 응답할 수 있게 하는 것이다.
interface Payment {
void pay();
}
class KakaoPay implements Payment {
public void pay() {
System.out.println("카카오페이 결제");
}
}
class NaverPay implements Payment {
public void pay() {
System.out.println("네이버페이 결제");
}
}
class OrderService {
private Payment payment;
public OrderService(Payment payment) {
this.payment = payment;
}
public void order() {
payment.pay();
}
}
OrderService는 Payment 인터페이스에만 의존한다.
결제 수단이 무엇이든 pay() 메시지만 있으면 동작할 수 있다.
→ 새로운 결제 수단이 추가되어도 기존 코드는 수정되지 않는다.
→ 즉, 확장에는 열려 있고, 변경에는 닫혀 있는 구조(OCP)이다.
상속은 코드 재사용에 편리하지만,
부모 클래스의 변화가 자식에게 바로 영향을 주기 때문에 결합도가 높다.
따라서 조합(Composition)을 우선 고려해야 한다.
class ElectricCar extends Car {
void charge() { ... }
}
→ 부모 클래스(Car)가 바뀌면 자식 클래스(ElectricCar)도 영향을 받는다.
class Engine {
void start() {
System.out.println("엔진 시동");
}
}
class Car {
private Engine engine = new Engine();
void drive() {
engine.start();
System.out.println("주행 시작");
}
}
→ Car는 Engine을 포함(Has-a)하므로,
엔진의 동작이 바뀌어도 Car는 그대로 유지된다.
객체지향의 최종 목표는 의존성을 줄이는 것이다.
의존성이 높으면 하나의 변경이 전체 코드에 영향을 미친다.
이를 방지하기 위해 DIP(Dependency Inversion Principle)를 적용한다.
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
public void send(String message) {
System.out.println("이메일 발송: " + message);
}
}
class AlertService {
private final Notification notification;
public AlertService(Notification notification) {
this.notification = notification;
}
public void alert(String message) {
notification.send(message);
}
}
AlertService는 Notification이라는 추상화에만 의존한다.
EmailNotification 대신 SlackNotification으로 교체하더라도 코드 수정이 필요 없다.
→ 구현이 아닌 추상화에 의존하는 설계가 유지보수를 단순하게 만든다.
| 핵심 개념 | 설명 |
|---|---|
| 역할과 책임 | 객체는 하나의 책임만 가지고 협력해야 한다. |
| 캡슐화 | 내부 데이터를 감추고, 필요한 기능만 외부에 공개한다. |
| 메시지 중심 | 데이터를 직접 조작하지 않고 객체에게 요청한다. |
| 추상화 | 공통 기능을 정의해 유연한 구조를 만든다. |
| 다형성 | 같은 메시지에 다른 반응을 하도록 설계한다. |
| 조합 | 상속보다 결합도를 낮추고 유연한 구조를 만든다. |
| 의존성 역전 | 구현이 아닌 추상화에 의존한다. |