회사에서 동료에게 추천받은 책. DDD 같은 레이어드 아키텍처나 객체지향을 계속 공부하면서도 실제 개발할 때 적용하는 것이 쉽지만은 않았다. 이 책은 '실용주의'라는 제목답게 이론에 치우치기보다 업무에 바로 적용할만한 예시를 알려준다. 마침 내가 겪고 있던 문제와 완전히 동일한 상황들이 예시로 나와서 많은 도움을 받고 있기에 한번 정리해보려고 한다. 다만 여기서는 자바/스프링으로 예시를 들어주기 때문에 적절하게 파이썬으로 변경했다.
객체지향
절차지향과 비교
- 절차지향은 순차지향과는 다르다. 순차지향 프로그래밍의 Sequential은 '순차적으로'라는 뜻으로 말 그대로 코드를 위에서 아래로 읽겠다는 의미.
- 절차는 Procedure로 컴퓨터 공학에서는 함수로 통하기 때문에 절차지향 프로그래밍은 사실상 '함수' 지향 프로그래밍이다.
-> 자바나 코틀린 같은 객체지향 언어를 써도 함수 위주로 프로그램을 만든다면 여전히 절차지향 패러다임으로 개발하고 있는 셈이다.
서비스 함수에 비즈니스 로직을 늘어놓는 것이 아니라 객체가 처리하도록 책임을 나누는 것이 객체지향으로의 첫걸음이다. 정리하자면 다음과 같다.
- 객체에 메세지를 전달
- 객체가 책임을 지게 됨
- 객체는 책임을 처리하는 방법을 스스로 알고 있음
책임과 역할
- 책임을 '함수가 아닌 객체에 할당하는 것'이 중요
- 객체를 추상화한 역할에 책임을 할당
- C언어가 객체지향 언어가 아닌이유 : C언의 구조체(struct)는 추상의 개념을 지원하지 못하기 때문에 절차지향 언어이다.
구현과 역할을 분리하고 역할에 책임을 할당하면 실제 객체가 어떤 객체인지 상관하지 않아도 되서 확장에 유연해진다. 새로운 요구사항에 대응하는 새로운 구현체만 만들어 주면 되기 때문.
객체지향의 본질은 언어나 문법, 더불어 추상화, 다형성과 같은 특징에 있는 것이 아니다. 핵심은 자아를 가진 객체들이 서로 협력하는 방식으로 개발되는 것에 가깝다.
TDA 원칙
Tell, Don't Ask - 물어보지 말고 시켜라
TDA를 따르지 않는 코드
class Order:
def __init__(self, items, status):
self.items = items
self.status = status
def process_order(order):
# Order 객체의 상태를 묻고 조건에 따라 처리
if order.status == 'PENDING':
# 주문 처리 로직
print("Processing order with items:", order.items)
else:
print("Order is not pending.")
order = Order(['item1', 'item2'], 'PENDING')
process_order(order)
TDA를 따르는 코드
class Order:
def __init__(self, items, status):
self.items = items
self.status = status
def process(self):
if self.status == 'PENDING':
print("Processing order with items:", self.items)
else:
print("Order is not pending.")
order = Order(['item1', 'item2'], 'PENDING')
order.process()
process_order 라는 함수에서 order.status를 직접 보고 처리하는 방식에서 Order 클래스 자체에 process를 추가해 상태 처리에 따른 로직을 Order 객체가 책임지게 함. 객체는 데이터 덩어리에서 벗어나 스스로 상태를 처리할 수 있는 능동적인 객체로 바뀜.
객체 종류
VO(Value Object: 값 객체)
값의 특징
- 불변성: 값은 변하지 않는다. (숫자 1은 영원히 숫자 1)
- 불변성을 추구하면 소프트웨어가 갖고 있는 불확실성을 제거함으로, 시스템이 예측가능하게 바뀌고 신뢰성 있는 시스템을 만들어나갈수 있다.
- 동등성: 값의 가치는 항상 같다. (모든 숫자 1은 위치 시간 관계 없이 항상 숫자 1)
- 어떤 객체가 값이고 상태가 모두 같다면 같은 객체로 봐야한다.
- 자가 검증: 값은 그 자체로 올바름 (1은 1.1이 아님)
VO의 목적은 신뢰할수 있는 객체를 어떻게 만들지, 어떤 값을 불변으로 만들지, 어디까지 값을 보장해야 할지 고민하는 과정에 있다. VO 자체를 추구하기 보다 불변성, 동등성, 자가 검증, 신뢰할 수 있는 객체를 추구해야 한다.
파이썬에서는 dataclass를 사용하면 자바처럼 equals나 hashCode를 따로 재정의하지 않아도 된다.
from dataclasses import dataclass
@dataclass(frozen=True)
class AccountVO:
account_id: int
account_name: str
balance: float
account1 = AccountVO(account_id=12345, account_name="John Doe", balance=1000.0)
account2 = AccountVO(account_id=12345, account_name="John Doe", balance=1000.0)
# 동등성 비교
print(account1 == account2) # True
# 해시값 비교
print(hash(account1) == hash(account2)) # True
DTO(Data Transfer Object)
다른 객체나 시스템에 데이터를 구조적으로 만들어 전달하기 위한 객체
DTO에 관한 오해
- DTO는 프로세스, 계층 간 데이터 이동에 사용된다. > DTO는 조금 더 단순하고 범용적인 개념으로 데이터를 전달하고 싶은 상황이라면 어디서든 사용될 수 있다.
- DTO는 반드시 게터, 세터를 갖고 있다. > 멤버 번수를 public으로 선언할 수도 있다.
- DTO는 데이터베이스에 데이터를 저장하기 위해 사용되는 객체다. > 데이터베이스의 데이터와 헷갈린 말이다.
DAO(Data Access Object)
데이터베이스 접근과 관련된 역할을 지닌 객체
- 데이터베이스와 연결을 관리
- 데이터베이스에 연결해 데이터에 대한 CRUD 연산 수행
- 보안 취약성을 고려한 쿼리 작성
도메인 로직과 데이터베이스 연결 로직을 분리하기 위해 존재한다.
Entity(개체)
엔티티는 여러군데에서 공통적으로 사용하는 단어로 사용처에 따라 의미가 조금씩 다르다.
도메인 엔티티
- 식별가능하고 비즈니스 로직을 갖고 있는, 조금 특별하게 관리되는 클래스로 만들어진 객체
- 결국 도메인 모델의 일종으로 도메인 비즈니스 로직을 포함하는 객체
DB 엔티티
- 데이터베이스 분야에서 사용하는 유/무형의 객체
행동
객체는 단순한 데이터 덩어리로서 존재하는 것이 아니라 '행동'하는 것이 중요하다. TDA 원칙을 적용하면 객체를 행동하게 만들 수 있다.
자동차 클래스를 만들어주세요 라는 요청이 왔을때
자동차에 필요한 속성 먼저 떠올린 개발자 A
class Car:
def __init__(self, frame, engine, wheel, speed=0, direction='north'):
self.frame = frame
self.engine = engine
self.wheel = wheel
self.speed = speed
self.direction = direction
자동차에 필요한 행동 먼저 떠올린 개발자 B
class Car:
def drive(self):
pass
def change_direction(self, amount):
pass
def accelerate(self, increment):
pass
def decelerate(self, decrement):
pass
A의 클래스는 절차지향 언어에서 구조적인 데이터 덩어리를 만드는데 사용하는 구조체와 다를바 없다. B는 행동을 정의하고 있어 객체끼리 협력하게 하기 수월하다.
행동과 구현
B 클래스의 drive 메서드를 구현하게 되면 Car 클래스에 속성이 생겨버린다.
class Car:
# 구현을 하려고 하니 속성이 생김
def __init__(self, speed=0):
self.speed = speed
def drive(self):
if self.speed > 0:
print(f"The car is driving {self.speed} km/h towards {self.direction}.")
else:
print("The car is not moving.")
메서드를 구현하려고 하니 데이터 위주로 사고로 돌아옴. 구현을 고민하지 말고 행동을 고민해야함. 어떤 메세지를 처리할지 고민하고 '어떻게'는 그 다음에 생각할 것.
이런 경우에 사용할 수 있는 것이 자바에서는 인터페이스인데, 파이썬에는 인터페이스가 없기 때문에 추상클래스로 유사한 역할을 할 수 있다. 우선 추상클래스를 통해 메서드들을 정의해놓는다면 세부 구현에 구애받지 않고 행동만 고민할 수 있다.
from abc import ABC, abstractmethod
class Car(ABC):
@abstractmethod
def drive(self):
pass
@abstractmethod
def change_direction(self, amount: float):
pass
행동과 역할
자동차 클래스를 만들어달라는 요청에 '자동차'라는 용어가 포함되어 데이터 위주의 사고를 하게된다.
탈것 클래스를 만들어 줄 수 있나요?
라는 요청에는 어떤 행동을 가져야 하는지 부터 상상할 수 있게 된다.
자동차는 실체여서 구현에 필요한 데이터 위주의 사고를 하고, 탈것은 역할 이어서 행동 위주의 사고를 하게 되는 것이다. 따라서 자동차 클래스를 만들어 줄 수 있나요? 같은 요청에 역으로 질문 할 것.
- 자동차는 어떤 행동을 하는 객체인가요?
- 꼭 자동차여야 하나요?
- 자동차 클래스를 만들어서 달성하려는 목표가 뭔가요?
탑승할 수 있고, 달릴 수 있으면 좋겠네요
라는 식으로 클라이언트 요구사항이 정리 될 수 있다.
만약 Car라는 세부구현에 집중했다면 자전거라는 요구사항이 추가될 때 아래와 같이 끔찍한 코드를 작성하게 된다.
class User:
def ride(self, type, obj):
if type == "CAR" and isinstance(obj, Car):
obj.ride()
elif type == "BICYCLE" and isinstance(obj, Bicycle):
obj.ride()
이렇게 하면 안되지만 시간이 부족해 작업방향이 분기문으로 흘러간다. 만약 말도 탈 수 있게 해달라고 한다면 분기문이 계속해서 늘어날 것이다.
-> 최근 AIDT 추천학습과 추천 코멘트 API 작업을 하면서 가장 많이 느낀 부분으로, 하나의 상세한 구현체에만 집중하다보니 확장성이 떨어지고 어거지 분기문이 추가되는 것을 보았다. 예제와 정말 동일하게 흐름이 흘러갔는데 처음에는 선생님 코멘트만 만들어달라는 요청이 있어 그 부분에만 포커스를 맞춰 구현을 진행했다. 하지만 후에 학생에게도 코멘트를 보내달라는 요청이 생겼을때 바로 분기문이 생기게 되었다. 이 상황에서 만약 학급에 코멘트를 추가해달라고 한다면? 벌써부터 머리가 아파온다.. 초반부터 행동과 역할에 맞춰 추상화를 했더라면 이런 일이 없었을 텐데 많이 아쉽다. 지속적인 리팩토링이 필요할 것 같다.
메서드
함수는 같은 입력에 같은 출력을 갖는다. 객체지향에서는 협력 객체에 요청할 때 '함수를 실행한다' 대신 '메시지를 전달한다'고 표현한다. 실제 어떤 방법(method)으로 일을 처리할지는 객체가 정한다. 그래서 객체지향에서 객체가 수행하는 함수를 메서드라고 부른다.
따라서 메서드를 어떻게 구현할지 집중하는 것은 결국 알고리즘 구현에 집착하게 되기 때문에 좋지 않은 방법이다. 결국 객체의 메서드를 작성하는 행위가 함수를 작성하는 것과 다를바 없어져 절차지향적 코드가 나오게 된다.
'책 & 스터디' 카테고리의 다른 글
데이터 플랫폼 설계와 구축 - 1장 데이터 플랫폼 소개 (0) | 2025.04.15 |
---|---|
[자바/스프링 개발자를 위한 실용주의 프로그래밍] - 1부 객체 지향(2) SOLID (0) | 2024.11.04 |
도메인 주도 설계 첫걸음 Part 2. 전술적 설계 5장, 6장 (2) | 2024.09.03 |
도메인 주도 설계 첫걸음 Part 1. 전략적 설계 (0) | 2024.08.26 |
object 스터디를 마치며.. (1) | 2024.05.31 |