객체 지향을 공부하면서 데이터를 다른 곳에서 변경하지 못하도록 접근 제어자를 private
으로 설정하고, getter/setter
를 사용해야 한다고 배운 적이 있다.
최근에 읽은 <자바 코딩의 기술>에서는
getter
와setter
가 이미 표준화가 잘 되어 있어서 자바 빈 명세까지 만들어졌다고 설명한다.
상태 값을 가지는 객체는 상태 값을 외부에서 변경하지 못하도록 캡슐화하고 메서드만 노출시키도록 설계하는 것이 일반적이다. 이때 주로 사용하는 것이 바로 getter/setter
이다.
class Member {
private String name;
public Member(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
getter
는 상태 값을 가지는 객체로부터 값을 가져오는 데 사용하고, setter
는 그 값을 수정하는 용도로 사용한다. 즉 변수에 대해 접근 제한자를 private
으로 설정하여 직접적인 접근은 막을 수 있다고 해도, getter/setter
를 통한 접근은 가능하다.
결론부터 말하면 getter/setter
는 가능한 한 만들지 않는 것이 좋다. 이는 클래스의 필드에 직접 접근하는 것을 막기 위함이다.
여기서 생각해보아야 할 점이 있다. 값을 변경할 가능성이 있는 setter
는 데이터의 보호를 위한다면 없애는 것이 이해가 가는데, getter
의 사용은 왜 지양해야 할까? 이번 글에서는 그 이유에 대해 생각해보고자 한다.
객체 지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다. - 조영호
객체는 캡슐화를 통해 외부에서의 접근으로부터 데이터 노출을 줄이고, 동시에 다른 객체와 메시지를 주고받으며 협력한다. 객체는 외부에서 메시지를 받으면 그에 대한 로직을 수행한다. 로직을 수행하면서 필요에 따라 본인의 상태 값을 변경하는 경우도 있다.
객체는 별다른 getter/setter
없이도 이미 해당 기능을 자체적으로 수행할 수 있다. 상태 값을 본인이 지니고 있으니 외부 노출에 대한 걱정이 없기 때문이다. 근데 여기에 getter/setter
를 추가로 사용한다면 외부에서의 데이터 접근/수정 경로를 제공하게 되므로 잘못된 데이터 사용의 원인이 될 수 있다. 즉 객체가 객체스럽지 못하게 된다.
getter
를 사용하는 것에 대해 더 생각해 볼 점이 있다. getter
를 무분별하게 사용하면 디미터의 법칙을 위반할 수도 있다.
디미터의 법칙은 Object-Oriented Programming: An Objective Sense of Style에서 처음 소개된 개념으로, 다른 객체가 어떤 데이터를 가지고 있는지 내부 사정을 몰라야 한다는 것을 의미한다.
다음의 코드를 살펴보자.
@Getter
public class Member {
private String email;
private String name;
private Address address;
}
@Getter
public class Address {
private String region;
private String details;
}
@Service
public class NotificationService() {
public void sendMessageFromSeoulMember(final Member member) {
if (user.getAddress().getRegion().equals("서울")) {
sendNotification(user);
}
}
}
sendMessageFromSeoulMember()
는 Member
가 서울에 살고 있을 때 알림을 보내주는 함수이다. 코드를 보면 객체에 메시지를 보내는 것이 아니라 객체의 데이터를 직접적으로 확인하고 있다. 즉 getter
로 인해 Member
내부에 email
, name
, address
가 있음을 너무도 잘 알 수 있다.
이제 디미터의 법칙을 준수하도록 변경한 코드를 살펴보자.
public class Member {
private String email;
private String name;
private Address address;
public boolean isSeoulMember() {
return address.isSeoulRegion();
}
}
public class Address {
private String region;
private String details;
public boolean isSeoulRegion() {
return region.equals("서울");
}
}
@Service
public class NotificationService() {
public void sendMessageFromSeoulMember(final Member member) {
if (member.isSeoulMember()) {
sendNotification(user);
}
}
}
이처럼 객체에게 보내는 메시지를 구현하면 불필요한 getter
를 지울 수 있다. 또한 Member
와 Address
가 어떤 데이터를 가지고 있는지 외부에서는 더 이상 알지 못하게 된다.
또 다른 예시를 살펴보자.
public class Cars {
public static final String DELIMITER = ",";
public static final int MINIMUM_TEAM = 2;
private List<Car> cars;
public Cars(String inputNames) {
String[] names = inputNames.split(DELIMITER, -1);
cars = Arrays.stream(names)
.map(name -> new Car(name.trim()))
.collect(Collectors.toList());
validateCarNames();
}
...
public List<String> findWinners() {
final int maximum = cars.stream()
.map(car -> car.getPosition())
.max(Integer::compareTo)
.get();
return cars.stream()
.filter(car -> car.getPosition() == maximum)
.map(Car::getName)
.collect(Collectors.toList());
}
...
}
findWinners()
메서드는 여러 자동차들 중에서 position
값이 제일 큰 자동차를 구한다. 이 메서드에서는 Car
의 getPosition()
을 통해 position
상태 값에 접근하고 있다.
그런데 생각해보면 굳이 Car
클래스에서 position
을 꺼내서 외부에서 값을 비교할 필요가 없다. position
값이 있다는 사실을 외부에 알릴 필요 없이 Car
클래스 안에서 값을 비교한 다음, 외부에 결괏값만 넘겨주기만 해도 된다.
이제 코드를 리팩토링해보자.
public class Car implements Comparable<Car> {
...
public boolean isSamePosition(Car other) {
return other.position == this.position;
}
@Override
public int compareTo(Car other) {
return this.position - other.position;
}
...
}
public class Cars {
...
public List<String> findWinners() {
final Car maxPositionCar = findMaxPositionCar();
return findSamePositionCars(maxPositionCar);
}
private Car findMaxPositionCar() {
Car maxPositionCar = cars.stream()
.max(Car::compareTo)
.orElseThrow(() -> new IllegalArgumentException("차량 리스트가 비었습니다."));
}
private List<String> findSamePositionCar(Car maxPositionCar) {
return cars.stream()
.filter(maxPositionCar::isSamePosition)
.map(Car::getName)
.collect(Collectors.toList());
}
}
Car
에서 Comparable
을 상속받아 compareTo()
를 구현하여 Car
내부에서 자동차들을 비교하도록 변경하였다. Car
의 position
값은 Car
내부에서만 처리하고, 결괏값만을 외부에 전달하여 데이터의 은닉성을 향상시켰다.
getter
와setter
는 지양하자
무분별한 getter
와 setter
의 사용은 객체 지향의 핵심인 정보 은닉을 해치는 원인이 될 수 있다. 이로 인해 외부에서 객체의 상태를 알게 되거나 객체의 상태를 그대로 수정하게 되고, 객체의 상태가 변경되면 의도하지 않은 동작을 수행하므로 문제가 될 수 있다.
setter
setter
는 값을 바꾸는 이유를 명확하게 알 수 없다.setter
를 사용하면 해당 객체가 해야 할 일(책임)을 다른 객체가 하게 된다.getter
getter
는 조회로 끝나지 않는 경우가 많다.getter
를 통해 조건을 검사하면 변경에 취약하다. (객체가 본인의 일을 책임지지 않는 문제)대안
setter
대신 명확한 의도를 가진 메서드를 사용하자.getter
로 조건을 검사하지 말고 결과를 반환하게 하자. getter
에 대한 안 좋은 이야기만 했지만, getter
의 사용이 완전히 잘못되었다는 것은 아니다. 값의 출력이나 순수 값 프로퍼티 조회에는 getter
가 여전히 유용하게 사용된다.
다만 getter
를 사용해야 한다면 외부에서 값을 유추하거나 변경할 수 없도록 더욱 신경써야 한다. 예를 들어 Collection
과 getter
를 함께 사용하는 경우, 외부에서 쉽게 값을 변경할 수 있으므로 Unmodifiable Collection
을 사용하여 이를 방지해야 한다.
// Bad
public List<Car> getCars() {
return cars;
}
// Good
public List<Car> getCars() {
return Collections.unmodifiableList(cars);
}
getter를 사용하는 대신 객체에 메시지를 보내자
[OOP] 디미터의 법칙(Law of Demeter)
getter 쓰지 말라고만 하고 가버리면 어떡해요