객체지향을 알기 전에 객체지향의 반대인 절차지향에 대해 알아보자.
절차지향 프로그래밍
절차지향 프로그래밍은 컴퓨터의 작업 처리 방식과 유사하며, 위에서 아래로 순차적으로 처리된다.
절차적으로 실행되기 때문에 각 프로그램의 흐름을 쉽게 추적할 수 있다.
절차지향의 주요 특징
- 함수 중심 : 데이터를 처리하는 함수가 중심이며, 입력 - 처리 - 출력 순서로 흘러간다.
- 하향식 설계 : 큰 기능을 먼저 생각하고, 그것을 구현하기 위해 작은 기능들로 쪼개 내려가는 방식이다.
예를 들면, "컴퓨터로 문서를 작성한다" 라는 큰 목표(기능)를 수행하기 위해, 순서대로 작은 행동(기능)들로 쪼갠다.- 전원 켜기
- 워드 실행하기
- 키보드 입력하기
- 파일 저장하기
따라서, 절차지향 프로그래밍은 각 코드가 유기성이 높아 유지보수가 어렵고 실행 순서가 정해져 있기 때문에 순서가 변경되면 결과가 달라질 수 있다.
유기성
작은 프로그램에서는 장점이지만, 큰 프로그램에서는 단점이 된다.
- 작은 프로그램: 컴퓨터의 처리 구조와 비슷해서 실행 속도가 빠르고, 흐름을 한눈에 파악하기 좋다.
- 큰 프로그램: 너무 유기적으로 얽혀 결합도가 높아지고 기능 하나를 수정하면 연결된 순서나 공유 데이터 변화로 전체가 흔들릴 수 있다.
절차지향 프로그래밍은 규모가 큰 프로그램에 부적합하고 유지보수가 어려워, 순서만으로 관리할 수 없다고 판단되었다.
이것을 "소프트웨어 위기"라고 한다.
소프트웨어 위기(객체지향 프로그래밍 등장 배경)
- 순서로는 감당이 안 되는 말도 안되는 복잡도
옛날 계산기 같은 경우에는 입력, 계산, 출력이라는 순서만으로도 충분했다.- 하지만, 윈도우 같은 거대한 프로그램들은 검색하면서, 음악이 나오고, 파일도 다운로드가 된다.
- 이런 기능들을 절차적으로 진행하면, 동시에 가능한가? 불가능하다
- 따라서, "기능을 가진 객체들이 스스로 동작도록 역할을 줘서 순서를 정하지 말자" 라는 해결책이 등장했고 역할을 나누는 단위로 객체가 선택된 것이다.
- 인간의 사고방식과 맞추기 위해 객체 중심 모델링
컴퓨터는 0과 1만 인식하는 기계이지만, 사람은 세상 모든 사물을 인식한다.
- 절차적 사고(컴퓨터 친화적) : "변수 A에 값을 넣고, 함수 B로 점프해서 더하고, 메모리 C에 저장해라." (사람이 이해하기 힘듦)
- 객체적 사고(사람 친화적) : "개(Dog)가 짖는다.", "자동차(Car)가 달린다."
- 따라서, 프로그램이 커질수록 개발자가 코드를 이해하는 난이도가 올라갔고 한계에 다다르자, "현실시계를 그대로 사용하여 사물 중심으로 코딩하자" 가 등장했다.
- 데이터가 엉키는 것을 막기 위해(데이터 보호)
절차지향의 치명적인 단점 중 하나가 "누구나 데이터를 건드릴 수 있다."라는 것이다.(C언어의 전역 변수의 문제)- 절차지향은 함수마다 직원 월급을 마음대로 변경이 가능하다. 예를 들어 C언어의 3개 함수를 살펴보면,
#include <stdio.h>
// [문제점 1: 데이터의 완전 개방]
int salary = 3000000;
void team_leader_A() {
// 정상적인 업무: 월급 인상
salary += 1000000;
printf("A 팀장: 승진 처리 완료. (현재 월급 예측: 400만)\n");
}
전역에 데이터를 개방하여 누군가 수정할 수 있다.
void intern_C() {
// [문제점 2: 의도치 않은 데이터 오염]
salary = 500;
printf("C 인턴: 보너스 지급 로직 실행. (치명적 실수 발생!)\n");
}
보너스를 + 연산자로 올려주려고 했으나 누군가의 수정으로, 대입 연사자(=)를 통해, 월급이 500원이 되었다.
void tax_manager_B() {
// [문제점 3: 부작용 (Side Effect)의 전파]
salary -= 300000;
printf("B 세무: 세금 공제 완료.\n");
}
월급이 500이 된 상태에서 30만 원을 차감한 순간 엉뚱한 값이 된다.
int main() {
printf("=== 월급 계산 시스템 시작 ===\n\n");
// [순서 의존성]
// 함수 호출 순서에 따라 결과가 완전히 달라짐 (유기성이 너무 높음)
team_leader_A();
intern_C(); // 여기서 사고 발생
tax_manager_B(); // 사고의 여파를 맞음
printf("\n---------------------------------\n");
// [문제점 4: 디버깅의 어려움]
printf("최종 지급 월급: %d원\n", salary);
printf("---------------------------------\n");
return 0;
}
그렇게 결과를 출력해 보니, 결과가 완전히 달려졌다.
결과를 추적하기 위해 모든 함수를 살펴봐야 한다.
따라서 유지보수 문제, 데이터 보안 문제, 기능의 한계 등을 보완하기 위해 등장했다.
객체지향 프로그래밍
객체지향 프로그래밍은 절차적 프로그래밍의 반대인 컴퓨터 프로그래밍 패러다임이다.
대표적으로 많이 알려진 Java, Python, C++, C#, Kotiln 등이 있다.
프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체로 만들고, 객체들 간의 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.
객체란?
객체는 프로그램에서 사용되는 데이터 또는 식별자에 의해 참조되는 공간을 의미하며, 값을 저장할 변수와 수행할 메서드를 서로 연관된 것들끼리 묶어서 만든 꾸러미이다.
쉽게 설명하면, 현실 세계의 사물이며, 그 사물을 그대로 옮겨 놓은 것이다.
객체는 상태와 행동을 가지고 있다.
- 상태 : 객체가 가지고 있는 정보. (프로그래밍에서는 변수/필드)
- 행동 : 객체가 할 수 있는 기능. (프로그래밍에서는 메서드/함수)
예를 들면, "아이언맨"이라는 객체가 있다고 할 때
- 상태는 슈트 이름, 비행 여부 등을 뜻한다.
- 행동은 비행하기, 자비스 호출하기 등을 뜻한다.
이러한 객체들을 조립해서 하나의 프로그램을 구성하면, 중복되는 코드의 양이 줄어들고 유지보수가 용이하게 만들어진다.
객체지향의 특성 4가지

추상화
객체들이 공통적으로 필요한 속성이나 동작을 하나로 추출하는 작업, 복잡한 현실에서 불필요한 사항을 버리고 핵심만 뽑아내는 작업이다.

Car이라는 클래스가 존재하면, 차 종류에 따라 객체가 달라진다.
class Car {
// 필드 (Field): 객체마다 다르게 가질 고유한 데이터 (상태)
String brand; // 브랜드 이름 (예: Audi)
String color; // 색상 (예: Black)
int price; // 가격
int maxSpeed; // 최고 속도
// 생성자 (Constructor): 공장에서 객체를 찍어낼 때 초기값을 세팅하는 역할
public Car(String brand, String color, int price, int maxSpeed) {
this.brand = brand; // this.brand는 내 필드, brand는 받아온 값
this.color = color;
this.price = price;
this.maxSpeed = maxSpeed;
}
// 메서드 (Method): 객체의 기능 (행동)
void drive() {
// 내(this)가 가진 브랜드와 최고속도 정보를 사용해서 출력
System.out.println(this.brand + "가 시속 " + this.maxSpeed + "km로 달립니다!");
}
}
라는 설계도가 있을 때
public class Main {
public static void main(String[] args) {
// new 키워드를 써서 메모리에 실제 자동차를 탄생시킴
Car myAudi = new Car("Audi", "Black", 100000000, 250);
Car myVolvo = new Car("Volvo", "White", 80000000, 180);
// 점(.)을 찍어서 그 객체의 방에 접근합니다.
System.out.println("내 차는 " + myAudi.brand + "이고 색깔은 " + myAudi.color + "입니다.");
System.out.println("친구 차는 " + myVolvo.brand + "이고 색깔은 " + myVolvo.color + "입니다.");
System.out.println("--------------------------------");
myAudi.drive(); // 결과: Audi가 시속 250km로 달립니다!
myVolvo.drive(); // 결과: Volvo가 시속 180km로 달립니다!
}
}
new라는 연산자로 생성자에 값을 넣어주면, 그에 맞는 객체가 탄생된다.
상속
여러 개체들이 지닌 공통된 특성을 부각해 하나의 개념이나 법칙으로 성립하는 과정이다.

위로 갈수록(일반화) 더 포괄적인 개념이 되고, 아래로 갈수록(구체화) 더 특수한 기능이 추가된다.
예를 들어, 최상위 전자제품 객체가 있다.
class Electronics {
void powerOn() {
System.out.println("전기를 연결합니다.");
}
}
중간에는 전자제품을 상속받는 통신기기가 있다.
class CommunicationDevice extends Electronics {
void sendData() {
System.out.println("데이터를 전송합니다.");
}
}
하위에는 휴대폰이 통신기기를 상속받는다.
class MobilePhone extends CommunicationDevice {
void carry() {
System.out.println("주머니에 넣고 다닙니다.");
}
}
최하위에는 아이폰이 휴대폰을 상속받는다.
class IPhone extends MobilePhone {
void useFaceID() {
System.out.println("얼굴로 잠금을 해제합니다 (Face ID).");
}
}
따라서, 객체지향에서 중요한 IS-A 관계이며, "아이폰은 전자제품이다."라는 말도 성립한다.
이렇게 상속을 사용하면, 하위 클래스는 상위 클래스의 변수와 기능을 물려받아, 재사용할 수 있어 코드의 중복을 줄일 수 있다.
- IS-A 관계는 상속을 해도 되는가? 에 대한 판단 기준이다.
- 반대로, HAS-A 관계는 포함/조립 관계이며, "사람이 심장을 가지고 있다." 처럼 무언가를 가지고 있는 것을 나타낸다.
다형성
같은 자료형에 여러 가지 타입의 데이터를 대입하여 다양한 결과를 얻어낼 수 있는 성질을 의미한다.
예를 들어, 운전자는 차가 바뀌어도 운전할 수 있다.
- 오늘은 소나타를 운전해 출근을 한다.
- 내일은 포르쉐를 운전해 여행을 간다.
이처럼 차가 바뀌어도 운전하는 사람은 엑셀을 밝고 이동하는 것은 동일하다.
메서드 오버라이딩

class Phone {
void ringBell() {
System.out.println("기본 벨소리");
}
}
class IPhone extends Phone {
// 부모의 ringBell을 똑같이 쓰고, 내용만 바꿈 (재정의)
@Override
void ringBell() {
System.out.println("아이폰: 애플 벨소리");
}
}
class SamsungPhone extends Phone {
// 부모의 ringBell을 똑같이 쓰고, 내용만 바꿈 (재정의)
@Override
void ringBell() {
System.out.println("갤럭시폰: 갤럭시 벨소리");
}
}
public class Main {
public static void main(String[] args) {
Phone myPhone = new IPhone();
myPhone.ringBell(); // 결과: (아이폰: 벨소리)
}
}
부모 Phone을 상속받아 부모에 정의된 ringBell 메서드를 자식에서 재정의(오버라이딩)한다.
참조 변수인 Phone(부모)을 통해 myPhone이라는 인스턴스로 생성하고 ringBell() 메서드를 실행하면, IPhone의 ringBell() 함수가 실행된다. 이때 동적 바인딩이 발생된다.
메서드 오버로딩
class Phone {
// 매개변수가 없는 call
void call() {
System.out.println("(번호 없음) 최근 통화 목록으로 다시 겁니다.");
}
// 매개변수가 있는 call (이름은 같지만 입력이 다름!)
void call(String number) {
System.out.println(number + " 번호로 전화를 겁니다.");
}
// 매개변수가 2개인 call (또 다르게 만듦)
void call(String number, String name) {
System.out.println(name + "(" + number + ")님에게 전화를 겁니다.");
}
}
public class Main {
public static void main(String[] args) {
Phone myPhone = new Phone();
myPhone.call(); // 1번 실행
myPhone.call("010-1234-5678"); // 2번 실행
myPhone.call("010-1234-5678", "홍길동"); // 3번 실행
}
}
메서드 명이 같은 call() 메서드에서 매개변수가 없거나, number 하나이거나, number, name 두 개인 경우 다른 메서드로 판단된다.
따라서 오버로딩을 사용하려고 한다면, 같은 메서드 명은 같지만, 매개변수의 개수나 타입이 달라야 한다.
매개변수의 개수나 타입이 다르면, 왜 다른 메서드로 판단하는가?
- 컴퓨터는 메서드 이름과 매개변수 리스트를 합쳐 고유한 이름인 시그니처로 인식하기 때문에, 이름이 같아도 매개변수가 다르다면, 다른 메서드로 인식된다.
이렇게 적절하게 다형성을 사용하면 코드의 재사용성과 유연성을 높이고, 불필요한 코드 중복과 결합도를 낮춰서 유지보수성을 높일 수 있다.
캡슐화
캡슐화는 데이터와 기능을 하나로 묶어 객체에 담고, 중요한 데이터는 외부에서 함부로 건드리지 못하도록 꽁꽁 숨기는 것이다.
- 묶는다는 표현은 변수와 메서드를 하나의 클래스 안에 모아 두는 것
- 숨긴다는 표현은 남에게 보여주지 않아도 될 내부 정보다 중요한 정보
왜 캡슐화를 하는가?
- 데이터 보호 : 소프트웨어 위기에서 예시로 든 코드 중 누군가가 고의로 또는 실수로 변경했다면, 프로그램이 망가진다.
- 내부 구현의 은폐 : 환자는 캡슐 알약 안에 어떤 성분이 어떤 배율로 섞여 있는지 알 필요 없이 복용법만 지키면 되는 것처럼, 사용자 또한 사용법만 알면 된다.
void intern_C() {
// [문제점 2: 의도치 않은 데이터 오염]
salary = 500;
printf("C 인턴: 보너스 지급 로직 실행. (치명적 실수 발생!)\n");
}
class Employee {
private int salary;
// 값을 넣을 때 (Setter): 검문 검색을 거침
public void setSalary(int salary) {
if (salary < 0) {
System.out.println("월급은 음수가 될 수 없습니다.");
} else {
this.salary = salary;
System.out.println("월급 수정 완료: " + salary);
}
}
// 값을 꺼낼 때 (Getter): 안전하게 보여줌
public int getSalary() {
return this.salary;
}
}
public class Main {
public static void main(String[] args) {
Employee worker = new Employee();
// worker.salary = -500; <-- private 접근 불가.
worker.setSalary(-500); // "월급은 음수가 될 수 없습니다."
worker.setSalary(300); // "월급 수정 완료"
}
}
멤버 변수(클래스 아래, 메서드 밖)를 private로 선언하면, 외부로부터 접근이 불가하며, 오로지 getter와 setter로만 가능하다.
또한 if문으로 조건 처리를 할 경우, 의도적이거나 실수로 잘못된 값을 넣을 수 없다.
자바는 왜 객체지향인가?
자바는 앞서 4가지의 특징을 문법적으로 완벽하게 지원하고 강제한다.
1. 클래스 없이는 존재할 수 없다.
다른 언어와 달리 자바는 불가능하다.
모든 코드는 반드시 어떤 클래스 안에 소속되어야 하며, 프로그램 시작하는 main 함수도 클래스 안에 존재한다.
이유 : 자바는 "세상의 모든 것을 객체로 이루어져 있다." 구조 자체에서 강요하기 때문이다.
2. 모든 객체의 조상 Object 클래스
자바에서 만든 모든 클래스는 개발자가 extends를 쓰지 않아도, 자동으로 Object라는 최상위 클래스를 상속받는다.
그렇기 때문에 toString(), equals() 등의 공통 기능을 기본적으로 가지고 있다.
3. 문법 자체가 4대 특성(추상화, 상속, 다형성, 캡슐화)을 위해 존재한다.
| 객체 지향 개념 | 자바 문법 키워드 | 역할 |
| 캡슐화 | private, protected, public | 접근 제어(데이터 보호) |
| 상속 | extends | 기능 물려받기 |
| 추상화 | interface, abstract | 설계도와 구현 분리 |
| 다형성 | @Override, Upcasting | 하나의 변수로 여러 객체 다루기 |
하지만, 자바는 순수 객체 지향이 아니다.
순수 객체 지향은 앨런 케이(Alan Kay)가 정의한 것이다.
- Abstraction / 추상화
- Encapsulation / 캡슐화
- Inheritance / 상속
- Polymorphism / 다형성
- All predefined types are objects / 모든 사전에 정의된 타입은 객체여야 함
- All operations performed on objects must be only through methods exposed at the objects. / 객체에 대해서 수행되는 모든 작업은 반드시 객체의 메서드를 통해서만 이루어져야 함
- All user-defined types are objects. / 모든 사용자가 정의한 타입은 객체여야 함
1. int, double, boolean와 같은 원시타입, for문 등의 제어문이 객체가 아니 때문이다.
5번을 위배한다.
자바가 이렇게 만들어진 이유는 성능 때문이다.
객체는 Heap 메모리에서 생성되고, GC에 의해 관리받아야 하므로 무겁고 느린 반면, int 같은 원시타입은 스택 메모리에 값만 저장하므로 빠르다. 따라서 순수성 대신 속도를 선택했다.
2. Static(정적 키워드)
static 메서드는 객체(인스턴스)를 생성하지 않아도 실행이 가능하다.
이것은 절차지향의 함수에 더 가깝다.
따라서, 6번을 위배한다.
// 객체 생성(new) 없이 클래스 이름으로 바로 호출함
Math.abs(-5);
3. Wrapper Class(래퍼 클래스)
자바는 원시 타입 때문에 객체지향의 규칙이 깨진다 것을 보완하기 위해 Integer, Double과 같은 래퍼 클래스를 만들었다.
만약 자바가 완벽한 순수 객체지향 언어였다면, int 자체가 객체였을 테니 Wrapper Class라는 개념 자체가 필요 없었을 것이다.
이것의 존재 자체가 "자바는 기본적으로 순수 객체지향이 아니다"라는 반증이다.
이렇게 객체지향부터 자바가 순수 객체지향이 아니라는 것까지 알아봤다.
그럼, 면접관들은 왜 물어보는 것인가?
(이 아래는 제 생각과 AI를 통해 주고 받은 대화를 정리한 것입니다.)
1. 객체지향에 대해 설명하라고 묻는 이유
유지보수 비용과 협업 능력을 확인하기 위해서이다.
면접관은 당신이 코드를 부품화(객체화)해서, 수정이 쉬운 구조를 만들 수 있는지 확인하고 싶어 질문하거나,
협업과 추상화를 통해 인터페이스를 잘 설계해서 내부 로직은 몰라도 동료가 편하게 쓸 수 있게 배려하는 능력이 있는지를 확인하기 위해 질문한다.
그럼 답변은?
객체지향 프로그래밍은 현실 세계의 사물을 모델링하여, 데이터와 이를 처리하는 행위를 하나의 객체로 묶어 조립하는 개발 방식입니다.
제가 생각하는 객체지향의 가장 큰 장점은 유지보수성입니다. 캡슐화를 통해 서로의 간섭을 줄이고, 다형성을 통해 부품을 쉽게 교체할 수 있기 때문입니다. 덕분에 요구사항이 변경되었을 때 코드 수정을 최소화하고 유연하게 대처할 수 있어, 협업과 대규모 프로젝트에 필수적인 패러다임이라고 생각합니다.
2. 자바는 객체 지향 언어?
간단한 질문처럼 보이지만,
- 성능과 이상의 타협점을 아는가?
- 순수 객체지향이 아닌 자바는 원시타입을 적용해 속도를 올려 성능과 이상을 타협했다.
- 따라서 이론보다 현실적인 성능을 선택해야 할 때가 있음을 이해하는 질문이라고 생각한다.
- 메모리 구조를 이해하는 가?
- 원시 타입은 스택 메모리에, 래퍼 클래스는 힙 메모리에 저장된다.
- int와 Integer의 차이를 알고 상황에 맞게 골라 쓸 줄 아는 걸까? 라는 질문이라고 생각한다.
| 비교 항목 | int(원시 타입) | Integer(Wrapper Class) |
| 정체 | 원시타입(데이터) | 래퍼 클래스(객체) |
| 메모리 위치 | 스택 메모리(가볍고 빠름) | 힙 메모리(무겁고 참조가 필요) |
| Null 허용 | 불가능 | 가능 |
| 기본값 | 0 | null |
| 제네릭 사용 | List<int> 불가능 | List<Integer> 가능 |
따라서 int는 연산 속도와 효율성을 위해 사용하고, Integer는 유연한 기능과 객체 처리를 위해 사용한다.
그럼 답변은?
엄밀히 말씀드리면, 자바는 실용성을 강조한 하이브리드 객체지향 언어라고 생각합니다.
자바는 모든 코드를 클래스 기반으로 작성해야 하고, 캡슐화와 상속 같은 객체지향의 특징을 강력하게 지원한다는 점에서는 객체지향 언어가 맞습니다.
하지만 성능 효율성을 위해 원시 타입(Primitive Type)과 정적(Static) 메서드를 허용했다는 점에서 100% 순수 객체지향과는 차이가 있습니다. 자바는 이러한 설계를 통해 객체지향의 안정성과 시스템의 속도라는 두 마리 토끼를 잡은 언어라고 생각합니다.
'Daily Dev Q&A 정리 템플릿' 카테고리의 다른 글
| 25.12.03 다형성을 좀 더 구체적으로 설명해주세요! 라는 질문에 대비하기 (0) | 2025.12.03 |
|---|---|
| 25.12.02 상속과 컴포지션(Composition)의 차이와, 언제 컴포지션을 사용할까? (0) | 2025.12.02 |
| 25.11.28 자바의 예외 처리(Exception)에 대하여 (0) | 2025.11.30 |
| 25.11.28 자바는 컴파일러일까? 인터프리터일까? (0) | 2025.11.28 |
| 25.11.26 자바 메모리 구조 (0) | 2025.11.26 |