Modifier란?
제어자(modifier)는 크게 접근 제어자와 그 외의 제어자로 나뉜다. 제어자는 클래스, 변수, 메서드의 선언부에 사용되어, 대상의 기능을 제어한다. 하나의 대상에 여러 제어자를 사용할 수도 있다.
접근 제어자에는 public, protected, default, private가 있으며, 그 외의 제어자는 static, final, abstract, native, transient, synchronized 등이 있다.
접근 제어자 (Access Modifier)
접근 제어자의 종류와 역할
접근 제어자(access modifier)는 멤버변수, 클래스, 메서드, 생성자에 사용되어, 적용된 대상에 접근을 제어하는 역할을 한다.
접근제어자에는 다음과 같은 종류가 있다.
- private: 같은 클래스 내에서만 사용 가능
- protected: 다른 패키지에서도 상속관계에 있는 하위 클래스에서 접근 가능 & 같은 패키지 내에서 접근 가능
- default: 같은 패키지 내에서만 접근 가능
- public: 접근 제한이 없음
멤버 변수의 접근 제어자의 필요성
아래는 멤버 변수의 접근 제어자 역할에 대한 예시이다. 아래의 예시에서 hour 멤버 변수의 범위는 0 ~ 24 범위를 가져야한다. 그런데 hour울 수정하기 위해, t.hour = 25;이렇게 멤버 변수에 직접 참조한다면 원치 않은 값으로 변경될수 있다.
public class Time {
public int hour;
public int minute;
public int second;
}
Time t = new Time();
t.hour = 25;
이러한 경우 멤버변수를 private나 protected로 제한하고 멤버변수의 값을 읽고 쓰기할 수 있는 public 메서드를 제공하면, 메서드내에서 변경할 값을 검증 후에 해당 값을 멤버변수에 저장할 수 있다.
hour 멤버 변수를 private 접근 제어자로 지정하여 멤버 변수에 직접 참조는 불가능하다. 이제는 public void setHour메서드로 변경할 값을 매개변수로 받아서 검증 후에 hour 멤버 변수에 저장할 수 있다. 이로써, 데이터를 안전하게 변경할수 있게 되었다.
public class Time {
private int hour;
private int minute;
private int second;
Time(int hour, int minute, int second) {
this.hour = hour;
this.minute = minute;
this.second = 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;
}
//...
}
일반적으로 멤버 변수를 읽는 메서드를 getter라 하고, 쓰는 메서드를 setter라 한다. getter 메서드명은 'get멤버변수명'과 'set멤버변수명'으로 짓는다.
메서드의 접근 제어자 필요성
만일, 메서드 하나를 변경해 한다고 가정했을때, 이 메서드의 접근 제어자가 public 이라면 메서드를 변경한 후에 오류가 없는지 테스트 해야하는 범위가 넓다. 그러나 접근 제어자가 default라면 패키지 내부만 확인 해보면 되고, private 이라면 클래스 하나만 살펴보면 된다. 이처럼 접근 제어자 하나로 상당한 차이를 만들어 낼수 있다. 따라서 접근 범위를 최소화해야한다.
접근 제어자를 이용한 캡슐화
클래스 내부의 선언된 데이터를 보호
주로 멤버에 접근 제어자를 사용하는 이유는 클래스 내부의 선언된 데이터를 보호하기 위해서이다. 멤버에 저장된 데이터가 유효한값을 유지하도록하고 비밀번호나 중요 데이터를 외부에서 함부로 변경하지 못하도록 하기 위해서 외부로부터의 접근을 제한하여야한다. 이것을 데이터 감추기(data hiding)이라고 하며, 객체지향개념의 캡슐화(encapsulation)에 해당되는 내용이다.
아래의 예시에서 BankAccount 클래스는 계좌 번호, 잔액, 비밀번호를 private 접근 제어자로 선언하여 외부에서 직접 접근하지 못하도록 하였다. 대신, public 메서드를 통해 안전하게 잔액을 조회하고 변경할 수 있게 했다. 이러한 방법으로 데이터 감추기(data hiding)를 구현하며, 객체지향 프로그래밍의 캡슐화(encapsulation) 개념을 적용하였다.
public class BankAccount {
// private 접근 제어자를 사용하여 외부에서 직접 접근할 수 없도록 함
private String accountNumber; // 계좌번호
private double balance; // 잔고
private String password; // 계좌 비밀번호
// 생성자를 통해 초기값 설정
public BankAccount(String accountNumber, double initialBalance, String password) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.password = password;
}
// balance를 안전하게 가져오는 public 메서드
public double getBalance(String password) {
if (this.password.equals(password)) {
return balance;
} else {
throw new SecurityException("Invalid password");
}
}
// balance를 안전하게 변경하는 public 메서드
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
} else {
throw new IllegalArgumentException("Deposit amount must be positive");
}
}
public void withdraw(double amount, String password) {
if (!this.password.equals(password)) {
throw new SecurityException("Invalid password");
}
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
throw new IllegalArgumentException("Insufficient funds or invalid amount");
}
}
// accountNumber를 안전하게 가져오는 public 메서드
public String getAccountNumber() {
return accountNumber;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("12345", 1000.0, "securePassword");
// 유효한 비밀번호로 잔액 조회
System.out.println("Balance: " + account.getBalance("securePassword")); // Balance: 1000.0
// 비밀번호가 유효하지 않으면 SecurityException 발생
// System.out.println(account.getBalance("wrongPassword")); // Exception 발생
// 예금을 통해 잔액 증가
account.deposit(500.0);
System.out.println("Balance after deposit: " + account.getBalance("securePassword")); // Balance after deposit: 1500.0
// 출금을 통해 잔액 감소 (유효한 비밀번호 필요)
account.withdraw(200.0, "securePassword");
System.out.println("Balance after withdrawal: " + account.getBalance("securePassword")); // Balance after withdrawal: 1300.0
}
}
외부 접근 제한으로 복잡성을 감소
또한 클래스 내에서만 사용되는 내부 작업을 위해 임시로 사용되는 멤버변수나 일부 작업을 처리하기 위한 메서드 등의 멤버들을 감추기 위해서, 외부에서 접근할 필요가 없는 멤버들은 private으로 지정하여 외부에 노출시키지 않도록하여 복잡성을 줄이고 필요한 멤버만 접근하도록한다.
아래는 클래스 내에서만 사용되는 멤버 변수를 private으로 지정하여 외부에 노출되지 않도록 하였다. 그리고 내부 작업을 처리하기 위한 여러 메서드를 private으로 지정하여 복잡성을 줄이고 필요한 멤버만 외부에서 접근할 수 있도록 캡슐화를 구현하였다.
public class PasswordManager {
// private 접근 제어자를 사용하여 외부에서 직접 접근할 수 없도록 함
private String password;
private int failedAttempts;
// 생성자를 통해 초기값 설정
public PasswordManager(String initialPassword) {
this.password = initialPassword;
this.failedAttempts = 0;
}
// 비밀번호 변경을 위한 public 메서드
public void changePassword(String oldPassword, String newPassword) {
if (verifyPassword(oldPassword)) {
this.password = newPassword;
resetFailedAttempts();
} else {
registerFailedAttempt();
throw new SecurityException("Invalid old password");
}
}
// 비밀번호 검증을 위한 private 메서드
private boolean verifyPassword(String inputPassword) {
return this.password.equals(inputPassword);
}
// 실패한 시도를 등록하는 private 메서드
private void registerFailedAttempt() {
failedAttempts++;
if (failedAttempts >= 3) {
lockAccount();
}
}
// 실패한 시도 카운트를 초기화하는 private 메서드
private void resetFailedAttempts() {
failedAttempts = 0;
}
// 계정을 잠그는 private 메서드
private void lockAccount() {
// 실제로는 더 복잡한 잠금 로직이 들어갈 수 있음
System.out.println("Account locked due to too many failed attempts.");
}
// 비밀번호 검증을 위한 public 메서드
public boolean authenticate(String inputPassword) {
if (verifyPassword(inputPassword)) {
resetFailedAttempts();
return true;
} else {
registerFailedAttempt();
return false;
}
}
}
public class Main {
public static void main(String[] args) {
PasswordManager manager = new PasswordManager("initialPassword");
// 비밀번호 검증
System.out.println(manager.authenticate("wrongPassword")); // false
System.out.println(manager.authenticate("wrongPassword")); // false
System.out.println(manager.authenticate("wrongPassword")); // false, Account locked due to too many failed attempts.
// 비밀번호 변경 시도
try {
manager.changePassword("wrongPassword", "newPassword"); // Exception 발생
} catch (SecurityException e) {
System.out.println(e.getMessage()); // Invalid old password
}
// 비밀번호 변경 성공
manager.changePassword("initialPassword", "newPassword");
System.out.println(manager.authenticate("newPassword")); // true
}
}
authenticate와 changePassword 메서드들은 public으로 선언되어 외부에서 접근할 수 있지만, verifyPassword, registerFailedAttempt, resetFailedAttempts, lockAccount 메서드들은 클래스 내부에서만 사용되는 private 메서드로 외부에서는 접근할 수 없도록 접근 제어자를 지정하였다. 이로써 불필요한 복잡성을 줄이고 필요한 멤버만 외부에 노출시키는 캡슐화의 개념을 구현하였다.
생성자의 접근 제어자
생성자(Constructor)에 접근 제어자를 사용하여 인스턴스의 생성을 제한할 수 있다. 일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 동일하게 지정하지만 다르게 지정할수 도 있다.
만일, 생성자의 접근 제어자를 private로 설정하면 외부에서 해당 클래스의 인스턴스는 생성할 수 없다. 그러나 클래스 내부에서는 인스턴스의 생성이 가능하여, 인스턴스를 생성해서 반환해주는 public 메서드를 호출할 수 있게 하면 외부에서 이 클래스의 인스턴스를 사용하도록 할 수 있다. 그리고 인스턴스를 생성하지 않고도 호출할수 있어야 하므로, 인스턴스를 반환하는 메서드는 static으로 지정해야한다.
또한 생성자가 private인 클래스를 상속받은 하위클래스의 인스턴스를 생성할때, 상위 클래스의 생성자 호출이 불가하여 하위 클래스의 인스턴스 생성 불가하다. 그리하여 상속할 수 없는 클래스라는 것을 알리기 위해서 final로 클래스를 지정해줘야한다.
위의 내용대로 아래의 코드에서 인스턴스 반환하는 메서드 getInstance의 제어자를 public과 static으로 지정해주어 외부에서 인스턴스를 반환받을 수 있다. 그리고 해당 클래스(Singletion)를 final로 지정하여 상속 불가능한 클래스라고 표시하였다. 아래는 싱글톤 패턴으로 한개의 인스턴스를 공유하므로, s1과 s2는 동일한 인스턴스 참조하여 주소가 동일하다.
final class Singleton {
private static Singleton s = new Singleton(); // 클래스의 인스턴스 개수 한개로 제한
private Singleton() {}
public static Singleton getInstance() {
if (s == null)
s = new Singleton();
return s;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
// 동일한 인스턴스이므로 주소 동일
System.out.println(s1);
System.out.println(s2);
}
}
이외의 제어자 (Modifier)
static
static은 말그대로 정적인 의미로, 클래스 메서드(static method)와 클래스 변수(static variable)는 클래스 파일 로드시에 생성되고 JVM 종료시점 직전까지 유지된다. 그리고 static을 붙은 멤버는 모든 인스턴스가 공유하는 멤버이며, 인스턴스를 생성하지 않고 사용이 가능하다.
static method는 인스턴스 없이 호출이 가능하며, 메서드 내에서 static variable을 사용한다. 그리고 static variable을 초기화하는 static initialize block은 클래스가 메모리에 로드될때 단 한번만 수행된다.
Instance method는 호출시 인스턴스의 실제 클래스 타입을 확인과 더불어, 첫 호출시 상위 클래스를 타고 올라고 메서드를 찾는 과정이 수반될수 있다. 그리하여 메서드 내에서 instance variable을 사용하지 않는다면, static method로 선언하는 것이 좋다.
final
final은 말그대로 저장 공간의 마지막 쓰기 값이란 뜻이다. 클래스, 메서드, 멤버변수, 지역변수에 모두 사용가능하다. 변수에 쓰이면 상수가 되며, 메서드에 적용하면 오버라이딩을 할수 없고 클래스에 적용하면 해당 클래스를 상속받지 못한다.
final이 붙은 변수는 상수이다. 상수는 일반적으로 선언과 동시에 초기화하지만, final이 붙은 인스턴스 변수의 경우에는 생성자에서 초기화를 할 수 있다.
public class Parent {
// couldn't change value after initialize
final int NUMBER;
Parent(int number) {
NUMBER = number;
}
// couldn't override
final void method(int n) {
// couldn't change value after initialize
final int num;
if (n > 10) {
num = n;
}
}
public static void main(String[] args) {
// 인스턴스 별로 서로 다른 인스턴스 상수 값을 저장할 수 있다.
final Parent f1 = new Parent(1);
final Parent f2 = new Parent(5);
final Parent f3 = new Parent(10);
}
}
그리하여 여러 인스턴스는 인스턴스 변수에 각기 다른 상수 값을 저장할 수 있다. 만일 그렇지 않았다면, final int NUMBER = 10;이렇게 선언과 동시에 초기화하여, 모든 인스턴스의 인스턴스 변수가 동일한 상수값을 가졌을 것이다.
abstract
메서드의 선언문만 작성하고 실제 구현부는 작성하지 않은 추상 메서드(abstract)를 선언하는데 사용한다. 추상 메서드는 추상 클래스(abstract class) 내에서 선언이 가능하다. 추상 클래스는 아직 구현되지 않은 클래스이므로, 인스턴스를 생성할 수 없다.
public abstract class AbstractTest {
int n;
abstract void method(int n);
}
public class AbstractMainTest {
public static void main(String[] args) {
// compille error
AbstractTest abstractTest = new AbstractTest();
}
}
추상 메서드가 없는 추상 클래스도 자바 라이브러리에 존재한다. 이는 인스턴스화가 필요없는 상위 클래스를 추상 클래스로 만들어서, 상속받아 원하는 메서드를 오버라이딩하여 사용할 수 있기 때문이다.
챰고 자료
- 자바의 정석 (남궁성 지음)
'Java > Java Language' 카테고리의 다른 글
[Java] 추상 클래스와 메서드 (Abstract) (0) | 2024.06.23 |
---|---|
[Java] 다형성 (Polymorphism) (0) | 2024.06.22 |
[Java] Package와 Import (0) | 2024.06.19 |
[Java] 오버라이딩 (Overriding) (0) | 2024.06.18 |
[Java] 상속 (Inheritance) (0) | 2024.06.18 |