[태그:] 접근제어자

  • 접근 제어자: 객체의 문을 지키는 4가지 열쇠 (public, private, protected, default)

    접근 제어자: 객체의 문을 지키는 4가지 열쇠 (public, private, protected, default)

    객체 지향 프로그래밍의 세계는 잘 설계된 작은 성(城)들의 집합과 같습니다. 각각의 성, 즉 객체는 자신만의 소중한 보물(데이터)과 비밀 통로(내부 로직)를 가지고 있습니다. 만약 아무나 성에 들어와 보물을 마음대로 가져가거나 구조를 바꿀 수 있다면, 그 성은 금방 무너지고 말 것입니다. 이처럼 객체의 데이터를 보호하고 내부의 복잡함을 감추어 안정성을 유지하는 핵심 원리가 바로 ‘캡슐화(Encapsulation)’이며, 이를 가능하게 하는 구체적인 문법 장치가 바로 ‘접근 제어자(Access Modifiers)’입니다.

    접근 제어자는 클래스 또는 클래스의 멤버(속성, 연산)에 대한 외부의 접근 수준을 통제하는 키워드로, 객체의 문을 지키는 4가지 종류의 열쇠와 같습니다. 이 열쇠들은 누가, 어디까지 접근할 수 있는지를 명확히 규정함으로써 의도치 않은 데이터의 변경을 막고, 클래스를 사용하는 쪽에서는 오직 허용된 기능만을 사용하도록 유도합니다. 제품 책임자(PO)의 관점에서 이는 사용자가 시스템의 허점을 이용해 자신의 등급을 마음대로 ‘VIP’로 바꾸는 것을 막고, 반드시 ‘결제’라는 공식적인 절차를 거치도록 만드는 안전장치와 같습니다. 정보처리기사 시험의 필수 개념이자, 견고한 소프트웨어 설계의 근간이 되는 4가지 접근 제어자의 역할을 완벽하게 이해해 봅시다.


    접근 제어자의 존재 이유: 캡슐화와 정보 은닉

    캡슐화란 무엇인가?

    접근 제어자를 이해하기 위해서는 먼저 캡슐화의 개념을 알아야 합니다. 캡슐화란, 서로 관련된 데이터(속성)와 그 데이터를 처리하는 함수(연산)를 하나의 ‘캡슐’, 즉 클래스라는 단위로 함께 묶는 것을 의미합니다. 마치 약의 가루가 캡슐 안에 담겨 내용물을 보호하듯, 클래스는 자신의 데이터와 기능을 하나로 감싸 외부로부터의 직접적인 간섭을 최소화합니다.

    하지만 단순히 함께 묶는 것만으로는 부족합니다. 캡슐화의 진정한 목적을 달성하기 위해서는 캡슐 내부를 외부로부터 보호하고, 정해진 통로로만 소통하게 만드는 규칙이 필요합니다. 바로 이 규칙을 정의하는 것이 접근 제어자이며, 이 규칙을 통해 캡슐화를 강화하는 원리를 ‘정보 은닉(Information Hiding)’이라고 부릅니다.

    정보 은닉의 중요성

    정보 은닉은 캡슐화된 객체의 내부 구현을 외부에 숨기는 것을 의미합니다. 외부에서는 객체의 내부가 어떻게 동작하는지 알 필요 없이, 공개된 기능(public 연산)만을 사용하여 객체와 상호작용합니다. 이렇게 함으로써 얻는 이점은 명확합니다. 첫째, 데이터의 무결성을 보장할 수 있습니다. 예를 들어, 계좌 객체의 잔액(balance) 속성을 외부에서 직접 수정하지 못하게 막고, 오직 deposit() 이나 withdraw() 라는 검증 로직이 포함된 연산을 통해서만 변경하게 하여 음수 잔고와 같은 오류를 방지할 수 있습니다.

    둘째, 유지보수성과 유연성이 향상됩니다. 객체의 내부 구현 방식을 바꾸더라도, 외부에 공개된 기능의 사용법만 그대로 유지된다면 이 객체를 사용하는 다른 코드에 전혀 영향을 주지 않습니다. 잔액을 저장하는 데이터 타입을 int에서 long으로 바꾸거나, 이자 계산 로직을 더 효율적으로 개선하더라도, 외부에서는 여전히 동일한 getBalance() 연산을 호출하면 됩니다. 이처럼 정보 은닉은 객체를 독립적인 부품처럼 만들어, 시스템 전체의 안정성을 높이는 핵심적인 설계 원칙입니다.


    Private (-): 오직 나 자신에게만 허락된 비밀의 방

    Private의 핵심 원칙

    private은 4개의 접근 제어자 중 가장 엄격하고 폐쇄적인 접근 수준을 제공합니다. UML에서는 - 기호로 표현되며, private으로 선언된 멤버(속성 또는 연산)는 오직 해당 멤버가 선언된 클래스 내부에서만 접근할 수 있습니다. 이는 외부의 어떤 클래스도, 심지어 그 클래스를 상속받는 자식 클래스조차도 직접 접근할 수 없음을 의미합니다. 말 그대로 클래스 자기 자신만이 알고 사용하는 완벽한 비밀 공간입니다.

    이러한 강력한 통제는 정보 은닉 원칙을 가장 충실하게 지키는 방법입니다. 클래스의 가장 핵심적이고 민감한 데이터나, 외부에서는 알 필요 없는 복잡한 내부 처리 로직은 모두 private으로 선언하여 외부의 간섭으로부터 완벽하게 보호하는 것이 객체 지향 설계의 기본입니다.

    왜 속성은 대부분 Private인가?

    객체 지향 설계에서 “모든 속성은 private으로 만들라”는 격언이 있을 정도로, 속성을 비공개로 두는 것은 매우 중요합니다. 만약 User 클래스의 age 속성이 public이라면, 어떤 코드에서든 user.age = -10 과 같이 비논리적인 값으로 쉽게 변경할 수 있어 데이터의 신뢰성이 깨지게 됩니다.

    하지만 age를 private으로 선언하면 이런 직접적인 접근이 원천 차단됩니다. 대신 나이를 변경해야 할 때는 public으로 공개된 setAge(int age) 라는 연산을 만들어, 그 내부에서 if (age > 0) 과 같은 유효성 검사를 수행한 후에만 실제 age 속성값을 변경하도록 강제할 수 있습니다. 값을 읽을 때도 getAge() 라는 연산을 통해 제공합니다. 이처럼 데이터에 대한 접근을 통제된 메서드를 통해서만 가능하게 하는 패턴을 ‘게터(Getter)/세터(Setter)’라고 하며, 이는 객체의 상태를 안전하게 관리하는 표준적인 방법입니다.


    Public (+): 세상과 소통하는 유일한 창구

    Public의 역할: 클래스의 API

    public은 private과 정반대로 가장 개방적인 접근 수준을 가집니다. UML에서는 + 기호로 표현되며, public으로 선언된 멤버는 프로젝트 내의 어떤 패키지, 어떤 클래스에서든 아무런 제약 없이 자유롭게 접근하고 사용할 수 있습니다. public 멤버들은 해당 클래스가 외부 세계에 공식적으로 제공하는 서비스이자 약속, 즉 ‘공개 API(Application Programming Interface)’를 구성합니다.

    클래스를 사용하는 입장에서는 이 public 멤버들만 보고 클래스의 기능을 이용하면 됩니다. 클래스의 내부가 얼마나 복잡하게 구현되어 있는지는 전혀 신경 쓸 필요가 없습니다. 마치 우리가 스마트폰을 사용할 때, 내부 회로도를 몰라도 화면의 아이콘(public 인터페이스)만 터치하여 모든 기능을 사용하는 것과 같은 원리입니다.

    무엇을 Public으로 만들어야 하는가?

    클래스를 설계할 때 무엇을 public으로 할지 신중하게 결정해야 합니다. 한번 public으로 공개된 기능은 많은 다른 코드들이 사용하게 될 수 있으므로, 나중에 마음대로 변경하기가 매우 어려워집니다. 따라서 클래스의 핵심적인 책임과 명확하게 부합하며, 외부에서 반드시 필요로 하는 최소한의 기능만을 public으로 공개하는 것이 좋습니다.

    일반적으로 객체를 생성하는 생성자, 객체의 상태를 안전하게 조회하는 게터(Getter) 메서드, 유효성 검사를 포함하여 상태를 변경하는 세터(Setter)나 비즈니스 로직을 수행하는 주요 연산들이 public으로 선언됩니다. 반면, 하나의 public 연산을 수행하기 위해 내부적으로 사용되는 여러 개의 보조 기능이나 헬퍼(Helper) 메서드들은 private으로 숨기는 것이 바람직합니다.


    Protected (#): 가족에게만 열리는 상속의 문

    Protected의 특별한 역할

    protected는 상속 관계와 깊은 연관을 맺고 있는 특별한 접근 제어자입니다. UML에서는 # 기호로 표현되며, protected로 선언된 멤버는 기본적으로 같은 패키지 내의 클래스들과, 패키지가 다르더라도 해당 클래스를 상속받은 자식 클래스에서는 접근이 가능합니다. 이 ‘상속받은 자식 클래스’라는 조건이 protected를 다른 제어자와 구분 짓는 핵심적인 특징입니다.

    이는 마치 일반 손님(public)에게는 공개하지 않는 집안의 비밀이지만, 가족(자식 클래스)에게는 알려주어 함께 사용하게 하는 것과 같습니다. private처럼 완전히 숨기기에는 자식 클래스의 기능 확장에 제약이 생기고, public처럼 모두에게 공개하기에는 캡슐화가 깨지는 딜레마 상황에서 유용한 절충안을 제공합니다.

    상속 관계에서의 활용법

    예를 들어, Shape(도형)라는 부모 클래스에 도형의 위치를 나타내는 xy 좌표 속성이 있다고 가정해 봅시다. 이 좌표를 private으로 만들면, Shape를 상속받는 Circle(원)이나 Rectangle(사각형) 클래스에서 자신의 위치를 그리거나 계산하기 위해 이 좌표에 접근할 수가 없어 불편합니다.

    이때 xy를 protected로 선언하면, 외부에서는 이 좌표를 함부로 변경할 수 없도록 보호하면서도, 자식 클래스인 Circle과 Rectangle은 부모로부터 물려받은 이 좌표를 자유롭게 사용하여 자신만의 draw() 연산을 구현할 수 있습니다. 이처럼 protected는 부모 클래스가 자식 클래스에게 상속을 통해 재사용하거나 확장할 수 있는 ‘구현의 일부’를 제공하고자 할 때 사용되는 강력한 도구입니다.


    Default (Package-Private) (~): 이웃끼리는 터놓고 지내는 사이

    Default 제어자의 범위

    default 접근 제어자는 자바(Java) 언어에서 접근 제어자를 아무것도 명시하지 않았을 때 적용되는 기본값입니다. 그래서 ‘패키지-프라이빗(Package-Private)’이라고도 불립니다. UML에서는 ~ 기호로 표현할 수 있습니다. default 멤버는 오직 동일한 패키지에 속한 클래스들 내에서만 접근이 가능합니다. 패키지가 다르면, 설령 상속 관계에 있는 자식 클래스일지라도 접근할 수 없습니다.

    이는 protected와 혼동하기 쉬운 지점입니다. protected가 ‘같은 패키지 + 다른 패키지의 자식 클래스’까지 허용하는 반면, default는 엄격하게 ‘같은 패키지’ 내로만 범위를 한정합니다.

    언제 Default를 사용하는가?

    default 제어자는 특정 기능 모듈(패키지) 내에서 여러 클래스들이 아주 긴밀하게 협력해야 할 때 사용됩니다. 예를 들어, com.bank.transaction 이라는 패키지 안에 TransactionManagerTransactionValidatorTransactionLogger 라는 세 개의 클래스가 있다고 상상해 봅시다. 이 클래스들은 트랜잭션 처리라는 하나의 큰 작업을 위해 서로의 내부 상태나 보조 기능을 공유해야 할 수 있습니다.

    이때 공유가 필요한 멤버들을 public으로 만들면 이 패키지 외부의 모든 클래스에게 불필요하게 노출되고, private으로 만들면 정작 협력해야 할 패키지 내 다른 클래스들이 사용할 수 없습니다. 바로 이런 경우에 default 접근 제어자를 사용하면, 패키지라는 울타리 안에서는 자유롭게 정보를 공유하며 효율적으로 협력하고, 울타리 밖으로는 내부 구현을 안전하게 숨기는 효과적인 모듈 설계를 할 수 있습니다.


    한눈에 보는 접근 범위 비교

    접근 범위 표

    네 가지 접근 제어자의 복잡한 규칙은 아래 표를 통해 명확하게 정리할 수 있습니다.

    제어자같은 클래스같은 패키지자식 클래스 (다른 패키지)전체 영역 (다른 패키지)
    publicOOOO
    protectedOOOX
    defaultOOXX
    privateOXXX

    결론: 견고한 설계를 위한 현명한 문단속

    접근 제어자는 규칙이 아닌 철학이다

    접근 제어자는 단순히 코드의 접근을 막는 문법 규칙을 넘어, 객체 지향 설계의 핵심 철학인 ‘캡슐화’를 실현하는 구체적인 도구입니다. 어떤 멤버에 어떤 접근 제어자를 부여할지 고민하는 과정은, 곧 클래스의 책임과 역할을 정의하고 외부와의 계약을 설계하는 과정과 같습니다. 무분별하게 모든 것을 public으로 열어두는 것은 편리해 보일 수 있지만, 장기적으로는 시스템의 안정성을 해치고 유지보수를 악몽으로 만드는 지름길입니다. 반면, 가능한 모든 것을 private으로 감추고 최소한의 통로만 public으로 열어두는 것은 당장은 번거로워도, 변화에 유연하게 대처할 수 있는 견고한 부품을 만드는 현명한 전략입니다.

    제품 기획자가 알아야 할 접근 제어

    제품 책임자(PO)나 기획자가 접근 제어의 개념을 이해하고 있다면 개발팀과의 소통에서 큰 이점을 가질 수 있습니다. “왜 이 기능은 바로 안 되고 API를 따로 만들어야 하나요?”라는 질문에 대해, 개발자가 “해당 데이터는 private으로 보호되고 있어서, 안전한 검증 로직을 포함한 public 메서드를 통해서만 접근하도록 설계해야 합니다”라고 설명할 때 그 의도를 명확히 파악할 수 있습니다. 이는 기술적 제약을 이해하고 더 현실적인 요구사항을 정의하는 데 도움을 주며, 궁극적으로는 더 안정적이고 품질 높은 제품을 만드는 데 기여하는 밑거름이 될 것입니다.


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

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

    복잡하게 얽힌 시스템의 구조를 명쾌하게 보여주는 클래스 다이어그램이라는 지도를 제대로 읽기 위해서는, 먼저 지도에 사용된 기호와 범례, 즉 그 언어의 기본적인 문법을 마스터해야 합니다. 클래스 다이어그램의 가장 핵심적인 문법 요소는 바로 클래스를 표현하는 사각형 안에 담긴 ‘클래스 이름’, ‘속성(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와 상속의 개념을 알면, 서비스의 확장성을 고려한 설계에 대해 더 깊이 있는 논의를 할 수 있습니다. 결국 클래스 다이어그램의 구성 요소를 이해하는 것은 기술적 장벽을 넘어, 제품의 논리적 구조를 함께 만들어가는 파트너가 되기 위한 필수적인 교양 지식이라고 할 수 있습니다.