본문 바로가기

모듈·캡슐화·객체·인터페이스

주문 금액 계산, 재고 변경, 알림 전송이 한 코드에 뒤섞이면 할인 규칙 하나를 고쳐도 재고와 알림을 함께 시험해야 한다. 반대로 아무 기준 없이 파일만 잘게 나누면 한 기능을 바꿀 때 여러 파일을 오가야 한다. 좋은 분리는 같은 이유로 변하는 상태와 행위를 묶고, 외부에는 필요한 계약만 드러내는 것이다.

코드가 커질수록 파일 수보다 변경 경계가 중요하다

주문 금액 계산과 재고 변경, 알림 전송이 한 코드에 뒤섞이면 할인 규칙 하나를 고쳐도 재고와 알림을 함께 시험해야 한다. 반대로 아무 기준 없이 파일만 잘게 나누면 한 기능을 바꿀 때 여러 파일을 오가야 한다. 그래서 좋은 분리는 같은 이유로 변하는 상태와 행위를 묶고, 외부에는 필요한 약속만 드러내는 것이다.

이 절의 네 개념은 그 분리를 돕는 도구다. 모듈은 변경 단위를 만들고, 캡슐화는 내부를 보호하는 원칙이 된다. 인터페이스 타입은 코드 수준에서 동작의 약속을 정하고, 객체와 조합은 상태와 행위를 묶어 더 큰 행동을 만든다.

먼저 되짚기: 함수와 인터페이스에서 시작한다

함수는 입력과 결과·효과를 가진 처리단위였고, 인터페이스는 두 요소가 교환하는 입력·출력·오류의 약속이었다. 여러 함수와 상태를 하나의 역할로 묶으면 모듈이 된다. 그리고 허용한 함수만 외부와의 약속으로 내놓으면 내부를 바꿔도 그 영향이 바깥으로 번지지 않는다.

함수와 인터페이스를 변경 경계로 확장한다

함수와 상태를 같은 변경 이유로 묶으면 모듈이 되고, 모듈은 어디까지가 자기 몫인지를 정한다. 캡슐화는 모듈의 공개 연산에서 상태 규칙을 지키게 하고, 인터페이스 타입은 외부가 의존할 동작의 약속을 정한다. 이 세 역할은 주문 모듈과 결제 계약의 연결에서 함께 본다.

모듈

모듈은 관련 기능과 데이터를 하나의 역할 아래 묶고 무엇을 공개할지 정한 구성 단위다. 모듈 안의 요소는 같은 이유로 함께 변하도록 모으고, 바깥에는 필요한 인터페이스만 제공한다. 언어와 시스템에 따라 모듈은 파일, 패키지, 네임스페이스, 라이브러리, 서비스로 구현될 수 있다. 그렇다고 모든 모듈이 독립 배포되거나 다른 곳에서 재사용되어야 하는 것은 아니다. 핵심은 무엇이 바뀔 때 이 모듈을 고치는지와 무엇을 공개하는지를 분명히 하는 것이다.

캡슐화

캡슐화(encapsulation)는 내부 상태와 구현을 숨기고 허용된 연산만 노출하는 원칙이다. 숨김 자체가 목적은 아니다. 상태를 올바르게 유지하는 규칙을 한곳에서 강제하는 것이 목적이다. 예를 들어 잔액을 직접 고치지 못하게 하고 입금, 출금 연산만 제공하면 음수 금지와 기록 규칙을 한곳에서 지킬 수 있다.

객체 조합 전에 캡슐화를 다시 잇는다

캡슐화는 상태를 직접 노출하지 않고 허용된 연산으로 규칙을 강제한다. 그래서 객체를 조합할 때도 각 객체는 자기 상태 규칙을 지키면서, 다른 객체에는 공개된 약속을 통해서만 의존해야 한다.

조합은 내부상태 공유가 아니라 계약 협력이다

주문 객체가 결제 객체를 부품으로 가져도 결제 내부의 잔액을 직접 고치지 않고 authorize() 같은 공개 연산을 호출한다. 즉 조합이 캡슐화를 깨는 지름길이 되어서는 안 된다.

값은 바뀌어도 지킬 조건은 남는다

불변조건(invariant)은 정한 확인 지점마다 성립해야 하는 상태의 조건이다. 여기서는 다른 코드가 공개된 연산을 호출하기 직전과, 연산이 정상적으로 끝나 결과를 돌려줄 때 지킬 상태 규칙을 말한다. 상태를 처음 만들 때도 이 조건을 만족시켜야 뒤의 연산이 그 상태를 믿고 사용할 수 있다.

잔액이 음수가 되면 안 되는 계좌에서 잔액 100에 출금 20을 적용하면 80이 된다. 값은 바뀌었지만 전후 모두 잔액 ≥ 0을 만족한다. 출금 120은 이 조건을 깨므로 거부하고 원래 상태를 보존해야 한다. 즉 불변조건은 값 자체를 고정한다는 뜻이 아니라, 값이 바뀌는 동안에도 정한 지점에서 지킬 약속이다.

내부 처리 도중에는 잠시 조건이 맞지 않는 순서로 값을 고칠 수도 있다. 다만 다른 코드가 그 중간 상태를 보지 못하게 보호하고, 공개된 연산의 결과로 돌려주기 전에는 조건을 복구해야 한다. 이 절의 계좌·주문 예에서는 거절이나 오류로 끝나도 외부에 남는 상태가 비음수 불변조건을 지켜야 한다. 그렇다고 모든 실패에서 모든 값을 원래대로 되돌린다는 뜻은 아니다. 주문은 결과에 따라 거절이나 확인대기 상태를 남길 수 있고, 그때도 합계 규칙은 유지한다. 접근을 숨겼다는 사실만으로 이런 보호가 저절로 생기지도 않는다. 여러 연산이 동시에 실행되거나 저장 중에 실패하는 경우를 다루는 방법은 뒤에서 따로 배우고, 여기서는 먼저 무엇을 지켜야 하는지 정한다.

인터페이스 타입

인터페이스 타입은 구현과 독립적으로 제공해야 할 동작의 형식적인 약속이다. 어떤 이름의 연산이 어떤 입력을 받고 어떤 결과를 돌려주는지 정하지만, 내부 알고리즘과 저장방식은 정하지 않는다. 그래서 여러 구현이 같은 약속을 지키면 사용하는 코드는 구체 구현 대신 이 약속에 의존할 수 있다. 다만 입력과 결과의 자료형이 맞는다고 실제 승인이나 상태 변화까지 같아지는 것은 아니다. 성공 결과의 의미와 오류 때 남는 상태는 모든 구현에 같은 시험을 돌려 확인한다.

객체와 조합

객체와 조합은 상태와 행위를 객체로 묶고, 작은 객체를 조합해 큰 행위를 만드는 모델이다. 객체는 식별 가능한 상태와 그 상태를 다루는 연산을 함께 가진다. 조합은 한 객체가 다른 객체를 부품으로 가지고 그 기능을 위임하는 관계다.

결제 방법을 조합하는 주문 모듈

주문 상태는 공개 연산으로 지키고 결제는 계약으로 바꾼다

상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.

주문 상태는 공개 연산으로 지키고 결제는 계약으로 바꾼다주문 모듈 안에서 placeOrder가 주문 상태와 합계 비음수 규칙을 지킨다. 모듈은 Payment의 pay(amount) 결과 계약을 사용하며 카드·계좌 구현은 같은 계약을 제공한다. 결과는 성공(승인번호)·거절(이유)·기술오류(확인 필요)다. 조합이 결제 내부 상태의 직접 수정을 허용하지는 않는다.order → payment: 사용order → state: 규칙 강제card → payment: 구현account → payment: 구현사용규칙 강제구현구현주문 모듈주문 공개 연산: placeOrder주문 공개 연산placeOrder주문 상태: total ≥ 0주문 상태total ≥ 0Payment 계약: pay(amount) → 결과 성공·거절·기술오류Payment 계약pay(amount) → 결과성공·거절·기술오류CardPayment: 카드 구현CardPayment카드 구현AccountPayment: 계좌 구현AccountPayment계좌 구현
주문 상태는 공개 연산으로 지키고 결제는 계약으로 바꾼다주문 모듈 안에서 placeOrder가 주문 상태와 합계 비음수 규칙을 지킨다. 모듈은 Payment의 pay(amount) 결과 계약을 사용하며 카드·계좌 구현은 같은 계약을 제공한다. 결과는 성공(승인번호)·거절(이유)·기술오류(확인 필요)다. 조합이 결제 내부 상태의 직접 수정을 허용하지는 않는다.order → payment: 사용order → state: 규칙 강제card → payment: 구현account → payment: 구현사용규칙 강제구현구현주문 모듈주문 공개 연산: placeOrder주문 공개 연산placeOrder주문 상태: total ≥ 0주문 상태total ≥ 0Payment 계약: pay(amount) → 결과 성공·거절·기술오류Payment 계약pay(amount) →결과성공·거절·기술오류CardPayment: 카드 구현CardPayment카드 구현AccountPayment: 계좌 구현AccountPayment계좌 구현

주문 모듈 안에서 placeOrder가 주문 상태와 합계 비음수 규칙을 지킨다. 모듈은 Payment의 pay(amount) 결과 계약을 사용하며 카드·계좌 구현은 같은 계약을 제공한다. 결과는 성공(승인번호)·거절(이유)·기술오류(확인 필요)다. 조합이 결제 내부 상태의 직접 수정을 허용하지는 않는다.

그림에서 주문 모듈 안의 placeOrder는 주문 상태의 total ≥ 0 규칙을 지키고, 모듈은 Payment 계약으로 표시된 결제 인터페이스를 사용한다. 카드 구현과 계좌 구현은 그 인터페이스가 정한 약속을 똑같이 제공한다. 즉 주문 모듈은 주문 상태와 검증 함수를 묶고, 상태를 직접 수정하지 못하게 placeOrder 같은 연산만 공개한다. 주문 모듈의 객체는 구체 카드 코드가 아니라 Payment 인터페이스 타입을 사용한다. 그래서 실행할 때 카드나 계좌 구현을 부품으로 넣으면 주문 로직을 바꾸지 않고 결제 방법을 교체할 수 있다. 이것이 조합을 통한 행동 구성이다.

도식의 pay(amount) -> 결과는 amount라는 금액 입력을 pay에 전달하고 화살표 오른쪽의 결과를 돌려받는다는 표기다. 교육용 공통 결과를 성공(승인번호), 거절(이유), 기술오류(확인 필요)로 정하면 카드와 계좌 구현 모두 이 셋의 의미를 지켜야 한다. 주문 모듈은 카드사 내부 번호가 아니라 이 결과를 보고 확정·거절·확인대기 상태를 고른다. 따라서 계좌 구현을 추가해도 주문 코드 곳곳에 결제사별 결과 해석을 새로 흩어 놓을 필요가 없다.

시험에서는 실제 외부 결제 대신 정해 둔 결과를 반환하는 시험 대역(test double)을 넣어 주문의 판단을 먼저 확인할 수 있다. 성공 대역에서는 확정, 거절 대역에서는 거절, 오류 대역에서는 확인대기가 되는지 비교한다. 이것은 주문 모듈의 결과 해석만 떼어 확인하는 방법이다. 실제 카드·계좌 구현이 외부 기관과 올바르게 통신하는지까지 대신 확인해 주지는 않는다.

ME-1: 두 변경요구를 경계에 배치한다

주문 시스템에 다음 요구가 들어왔다고 하자.

  1. 카드 외에 계좌이체 결제를 추가한다.
  2. 주문 합계가 음수가 되지 않도록 규칙을 강화한다.

첫 요구는 PaymentMethod.pay(amount) 같은 인터페이스가 정한 약속 아래 새 구현을 조합하는 변화다. 이 예의 PaymentMethod는 앞 도식의 Payment와 같은 교육용 결제 인터페이스를 가리킨다. 두 번째 요구는 주문 상태를 바꾸는 공개 연산에서 total >= 0 불변조건을 지키게 하는 변화다.

결제수단을 추가하려고 주문 내부 필드를 모두 공개해야 하거나, 합계 규칙 때문에 모든 결제 구현을 고쳐야 한다면 모듈의 나눔이 잘못 얽혀 있다는 신호다. 같은 파일에 있는지가 아니라 같은 이유로 변하는지를 보아야 한다.

실행 예에서는 합계 3000원, 상태 접수인 주문에 결제 구현을 넣어 placeOrder를 호출한다. 주문 모듈은 합계가 0 이상인지 먼저 검사하고 결제 pay에 3000원을 전달하며, 성공 승인번호를 받으면 주문을 확정한다. 카드 대신 계좌 구현을 넣어도 같은 성공 결과라면 같은 확정 상태가 된다. 반대로 새 입력으로 계산한 후보 합계가 -3000원이면, 주문 상태에 반영하거나 결제를 호출하기 전에 거부하고 기존 합계와 접수 상태를 보존한다. 음수 합계가 이미 저장된 주문을 정상으로 본다는 뜻이 아니다. 거절이면 거절 상태를, 기술오류면 확인대기 상태와 원인을 남겨 성공처럼 보이지 않게 한다. 이 상태 전이와 결제 결과는 교육용으로 정한 규칙이며, 실제 실패와 동시 처리를 막는 방법은 다음 절과 뒤의 전문 장에서 배운다.

풀이를 가리고 세 실행 결과를 추적한다

각 경우는 서로 독립적이며, 시작 주문의 합계는 3000원이고 상태는 접수다. placeOrder는 후보 합계를 검사한 뒤에만 결제를 호출한다. 외부 승인 효과는 이 시험의 대상이 아니므로 결제 결과는 대역이 정해 준다. 각 경우에 결제를 호출하는지, 호출 뒤 남을 합계와 상태는 무엇인지 적어 보자.

경우후보 합계결제 대역의 결과
A-3000원호출되면 성공을 반환하도록 설정
B3000원거절(한도 초과)
C3000원기술오류(응답 확인 필요)
상태와 호출 여부를 각각 대조하기

A는 입력 검증에서 거부되므로 성공 대역을 준비해도 결제 호출은 0회다. 기존 합계 3000원과 접수 상태가 그대로 남는다. B는 결제를 호출한 뒤 합계 3000원과 거절 상태, 거절 이유를 남긴다. C는 결제를 호출한 뒤 합계 3000원과 확인대기 상태, 원인을 남긴다. 이때 C의 기술오류를 결제가 확실히 일어나지 않았다는 뜻으로 해석하거나, 곧바로 새 승인을 요청하지 않는다.

세 경우 모두 외부에 보이는 합계는 0 이상이다. 그런데 상태를 보존할지 전이할지는 실패 종류에 따라 다르다. 그러니 불변조건을 지킨다는 말만으로 모든 실패의 결과를 하나로 합치지 않는다. 이 비교는 주문의 판단을 확인하는 시험일 뿐, 실제 외부 승인이나 동시 처리의 안전성까지 확인하지는 않는다.

상속과 조합을 판단한다

상속은 기존 타입의 성질과 동작을 물려받아 더 구체적인 타입을 만드는 관계다. “B는 A의 한 종류이며, A를 기대하는 모든 곳에서 같은 약속을 지킨다”가 참이고 공통 규칙이 안정적일 때 유용하다. 반면 조합은 “A가 B의 기능을 사용한다”는 관계여서 부품을 바꾸거나 여러 행동을 섞기 쉽다.

판단상속조합
관계무엇의 한 종류무엇을 가지고 사용
변경 결합부모 내부 변화가 자식에 퍼질 수 있음인터페이스의 약속을 통해 영향 제한
실행 중 교체일반적으로 어려움부품 교체가 쉬움
적합한 조건안정적인 분류와 대체 가능성기능 위임·정책 교체·다중 조립

그래서 기본 선택은 조합으로 두고, 정말로 대체 가능한 종류 관계가 명확할 때 상속을 검토하면 잘못된 계층을 줄일 수 있다. 모듈을 어디서 나눌지는 조직의 편의보다 함께 변하는 역할을 기준으로 정한다.

실패조건과 방어

자료만 숨긴 캡슐화에서는 접근만 제한될 뿐, 상태를 올바르게 바꾸는 규칙은 여러 곳에 흩어져 있다. 불변조건을 지키는 연산을 모듈 안으로 모은다.

비대한 인터페이스는 일부 구현에 필요하지 않은 연산까지 구현하도록 강제한다. 사용하는 쪽이 실제로 필요한 연산만 담은 작은 인터페이스로 나눈다.

잡동사니 모듈에는 관련 없는 공통 함수가 한곳에 모여, 결국 모든 코드가 이 모듈에 의존하게 된다. 변경 이유와 업무상 역할이 같은 것끼리 다시 나눈다.

잘못된 상속은 코드를 재사용하려는 목적만으로 종류 관계가 아닌 대상을 부모와 자식으로 묶는 경우다. 부모가 쓰이던 자리에 자식을 넣어도 같은 약속이 지켜지는지 확인하고, 그렇지 않으면 조합으로 바꾼다.

왕복 위임은 모듈이 아무 판단 없이 다른 모듈 호출만 전달하는 경우다. 그 계층이 따로 맡는 일이 없다면 계층을 없애거나, 맡을 일을 분명히 정한다.

자체 10점 문제

모듈·캡슐화·객체 조합·인터페이스 타입의 관계를 설명하시오.

요구어는 설명이다. 변경 이유와 상태 규칙, 공개된 약속, 구현 교체를 연결한다.

10점 풀이 방향과 모범답안 보기

10점 답안 목차

  1. 모듈의 책임경계
  2. 캡슐화와 불변조건
  3. 인터페이스 타입
  4. 객체 조합
  5. 과도한 상속·공개상태 실패

10점 답안 도식

주문 경계 그림의 상태 규칙과 Payment 의존을 함께 설명한다. Card와 계좌 구현이 같은 약속을 지키면 결제수단을 바꿔도 주문의 판단은 그대로다.

모범답안 축약본: 변경 이유와 상태 규칙으로 경계를 잡는다

모듈은 관련 기능과 데이터를 하나의 역할 아래 묶고 공개 범위를 정한 구성 단위이며, 반드시 독립 배포·재사용 단위인 것은 아니다. 캡슐화는 내부 상태를 직접 노출하지 않고 상태 불변조건을 지키는 연산만 허용한다. 객체는 상태와 행동을 한 개체로 다루는 모델이고, 인터페이스 타입은 구체 구현과 분리된 연산의 형식적인 약속이다. 주문 모듈이 주문 상태와 검증을 묶어 placeOrder만 공개하고, 주문 서비스가 Payment 인터페이스를 통해 카드·계좌 구현을 조합하면 내부 변경의 영향을 모듈 안에 가둘 수 있다. 분리가 잘 되었는지는 같은 이유로 변하는 일이 얼마나 모여 있는지, 바깥 의존이 몇 개인지, 공개한 연산이 얼마나 작은지로 판단한다. 관련 없는 공통기능을 모은 잡동사니 모듈, 종류 관계가 아닌 상속, 판단 없이 호출만 넘기는 계층은 맡을 일을 다시 나누거나 조합으로 고친다.

자체 25점 문제

결제수단 확장과 주문 상태규칙 변경을 동시에 수용하는 모듈 경계를 설계하시오.

요구어는 설계다. 서로 다른 변경이유를 분리하고, 공개할 연산의 약속과 상태 불변조건을 제시한다.

25점 풀이 방향과 모범답안 보기

25점 답안 목차

  1. 변경이유 분리
  2. Order 상태·불변조건
  3. Payment 공개계약
  4. 구현체 조합
  5. 오류·취소 계약
  6. 테스트 대역
  7. 새 구현 추가 영향범위
  8. 경계 누수 점검

25점 답안 도식

주문 모듈의 상태 경계와 결제 계약을 적용한다. 주문 담당은 total 불변조건과 세 결과 대역을 검증하고, 결제 담당은 PaymentMethod의 입력 단위와 결과 의미, 실제 외부 통신을 따로 검증한다.

25점 모범답안

주문 모듈은 품목·합계·상태처럼 주문 규칙 때문에 함께 변하는 데이터를 소유하고, 공개 연산 전후에 합계 음수 금지 같은 불변조건을 검사한다. 내부 필드를 직접 수정하는 통로는 공개하지 않는다.

결제는 PaymentMethod 인터페이스로 금액 입력과 성공·거절·오류 결과를 약속하고, 카드·계좌이체 구현을 조합한다. 새 결제수단을 추가할 때 고치는 범위는 인터페이스를 사용하는 코드와 새 구현으로 제한하며, 주문의 상태규칙은 건드리지 않는다.

ME-1처럼 결제수단 추가와 주문 규칙 강화를 서로 다른 변화로 검증한다. 구현 세부를 외부에 노출하거나 모든 구현이 공통 부모의 내부상태에 강하게 묶이면 조합과 캡슐화의 이점을 잃는다.

교육용 규칙에서는 접수 상태인 합계 3000원 주문을 placeOrder로 검증해 PaymentMethod.pay에 넘기고, 성공·거절·기술오류를 각각 확정·거절·확인대기로 바꾼다. 음수인 후보 합계는 주문 상태에 반영하거나 결제를 호출하기 전에 거부하고, 기존의 유효한 합계와 상태를 보존한다. 새 계좌 구현도 같은 입력 단위와 결과 의미를 지키게 하고, 주문 담당은 세 결과 대역과 음수 입력으로 상태 규칙을 시험한다. 결제 담당은 실제 외부 통신을 따로 확인한다. 주문 필드를 공개하는 방식은 불변조건을 우회하게 하고, 카드 내부 결과를 주문에서 직접 해석하는 방식은 주문을 특정 구현에 묶으므로 둘 다 제외한다.

출처와 적용 경계

본문은 안정된 모듈성·캡슐화·객체 조합 기초와 자체 주문/결제 예를 사용한다. 구현과 분리한 메서드 형식과 호출 약속의 구체 언어 예는 Oracle Java Tutorial의 Interfaces(JDK 8용 자료)에서 확인할 수 있다. 다만 이것은 본문의 가상 결제 인터페이스를 실제 Java 선언으로 검증했다는 뜻이 아니며, 최신 Java 문법 전체를 설명하는 자료도 아니다. 구체 언어의 interface/class/module 문법과 가시성 규칙은 해당 언어 사양을 확인한다.

30초 정리

  1. 모듈은 함께 변하는 기능과 데이터를 묶고, 캡슐화는 상태 규칙을 지키는 연산만 노출한다.
  2. 인터페이스 타입은 구현과 독립적인 형식 계약이며 객체 조합은 작은 행동을 부품처럼 연결한다.
  3. 안정적인 종류 관계가 아니면 상속보다 조합을 우선하고, 비대한 계약과 책임 없는 모듈을 경계한다.

자기교정과 조건 변경

잘못 쓴 답수정복습
모듈은 파일 하나다핵심은 같은 변경이유와 공개 범위다모듈
캡슐화는 모든 필드를 private로 만드는 문법이다상태 규칙을 허용된 연산으로 강제하는 목적을 본다캡슐화
인터페이스는 내부 알고리즘까지 정한다외부에 약속한 동작과 구현을 분리한다정의
조합하면 상대 객체의 내부상태를 직접 써도 된다공개된 연산을 통해 협력한다조합
새 결제수단마다 주문 모듈 전체를 고친다안정된 결제 인터페이스 뒤에 새 구현을 추가한다ME-1

조건을 바꿔 보자. 환불 기능이 추가되면 결제 인터페이스에 환불이라는 의미가 필요한지와 주문 상태 전이가 필요한지를 나누어 판단한다.

결제수단마다 결과형식이 다르다면 인터페이스가 약속할 공통 결과와 구현마다 따로 둘 세부정보를 나누어 설계한다.

주문 합계를 외부에서 직접 수정할 수 있다면 불변조건을 우회하게 되므로, 공개 setter 대신 의미 있는 연산으로 접근을 좁힌다.