데이터만으로는 부족했던 이유
9장에서는 책마루의 결제 수단을 카드결제와 계좌이체로 나누고, 공통 속성을 "결제"라는 상위 엔티티 타입으로 묶었다. 그런데 이 모델에는 빠진 게 있다. "결제를 승인 처리한다", "결제를 취소한다" 같은 동작은 어디에도 나타나지 않는다. EER 모델은 데이터의 정적인 구조는 정확히 그려내지만, 그 데이터를 가지고 무엇을 할 수 있는지는 다루지 않는다.
전통적인 개발 방식에서는 이 문제를 데이터와 함수를 서로 다른 곳에 따로 관리하는 식으로 풀어왔다. 결제 데이터는 데이터베이스 테이블에, 결제를 처리하는 함수는 별도의 프로그램 코드에 있는 식이다. 이 구조의 문제는 결제 방식이 하나 추가될 때마다(예: 책마루가 간편결제를 새로 받기 시작할 때) 데이터 구조와 처리 로직을 각각 따로 찾아서 고쳐야 한다는 점이다 — 두 군데가 따로 놀수록 하나를 고치고 다른 하나를 빠뜨릴 위험이 커진다.
**객체지향 분석(Object-Oriented Analysis)**은 데이터와 그 데이터에 대한 동작을 하나의 단위로 묶어서 다루는 접근이다. 이 장에서는 그 기본 단위인 객체와 클래스, 클래스들 사이의 세 가지 관계(연관·일반화·집단화), 그리고 정보 모델에 동작을 결합하는 과정을 살펴본다.
상향식 사고로의 전환
전통적인 구조적 분석 기법(7장에서 다룬 자료흐름도 중심의 접근)은 "시스템이 수행할 기능"에서 출발해 그 기능을 점점 더 작은 하위 기능으로 쪼개는 하향식(top-down) 방식을 따른다. "온라인 주문 처리"라는 큰 기능을 "재고 확인", "결제 처리", "배송지 확인" 같은 작은 기능으로 나누는 식이다.
객체지향 분석은 반대 방향으로 접근한다. 시스템에 존재하는 데이터 중심의 객체(고객, 상품, 주문)를 먼저 식별하고, 그 객체들이 서로 협력해 기능을 수행하는 방식을 나중에 조립하는 **상향식(bottom-up)**에 가깝다. 이 차이는 실무에서 꽤 다른 결과를 낳는다. 기능 중심으로 설계하면 "결제 방식을 하나 추가한다"는 요구사항이 들어왔을 때 여러 기능 모듈을 함께 고쳐야 하는 경우가 많다. 반면 객체 중심으로 설계하면 결제라는 객체(정확히는 클래스)에 변경이 집중되고, 그 객체를 사용하는 다른 코드는 거의 그대로 둘 수 있다 — 데이터 구조를 중심에 놓았기 때문에 데이터의 변화에는 강하지만, 반대로 기능 자체가 근본적으로 바뀌는 상황에는 하향식만큼 직관적이지 않을 수 있다.
객체와 클래스 — 캡슐화의 기본 단위
**객체(object)**는 데이터(속성)와 그 데이터에 가해지는 동작(오퍼레이션)을 하나로 묶은 단위다. 이렇게 데이터와 행위를 하나의 경계 안에 묶어서, 바깥에서는 그 객체가 제공하는 동작을 통해서만 데이터에 접근하게 만드는 것을 **캡슐화(encapsulation)**라 한다. 유사한 객체들의 모임이 **클래스(class)**이며, 9장에서 다룬 엔티티·엔티티 타입과 사실상 같은 개념이다 — 정보 모델링의 "엔티티"가 여기서는 "객체"로, "엔티티 타입"이 "클래스"로 이름만 바뀐다.
책마루의 "결제" 클래스를 예로 들면, 캡슐화 이전에는 "결제금액을 조회하는 코드"가 시스템 여기저기 흩어져 있을 수 있다. 캡슐화 이후에는 결제 클래스가 결제금액_조회()라는 동작을 스스로 제공하고, 다른 코드는 이 동작을 호출하기만 하면 된다. 결제금액이 어떻게 저장되고 계산되는지는 결제 클래스 내부의 일이 되고, 바깥에서는 그 세부사항을 몰라도 된다.
캡슐화가 실무에서 주는 이득: 책마루가 나중에 "결제금액에 배송비를 포함해서 계산한다"로 규칙을 바꾼다고 하자. 캡슐화가 되어 있다면
결제금액_조회()내부 로직 한 곳만 고치면 된다. 캡슐화가 안 되어 있고 결제금액 계산 코드가 여러 화면에 복사돼 있었다면, 그 모든 곳을 찾아 똑같이 고쳐야 하고 하나라도 빠뜨리면 화면마다 결제금액이 다르게 표시되는 버그가 생긴다.
클래스 사이의 관계
클래스 하나만으로는 시스템이 되지 않는다는 점도 정보 모델링과 같다. 객체지향 분석에서 클래스 사이의 관계는 크게 세 가지 — 연관관계, 일반화, 집단화 — 로 나뉜다.
연관관계와 다중성
연관관계는 9장의 "관계"에 대응한다. 표기법도 비슷해서, 클래스 사이를 선으로 잇고 양쪽 끝에 참여 가능한 개수(다중성)를 적는다. 책마루 예제로 표현하면 이렇다.
| 클래스 A | 다중성 | 클래스 B | 의미 |
|---|---|---|---|
| 고객 | 1 | 주문 | * |
| 주문 | 1 | 결제 | 0..1 |
| 직원 | 0..1 | 주문 | * |
9장의 매핑 제약조건·참여 제약조건이 여기서는 "다중성(multiplicity)"이라는 하나의 표기로 합쳐진다는 점만 다르고, 담고 있는 정보는 같다. 다만 연관관계 자체가 고유한 속성이나 동작을 가져야 할 때는 그 관계를 별도의 클래스로 승격시킬 수 있다 — 책마루의 "주문상품" 관계가 좋은 예다. 주문과 상품은 다대다 관계지만, "이 주문에서 이 상품을 몇 개 샀는가"라는 수량 정보와 "수량을 변경한다"는 동작을 가지므로 주문상품을 독립된 클래스로 다루는 편이 자연스럽다.
일반화와 상속, 그리고 다중상속의 함정
9장에서 정보 모델링의 일반화가 "공통 속성을 상위 클래스로 끌어올리는 것"이었다면, 객체지향 분석의 일반화는 여기에 동작까지 포함시킨다. 책마루의 결제 클래스 구조를 다시 가져와보자.
- 상위 클래스 결제: 속성(결제번호, 결제금액, 결제상태), 동작(
결제상태_조회()) - 하위 클래스 카드결제: 속성(승인번호) + 상위 클래스 상속, 동작(
카드승인_요청()) - 하위 클래스 계좌이체: 속성(입금자명, 입금예정일) + 상위 클래스 상속, 동작(
입금확인_대조())
하위 클래스는 상위 클래스의 속성과 동작을 모두 상속받으므로, 카드결제 객체는 결제상태_조회()를 별도로 정의하지 않아도 그대로 쓸 수 있다. 이렇게 공통 정의를 한 번만 하고 재사용하는 구조는 분석 결과의 일관성을 높이고 중복을 줄인다.
한 가지 주의할 점은 다중상속이다. 하나의 하위 클래스가 상위 클래스를 둘 이상 가지면(예: "배달겸직원"이 "직원" 클래스와 "배달원" 클래스를 동시에 상속) 두 상위 클래스에 같은 이름의 속성이나 동작이 있을 때 어느 쪽을 따를지 모호해질 수 있다. 책마루처럼 작은 조직에서는 다중상속이 필요한 상황 자체가 드물지만, 만약 등장한다면 어떤 속성이 어느 상위 클래스에서 왔는지를 요구사항 명세서나 자료 사전에 명시적으로 기록해 모호성을 없애야 한다.
집단화 — 부분과 전체
**집단화(aggregation)**는 "부분-전체" 관계를 나타낸다. 여러 부속 객체가 모여 하나의 객체를 이루는 구조로, 일반화(is_a 관계)와는 성격이 다르다. 책마루의 "주문" 클래스는 여러 "주문상품" 객체를 부분으로 갖는다 — 주문 하나가 주문상품 여러 개로 "구성"되는 것이지, 주문상품이 주문의 한 "종류"인 것은 아니다. 이 차이가 일반화와 집단화를 구별하는 기준이다.
| 구분 | 관계의 성격 | 책마루 예 |
|---|---|---|
| 일반화 | A는 B의 한 종류다(is_a) | 카드결제는 결제의 한 종류다 |
| 집단화 | A는 B로 구성된다(part-of) | 주문은 여러 주문상품으로 구성된다 |
집단화는 여러 단계로 중첩될 수도 있다. 예를 들어 "장바구니"가 여러 "장바구니항목"으로 구성되고, 각 장바구니항목이 다시 "상품"을 참조하는 구조처럼, 집단화 관계를 계속 쌓아 더 큰 객체를 표현할 수 있다.
집단화를 판단할 때 자주 헷갈리는 경우는 "이게 집단화인지, 그냥 연관관계인지"다. 기준은 부분 객체가 전체 객체 없이도 독립적으로 의미가 있는가에 있다. 책마루의 "주문상품"은 특정 주문에 속하지 않고서는 존재할 이유가 없으므로 집단화가 자연스럽다. 반면 "상품"은 어떤 주문에 담기지 않더라도 매대에 진열된 독립적인 개체로 의미가 있으므로, 주문과 상품 사이는 집단화가 아니라 앞서 다룬 연관관계(다대다, 주문상품을 매개로)로 표현하는 것이 맞다.
세 모델을 통합해 객체를 완성하기
7장과 8장에서 각각 기능 모델(자료흐름도)과 동적 모델(상태변화도, 사건 추적도)을 다뤘다. 객체지향 분석의 마지막 단계는 이 두 모델의 결과를 지금까지 만든 객체 모델에 통합하는 것이다. 통합의 원칙은 단순하다.
- 동적 모델에서 어떤 상태 전환이 특정 동작에 의해 일어난다면, 그 동작은 해당 객체의 오퍼레이션이 된다.
- 기능 모델(자료흐름도)의 프로세스가 어떤 객체의 데이터를 읽거나 갱신한다면, 그 프로세스는 그 객체의 오퍼레이션으로 흡수된다.
책마루의 "주문" 객체로 예를 들면, 8장에서 다룬 주문 상태변화도의 "포장완료 → 배달중" 전환은 배달시작()이라는 오퍼레이션으로 매핑되고, 7장에서 그린 "재고 차감" 프로세스는 상품 객체의 재고_차감(수량) 오퍼레이션으로 흡수된다. 이렇게 세 모델을 통합하고 나면, 각 클래스는 자신의 속성과 자신이 수행할 수 있는 동작을 모두 갖춘 완결된 형태가 된다 — 이 결과물이 다음 단계인 설계(12장 이후)의 출발점이 된다.
통합 결과가 실제로 어떤 모습인지 "상품" 클래스로 확인해보자.
클래스: 상품
속성: 상품번호, 상품명, 카테고리, 가격, 재고수량
오퍼레이션:
재고_차감(수량)
{ 재고수량에서 수량만큼 뺀다. 재고수량이 0 미만이 되면 처리를 거부한다.
이 오퍼레이션은 8장 상태변화도의 "주문접수 → 포장중" 전환에서 유도되었다. }
재고_보충(수량)
{ 재고수량에 수량만큼 더한다. 7장 자료흐름도의 "입고 처리" 프로세스에서 유도되었다. }
품절여부_확인()
{ 재고수량이 0이면 참을 반환한다. }
이 클래스 정의를 보면 속성은 9장의 정보 모델에서 그대로 가져왔고, 오퍼레이션은 앞서 만든 동적 모델과 기능 모델에서 유도되었다는 출처가 주석으로 남아 있다. 이렇게 오퍼레이션이 어느 모델에서 왔는지 기록해두면, 나중에 상태변화도나 자료흐름도가 바뀌었을 때 어떤 오퍼레이션을 함께 고쳐야 하는지 추적하기 쉬워진다 — 분석 단계의 산출물들 사이에 추적성을 유지하는 것도 객체지향 분석이 강조하는 원칙 중 하나다.
객체지향 방법의 득과 실
객체지향 분석이 만능은 아니다. 장점과 함께 실무에서 마주치는 현실적인 제약도 짚어둘 필요가 있다.
- 장점: 분석·설계·구현 사이에 표현 방식이 크게 다르지 않아 개발이 이어지기 쉽고, 정의된 클래스를 다른 프로젝트에서도 재사용할 수 있다. 요구사항이 바뀔 때도 관련 클래스만 수정하면 되는 경우가 많아 변경에 유연하다.
- 한계: 객체지향 개념과 표기법 자체를 익히는 데 학습 비용이 든다. 기능 중심으로 사고해온 개발자가 객체 중심 사고로 전환하는 데도 시간이 걸린다. 클래스 계층이 깊어지면 상속 구조를 추적하기 어려워지고, 실행 중 객체를 계속 생성·소멸시키는 경우 성능 문제가 제기될 수도 있다.
책마루처럼 규모가 작은 프로젝트라면 클래스 계층을 무리하게 깊이 만들 필요는 없다. 결제처럼 실제로 하위 유형이 명확히 갈리는 곳에만 일반화를 적용하고, 나머지는 단순한 클래스로 두는 것이 오히려 더 유지보수하기 쉬운 설계가 된다. "언젠가 필요할지도 모른다"는 이유만으로 상속 구조나 집단화 단계를 미리 만들어두는 것은, 지금 당장 풀어야 할 문제를 뒤로 미루면서 복잡도만 앞당겨 떠안는 선택이 되기 쉽다.
핵심 정리
- 객체지향 분석은 데이터와 동작을 캡슐화해 하나의 단위(객체)로 묶는다. 유사한 객체의 모임이 클래스이며, 9장의 엔티티·엔티티 타입에 대응한다.
- 클래스 사이의 관계는 연관관계(다중성으로 표현), 일반화(is_a, 속성과 동작을 상속), 집단화(part-of, 부분-전체 구조) 세 가지로 나뉜다.
- 다중상속은 상위 클래스가 둘 이상일 때 속성·동작의 출처가 모호해질 수 있어 요구사항 명세서에 명시적으로 기록해야 한다.
- 객체지향 분석의 마지막 단계는 동적 모델의 상태 전환과 기능 모델의 프로세스를 객체의 오퍼레이션으로 흡수해, 정적 구조와 행위를 모두 갖춘 클래스를 완성하는 것이다.
- 객체지향 방법은 재사용성과 변경 유연성이 강점이지만, 학습 비용과 계층 구조 관리 부담이라는 현실적인 제약도 함께 고려해야 한다.
다음 장(11장, 유스케이스와 UML)에서는 지금까지 만든 클래스들이 실제로 어떤 상황에서, 누구에 의해 사용되는지를 사용자 관점에서 정리하는 유스케이스 기법을 다룬다. 책마루의 손님과 직원이 각각 시스템을 어떻게 사용하는지를 액터와 유스케이스로 식별하고, 이번 장에서 만든 결제 클래스 구조가 유스케이스 명세서에서 어떻게 다시 등장하는지 살펴본다.