객체지향 프로그래밍(OOP)의 세계를 탐험하다 보면 수많은 개념과 마주하게 됩니다. 클래스, 상속, 캡슐화, 다형성… 하지만 이 모든 개념이 존재하고 또 의미를 가지는 이유는 단 하나, 바로 객체(Object)를 효과적으로 다루기 위해서입니다. 객체는 OOP의 가장 기본적인 구성 단위이자, 그 이름처럼 모든 것의 중심에 있는 핵심 ‘주체’입니다. 우리가 OOP를 통해 만들고자 하는 것은 결국 현실 세계의 문제 해결을 돕는 소프트웨어 시스템이며, 그 시스템 안에서 살아 숨 쉬며 실제 작업을 수행하는 존재가 바로 객체입니다. 객체는 단순히 데이터를 저장하는 변수 덩어리나 순차적으로 실행되는 코드 뭉치가 아닙니다. 자신만의 상태(데이터)를 가지고, 스스로 행동(기능)할 수 있으며, 다른 객체들과 관계를 맺고 상호작용하는, 마치 코드 속의 작은 생명체와 같은 존재입니다. 이 글에서는 OOP의 심장이라 할 수 있는 ‘객체’란 정확히 무엇인지, 어떤 요소로 구성되고 어떻게 다른 객체들과 관계를 맺는지, 그리고 왜 객체 중심적인 사고가 중요한지에 대해 개발자의 시각으로 깊이 있게 파헤쳐 보겠습니다.
객체의 민낯: 무엇으로 이루어져 있나?
객체지향의 세계에서 ‘객체’는 명확한 정의를 가지고 있습니다. 모든 객체는 크게 세 가지 핵심적인 요소로 구성됩니다. 바로 상태(State), 행동(Behavior), 그리고 식별성(Identity)입니다. 이 세 가지 요소를 이해하는 것이 객체의 본질을 파악하는 첫걸음입니다.
객체의 3요소: 상태 행동 식별성 파헤치기
마치 우리가 사람을 이해할 때 그 사람의 특징(키, 몸무게, 이름 등), 할 수 있는 일(말하기, 걷기, 생각하기 등), 그리고 다른 사람과 구별되는 고유함(주민등록번호, 지문 등)을 생각하는 것처럼, 객체도 이 세 가지 측면으로 이해할 수 있습니다.
- 상태 (State): 객체가 현재 가지고 있는 정보나 속성들의 집합입니다. 객체의 ‘존재 방식’을 나타냅니다.
- 행동 (Behavior): 객체가 할 수 있는 동작이나 기능을 의미합니다. 객체의 상태를 변경하거나 다른 객체와 상호작용하는 ‘수행 방식’입니다.
- 식별성 (Identity): 각 객체를 다른 모든 객체와 유일하게 구별할 수 있는 고유한 특성입니다. 이름이 같거나 상태가 완전히 동일하더라도 서로 다른 객체로 인식될 수 있게 합니다.
이 세 가지 요소가 결합되어 하나의 완전한 객체를 이룹니다.
객체의 기억: 상태(State)와 속성
상태(State)는 특정 시점에 객체가 가지고 있는 모든 데이터를 의미합니다. 객체의 특징이나 현재 상황을 나타내는 값들의 집합이라고 할 수 있습니다. 프로그래밍에서는 주로 객체의 속성(Attribute), 멤버 변수(Member Variable), 또는 필드(Field) 등으로 표현됩니다.
예를 들어, ‘자동차’ 객체의 상태는 다음과 같은 속성들로 나타낼 수 있습니다.
색상
: “빨강”현재 속도
: 60 (km/h)주행 거리
: 15000 (km)연료량
: 30 (리터)시동 상태
: “켜짐”
이러한 상태 값들은 시간에 따라 변할 수 있습니다. 자동차가 가속하면 현재 속도
상태가 변하고, 주행하면 주행 거리
와 연료량
상태가 변합니다. 객체의 행동(메서드)은 종종 이 상태를 변경시키는 역할을 합니다. 상태는 객체의 ‘기억’이라고 볼 수 있으며, 객체가 어떤 존재인지를 규정하는 중요한 요소입니다.
객체의 재능: 행동(Behavior)과 메서드
행동(Behavior)은 객체가 수행할 수 있는 동작이나 기능을 의미합니다. 객체는 자신의 상태를 변경하거나, 다른 객체에게 메시지를 보내 특정 작업을 요청하는 등의 행동을 할 수 있습니다. 프로그래밍에서는 주로 메서드(Method) 또는 오퍼레이션(Operation)으로 구현됩니다.
‘자동차’ 객체의 행동은 다음과 같은 메서드들로 나타낼 수 있습니다.
startEngine()
: 시동을 건다. (내부적으로시동 상태
를 “켜짐”으로 변경)accelerate(amount)
: 속도를 높인다. (현재 속도
상태를 증가시킴)brake()
: 속도를 줄인다. (현재 속도
상태를 감소시킴)refuel(amount)
: 연료를 채운다. (연료량
상태를 증가시킴)getCurrentSpeed()
: 현재 속도를 알려준다. (현재 속도
상태 값을 반환)
행동은 객체의 ‘능력’ 또는 ‘책임’이라고 볼 수 있습니다. 객체는 외부로부터 특정 행동을 수행하라는 요청(메서드 호출)을 받으면, 그에 맞는 동작을 수행하고 결과를 반환하거나 자신의 상태를 변경합니다. 객체지향 시스템은 이러한 객체들의 행동(메서드 호출)을 통해 상호작용하며 전체 기능을 완성해 나갑니다.
세상에 단 하나: 식별성(Identity)과 고유함
식별성(Identity)은 각 객체를 다른 객체와 유일하게 구별할 수 있는 고유한 정체성을 의미합니다. 설령 두 객체의 상태(모든 속성 값)가 완전히 동일하더라도, 식별성이 다르면 서로 다른 객체로 취급됩니다.
예를 들어, 똑같은 모델, 똑같은 색상, 똑같은 옵션을 가진 두 대의 자동차가 공장에서 막 출고되었다고 가정해 봅시다. 이 두 자동차는 현재 상태가 완전히 동일하지만, 우리는 두 대의 자동차를 별개의 존재로 인식합니다. 왜냐하면 각각 고유한 차대번호를 가지고 있고, 물리적으로 다른 공간을 차지하는 별개의 실체이기 때문입니다.
프로그래밍 세계에서도 마찬가지입니다. 클래스로부터 객체를 생성하면, 각 객체는 메모리 상에 고유한 주소를 할당받습니다. 이 메모리 주소가 일반적으로 객체의 식별성 역할을 합니다. 따라서 동일한 클래스로부터 생성되고 모든 속성 값이 같은 두 객체라도, 메모리 주소가 다르기 때문에 서로 다른 객체로 구분됩니다.
Python
class Person:
def __init__(self, name):
self.name = name
person1 = Person("홍길동")
person2 = Person("홍길동")
person3 = person1 # person1과 동일한 객체를 참조
print(f"person1의 ID: {id(person1)}")
print(f"person2의 ID: {id(person2)}")
print(f"person3의 ID: {id(person3)}")
print(f"person1 == person2 ? {person1 == person2}") # 상태 비교 (구현에 따라 다름)
print(f"person1 is person2 ? {person1 is person2}") # 식별성 비교 (메모리 주소 비교) - False
print(f"person1 is person3 ? {person1 is person3}") # 식별성 비교 (메모리 주소 비교) - True
위 Python 코드에서 person1
과 person2
는 name
속성 값이 “홍길동”으로 동일하지만, id()
함수로 확인해보면 서로 다른 메모리 주소(식별성)를 가집니다. 따라서 is
연산자(식별성 비교)로 비교하면 False
가 나옵니다. 반면 person3
는 person1
과 동일한 객체를 참조하므로 id()
값과 is
비교 결과가 모두 같습니다. 식별성은 객체가 독립적인 존재로서 존재할 수 있게 하는 근본적인 특성입니다.
코드와 현실의 연결고리: 객체로 세상 바라보기
결국 객체는 현실 세계의 사물이나 개념을 코드 세계로 가져와 표현하기 위한 핵심적인 추상화 도구입니다. 책상 위의 ‘컵’ 객체는 색상
, 용량
, 내용물
등의 상태와 채우다()
, 비우다()
, 마시다()
등의 행동을 가질 수 있습니다. 온라인 쇼핑몰의 ‘고객’ 객체는 아이디
, 이름
, 주소
, 포인트
등의 상태와 로그인하다()
, 상품을 장바구니에 담다()
, 주문하다()
등의 행동을 가질 수 있습니다. 이처럼 주변의 모든 것을 상태와 행동, 그리고 식별성을 가진 객체로 바라보고 모델링하는 것이 객체지향적 사고의 시작입니다.
설계도와 완성품: 클래스와 객체 다시 보기
객체는 어떻게 만들어지고 관리될까요? 여기서 다시 한번 클래스와 객체의 관계를 명확히 할 필요가 있습니다. 객체는 홀로 존재하는 것이 아니라, 클래스라는 설계도를 바탕으로 생명을 얻고 소멸하기 때문입니다.
클래스: 객체를 찍어내는 틀
이전 글에서도 강조했듯이, 클래스(Class)는 객체를 만들기 위한 설계도, 템플릿, 또는 청사진입니다. 클래스에는 특정 종류의 객체가 가져야 할 공통적인 속성(데이터의 종류와 이름)과 메서드(수행할 수 있는 기능)가 정의되어 있습니다. 클래스 자체는 설계도일 뿐, 메모리를 차지하는 실제 데이터나 실체가 아닙니다. 코드로 작성되어 존재하지만, 프로그램 실행 시점에 직접적인 역할을 하지는 않습니다.
인스턴스: 클래스에서 태어난 실체 객체
객체(Object)는 이 클래스라는 설계도를 바탕으로 실제로 메모리에 생성된 실체입니다. 클래스에 정의된 속성들을 위한 메모리 공간을 할당받고, 그 공간에 실제 데이터를 저장하며, 클래스에 정의된 메서드를 실행할 수 있습니다. 클래스로부터 생성된 객체를 특별히 인스턴스(Instance)라고 부르기도 합니다. ‘객체’와 ‘인스턴스’는 거의 동일한 의미로 사용되지만, ‘인스턴스’는 특정 클래스로부터 만들어진 실체라는 점을 강조할 때 주로 사용됩니다. (예: “이것은 Car
클래스의 인스턴스이다.”)
하나의 클래스로부터 수많은 인스턴스(객체)를 생성할 수 있으며, 각 인스턴스는 자신만의 상태 값을 가질 수 있습니다. 예를 들어, Person
클래스로부터 “홍길동” 객체, “김철수” 객체, “이영희” 객체 등 여러 사람 인스턴스를 만들 수 있습니다.
탄생의 순간: 생성자와 객체 초기화
객체는 어떻게 태어날까요? 프로그래밍 언어에서는 보통 new
키워드(Java, C# 등)나 클래스 이름을 함수처럼 호출(Python 등)하여 객체(인스턴스)를 생성합니다. 이때 특별한 역할을 하는 것이 바로 생성자(Constructor)입니다.
생성자는 클래스 이름과 동일한 이름을 가진 (또는 특별히 약속된 이름, 예: Python의 __init__
) 특수한 메서드입니다. 객체가 생성될 때 단 한 번 자동으로 호출되며, 주로 객체의 초기 상태(속성 값)를 설정하는 역할을 합니다.
Python
class Person:
# 생성자 메서드 (__init__)
def __init__(self, name, age):
print(f"Person 객체 생성 중... 이름: {name}, 나이: {age}")
self.name = name # 속성 초기화
self.age = age # 속성 초기화
# 객체 생성 시 생성자가 자동으로 호출됨
person1 = Person("홍길동", 30) # 출력: Person 객체 생성 중... 이름: 홍길동, 나이: 30
person2 = Person("김철수", 25) # 출력: Person 객체 생성 중... 이름: 김철수, 나이: 25
print(person1.name, person1.age) # 출력: 홍길동 30
print(person2.name, person2.age) # 출력: 김철수 25
생성자를 통해 객체가 필요한 초기 데이터를 전달받고, 이를 바탕으로 객체가 처음 가져야 할 상태를 설정합니다. 이 과정을 객체 초기화(Initialization)라고 합니다.
왔다 가는 존재: 객체의 삶과 죽음 (메모리 이야기)
객체가 생성되면 프로그램이 실행되는 동안 메모리(주로 힙(Heap) 영역)의 특정 공간을 차지하게 됩니다. 그리고 더 이상 해당 객체를 참조하는 곳이 없어지면(객체가 필요 없어지면), 메모리 낭비를 막기 위해 객체가 차지하던 메모리 공간을 회수해야 합니다. 이 과정을 객체 소멸이라고 합니다.
과거 C++ 같은 언어에서는 개발자가 직접 소멸자(Destructor)를 호출하고 메모리 해제 코드를 작성해야 했습니다. 하지만 Java, Python, C# 등 현대의 많은 OOP 언어들은 가비지 컬렉터(Garbage Collector, GC)라는 시스템을 내장하고 있습니다. 가비지 컬렉터는 더 이상 사용되지 않는 객체(쓰레기 객체)를 자동으로 탐지하여 메모리에서 제거해주는 역할을 합니다. 덕분에 개발자는 메모리 관리에 대한 부담을 크게 덜고 비즈니스 로직 개발에 더 집중할 수 있습니다.
물론 가비지 컬렉션이 만능은 아니며, 때로는 메모리 누수(Memory Leak) 문제가 발생할 수도 있고 GC 동작 시점에 예측하지 못한 성능 저하가 발생할 수도 있습니다. 따라서 객체의 생명주기와 메모리 관리에 대한 기본적인 이해는 여전히 중요합니다.
혼자는 외로워: 객체들의 관계 네트워크
객체지향 시스템은 수많은 객체들이 각자의 역할을 수행하며 서로 상호작용하는 방식으로 동작합니다. 마치 사람들의 사회처럼, 객체들도 서로 다양한 관계(Relationship)를 맺으며 협력합니다. 객체 간의 관계를 잘 설계하는 것은 유연하고 확장 가능한 시스템을 만드는 데 매우 중요합니다. 객체 간의 주요 관계 유형을 살펴보겠습니다.
객체는 홀로 존재하지 않는다: 객체 간의 상호작용
단일 객체만으로는 복잡한 기능을 수행하기 어렵습니다. 예를 들어, 온라인 쇼핑몰에서 고객이 상품을 주문하는 과정을 생각해 봅시다. 이 과정에는 Customer
(고객) 객체, Product
(상품) 객체, ShoppingCart
(장바구니) 객체, Order
(주문) 객체 등 여러 객체가 관여합니다.
Customer
객체는Product
객체의 정보를 조회하고,ShoppingCart
객체에 상품을 담습니다.ShoppingCart
객체는 여러Product
객체들을 관리하고 총액을 계산합니다.Customer
객체는ShoppingCart
객체의 정보를 바탕으로Order
객체를 생성합니다.Order
객체는 주문 처리 로직을 수행하며, 필요하다면Payment
(결제) 객체와 상호작용할 수도 있습니다.
이처럼 객체들은 서로 메서드를 호출하고 데이터를 주고받으며 협력합니다. 이러한 협력 관계를 잘 설계하는 것이 객체지향 설계의 핵심 과제 중 하나입니다.
서로를 알다: 연관 관계 (Association)
연관 관계(Association)는 한 객체가 다른 객체를 지속적으로 알고 참조하는 관계를 의미합니다. 보통 한 객체가 다른 객체를 멤버 변수(속성)로 가지고 있는 형태로 표현됩니다. 연관 관계는 방향성을 가질 수도 있고(단방향 연관), 양방향성을 가질 수도 있습니다(양방향 연관). 또한, 관계의 개수(Multiplicity)를 표현할 수 있습니다 (일대일, 일대다, 다대다).
- 예시:
Student
객체와Professor
객체 간의 관계 (한 명의 교수는 여러 학생을 가르칠 수 있고, 한 명의 학생은 여러 교수에게 배울 수 있음 – 다대다 연관)Order
객체와Customer
객체 간의 관계 (하나의 주문은 반드시 한 명의 고객에게 속함 – 일대다 또는 일대일 연관, 주문 객체가 고객 객체를 참조)
- 특징: 연관된 객체들은 서로의 생명주기에 영향을 주지 않을 수도 있고, 비교적 동등한 관계일 수 있습니다.
잠시만 신세 좀 질게: 의존 관계 (Dependency)
의존 관계(Dependency)는 한 객체가 다른 객체를 일시적으로 사용하는 관계를 의미합니다. 연관 관계처럼 멤버 변수로 참조하는 것이 아니라, 특정 메서드를 실행하는 동안에만 매개변수(Parameter)나 지역 변수(Local Variable) 등을 통해 다른 객체를 사용하는 경우입니다.
- 예시:
Printer
객체가print(Document document)
메서드를 통해Document
객체를 인자로 받아 출력하는 경우.Printer
객체는Document
객체를 소유하지는 않지만,print
메서드 실행 동안Document
객체에 의존합니다.OrderService
객체가processOrder(Order order, PaymentGateway paymentGateway)
메서드를 실행하면서PaymentGateway
객체를 사용하여 결제를 처리하는 경우.OrderService
는PaymentGateway
를 잠시 사용하고 관계가 끝납니다.
- 특징: 관계 중에서 가장 약한 결합도를 가지며, 한 객체의 변경이 다른 객체에 미치는 영향이 비교적 적습니다.
부품 조립하기 (느슨하게): 집합 관계 (Aggregation)
집합 관계(Aggregation)는 전체(Whole)와 부분(Part)의 관계를 나타내지만, 부분 객체가 전체 객체와 독립적인 생명주기를 가지는 경우입니다. 즉, 전체 객체가 사라져도 부분 객체는 여전히 존재할 수 있습니다. “has-a”(~를 가진다) 관계로 표현되며, 연관 관계의 특수한 형태입니다.
- 예시:
Computer
객체와Monitor
,Keyboard
객체 간의 관계. 컴퓨터가 없어져도 모니터나 키보드는 다른 컴퓨터에 연결하여 사용할 수 있습니다. 컴퓨터 객체는 모니터와 키보드 객체를 ‘부분’으로 가지지만, 그들의 생명주기를 소유하지는 않습니다.Department
(부서) 객체와Professor
(교수) 객체 간의 관계. 부서가 사라져도 교수는 다른 부서로 이동하거나 독립적으로 존재할 수 있습니다.
- 특징: 전체와 부분 간의 관계가 비교적 느슨합니다. 부분 객체가 여러 전체 객체에 속할 수도 있습니다.
운명 공동체 (강하게): 복합 관계 (Composition)
복합 관계(Composition)도 전체(Whole)와 부분(Part)의 관계를 나타내지만, 부분 객체가 전체 객체에 완전히 종속되어 생명주기를 함께하는 경우입니다. 즉, 전체 객체가 생성될 때 부분 객체도 함께 생성되거나 외부에서 생성되어 전체에 속하게 되고, 전체 객체가 소멸될 때 부분 객체도 함께 소멸됩니다. 집합 관계보다 더 강한 “has-a” 관계입니다.
- 예시:
Person
(사람) 객체와Heart
(심장) 객체 간의 관계. 사람은 심장을 ‘부분’으로 가지며, 사람이 태어날 때 심장도 함께 존재하고 사람이 죽으면 심장도 기능을 멈춥니다. 심장은 다른 사람에게 속할 수 없습니다(일반적으로).Building
(건물) 객체와Room
(방) 객체 간의 관계. 건물에 속한 방은 건물이 철거되면 함께 사라집니다. 방이 건물과 독립적으로 존재하기 어렵습니다.
- 특징: 전체와 부분 간의 관계가 매우 강합니다. 부분 객체는 오직 하나의 전체 객체에만 속하며, 생명주기를 공유합니다.
이러한 객체 간의 관계를 이해하고 적절하게 설계하는 것은 시스템의 구조를 명확하게 하고, 변경에 유연하게 대처하며, 코드의 재사용성을 높이는 데 필수적입니다. UML(Unified Modeling Language) 클래스 다이어그램은 이러한 객체(클래스) 간의 관계를 시각적으로 표현하는 데 유용한 도구입니다.
OOP의 주인공은 나야 나: 객체가 중요한 이유
객체지향 프로그래밍에서 왜 ‘객체’가 그토록 중요할까요? 객체는 OOP의 여러 특징과 원칙을 구현하고 실현하는 근본적인 단위이기 때문입니다.
세상을 담는 그릇: 현실 모델링 도구로서의 객체
앞서 언급했듯이, 객체는 상태와 행동을 가짐으로써 현실 세계의 사물이나 개념을 가장 자연스럽게 모델링할 수 있는 단위입니다. 복잡한 문제를 이해하기 쉬운 객체 단위로 분해하고, 각 객체의 책임과 역할을 정의함으로써 문제 해결 과정을 더 체계적이고 직관적으로 만들 수 있습니다. 이는 Product Owner나 기획자가 정의한 요구사항을 개발자가 코드로 옮기는 과정을 더 원활하게 합니다.
비밀을 지키는 금고: 캡슐화와 정보 은닉의 실현
캡슐화는 객체의 핵심적인 특징 중 하나입니다. 객체는 자신의 상태(데이터)와 그 상태를 조작하는 행동(메서드)을 하나로 묶고, 내부의 중요한 구현 세부 사항을 외부로부터 숨깁니다(정보 은닉). 이를 통해 객체는 자신의 무결성을 유지하고, 외부의 간섭으로부터 보호받으며, 독립적인 단위로서의 역할을 수행할 수 있습니다. 캡슐화는 객체가 없다면 존재할 수 없는 개념입니다.
팔색조 매력 발산: 다형성을 가능하게 하는 객체
다형성은 동일한 메시지(메서드 호출)에 대해 객체가 자신의 실제 타입에 따라 다르게 반응하는 능력입니다. Animal
타입 변수가 Dog
객체를 참조할 때는 speak()
메서드가 “멍멍”으로, Cat
객체를 참조할 때는 “야옹”으로 동작하는 것은 Dog
객체와 Cat
객체가 각각 speak()
라는 메시지에 다르게 반응하기 때문입니다. 이처럼 다형성은 객체가 메시지를 수신하고 스스로 행동을 결정하는 주체이기 때문에 가능합니다.
레고 블록의 재탄생: 재사용 가능한 부품 객체
잘 설계된 객체는 독립적인 부품처럼 작동하여 재사용성을 높입니다. 특정 기능을 수행하는 객체를 만들어두면, 다른 시스템이나 다른 부분에서 필요할 때 해당 객체를 가져다 쉽게 사용할 수 있습니다. 예를 들어, 날짜 처리 기능을 가진 Date
객체나 파일 입출력 기능을 가진 FileHandler
객체 등은 다양한 프로그램에서 재사용될 수 있습니다. 객체 단위의 재사용은 개발 생산성을 크게 향상시킵니다.
변화를 두려워 마: 유지보수와 확장성의 열쇠
객체지향 시스템은 객체 단위로 구성되므로 유지보수와 확장성 측면에서 유리합니다. 특정 기능의 수정이 필요할 때 해당 기능을 책임지는 객체만 수정하면 되므로 변경의 영향 범위를 제한할 수 있습니다. 또한, 새로운 기능이 필요할 경우 새로운 객체를 추가하거나 기존 객체와의 관계를 설정하는 방식으로 시스템을 확장하기 용이합니다. 객체 간의 결합도를 낮추도록 잘 설계되었다면 이러한 장점은 더욱 극대화됩니다.
결국, OOP의 모든 장점(재사용성, 유지보수성, 확장성, 유연성 등)은 ‘객체’라는 기본 단위의 특징과 객체 간의 관계 설계를 통해 실현된다고 해도 과언이 아닙니다.
코드로 만나는 객체: 실제 모습 엿보기
이론적인 설명을 넘어, 실제 코드를 통해 객체가 어떻게 생성되고 사용되며 관계를 맺는지 구체적으로 살펴보겠습니다. (Python 예제 사용)
Hello Object!: 객체 생성과 상태 조작 예제
Python
class Circle:
# 클래스 변수 (모든 Circle 객체가 공유)
PI = 3.14159
# 생성자: 반지름(radius)으로 객체 초기화
def __init__(self, radius):
if radius <= 0:
raise ValueError("반지름은 0보다 커야 합니다.")
self._radius = radius # 상태 (속성) - 캡슐화 (_ 사용)
self._color = "white" # 상태 (속성) - 기본값 설정
# 행동 (메서드) - 원의 넓이 계산
def calculate_area(self):
return self.PI * (self._radius 2)
# 행동 (메서드) - 원의 둘레 계산
def calculate_circumference(self):
return 2 * self.PI * self._radius
# 행동 (메서드) - 반지름 변경 (Setter 역할)
def set_radius(self, radius):
if radius <= 0:
raise ValueError("반지름은 0보다 커야 합니다.")
self._radius = radius
print(f"반지름이 {radius}(으)로 변경되었습니다.")
# 행동 (메서드) - 반지름 조회 (Getter 역할)
def get_radius(self):
return self._radius
# 행동 (메서드) - 색상 변경
def set_color(self, color):
self._color = color
print(f"색상이 {color}(으)로 변경되었습니다.")
def get_color(self):
return self._color
# 객체(인스턴스) 생성
circle1 = Circle(5)
circle2 = Circle(10)
# 객체의 행동(메서드) 호출 및 상태 확인
print(f"원1 넓이: {circle1.calculate_area()}")
print(f"원1 둘레: {circle1.calculate_circumference()}")
print(f"원1 색상: {circle1.get_color()}") # 초기 색상: white
circle1.set_radius(7) # 원1의 상태 변경
circle1.set_color("blue") # 원1의 상태 변경
print(f"변경된 원1 반지름: {circle1.get_radius()}")
print(f"변경된 원1 넓이: {circle1.calculate_area()}")
print(f"변경된 원1 색상: {circle1.get_color()}")
print("-" * 20)
print(f"원2 넓이: {circle2.calculate_area()}") # 원2는 원1의 상태 변경에 영향받지 않음
print(f"원2 반지름: {circle2.get_radius()}")
위 예제는 Circle
클래스를 정의하고, radius
와 color
라는 상태, 그리고 넓이/둘레 계산, 반지름/색상 변경 및 조회 등의 행동(메서드)을 가진 객체를 생성하고 사용하는 모습을 보여줍니다. 각 Circle
객체(circle1
, circle2
)는 독립적인 상태를 가지며, 메서드 호출을 통해 자신의 상태를 변경하거나 정보를 제공합니다.
우리 같이 일하자!: 객체 간 관계 표현 예제 (연관 관계)
Python
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
print(f"엔진 시동! (출력: {self.horsepower} 마력)")
class Car:
def __init__(self, model_name, engine): # Engine 객체를 생성자 인자로 받음
self.model_name = model_name
# 연관 관계: Car 객체가 Engine 객체를 속성으로 가짐 (has-a)
self.engine = engine
def start_car(self):
print(f"{self.model_name} 시동을 겁니다.")
# Car 객체가 가지고 있는 Engine 객체의 메서드 호출
self.engine.start()
# 객체 생성
my_engine = Engine(200)
my_car = Car("소나타", my_engine) # Car 객체 생성 시 Engine 객체를 전달 (의존성 주입 형태)
# 객체 간 상호작용
my_car.start_car()
# 출력:
# 소나타 시동을 겁니다.
# 엔진 시동! (출력: 200 마력)
# Engine 객체는 Car 객체와 별개로 존재 가능 (연관 또는 집합 관계 가능성)
another_engine = Engine(300)
print(another_engine.horsepower)
이 예제는 Car
객체가 Engine
객체를 속성(self.engine
)으로 가지는 연관 관계를 보여줍니다. Car
객체의 start_car()
메서드는 자신이 가지고 있는 Engine
객체의 start()
메서드를 호출하여 협력합니다. Engine
객체는 Car
객체와 독립적으로 생성될 수도 있습니다. 이는 객체들이 어떻게 서로 관계를 맺고 협력하여 더 큰 기능을 완성하는지를 보여주는 간단한 예시입니다.
너는 누구니?: 객체의 고유 식별성 확인
Python
circle_a = Circle(5)
circle_b = Circle(5) # 상태는 circle_a와 동일
circle_c = circle_a # circle_a와 동일한 객체 참조
print(f"circle_a ID: {id(circle_a)}")
print(f"circle_b ID: {id(circle_b)}")
print(f"circle_c ID: {id(circle_c)}")
# 상태 비교는 내부적으로 어떻게 구현하느냐에 따라 다름 (여기서는 비교 X)
# 식별성 비교 (메모리 주소 비교)
print(f"circle_a is circle_b ? {circle_a is circle_b}") # False - 상태는 같지만 다른 객체
print(f"circle_a is circle_c ? {circle_a is circle_c}") # True - 동일한 객체
이 코드는 앞서 설명한 객체의 식별성을 다시 한번 확인시켜 줍니다. circle_a
와 circle_b
는 반지름 5인 원 객체로 상태는 동일하지만, id()
값과 is
비교 결과에서 볼 수 있듯이 서로 다른 객체입니다. 반면 circle_c
는 circle_a
와 동일한 객체를 가리키므로 식별성이 같습니다.
객체를 품은 개발자 되기
객체지향 프로그래밍의 핵심인 ‘객체’에 대해 깊이 이해하는 것은 단순히 기술적인 지식을 넘어, 문제를 바라보고 해결하는 방식을 바꾸는 중요한 과정입니다.
객체지향적 사고: 세상을 객체로 분해하고 조립하기
객체지향적으로 생각한다는 것은 세상을 객체들의 집합으로 바라보는 것입니다. 어떤 문제 상황이나 시스템 요구사항을 접했을 때, 관련된 주요 개념들을 객체로 식별하고, 각 객체가 어떤 상태를 가져야 하며 어떤 행동을 책임져야 하는지 정의하고, 이 객체들이 어떻게 서로 관계를 맺고 협력해야 전체 시스템이 동작할 수 있을지를 고민하는 것입니다. 이러한 객체 중심적 사고방식은 복잡한 문제를 더 작은 단위로 나누어 관리하고, 각 부분의 역할과 책임을 명확히 하여 시스템 전체의 구조를 더 명확하고 이해하기 쉽게 만듭니다.
좋은 객체란 무엇일까?: 책임과 협력의 균형
좋은 객체지향 설계는 결국 좋은 객체를 설계하는 것에서 시작됩니다. 좋은 객체는 다음과 같은 특징을 가집니다.
- 명확한 책임: 객체는 자신이 맡은 역할, 즉 책임(Responsibility)이 명확해야 합니다. 너무 많은 책임을 지거나(낮은 응집도), 책임이 불분명하면 좋지 않은 객체입니다. (SRP 원칙 관련)
- 적절한 상태와 행동: 자신의 책임을 수행하는 데 필요한 최소한의 상태 정보와 행동(메서드)만을 가져야 합니다.
- 낮은 결합도: 다른 객체와의 의존성을 최소화하여, 변경이 발생했을 때 파급 효과를 줄여야 합니다. (느슨한 결합 Loose Coupling)
- 높은 응집도: 객체 내부의 속성과 메서드들이 응집력 있게 서로 관련되어 있어야 합니다. (높은 응집도 High Cohesion)
좋은 객체는 스스로 자신의 일을 처리할 수 있어야 하며(자율성), 다른 객체와 협력할 때는 명확한 인터페이스를 통해 소통해야 합니다. 각 객체에게 적절한 책임을 할당하고, 객체 간의 효과적인 협력 관계를 설계하는 것이 좋은 객체지향 설계의 핵심입니다.
다시, 기본으로: OOP 여정의 출발점 객체
객체지향 프로그래밍의 여정은 결국 ‘객체’라는 기본 단위에 대한 깊은 이해에서 시작됩니다. 클래스, 상속, 다형성, 디자인 패턴 등 수많은 고급 개념들도 결국은 좋은 객체를 만들고 효과적으로 활용하기 위한 도구들입니다. OOP의 세계를 더 깊이 탐험하고 싶다면, 잠시 걸음을 멈추고 이 모든 것의 근간이 되는 ‘객체’의 본질에 대해 다시 한번 생각해보는 시간을 갖는 것이 큰 도움이 될 것입니다. 객체에 대한 탄탄한 이해 위에 여러분의 OOP 실력을 쌓아나가시길 바랍니다.
#객체 #Object #객체지향프로그래밍 #OOP #클래스 #인스턴스 #상태 #행동 #식별성 #객체관계