자바에서의 클래스 설계 시, 객체 지향 원칙을 준수하는 것이 중요합니다. 이 원칙 중 하나는 클래스의 상태를 외부에 노출하지 않고, 접근자 메서드를 통해 접근하게 하는 것입니다. 이러한 설계는 클래스의 유연성을 높이고, 유지보수성을 향상시킵니다.
1. Public 필드의 문제점
클래스의 필드를 public으로 선언하게 되면, 그 필드에 직접 접근이 가능합니다. 이 방법은 코드 작성이 간단하고, 초기 학습 단계에서는 매력적으로 보일 수 있습니다. 하지만, 이 방식에는 다음과 같은 문제점들이 존재합니다:
캡슐화의 위반:
객체 지향 프로그래밍에서 중요한 원칙 중 하나인 캡슐화(encapsulation)는 객체의 상태를 외부에서 직접 변경하지 못하도록 보호하는 것입니다. 그러나 필드가 public으로 선언되면, 외부에서 직접 해당 필드를 조작할 수 있어 객체의 상태가 불안정해질 수 있습니다. 이로 인해 버그가 발생할 가능성이 높아집니다.
변경에 취약:
public 필드를 사용하는 경우, 해당 필드를 사용하는 모든 코드가 그 필드의 내부 구조에 의존하게 됩니다. 이로 인해 필드의 타입이나 의미를 변경하려면 그 필드를 참조하는 모든 코드를 수정해야 합니다. 반면, 접근자 메서드를 사용하면 메서드 내부에서 필드에 대한 변경 사항을 감출 수 있습니다.
불변성을 보장하기 어려움:
불변 객체는 다중 스레드 환경에서 안전하게 사용할 수 있어 자바에서 중요한 개념입니다. 하지만 public 필드를 사용하면 객체의 불변성을 보장하기 어렵습니다. 필드를 public으로 노출하면, 외부에서 해당 필드를 변경할 수 있기 때문에, 객체의 상태가 변하게 됩니다.
예시: Public 필드 사용의 문제점
public class Point {
public int x;
public int y;
}
Point p = new Point();
p.x = 5;
p.y = 10;
위의 코드는 직관적이고 간단해 보일 수 있지만, 문제의 소지가 있습니다. x와 y의 값을 아무나 변경할 수 있으며, 이러한 변경이 예상치 못한 버그를 일으킬 수 있습니다.
2. 접근자 메서드를 사용해야 하는 이유
접근자 메서드를 사용하면, 객체의 내부 구현을 숨기고 외부와의 상호작용을 제어할 수 있습니다. 다음은 접근자 메서드를 사용해야 하는 몇 가지 주요 이유입니다:
캡슐화:
필드를 private으로 선언하고, 접근자(getter)와 설정자(setter) 메서드를 제공하면, 클래스 외부에서는 이 필드에 직접 접근할 수 없습니다. 이로 인해 클래스의 내부 구현이 외부로부터 보호됩니다.
유연성:
접근자 메서드를 사용하면, 필드의 값이 읽히거나 쓰일 때 추가적인 로직을 삽입할 수 있습니다. 예를 들어, 값을 읽을 때 캐싱을 사용하거나, 값을 설정할 때 유효성 검사를 추가할 수 있습니다.
불변성 강화:
클래스의 필드를 final로 선언하고, 생성자에서만 초기화하게 하면, 해당 클래스는 불변 객체가 됩니다. 이는 다중 스레드 환경에서 안전성을 제공합니다.
인터페이스와의 일관성:
접근자 메서드를 사용하면, 클래스의 필드를 외부에 공개하지 않고도 인터페이스를 통해 일관된 방법으로 접근할 수 있습니다. 이는 코드의 가독성과 유지보수성을 높이는 데 도움이 됩니다.
예시: 접근자 메서드를 사용한 개선된 코드
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
위의 코드에서는 x와 y 필드가 private으로 선언되어 있으며, 이를 통해 캡슐화가 이루어집니다. 외부에서는 getX()와 getY() 메서드를 통해서만 x와 y의 값을 읽을 수 있습니다.
3. 불변 클래스의 설계
불변 클래스는 객체가 생성된 이후 그 상태를 변경할 수 없는 클래스를 의미합니다. 이러한 클래스는 다중 스레드 환경에서 안전하게 사용될 수 있어, 자바에서 특히 중요합니다. 불변 클래스를 설계할 때 접근자 메서드를 사용하는 것은 필수적입니다.
예시: 불변 클래스 설계
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
위의 ImmutablePoint 클래스는 불변 객체로, x와 y 필드는 final로 선언되어 있으며, 생성자를 통해 초기화됩니다. 필드를 외부에 노출하지 않고, 접근자 메서드를 통해서만 값을 읽을 수 있습니다. 이로써, 해당 클래스는 불변성을 보장하며, 안전하게 사용할 수 있습니다.
4. Lombok을 사용한 접근자 메서드 자동 생성
자바에서 접근자 메서드를 수동으로 작성하는 것은 반복적이고 번거로울 수 있습니다. 이럴 때, Lombok 라이브러리를 사용하면 접근자 메서드를 자동으로 생성할 수 있습니다. Lombok을 사용하면 코드가 간결해지고, 유지보수성이 향상됩니다.
Lombok 예시
import lombok.Getter;
public class Point {
@Getter
private int x;
@Getter
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
위 코드에서 Lombok의 @Getter 애노테이션을 사용하여 x와 y 필드에 대한 접근자 메서드가 자동으로 생성됩니다. 이로 인해 코드를 더 간결하게 유지할 수 있습니다.
6. 캡슐화와 정보 은닉의 중요성
캡슐화(encapsulation)와 정보 은닉(information hiding)은 객체 지향 프로그래밍의 핵심 개념으로, 클래스 설계에서 중요한 역할을 합니다. 이 두 개념은 클래스 내부의 구현 세부 사항을 외부로부터 숨기고, 외부와의 상호작용을 제한함으로써 코드의 안정성과 유지보수성을 높이는 데 기여합니다.
public 필드를 사용하는 것은 캡슐화와 정보 은닉의 원칙을 위반하는 행위입니다. 반면, 접근자 메서드를 통해 필드에 접근하도록 설계하면, 클래스 내부의 구현을 보호할 수 있습니다.
캡슐화의 예시
캡슐화의 주요 목표는 객체의 내부 상태를 보호하는 것입니다. 예를 들어, 계좌의 잔액을 다루는 BankAccount 클래스가 있다고 가정해 봅시다.
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
}
위의 코드에서 balance 필드는 private으로 선언되어 있으며, 외부에서 직접 접근할 수 없습니다. 대신 deposit()과 withdraw() 메서드를 통해 잔액을 변경할 수 있습니다. 이렇게 하면 잘못된 값이 잔액으로 설정되는 것을 방지할 수 있습니다.
정보 은닉의 예시
정보 은닉은 캡슐화와 밀접한 관련이 있으며, 클래스 내부의 세부 구현을 외부에서 숨기는 것을 목표로 합니다. 예를 들어, TemperatureSensor 클래스가 온도를 측정하는 방식이 변경될 가능성이 있다고 가정해 봅시다.
public class TemperatureSensor {
private double temperatureInCelsius;
public double getTemperatureInCelsius() {
return temperatureInCelsius;
}
public void updateTemperature(double newTemperature) {
this.temperatureInCelsius = newTemperature;
}
}
이 클래스의 내부 구현이 변경되어 온도를 섭씨에서 화씨로 저장해야 한다고 해도, 외부 코드에는 영향을 주지 않습니다. 외부에서는 여전히 getTemperatureInCelsius() 메서드를 통해 섭씨 온도를 읽을 수 있습니다. 이러한 설계는 정보 은닉을 통해 코드의 유연성과 확장성을 높입니다.
7. 접근자 메서드의 유효성 검사 및 추가 로직
접근자 메서드를 사용하는 또 다른 중요한 이유는 필드 값에 대한 유효성 검사와 추가 로직을 적용할 수 있다는 점입니다. 필드에 직접 접근하면 이러한 로직을 적용할 수 있는 기회가 사라지지만, 접근자 메서드를 사용하면 필요에 따라 다양한 검사를 추가할 수 있습니다.
유효성 검사의 예시
아래의 예시는 유효성 검사를 통해 잘못된 데이터가 설정되는 것을 방지하는 예시입니다.
public class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
if (width > 0) {
this.width = width;
} else {
throw new IllegalArgumentException("Width must be positive");
}
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
if (height > 0) {
this.height = height;
} else {
throw new IllegalArgumentException("Height must be positive");
}
}
}
위 코드에서 setWidth()와 setHeight() 메서드는 폭과 높이가 0보다 큰지 확인합니다. 이러한 유효성 검사를 통해 잘못된 값이 필드에 설정되는 것을 방지할 수 있습니다.
추가 로직의 예시
public class Employee {
private double salary;
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
if (salary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
this.salary = salary;
updateTaxInformation();
}
private void updateTaxInformation() {
// 세금 정보를 업데이트하는 추가 로직
}
}
setSalary() 메서드는 급여를 설정하면서 추가적으로 세금 정보를 업데이트하는 로직을 포함합니다. 이렇게 하면 필드 값의 변경과 관련된 모든 작업을 중앙 집중화할 수 있습니다.
8. 접근자 메서드와 객체 직렬화
객체 직렬화는 객체의 상태를 저장하거나 네트워크를 통해 전송할 때 사용됩니다. 자바에서는 Serializable 인터페이스를 사용하여 객체를 직렬화할 수 있습니다. 하지만, 직렬화 가능한 객체의 필드를 public으로 선언하는 것은 위험할 수 있습니다. 필드가 외부에 노출되면 객체의 내부 구현이 고정되어 버리기 때문에, 향후 변경이 어려워집니다.
접근자 메서드와 직렬화의 관계
접근자 메서드를 사용하면, 필드의 직렬화 여부를 제어할 수 있으며, 직렬화 과정에서 필드의 노출을 방지할 수 있습니다. 예를 들어, 민감한 데이터가 포함된 객체를 직렬화할 때는 이러한 필드를 transient로 선언하여 직렬화에서 제외할 수 있습니다.
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 직렬화에서 제외됨
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
위의 코드에서 password 필드는 transient 키워드를 사용하여 직렬화에서 제외되었습니다. 이를 통해 민감한 정보가 직렬화될 때 유출되는 것을 방지할 수 있습니다.
9. 접근자 메서드의 성능 고려사항
일부 개발자들은 성능상의 이유로 필드를 public으로 선언하기도 합니다. 접근자 메서드를 호출하는 것보다 필드에 직접 접근하는 것이 더 빠르다고 생각할 수 있기 때문입니다. 하지만, 현대의 자바 컴파일러와 JIT(Just-In-Time) 컴파일러는 이러한 차이를 최소화하며, 실제로는 접근자 메서드를 사용하는 것이 더 안전하고 권장됩니다.
성능 최적화와 접근자 메서드
실제 애플리케이션에서 성능 문제가 발생하는 경우는 드뭅니다. 대부분의 경우, 접근자 메서드를 사용하는 것이 성능에 미치는 영향은 미미하며, 코드의 가독성과 유지보수성을 높이는 데 더 큰 이점을 제공합니다. 따라서 성능 최적화보다는 안전성과 유연성을 우선시하는 것이 좋습니다.