객체 지향 프로그래밍의 세계는 잘 설계된 작은 성(城)들의 집합과 같습니다. 각각의 성, 즉 객체는 자신만의 소중한 보물(데이터)과 비밀 통로(내부 로직)를 가지고 있습니다. 만약 아무나 성에 들어와 보물을 마음대로 가져가거나 구조를 바꿀 수 있다면, 그 성은 금방 무너지고 말 것입니다. 이처럼 객체의 데이터를 보호하고 내부의 복잡함을 감추어 안정성을 유지하는 핵심 원리가 바로 ‘캡슐화(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
(도형)라는 부모 클래스에 도형의 위치를 나타내는 x
, y
좌표 속성이 있다고 가정해 봅시다. 이 좌표를 private
으로 만들면, Shape
를 상속받는 Circle
(원)이나 Rectangle
(사각형) 클래스에서 자신의 위치를 그리거나 계산하기 위해 이 좌표에 접근할 수가 없어 불편합니다.
이때 x
, y
를 protected
로 선언하면, 외부에서는 이 좌표를 함부로 변경할 수 없도록 보호하면서도, 자식 클래스인 Circle
과 Rectangle
은 부모로부터 물려받은 이 좌표를 자유롭게 사용하여 자신만의 draw()
연산을 구현할 수 있습니다. 이처럼 protected
는 부모 클래스가 자식 클래스에게 상속을 통해 재사용하거나 확장할 수 있는 ‘구현의 일부’를 제공하고자 할 때 사용되는 강력한 도구입니다.
Default (Package-Private) (~): 이웃끼리는 터놓고 지내는 사이
Default 제어자의 범위
default
접근 제어자는 자바(Java) 언어에서 접근 제어자를 아무것도 명시하지 않았을 때 적용되는 기본값입니다. 그래서 ‘패키지-프라이빗(Package-Private)’이라고도 불립니다. UML에서는 ~
기호로 표현할 수 있습니다. default
멤버는 오직 동일한 패키지에 속한 클래스들 내에서만 접근이 가능합니다. 패키지가 다르면, 설령 상속 관계에 있는 자식 클래스일지라도 접근할 수 없습니다.
이는 protected
와 혼동하기 쉬운 지점입니다. protected
가 ‘같은 패키지 + 다른 패키지의 자식 클래스’까지 허용하는 반면, default
는 엄격하게 ‘같은 패키지’ 내로만 범위를 한정합니다.
언제 Default를 사용하는가?
default
제어자는 특정 기능 모듈(패키지) 내에서 여러 클래스들이 아주 긴밀하게 협력해야 할 때 사용됩니다. 예를 들어, com.bank.transaction
이라는 패키지 안에 TransactionManager
, TransactionValidator
, TransactionLogger
라는 세 개의 클래스가 있다고 상상해 봅시다. 이 클래스들은 트랜잭션 처리라는 하나의 큰 작업을 위해 서로의 내부 상태나 보조 기능을 공유해야 할 수 있습니다.
이때 공유가 필요한 멤버들을 public
으로 만들면 이 패키지 외부의 모든 클래스에게 불필요하게 노출되고, private
으로 만들면 정작 협력해야 할 패키지 내 다른 클래스들이 사용할 수 없습니다. 바로 이런 경우에 default
접근 제어자를 사용하면, 패키지라는 울타리 안에서는 자유롭게 정보를 공유하며 효율적으로 협력하고, 울타리 밖으로는 내부 구현을 안전하게 숨기는 효과적인 모듈 설계를 할 수 있습니다.
한눈에 보는 접근 범위 비교
접근 범위 표
네 가지 접근 제어자의 복잡한 규칙은 아래 표를 통해 명확하게 정리할 수 있습니다.
제어자 | 같은 클래스 | 같은 패키지 | 자식 클래스 (다른 패키지) | 전체 영역 (다른 패키지) |
public | O | O | O | O |
protected | O | O | O | X |
default | O | O | X | X |
private | O | X | X | X |
결론: 견고한 설계를 위한 현명한 문단속
접근 제어자는 규칙이 아닌 철학이다
접근 제어자는 단순히 코드의 접근을 막는 문법 규칙을 넘어, 객체 지향 설계의 핵심 철학인 ‘캡슐화’를 실현하는 구체적인 도구입니다. 어떤 멤버에 어떤 접근 제어자를 부여할지 고민하는 과정은, 곧 클래스의 책임과 역할을 정의하고 외부와의 계약을 설계하는 과정과 같습니다. 무분별하게 모든 것을 public
으로 열어두는 것은 편리해 보일 수 있지만, 장기적으로는 시스템의 안정성을 해치고 유지보수를 악몽으로 만드는 지름길입니다. 반면, 가능한 모든 것을 private
으로 감추고 최소한의 통로만 public
으로 열어두는 것은 당장은 번거로워도, 변화에 유연하게 대처할 수 있는 견고한 부품을 만드는 현명한 전략입니다.
제품 기획자가 알아야 할 접근 제어
제품 책임자(PO)나 기획자가 접근 제어의 개념을 이해하고 있다면 개발팀과의 소통에서 큰 이점을 가질 수 있습니다. “왜 이 기능은 바로 안 되고 API를 따로 만들어야 하나요?”라는 질문에 대해, 개발자가 “해당 데이터는 private
으로 보호되고 있어서, 안전한 검증 로직을 포함한 public
메서드를 통해서만 접근하도록 설계해야 합니다”라고 설명할 때 그 의도를 명확히 파악할 수 있습니다. 이는 기술적 제약을 이해하고 더 현실적인 요구사항을 정의하는 데 도움을 주며, 궁극적으로는 더 안정적이고 품질 높은 제품을 만드는 데 기여하는 밑거름이 될 것입니다.