클래스 내의 public 변수 선언시 주의사항
우아한테크코스 프리코스를 진행하면서 사용자 클래스를 생성했는데, userNumbers
를 static
으로 선언하여 외부 클래스에서도 접근하여 볼, 스트라이크를 구하고 유효성 검사를 수행이 가능하도록 설계했다. 하지만 이때 경고 밑줄이 등장하면서 내가 잘못 설계한 두 가지 오류를 알려준다.하나씩 설명하겠다.
Utility classes should not have public constructors
위 오류는 이전 발행 글에 기재하였다. static
으로 선언하여 인스턴스를 생성하지 않고 바로 사용할 수 있도록 설계하였기에, 혹시 모를 실수를 방지하려면 설계 시 private
생성자를 이용하여 인스턴스 생성을 막아둬야 한다. 자세한 내용은 아래 글을 참고하기 바란다.
[Java] private 생성자의 사용 이유, final, 자바 메모리 구조
sonarlint의 코드리뷰 프리코스를 진행하면서 내 코드를 자동으로 리뷰해주는 IDE 확장 플러그인인 sonarlint의 도움을 많이 받았다. sonarlint는 코드에 문제가 있다고 판단되면 해당 코드에 물결표 밑줄을 표시해 경고해준다.
Class variable fields should not have public accessibility
이슈의 생성 이유
public 클래스 변수 필드는 캡슐화 원칙을 지키지 않으며 세 가지 주요한 단점이 존재한다.
첫 번째, 유효성 검사와 같은 추가적인 행동을 추가할 수 없다.
두 번째, 내부 표현이 노출되어 나중에 변경할 수 없다.
세 번째, 멤버 값은 코드 어디서든 변경 가능하며 프로그래머의 예상과 달라질 수 있다.
반면에 private 속성과 접근자 메서드(getter & setter)를 사용하면, 무단 수정을 방지할 수 있다.
그렇다. public static List<Integer> userNumbers
는 어디서나 불러올 수 있기 때문에 어디서든 변경이 가능하여 캡슐화를 깨뜨릴 수 있다. 따라서 우선 가장 먼저 생각한 것은 public static List<Integer> userNumbers
를 private static List<Integer> userNumbers
로 변경하고, getter
를 생성하여 값을 불러오는 것이다.
이렇게 하니 오류 문구가 사라졌다.
하지만 프리코스를 진행하면서 자주 마주친 문구가 머릿 속을 스쳐 지나갔다.
Getter의 사용을 지양하라
왜 getter의 사용을 지양해야 할까?
이런 의문을 가졌다.
하지만 곰곰히 생각해보면, 객체지향의 원칙과 밀접한 연관이 있다.
객체지향의 원칙 중 하나는 정보 은닉(Information Hiding), 즉 객체의 중요한 정보를 외부에 노출하지 않는 것이다.
이러한 원칙을 지키기 위해 자바에서 클래스 작성 시 모든 필드를 private
로 숨기고 public
메서드를 통해 간접적으로 필드를 다룬다.
엄밀히 말하자면 필드를 private
로 은닉하고 getter
와 setter
를 사용한다면 정보 은닉이라고 할 수 없다.
필드를 숨겼지만 getter
로 언제든지 조회할 수 있고, setter
로 언제든지 값을 변경할 수 있으니 말이다.
또한 객체지향의 사실과 오해라는 책에서 자율적인 객체란, 스스로 의지와 판단에 따라 각자 맡은 책임을 수행하는 객체를 의미한다고 하였다. 그리고 객체는 캡슐화된 상태와 외부에 노출되어 있는 행동을 갖고 있으며, 다른 객체와 메시지를 주고 받으며 협력한다.
객체는 메시지를 받으면 메시지에 해당하는 행동(로직)을 수행하고, 필요하다면 객체 스스로 내부의 상태값을 변경한다.
정리하자면, 객체지향 프로그래밍은 객체가 자율적으로 행동하는 프로그래밍이다.
그래서 getter를 사용하는 것이 뭐가 문젠데?
getter
는 단순히 조회로 끝나지 않는 경우가 많다. 많은 경우에 getter
로 상태값을 조회하면 그 값이 조건에 맞는지 확인하여 비즈니스 로직을 수행하게 된다. 이것은 객체가 행동(로직)을 갖고 있는 형태가 아니면서 메시지를 주고 받는 형태도 아니게 된다. 이것은 객체 스스로 상태값을 변경하는 것도 아니고, 외부에서 상태값을 변경할 수 있는 위험성도 생긴다.
이것은 즉 객체가 자율적인 객체가 되지 못한다는 것을 의미한다.
디미터의 법칙 위반, 가독성 저하
getter
를 남용하게 되면 디미터의 법칙을 위반할 가능성이 높아지고, 가독성이 떨어진다.
디미터의 법칙(Law of Demeter; LoD)
"최소한의 지식 원칙(The Principle of Least Knowledge)"으로 알려져 있으며, 모듈이 자신이 조작하는 객체의 속사정을 몰라야 한다는 것을 의미한다.
어떤 객체가 다른 객체에 대해 지나치게 많은 정보를 알고 있으면 서로에 대한 결합도가 높아지고 이로 인해 좋지 못한 설계를 야기한다.
즉, 여러 개의 .(dot)을 지양하라는 말이다.
디미터의 법칙을 준수하게 되면 캡슐화를 수준을 높일 수 있고 그에 따라 객체의 자율성과 응집도을 높일 수 있다.
[예시] 자동차 경주 게임
Cars
클래스는 여러 자동차의 역할을 한다.
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
값을 가지는 자동차들을 구하는 메서드
public List<String> findWinners() {
final int maximum = cars.stream()
.map(car -> car.getPosition()) // Car객체의 position = 자동차가 움직인 거리
.max(Integer::compareTo)
.get();
return cars.stream()
.filter(car -> car.getPosition() == maximum)
.map(Car::getName)
.collect(Collectors.toList());
}
Car
객체에서 getPosition()
을 사용해 position
의 상태값을 직접 꺼내서 비교한다. 과연 이렇게 로직을 수행하는 것이 객체지향적일까?
Car
의 접근 제한자가 private
인 멤버변수 position
값끼리 비교하는 로직이다. 따라서 Car
객체에게 position
값을 비교할 때 또 다른 Car
객체를 넘겨주고 Car
끼리 position
을 비교해야 한다. Cars
가 아니라 Car
에서 해야하는 일인 것이다.
Car
객체 내에서 같은 자동차끼리 position
값을 비교하고, Car
객체 내에서 maximum
과 일치하는지 비교하도록 Cars
의 로직을 Car
안으로 옮겨야 한다.
즉, Car
객체에게 position
값을 비교할 수 있도록 메시지를 보내고, Car
객체에게 maximum
값과 자신의 position
값이 같은지 물어보는 메시지를 보내 getPosition()
을 사용하지 않도록 리팩토링 해보겠다.
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());
}
}
getPosition()
을 없애는 방향으로 리팩토링한 코드이다. Car
에서 Comparable
을 상속받아 compareTo()
를 구현해 Car
내부에서 자동차끼리 비교해준다. max
를 통해 cars
중, 최대 길이의 position
을 가진 Car
를 찾을 수 있다. 그리고 isSamePosition()
을 구현해 Car
내부에서 직접 position
값을 비교할 수 있게 된다.
포비의 팁
상태를 가지는 객체를 추가했다면 객체가 제대로 된 역할을 하도록 구현해라.
객체가 로직을 구현하도록 해야 한다.
상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링하라.
getter를 무조건 사용하지 말라는 말이 아니다.
당연히 getter
를 무조건 사용하지 않고서는 모든 기능을 구현하기가 어려울 것이다. 출력을 위한 값과 같이 순수한 값 프로퍼티를 가져오기 위해서라면 어느 정도의 getter
는 허용된다. 그러나 Collection
인터페이스를 사용하는 경우 외부에서 getter
메서드로 얻은 값을 통해 상태값을 변경할 수 있다.
public List<Car> getCars() {
return cars;
} (x)
public List<Car> getCars() {
return Collections.unmodifiableList(cars);
} (o)
이처럼 Collections.unmodifiableList()
와 같은 Unmodifiable Collection
을 사용해 외부에서 변경하지 못하도록 하는 게 좋다.
참고자료
https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/
https://colabear754.tistory.com/173
https://dkswnkk.tistory.com/687
'Java' 카테고리의 다른 글
[JUnit - 1] Junit 5란 무엇인가? (어노테이션, 정의, 테스트 클래스와 메서드) (0) | 2023.11.01 |
---|---|
[java] private static은 언제 필요할까?(접근 제한자, static 키워드, static 메모리 구조) (0) | 2023.10.26 |
[Java] private 생성자의 사용 이유, final, 자바 메모리 구조 (0) | 2023.10.25 |
[Java] 익명클래스, 람다와 스트림 (1) | 2023.10.11 |
[Java] 컬렉션 프레임워크(List, Set, Map, Iterator) (0) | 2023.10.11 |