클래스 다이어그램의 언어: 이름, 속성, 연산, 접근 제어자 완벽 분석

복잡하게 얽힌 시스템의 구조를 명쾌하게 보여주는 클래스 다이어그램이라는 지도를 제대로 읽기 위해서는, 먼저 지도에 사용된 기호와 범례, 즉 그 언어의 기본적인 문법을 마스터해야 합니다. 클래스 다이어그램의 가장 핵심적인 문법 요소는 바로 클래스를 표현하는 사각형 안에 담긴 ‘클래스 이름’, ‘속성(Attributes)’, ‘연산(Operations)’, 그리고 이들 앞에 붙는 ‘접근 제어자(Access Modifiers)’입니다. 이 네 가지 구성 요소는 단순한 표기를 넘어, 객체 지향의 핵심 철학인 캡슐화, 정보 은닉, 책임과 역할 등을 시각적으로 응축하고 있습니다.

이 구성 요소들을 정확히 이해하는 것은 개발자뿐만 아니라, 시스템의 논리적 설계를 파악해야 하는 제품 책임자(PO)나 기획자에게도 필수적입니다. 각 요소가 어떤 의미를 가지며 왜 그렇게 표현되는지를 알게 되면, 기술팀이 작성한 설계도를 더 깊이 있게 해석하고, 비즈니스 요구사항이 어떻게 기술적으로 반영되는지에 대해 훨씬 더 정교하고 원활한 소통을 할 수 있게 됩니다. 정보처리기사 시험의 단골 문제이기도 한 이 네 가지 기본 문법을 하나씩 상세히 분석하여, 클래스 다이어그램이라는 언어를 자유자재로 구사하는 능력을 길러보겠습니다.


클래스 이름 (Class Name): 모든 것의 정체성

이름, 그 이상의 의미

클래스 다이어그램의 시작은 하나의 클래스를 나타내는 사각형과 그 최상단에 위치한 ‘클래스 이름’입니다. 이 이름은 해당 클래스가 시스템 내에서 어떤 개념적, 실체적 대상을 모델링하는지를 나타내는 고유한 정체성입니다. 좋은 클래스 이름은 프로젝트에 참여하는 모두가 그 역할을 즉시 이해할 수 있도록 명확하고 간결해야 하며, 주로 해당 개념을 가장 잘 나타내는 단일 명사를 사용합니다. 예를 들어, UserOrderProduct 처럼 도메인(해당 업무 영역)에서 통용되는 용어를 사용하는 것이 이상적입니다.

이름을 짓는 방식에도 관례가 있습니다. 여러 단어가 조합될 경우, 각 단어의 첫 글자를 대문자로 쓰는 ‘파스칼 케이스(PascalCase)’를 따르는 것이 일반적입니다. ShoppingCartPaymentGateway 등이 그 예입니다. 클래스 이름은 단순한 라벨이 아니라, 시스템의 어휘를 구성하는 첫 단추입니다. 명확하고 일관된 이름 체계는 다이어그램의 가독성을 높이고, 궁극적으로는 코드의 품질까지 향상시키는 중요한 첫걸음입니다.

추상 클래스와의 구분: 기울임꼴의 약속

모든 클래스가 구체적인 실체, 즉 인스턴스를 만들기 위해 존재하는 것은 아닙니다. 어떤 클래스들은 자식 클래스들이 상속받아야 할 공통적인 특징만을 정의하고, 스스로는 인스턴스화될 수 없도록 설계되는데, 이를 ‘추상 클래스(Abstract Class)’라고 합니다. 클래스 다이어그램에서는 이러한 추상 클래스를 일반 클래스와 구분하기 위해 클래스 이름을 기울임꼴(Italics)로 표기하거나, 이름 아래 {abstract} 라는 제약 조건을 명시하는 약속을 사용합니다.

예를 들어, Shape 라는 추상 클래스는 draw() 라는 추상 연산을 가질 수 있습니다. Shape 자체는 인스턴스를 만들 수 없지만, 이를 상속받는 CircleRectangle 같은 구체적인 클래스들이 각자의 draw() 연산을 반드시 구현하도록 강제하는 역할을 합니다. 다이어그램에서 Shape 라는 이름이 기울임꼴로 되어 있다면, 우리는 이 클래스가 직접 사용되기보다는 다른 클래스들의 부모 역할을 하는 템플릿이라는 중요한 정보를 즉시 파악할 수 있습니다.


속성 (Attributes): 객체의 상태를 정의하다

속성의 기본 문법과 데이터 타입

클래스 이름 아래, 사각형의 두 번째 구획은 클래스의 ‘속성’을 나열하는 공간입니다. 속성은 해당 클래스의 인스턴스가 가지게 될 정적인 데이터나 상태 정보를 의미하며, 클래스의 구조적 특징을 나타냅니다. 각각의 속성은 일반적으로 접근제어자 이름: 타입 = 기본값의 형식을 따릅니다. 예를 들어, User 클래스의 속성 - name: String = "Guest" 는 name 이라는 속성이 비공개(private) 접근 권한을 가지며, 문자열(String) 타입의 데이터를 저장하고, 별도로 지정하지 않으면 “Guest”라는 기본값을 가진다는 풍부한 정보를 담고 있습니다.

속성의 데이터 타입은 intboolean 과 같은 원시적인 데이터 타입을 명시할 수도 있고, AddressDate 와 같이 다른 클래스의 이름을 타입으로 지정할 수도 있습니다. 이는 해당 속성이 다른 객체에 대한 참조를 저장한다는 것을 의미하며, 클래스 간의 관계를 암시하는 중요한 단서가 됩니다. 이처럼 속성 정의는 클래스가 어떤 종류의 데이터를 품고 있는지를 명확하게 보여주는 역할을 합니다.

정적 속성과 파생 속성: 특별한 의미를 담다

일반적인 속성 외에도 특별한 의미를 지닌 속성들이 있습니다. ‘정적 속성(Static Attribute)’은 특정 인스턴스에 종속되지 않고 클래스 자체에 속하는 변수를 의미합니다. 다이어그램에서는 속성 이름에 밑줄을 그어 표현합니다. 예를 들어, User 클래스에 _numberOfUsers: int 라는 정적 속성이 있다면, 이는 생성된 모든 User 인스턴스가 공유하는 값으로, 전체 사용자 수를 나타내는 데 사용될 수 있습니다.

‘파생 속성(Derived Attribute)’은 다른 속성의 값으로부터 계산되어 유추할 수 있는 속성을 의미하며, 이름 앞에 슬래시(/)를 붙여 표현합니다. 예를 들어, Person 클래스에 - birthDate: Date 라는 속성이 있을 때, / age: int 라는 파생 속성을 정의할 수 있습니다. age는 birthDate 와 현재 날짜만 있으면 언제든지 계산할 수 있으므로 별도의 데이터로 저장할 필요가 없음을 나타냅니다. 이는 데이터의 중복을 피하고 모델을 더 명확하게 만드는 데 도움을 줍니다.


연산 (Operations): 객체의 행동을 설계하다

연산의 시그니처: 무엇을 받고 무엇을 돌려주는가

사각형의 가장 아래 구획을 차지하는 ‘연산’은 클래스가 수행할 수 있는 행동, 즉 동적인 책임을 나타냅니다. 각 연산은 고유한 시그니처(Signature)를 가지며, 이는 접근제어자 이름(파라미터 목록): 반환 타입의 형식으로 구성됩니다. 예를 들어, + calculatePrice(quantity: int, discountRate: float): float 라는 연산 시그니처는 다음과 같은 정보를 제공합니다. 이 연산은 외부에서 호출할 수 있으며(public), 이름은 calculatePrice 이고, 정수형 quantity 와 실수형 discountRate를 입력받아, 계산 결과를 실수형(float)으로 반환한다는 것입니다.

파라미터 목록과 반환 타입은 이 연산이 다른 객체와 어떻게 상호작용하는지를 보여주는 명세서와 같습니다. 이를 통해 개발자는 연산의 구체적인 구현 코드를 보지 않고도 이 기능을 어떻게 사용해야 하는지를 정확히 알 수 있습니다.

생성자와 소멸자: 인스턴스의 탄생과 죽음

연산 중에는 인스턴스의 생명주기와 관련된 특별한 연산들이 있습니다. ‘생성자(Constructor)’는 클래스의 인스턴스가 생성될 때 단 한 번 호출되는 특별한 연산으로, 주로 속성을 초기화하는 역할을 합니다. UML에서는 <<create>> 라는 스테레오타입을 붙여 표현하거나, 클래스와 동일한 이름을 가진 연산으로 표기하기도 합니다.

반대로 ‘소멸자(Destructor)’는 인스턴스가 메모리에서 해제될 때 호출되는 연산으로, 객체가 사용하던 자원을 정리하는 역할을 합니다. 이는 <<destroy>> 스테레오타입으로 표현됩니다. 자바처럼 가비지 컬렉터가 자동 메모리 관리를 해주는 언어에서는 소멸자를 명시적으로 사용하는 경우가 드물지만, C++과 같이 수동 메모리 관리가 필요한 언어에서는 매우 중요한 역할을 합니다.

정적 연산과 추상 연산: 공유되거나 약속된 행동

속성과 마찬가지로 연산에도 정적(Static)이거나 추상(Abstract)적인 경우가 있습니다. ‘정적 연산’은 특정 인스턴스를 생성하지 않고도 클래스 이름을 통해 직접 호출할 수 있는 연산으로, 이름에 밑줄을 그어 표현합니다. 주로 인스턴스의 상태와 관계없는 유틸리티 기능을 제공할 때 사용됩니다. Math.max(a, b) 와 같이 객체 생성 없이 사용하는 기능이 대표적인 예입니다.

‘추상 연산’은 추상 클래스 내부에 선언되며, 실제 구현 코드가 없는 껍데기뿐인 연산입니다. 이름 부분을 기울임꼴(Italics)로 표기하여 나타냅니다. 이는 자식 클래스에게 “이러한 이름과 시그니처를 가진 연산을 너희 각자의 상황에 맞게 반드시 구현해야 한다”고 강제하는 일종의 계약서 역할을 합니다.


접근 제어자 (Access Modifiers): 정보 은닉과 캡슐화의 미학

Public (+): 모두를 위한 공개 창구

+ 기호로 표시되는 public은 가장 개방적인 접근 수준을 의미합니다. public으로 선언된 속성이나 연산은 프로젝트 내의 어떤 다른 클래스에서도 자유롭게 접근하고 사용할 수 있습니다. 일반적으로 클래스가 외부에 제공해야 할 공식적인 기능, 즉 API(Application Programming Interface) 역할을 하는 연산들을 public으로 지정합니다. 이를 통해 객체는 자신의 내부는 감추면서도 외부와 소통할 수 있는 명확한 창구를 제공하게 됩니다.

Private (-): 나만이 아는 비밀

- 기호로 표시되는 private은 가장 폐쇄적인 접근 수준입니다. private으로 선언된 속성이나 연산은 오직 해당 클래스 내부에서만 접근할 수 있으며, 외부에서는 존재조차 알 수 없습니다. 이는 객체 지향의 핵심 원리인 ‘캡슐화(Encapsulation)’와 ‘정보 은닉(Information Hiding)’을 구현하는 가장 중요한 장치입니다. 클래스의 민감한 데이터나 내부적으로만 사용되는 복잡한 로직을 private으로 감춤으로써, 데이터의 무결성을 지키고 외부의 변경에 흔들리지 않는 안정적인 객체를 만들 수 있습니다. 일반적으로 모든 속성은 private으로 선언하는 것이 권장됩니다.

Protected (#): 우리 가족에게만

# 기호로 표시되는 protected는 private과 public의 중간적인 성격을 가집니다. protected로 선언된 멤버는 해당 클래스 내부와, 그 클래스를 상속받은 자식 클래스 내부까지만 접근이 허용됩니다. 이는 상속 관계에 있는 클래스들, 즉 하나의 ‘가족’ 내에서만 공유하고 싶은 정보나 기능을 정의할 때 유용하게 사용됩니다. 외부에는 공개하고 싶지 않지만, 자식 클래스가 부모의 기능을 확장하거나 재정의하는 데 필요한 최소한의 정보를 제공하는 역할을 합니다.

Package (~): 우리 동네 이웃에게만

~ 기호로 표시되는 package 접근 제어자는 동일한 패키지(또는 네임스페이스)에 속한 클래스들 사이에서의 접근을 허용합니다. 패키지는 서로 관련 있는 클래스들을 묶어놓은 하나의 디렉토리와 같은 개념입니다. package 접근 제어는 아주 밀접하게 협력해야 하는 클래스들의 그룹 안에서는 비교적 자유로운 접근을 허용하되, 이 그룹 외부에서는 해당 멤버를 감추고 싶을 때 사용됩니다. 이는 시스템을 기능 단위의 모듈(패키지)로 설계할 때 모듈 내부의 응집도를 높이는 데 도움을 줍니다.


종합 예제: 온라인 서점의 ‘Book’ 클래스 분석

지금까지 배운 모든 구성 요소를 종합하여 온라인 서점의 Book 클래스를 분석해 봅시다.

### Book (클래스 이름)

- isbn: String {isID} - title: String - price: int # author: Author _minStock: int = 10 / finalPrice: float

+ Book(isbn: String, title: String)

+ getDetailInfo(): String

– checkStock(): boolean

# applyDiscount(rate: float): void

_getTaxRate(): float

위 다이어그램은 다음과 같이 해석할 수 있습니다. Book이라는 클래스가 있으며, 고유 식별자인 isbn과 titleprice는 외부에서 직접 수정할 수 없는 private 속성입니다. 저자 정보(author)는 Author 클래스의 인스턴스로, 상속 관계에 있는 클래스에서는 접근 가능한 protected 입니다. 모든 책이 공유하는 최소 재고량(minStock)은 10이라는 기본값을 가진 static 속성입니다. 최종 판매가(finalPrice)는 가격과 세금 등을 조합하여 계산되는 derived 속성입니다.

연산으로는 ISBN과 제목으로 인스턴스를 생성하는 public 생성자가 있고, 책의 상세 정보를 외부에 제공하는 public 연산 getDetailInfo()가 있습니다. 재고를 확인하는 checkStock()은 내부적으로만 사용되는 private 연산이며, 할인율을 적용하는 applyDiscount()는 상속받은 특별한 책(예: SaleBook)에서만 사용할 수 있는 protected 연산입니다. 마지막으로, 모든 책에 공통으로 적용되는 세율을 반환하는 getTaxRate()는 인스턴스 생성 없이 호출 가능한 static 연산입니다.


결론: 시스템 설계를 읽고 쓰는 능력의 기초

구성 요소 이해의 중요성

클래스 다이어그램의 네 가지 핵심 구성 요소는 단순히 그림을 그리기 위한 기호가 아닙니다. 이들은 객체 지향 설계의 핵심 원칙과 철학을 담아내는 정교한 언어 체계입니다. 클래스 이름은 시스템의 어휘를, 속성은 데이터의 구조와 상태를, 연산은 객체의 책임과 행동을, 접근 제어자는 캡슐화와 정보 은닉의 수준을 결정합니다. 이 언어를 정확히 이해하고 사용할 때, 우리는 비로소 모호함 없이 견고하고 유연한 시스템의 청사진을 그리고 읽을 수 있게 됩니다.

제품 설계 관점에서의 시사점

제품 책임자나 기획자에게 이러한 이해는 개발팀과의 소통 수준을 한 차원 높여줍니다. 속성이 왜 대부분 private인지 이해하면, 특정 데이터를 변경하기 위해 왜 별도의 public 연산(예: updateProfile())이 필요한지를 납득하게 됩니다. protected와 상속의 개념을 알면, 서비스의 확장성을 고려한 설계에 대해 더 깊이 있는 논의를 할 수 있습니다. 결국 클래스 다이어그램의 구성 요소를 이해하는 것은 기술적 장벽을 넘어, 제품의 논리적 구조를 함께 만들어가는 파트너가 되기 위한 필수적인 교양 지식이라고 할 수 있습니다.