접근제어자(Access Modifier) 무조건 이해하기
접근제어자 개념
접근제어자를 통해서 변수, 메서드, 생성자에 대한 접근 권한을 지정할 수 있다.
종류는 4가지가 존재한다.
1. private : 해당 클래스 내에서만 접근 가능
2. public : 모든 클래스에서 접근 가능
3. default : (아무것도 적지 않았을 때) 같은 패키지 내에서만 접근 가능
4. protected : 같은 패키지 내에서, 다른 패키지인 경우 자식 클래스에서 접근 가능
왜 굳이 4가지 방식으로 나눠서 권한을 부여하는지?
외부에서 정보를 확인하지 못하게 설정하고 싶은 변수를 private으로 설정해서 외부에서 접근하지 못하도록 보호 할 수 있기 때문이다.
이를 통해 정보 은닉(information hiding)이 가능해진다.
또한 외부에서 멤버 변수 값을 잘못 변경하는 경우도 존재하기 때문에 멤버 변수를 private으로 설정한 후 Setter를 통해서 값을 넣어주고 Getter를 통해서 값을 출력하게해서 더욱 더 안전한 코드를 구현할 수 있게 된다.
get(), set() 메서드가 뭘까?
private으로 선언된 멤버 변수에 대해 접근 및 수정 할 수 있는 메서드를 public으로 제공해주는 역할을 한다.
get()메서드만 제공될 경우에는 read-only 필드
private이면 접근 및 수정을 할 수 없지만 getter,setter를 사용하면 접근하고 수정이 가능해진다고 생각하면 된다.
여기서 우리가 알아야할 단어가 하나 있는데 그건 캡슐화(Encapsulation)이다.
캡슐화를 설명해보자면,
꼭 필요한 정보와 기능만 외부에 오픈하는 것
대부분의 멤버 변수와 메서드를 감추고 외부에 통합된 인터페이스만 제공해서 일관된 기능을 구현하게 하는 것
각각의 메서드나 멤버 변수를 접근함으로써 발생하는 오류를 최소화 할 수 있는 장점이 있다.
그럼 어떻게 진행되는지 한번 써볼까?
package com.Study.d;
import java.time.LocalTime;
import static java.time.LocalTime.now;
public class Time {
private int hour;
private int minute;
private int second;
public Time(int hour, int minute, int second){
setHour(hour);
setMinute(minute);
setSecond(second);
}
public int getHour() {
return hour;
}
public void setHour(int hour) {
if (hour < 0 || hour > 23) return;
this.hour = hour;
}
public int getMinute() {
return minute;
}
public void setMinute(int minute) {
if (minute < 0 || minute > 59) return;
this.minute = minute;
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
if (second < 0 || second > 59) return;
this.second = second;
}
public static void main(String[] args) {
LocalTime currentTime = LocalTime.now();
Time time = new Time(11, 30, 45);
System.out.println(currentTime.getHour()+ "시 " + currentTime.getMinute() + "분 " + currentTime.getSecond()+ "초");
}
}
이렇게 시간을 구하는 코드를 작성했다고 해보자
잠깐 상단 코드에 대해 이야기하자면,
Time time 으로 작성한 코드를 사용해서
time.getHour()라고 한다면 현재 내가 값으로 입력한 시간을 기준으로 출력되지만
현재 실시간 타임을 얻고자 한다면
java.time.LocalTime클래스 를 사용하면된다.
LocalTime 이라고 입력하면 빨간줄이 나오는데 import하라고 하면 import 시켜주면 에러가 사라진다.
본론으로 들어가서
상단코드를 보면 setHour, getHour 등 Setter, Getter를 사용하는 것을 볼 수 있다.
그렇다면 이렇게 작성하는 이유를 알아보고 예시로 설명해보려고 한다.
Getter, Setter는 객체지향 프로그래밍에서 캡슐화를 위해 사용된다.
클래스 외부에서 해당 클래스의 멤버 변수에 직접 접근하는 것이 아니라, Getter와 Setter를 통해 간접적으로 접근하도록 하는 것이다.
이를 통해 클래스 외부에서 클래스의 내부 구조를 변경하지 못하도록 보호하고, 유효성 검사를 수행하여 부적절한 값이 입력되는 것을 방지할 수 있게된다.
Getter는 멤버 변수의 값을 읽어오는 메서드이며, Setter는 멤버 변수의 값을 설정하는 메서드이다.
Getter, Setter를 사용하지 않았을 경우에는 멤버 변수에 접근하기 위해 public으로 선언된 변수에 직접 접근해야 한다.
하지만 이는 해당 클래스의 내부 구조를 노출시키는 것이므로, 앞서 설명한 캡슐화(Encapsulation)를 위반하는 것이다.
따라서 위에서 보여준 코드 같이 Getter와 Setter를 사용하여 캡슐화를 구현할 수 있다.
위 코드에서는 hour, minute, second 멤버 변수에 직접 접근하는 것이 아니라, Getter와 Setter를 통해 간접적으로 접근했다.
예를 들어 setHour 메서드에서는 hour 변수에 값을 설정하기 전에 입력된 값이 유효한지 확인하고, 이를 통해 hour 변수의 값을 유효한 값으로 유지하고, 잘못된 값이 입력되는 것을 방지할 수 있다.
Getter와 Setter를 사용하면 클래스 내부의 변수에 대한 접근을 통제할 수 있고, 이를 통해 안정적인 코드를 작성할 수 있게된다.
여기서 잠깐 멤버변수, 지역변수, 메서드, 클래스 제대로 정리하고 넘어가자 !
우선 클래스란 일종의 틀이며, 클래스 내부에는 멤버 변수와 메서드가 존재한다.
멤버 변수는 해당 클래스의 속성을 나타내는 데이터를 저장하는 변수이며, 메서드는 클래스의 기능을 구현하는 것이다.
Java에서는 클래스 내부에 메인 메서드를 정의하고, 이 메서드를 통해서 프로그램이 실행된다. 메인 메서드는 다른 메서드와 동일하게 클래스 내부에 정의될 수 있으며, 일반적으로 프로그램의 시작점을 나타내는 역할을 한다.
따라서 클래스 내부의 구조는 멤버 변수와 메서드가 함께 존재하며, 이들은 상황에 따라 다른 순서로 정의될 수 있다.
멤버 변수가 먼저 정의되고, 그 후에 메서드가 정의되는 경우가 많다.
쉽게말해 멤버 변수는 클래스의 필드(field)라고도 불리며, 클래스 전체에서 사용할 수 있는 변수이다.
메서드는 클래스에 정의된 기능을 구현하기 위한 코드
멤버변수와 메서드는 한 쌍이라고 생각하면 된다. 멤버변수가 존재해도 메서드가 없으면 클래스의 기능을 구현할 수 없기 때문이다.
지금까지 알게된 것으로 간단한 예시를 들어보자
예를 들어, 우리가 학교에서 교실(Class)에 들어가려면 문을 열어야한다. 그 문의 열쇠를 열쇠꾸러미에 넣어두는 상황을 가정해보자
이때, 열쇠꾸러미는 교실 안에 두고, 교실 외부에서는 접근할 수 없게끔 막아둔다.
이때 열쇠꾸러미는 멤버변수, 교실 외부에서 접근할 수 없게끔 막아둔 것은 private 접근 제어자라고 생각하면 된다.
반면에, 교실 안에 들어가면 책상 위에 필통이 있고 그 책상에서 그 필통을 두고 사용한다고 해보자.
이때 필통은 교실 안에서만 사용되고, 교실 외부에서는 필통을 사용할 수 없다. 이런 경우, 필통은 지역변수가 된다.
즉, 멤버변수는 클래스(교실) 내에서 어디서든지 사용 가능한 변수이며,
지역변수(필통)는 변수가 선언된 지역(책상)에서만 사용 가능한 변수이다.
이를 완벽하게 이해하기 위해서는 객체, 인스턴스 변수, 멤버 변수, static에 대해서 알아야한다.
머리속에 잘 들어오지 않는 사람은 아래 예시를 한번 더 읽어보고 이해가 되면 스킵해도 된다.
우선 객체는 물건을 의미한다. 예를 들어, 자전거는 객체이고, 자전거마다 다른 색, 브랜드, 크기 등의 속성을 가지고 있다.
이렇게 객체마다 각각의 속성을 가질 수 있도록 만들어진 변수를 인스턴스 변수라고 한다.
즉, 인스턴스 변수는 객체마다 다른 값을 가지는 변수를 의미한다.
그리고 모든 객체가 공유하는 변수를 멤버 변수라고 한다.
객체 지향 프로그래밍에서 멤버 변수(member variable)란, 클래스 내에 선언된 변수로서, 해당 클래스의 인스턴스(객체)가 생성될 때마다 생성되는 변수를 말한다.
예를 들어, '사람' 클래스를 만들 때, 그 안에 '이름', '나이', '성별' 등의 속성을 변수로 선언할 수 있다. 이때 '이름', '나이', '성별'은 모두 '사람' 클래스의 멤버 변수가 된다. 즉, 멤버 변수는 객체가 가지는 속성을 저장하기 위해 사용되는 변수이며, 객체의 상태(state)를 나타내는 데 중요한 역할을 한다.
예를 들어, 자전거의 바퀴 크기는 모든 자전거에서 동일한 값으로 설정될 수 있다.
그렇기 때문에 바퀴 크기는 모든 자전거에서 공유하는 변수, 즉 멤버 변수로 정의할 수 있다.
마지막으로 static은 모든 객체가 공유하는 변수를 정의할 때 사용하는 키워드로 static으로 정의된 변수는 모든 객체에서 동일한 값을 가지고 있다. 예를 들어, 자전거가 100대 있을 때 모든 자전거가 같은 브랜드를 가지고 있다면 브랜드를 static으로 정의할 수 있는 것이다.
그래서, 객체는 자전거라고 생각하고, 인스턴스 변수는 자전거마다 다른 색, 브랜드, 크기 등의 속성, 멤버 변수는 자전거들이 공유하는 바퀴 크기, 브레이크 종류 등의 속성, static은 모든 자전거에서 동일한 브랜드를 의미한다고 생각하면 되겠다.
자 그럼 이제 Private에 대해서 한번 더 정리해보자!
private은 객체의 내부 데이터를 보호하기 위한 접근 제어자 중 하나이다. 즉, private으로 선언된 멤버 변수는 외부에서 직접적으로 접근이 불가능하며, 해당 클래스 내부에서만 접근이 가능하다. 이렇게 함으로써 클래스 외부에서 객체의 내부 데이터를 무분별하게 수정하거나 참조하는 것을 방지할 수 있다. 대신에 public 메서드를 통해 private 멤버 변수에 접근하도록 해서 안전하게 객체의 데이터를 관리할 수 있다.
이러한 캡슐화(Encapsulation)의 개념은 객체 지향 프로그래밍의 핵심 개념 중 하나이다.
"대신에 public 메서드를 통해 private 멤버 변수에 접근하도록 해서" <--- Getter, Setter 메서드를 사용하는 것! 즉 캡슐화를 구현하는 행위이다.
가장 제한적인 Private을 알아봤으니 이번에는 가장 만만한(?) Public에 대해서 알아보자
퍼블릭(Public) <-- 이름만 들어도 뭔가 Free한 느낌이 강하게 온다!
말그대로 퍼블릭한! 자유도가 가장 높은! 접근제어자라고 할 수 있다.
퍼블릭은 해당 클래스 또는 멤버 변수/메서드를 어디서나 접근 가능하도록 지정한다. 즉, 다른 패키지나 클래스에서도 해당 클래스 또는 멤버 변수/메서드에 접근할 수 있도록 해준다.
예를 들어, public으로 선언된 클래스 내의 public 메서드는 해당 클래스를 인스턴스화한 객체에서 호출이 가능하며, 다른 클래스에서도 해당 클래스의 인스턴스를 생성하여 public 메서드를 호출할 수 있다. 마찬가지로, public으로 선언된 멤버 변수는 해당 클래스 내에서 뿐만 아니라 다른 클래스에서도 인스턴스화한 객체를 통해 접근할 수 있다.
하지만 public으로 선언된 클래스나 멤버 변수/메서드는 그만큼 접근 범위가 넓어져서, 다른 클래스에서 접근하여 변경할 수 있는 경우가 많아져서 더욱 신중하게 사용해야 한다.
따라서 보안 상 이슈가 있거나 굳이 외부에서 접근이 필요하지 않은 경우에는 다른 접근 제한자를 사용하거나 적절한 접근 권한 설정을 해줄 필요가 있다.
비교적 이해하기 쉬운 퍼블릭을 알아보았고 이제 남은 녀석은 2개!
default와 protected이다.
먼저 default를 알아보자!!
default는 접근 제어자(access modifier) 중 하나로, 해당 멤버(필드 또는 메서드)가 같은 패키지 내에서는 접근이 가능하도록 지정해주는 역할을 한다.
다시 말해, 같은 패키지 내에서는 public과 마찬가지로 해당 멤버에 접근이 가능하며, 다른 패키지에서는 private으로 취급된다.
접근 제어자는 보안과 관련된 중요한 역할을 수행하는데, default로 지정된 멤버는 같은 패키지 내에서는 자유롭게 접근 가능하기 때문에 다른 패키지에서의 접근을 제어하기 위해서는 다른 접근 제어자를 사용해야 한다.
그럴 경우 private으로 수정하고 getter, setter 메서드를 사용해서 public으로 접근할 수 있도록 한다. 이렇게 함으로 다른 패키지에서도 해당 클래스나 변수에 접근이 가능해지며 이러한 방식은 캡슐화(Encapsulation)를 보장하면서 외부에서 필요한 부분만 노출할 수 있도록 하는 좋은 방법이다.
잠깐! 그럼 private이 아닌 protected를 사용하는 것은 어떨까? 또 private과 protected중에 어떤 것을 사용하는 것이 좋을까?
그것을 선택하는 기준과 차이점에 대해서도 짚고 넘어가자!
private과 protected는 모두 클래스 내부의 정보를 숨길 수 있는 접근 제어자이다. 하지만 두 접근 제어자는 사용하는 목적과 범위에서 차이가 있다. private은 해당 멤버 변수나 메서드가 속한 클래스 내에서만 접근이 가능하다. 즉, 해당 클래스의 외부에서는 접근할 수 없다. private으로 선언된 멤버 변수나 메서드는 해당 클래스의 구현 내부에서만 사용되는 것으로 제한되며, 클래스 외부의 다른 코드에서 이를 직접적으로 접근할 수 없는 것이다.
반면 protected는 해당 멤버 변수나 메서드가 속한 클래스와 해당 클래스를 상속한 자식 클래스에서 접근이 가능하다. 따라서, 상속 관계가 아닌 클래스에서는 protected로 선언된 멤버 변수나 메서드에 접근할 수 없다. protected로 선언된 멤버 변수나 메서드는 클래스 내부에서 사용되는 것으로 제한되지만, 해당 클래스를 상속한 자식 클래스에서도 사용될 수 있기 때문에 보다 다양한 상황에서 활용할 수 있다.
따라서, private은 해당 멤버 변수나 메서드를 완전히 숨기고 싶을 때 사용하며, protected는 상속 관계에 있는 클래스에서 공유하고 싶은 정보가 있을 때 사용한다. 이를 기준으로 선택하면 된다.
또한 우리가 따로 선언하지 않고 사용할 경우 우리가 작성하지 않아도 기본적인 접근 제어자는 전부 default로 내부적으로 선언되어있다.
그러니까 default라고 하는 것이다. 여기서 우리가 퍼블릭, 프라이빗 등등 선언하는 순간 무에서 퍼블릭으로 변화하는 것이 아니라
기존에 내부적으로 숨어있던 default 접근제어자가 public 또는 private 또는 protected 로 변경된다는 것을 꼭 명심하자!
자 이제 대망의 마지막! Protected 접근제어자를 설명해보자 이제 다 왔으니 조금만 참자!
protected 접근 제어자는 default 접근 제어자와 private 접근 제어자보다 더 넓은 범위에서 접근이 가능하다.
protected로 선언된 변수나 메서드는 같은 패키지 내에서는 default와 마찬가지로 접근이 가능하지만, 다른 패키지에서도 상속받은 자식 클래스에서는 접근이 가능하다.
즉, 같은 패키지에서도 접근 가능하고, 다른 패키지에서는 상속받은 자식 클래스에서만 접근이 가능하다.
따라서, 만약 같은 패키지 내에서는 사용하지 않고 다른 패키지에서는 자식 클래스에서 사용하고자 하는 변수나 메서드가 있다면 protected 접근 제어자를 사용할 수 있다.
조금 아리송하기 때문에 코드를 예시로 보면서 설명해보려고 한다.
protected 접근 제어자는 클래스 내부와 해당 클래스를 상속받은 클래스에서만 접근이 가능한 멤버를 선언할 때 사용된다. 이를 이해하기 위해서는 클래스와 상속에 대한 개념이 필요하다.
예를 들어, 아래와 같이 Animal 클래스를 정의하고, name 필드와 cry() 메서드를 포함시켰다고 가정해보자
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
protected void cry() {
System.out.println("The animal cries.");
}
}
그리고 Cat 클래스에서 Animal 클래스를 상속받도록 정의하였다.
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
public void cry() {
System.out.println("Meow!");
}
}
여기서 Animal 클래스의 name 필드와 cry() 메서드는 protected 접근 제어자로 선언해줬다. 이렇게 protected로 선언된 멤버는 Animal 클래스 내부와 Animal 클래스를 상속받은 Cat 클래스에서는 직접 접근이 가능하다.
그러나, Animal 클래스를 상속받지 않은 다른 클래스에서는 name 필드와 cry() 메서드에 접근할 수 없다. 만약 public으로 선언되었다면, 다른 클래스에서도 Animal 객체의 name 필드와 cry() 메서드에 접근할 수 있을 것이다.
이렇게 protected 접근 제어자를 사용하면, 자식 클래스에서도 부모 클래스의 멤버에 접근이 가능하면서, 다른 클래스에서는 접근을 제한할 수 있게된다.
