들어가며
개발을 하다 보면 자연스럽게 “객체지향 프로그래밍(OOP)”이라는 단어를 자주 듣게 됩니다.
저 역시 지금까지 자바나 스프링 같은 객체지향 언어로 코드를 작성해 왔지만 문득 “나는 정말 객체지향적으로 코드를 작성하고 있을까?”라는 생각이 들었습니다.
막상 개념을 설명하려고 하면 머릿속이 잘 정리되어 있지 않더군요.
그래서 이번 글에서는 객체지향 프로그래밍의 본질부터 핵심 개념, 그리고 실제 코드 설계에 어떻게 녹아드는지까지 정리해보려 합니다.
이 글이 저 스스로의 정리이자 객체지향을 처음 접하는 분들에게도 도움이 되었으면 합니다.
1. 객체지향 프로그래밍이란?
객체지향 프로그래밍(Object-Oriented Programming, 이하 OOP)은 “세상을 객체(Object)로 바라보고 이 객체들 간의 상호작용으로 프로그램을 구성하는 방법론”입니다.
즉 프로그램을 데이터와 로직이 따로 존재하는 구조(절차지향)로 보는 것이 아니라
“데이터와 그 데이터를 다루는 기능(메서드)”을 하나의 객체로 묶어 표현하는 방식이죠.
예를 들어
- 절차지향에서는 고양이 이름, 고양이 나이, 먹기() 함수가 따로 떨어져 있습니다.
- 하지만 객체지향에서는 “고양이”라는 객체 안에 속성(이름, 나이)과 행동(먹기, 울기)를 함께 담습니다.
이렇게 함으로써 프로그램이 현실 세계와 유사한 구조를 가지게 되고, 유지보수와 확장이 훨씬 쉬워집니다.
2. 객체지향의 핵심 개념
1. 클래스(Class)와 객체(Object)
- 클래스는 객체를 만들기 위한 설계도입니다.
예: class Cat { String name; int age; void meow() { ... } } - 객체는 클래스라는 설계도를 바탕으로 만들어진 **실제 실체(Instance)**입니다.
예: Cat nabi = new Cat();
클래스는 “틀”, 객체는 “틀로 찍어낸 결과물”로 이해하면 쉽습니다.
2. 캡슐화(Encapsulation)
“데이터와 기능을 하나로 묶고, 외부에서의 직접 접근을 제한하라.”
캡슐화는 정보 은닉(Information Hiding)을 통해 객체 내부의 데이터(필드)를 보호하는 개념입니다.
예를 들어 객체의 balance(잔액)를 직접 변경하지 못하게 하고 deposit() withdraw() 같은 메서드로만 접근하게 합니다.
public class BankAccount {
private int balance;
public void deposit(int amount) {
if (amount > 0) balance += amount;
}
public int getBalance() {
return balance;
}
}
이렇게 하면 데이터 무결성이 보장되고 내부 구현을 바꾸더라도 외부 코드는 영향을 받지 않습니다.
3. 상속(Inheritance)
“부모 클래스의 특성을 자식 클래스가 물려받아 재사용하라.”
pulbic class Animal { void eat() { System.out.println("먹는다"); } } public class Dog extends Animal { void bark() { System.out.println("짖는다"); } }
상속은 기존 클래스를 기반으로 새로운 클래스를 만들어 코드의 재사용성과 확장성을 높이는 방법입니다.
Dog는 Animal의 eat() 기능을 그대로 사용하면서, 자신만의 기능(bark())을 추가할 수 있습니다.
자식은 부모의 기능을 사용할수 있지만, 부모는 자식의 기능을 사용할 수 없습니다.
다만 상속을 남용하면 코드의 결합도가 높아지므로 주의가 필요합니다.
4. 다형성(Polymorphism)
“같은 인터페이스로 서로 다른 동작을 구현할 수 있다.”
다형성은 같은 메서드 호출이지만 객체의 타입에 따라 다르게 동작하는 성질을 말합니다.
class Animal { void sound() { System.out.println("동물 소리"); } }
class Dog extends Animal { void sound() { System.out.println("멍멍"); } }
class Cat extends Animal { void sound() { System.out.println("야옹"); } }
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.sound(); // 멍멍
a2.sound(); // 야옹
이처럼 다형성을 이용하면, 새로운 클래스가 추가되어도 기존 코드를 거의 수정하지 않고 확장할 수 있습니다.
5. 추상화(Abstraction)
“필요한 부분만 드러내고, 복잡한 내부는 숨겨라.”
추상화는 객체의 복잡한 내부 구현보다는 공통적인 특징이나 기능만 표현하는 개념입니다.
예를 들어, “자동차”를 생각할 때 우리는 엔진 내부 구조보다 “시동 걸기”, “브레이크 밟기” 같은 동작만 신경 씁니다.
abstract class Vehicle {
abstract void move();
}
class Car extends Vehicle {
void move() { System.out.println("도로를 달린다."); }
}
class Boat extends Vehicle {
void move() { System.out.println("물 위를 떠다닌다."); }
}
이처럼 추상 클래스를 통해 공통 인터페이스를 정의하고, 구체적인 동작은 하위 클래스에서 구현하게 할 수 있습니다.
3. 객체지향의 설계 원칙 (SOLID)
객체지향의 본질은 단순히 문법이 아니라 좋은 설계에 있습니다.
그 중심에는 5가지 원칙, 즉 SOLID 원칙이 있습니다.
| S - 단일 책임 원칙 (SRP) | 클래스는 하나의 책임만 가져야 한다. |
| O - 개방-폐쇄 원칙 (OCP) | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다. |
| L - 리스코프 치환 원칙 (LSP) | 자식 클래스는 부모 클래스로 대체 가능해야 한다. |
| I - 인터페이스 분리 원칙 (ISP) | 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다. |
| D - 의존 역전 원칙 (DIP) | 구체적인 클래스가 아닌 추상화(인터페이스)에 의존해야 한다. |
1. S — Single Responsibility Principle (단일 책임 원칙, SRP)
정의:
클래스(모듈)는 오직 하나의 책임(reason to change)만 가져야 한다. 즉 하나의 클래스는 하나의 변경 이유만 허용해야 한다.
왜 필요한가:
책임이 뒤섞인 클래스는 변경할 때 연쇄적으로 영향을 주어 유지보수성이 급격히 떨어집니다. SRP는 결합도를 낮추고 테스트와 재사용성을 향상시킵니다.
2. O — Open/Closed Principle (개방-폐쇄 원칙, OCP)
정의:
소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다 — 즉 기존 코드를 수정하지 않고 기능을 추가할 수 있어야 한다.
왜 필요한가:
기존 코드를 건드리지 않고 기능을 추가하면 버그 유입 위험이 줄고 배포 리스크가 낮아집니다.
이 원칙들은 객체지향 설계의 방향성을 제시하며 유지보수성과 확장성 높은 코드를 만드는 핵심입니다.
3. L — Liskov Substitution Principle (리스코프 치환 원칙, LSP)
정의:
서브타입(자식 클래스)은 언제나 기반 타입(부모 클래스)으로 대체 가능해야 하며, 그 대체로 인해 프로그램의 정상 동작이 깨지면 안 된다.
왜 필요한가:
다형성을 이용할 때, 서브타입이 부모의 규약(사전조건, 사후조건, 불변식)을 어기면 예기치 못한 버그가 발생합니다. LSP는 안전한 확장을 보장합니다.
4. I — Interface Segregation Principle (인터페이스 분리 원칙, ISP)
정의:
클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다. 즉 큰 인터페이스를 여러 개의 작은 인터페이스로 분리하라.
왜 필요한가:
거대한 인터페이스는 구현 부담을 키우고, 불필요한 의존성을 만들며, 변경에 민감해진다.
5. D — Dependency Inversion Principle (의존 역전 원칙, DIP)
정의:
고수준 모듈은 저수준 모듈에 의존해서는 안 되고 둘 다 추상화에 의존해야 한다. 또한 추상화는 세부사항에 의존하면 안 되고, 세부사항이 추상화에 의존해야 한다.
왜 필요한가:
구현(세부사항)에 의존하면 변경에 취약하다. 추상화(인터페이스)에 의존하면 구현을 자유롭게 교체할 수 있다.
4. 객체지향의 장점
- 코드의 재사용성 증가
- 유지보수가 용이
- 확장성이 뛰어남
- 현실 세계의 개념을 코드로 직관적으로 모델링 가능
- 협업 시 역할 분리와 구조 이해가 쉬움
5. 객체지향 면접 단골 질문 TOP 5
1. 객체지향의 4대 특징이란 무엇이며, 각각의 의미는 무엇인가요?
질문 의도:
지원자가 단순히 용어를 외운 게 아니라, 설계적 의미를 이해하고 있는지 확인하려는 질문입니다.
답변
객체지향의 4대 특징은 캡슐화, 상속, 다형성, 추상화입니다.
먼저 캡슐화는 데이터와 메서드를 하나로 묶고 외부에는 꼭 필요한 정보만 공개하는 것입니다.예를 들어 클래스의 필드를 private으로 두고 getter/setter로 접근하게 하는 방식입니다.
상속은 상위 클래스의 기능을 하위 클래스가 물려받아 재사용할 수 있게 하는 개념입니다.코드의 중복을 줄이고 유지보수를 쉽게 만듭니다.
다형성은 하나의 객체가 여러 형태로 동작할 수 있게 하는 성질로,부모 타입으로 자식 객체를 참조할 수 있습니다.스프링에서 인터페이스를 주입받아 구현체를 교체할 수 있는 구조가 바로 다형성의 예시입니다.
마지막으로 추상화는 공통된 특성을 뽑아내어 설계하는 것입니다.예를 들어 Repository 인터페이스처럼 구체 로직은 숨기고 역할만 정의하는 방식입니다.
2. 객체지향 설계 원칙(SOLID)을 설명해보세요.
질문 의도:
OOP를 설계 관점에서 얼마나 깊이 이해하고 있는지 판단하기 위함입니다.
스프링 구조 전체가 SOLID 원칙을 기반으로 설계되어 있으므로 매우 자주 등장합니다.
답변
- S (SRP): 클래스는 하나의 책임만 가져야 한다.
→ 변경 이유가 하나여야 유지보수가 쉽다. - O (OCP): 확장에는 열려 있고, 변경에는 닫혀 있어야 한다.
→ 스프링의 DI 구조가 대표적 예시. - L (LSP): 자식 클래스는 부모 클래스의 규약을 깨지 않아야 한다.
→ 다형성을 안전하게 보장. - I (ISP): 인터페이스는 클라이언트가 필요로 하는 기능만 가져야 한다.
→ 불필요한 의존 최소화. - D (DIP): 추상화(인터페이스)에 의존해야 한다.
→ 스프링의 IoC 컨테이너와 의존성 주입 구조의 핵심.
네, SOLID는 좋은 객체지향 설계의 5가지 원칙을 말합니다.
S, 단일 책임 원칙은 클래스는 하나의 역할만 가져야 한다는 원칙입니다. 예를 들어, UserService가 회원 가입만 처리하고 이메일 전송은 MailService가 담당하는 식으로 분리해야 합니다.
O, 개방 폐쇄 원칙은 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 뜻입니다. 즉, 새로운 기능 추가 시 기존 코드를 수정하지 않고 인터페이스를 통해 확장하는 구조죠.
L, 리스코프 치환 원칙은 부모 타입으로 자식 객체를 대체해도 문제가 없어야 한다는 원칙입니다. 다형성의 안정성을 보장하죠.
I, 인터페이스 분리 원칙은 불필요한 의존을 피하기 위해 인터페이스를 작게 나누자는 것입니다.
D, 의존 역전 원칙은 구체 클래스가 아닌 추상화(인터페이스)에 의존해야 한다는 의미입니다. 스프링의 의존성 주입(DI)이 바로 이 원칙을 구현한 사례입니다.
3. 스프링에서 객체지향의 다형성은 어떻게 활용되나요?
질문 의도:
이 질문은 실무 맥락에서 객체지향을 이해하고 있는지를 묻는 질문입니다.
답변
스프링의 의존성 주입(DI)은 다형성을 기반으로 합니다.
예를 들어 PaymentService 인터페이스를 만들고,
KakaoPaymentService, CardPaymentService 같은 구현체를 만들어두면,
런타임 시점에 Bean 주입을 통해 어떤 구현체를 사용할지 유연하게 결정할 수 있습니다.
이는 코드 수정 없이 새로운 결제 방식을 추가할 수 있게 해주며,
OCP(개방 폐쇄 원칙)를 실현한 예시입니다.
4. 객체지향과 절차지향의 차이점은 무엇인가요?
질문 의도:
객체지향을 왜알고 쓰는가?
절차지향은 프로그램을 데이터 처리 절차 중심으로 구성하는 방식입니다. 코드가 위에서 아래로 순차적으로 실행되며, 데이터와 로직이 분리되어 있는 구조입니다.
반면 객체지향은 “객체와 그 객체 간의 협력”을 중심으로 합니다. 데이터와 기능을 하나의 단위로 묶어서 각 객체가 자신의 역할과 책임을 가지고 동작합니다.
절차지향은 구현이 단순하고 빠르지만, 기능이 많아질수록 코드의 결합도가 높아지고 유지보수가 어려워집니다.
반대로 객체지향은 처음에는 설계가 복잡해 보일 수 있지만, 변경과 확장이 용이하고 재사용성이 높습니다. 예를 들어 스프링에서는 인터페이스를 통해 객체 간 결합을 낮추고 의존성 주입(DI)을 이용해 객체의 역할을 유연하게 바꿀 수 있습니다.
그래서 저는 단순한 프로그램이라면 절차지향도 충분하지만 변경이 잦고 규모가 커질수록 객체지향 설계가 훨씬 유리하다고 생각합니다.
5. 의존성 주입(DI)과 객체지향의 관계는 무엇인가요?
질문 의도:
스프링을 제대로 이해하고 있는지, 객체지향 설계 원칙과 연결 지을 수 있는지를 판단합니다.
답변
DI는 객체 간 결합도를 낮추는 객체지향 설계 방식입니다.
객체가 필요한 의존 객체를 직접 생성하지 않고, 외부에서 주입받는 구조이기 때문에 구현체 교체가 자유롭습니다.
이렇게 하면 DIP(의존 역전 원칙)과 OCP(개방 폐쇄 원칙)를 자연스럽게 지킬 수 있습니다. 즉, 스프링의 DI는 객체지향 설계 원칙을 코드 레벨에서 구현한 대표적인 사례입니다.
6. 마무리하며
객체지향은 단순히 클래스와 객체를 쓰는 것이 아닙니다.
“변화에 유연한 구조를 만들기 위한 사고방식”이며, 프로그램을 사람이 이해하기 쉬운 형태로 설계하기 위한 철학입니다.
아직 완벽하게 객체지향적으로 코드를 짜지는 못하지만
이 글을 통해 저 자신도 “왜” 객체지향을 쓰는지 다시 한번 정리할 수 있었습니다.
앞으로는 단순히 문법적으로 객체를 사용하는 데서 벗어나, 객체 간의 관계와 책임 중심으로 코드를 설계해보려 합니다.
'자바' 카테고리의 다른 글
| Java Class 하위에 변수를 몇개까지 선언할 수 있을까? (0) | 2025.11.11 |
|---|---|
| [JVM 완전정복 #1] JVM 구성요소 총정리: 가상 머신이 자바 코드를 실행하는 비밀 (0) | 2025.11.11 |
| 프로그래밍 언어의 시작 – 고급어와 저급어의 이해 (0) | 2025.11.04 |
| 프로그래밍이란 무엇일까? (0) | 2025.10.23 |
| 자바 메모리 구조(기초) — Stack / Heap / Method Area (2) | 2025.09.22 |