객체지향 생활체조의 원칙을 보면
- 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
가 있습니다.
위의 규칙을 적용해보며 느낀점에 대해 이야기해보도록 할게요!
Getter을 사용한다면?
Getter을 쓴다는 것은 내부의 상태를 노출한다는 것입니다.
내부 필드를 밖으로 꺼내 로직을 구현한다면 어떻게 될까요?
class Car {
private final Handle handle
...
public getHandel(){
return handle;
]
}
class 외부 {
...
void moveRightCar(Car car) }{
Handel handle = car.getHandle();
handle.turnRight();
}
}
이해하기 쉬운 예시를 만들어보았습니다.
"차를 오른쪽으로 가기 위해 핸들을 꺾는다"를 적용했을 때 이 핸들의 움직임은 외부와 결합되는 것입니다.
만약 Car가 다양한 클라이언트(Car을 호출하는) 클래스와 결합이 된 상태에서 어느날 자율주행으로 차가 업그레이드 된다면??
차는 이제 핸들로 방향전환을 하지않고 인공지능의 오른쪽 신호로 방향을 전환합니다.
차를 오른쪽으로 움직이기 위해서 핸들을 꺾는 방식과 결합된 모든 외부의 코드를 수정해야합니다.
위 같은 경우 간단한 코드이지만 해당 로직이 클라이언트의 로직과 단단하게 결합이 되어버린다면 유지보수를 위한 Cost가 상승하게됩니다.
책임과 캡슐화
Getter를 쓰지 않는다는 것은
- 내부 클래스가 정말 적절한 책임을 가지고 있는가?
- 내부 클래스가 정말 캡슐화 되어있는가?
라고 생각합니다.
위의 코드를 한번 바꿔 볼까요?
class Car {
private final Handle handle
...
private void moveRightCar() }{
handle.turnRight();
}
}
class 외부 {
...
void (Car car) {
..로직
car.moveRightCar()
..
}
}
핸들을 바꾸는 책임을 Car이 가지게 되었으며 내부를 밖으로 노출하지도 않았습니다.
이제 갑자기 자율주행으로 기획이 변경되었다고 가정해보겠습니다.
class Car {
private final SelfDrive selfDrive;
...
private void moveRightCar() }{
selfDrive.turnRight();
}
}
class 외부 {
...
void (Car car) {
..로직
car.moveRightCar()
..
}
}
외부의 수정이 일어나지 않았습니다.
절대로 쓰지 말아야하나?
"왜 Getter을 쓰지말아야할까?" 를 생각해보면 조금 답을 알 수 있을 것 같습니다.
적절한 책임 할당, 캡슐화 부분에서 이상이 없다면 DTO 생성 등의 값을 활용하는 부분에서 사용해도 된다고 생각합니다.
어떤 규칙에 있어서 절대란 없으며 해당 규칙의 근본적 이유를 생각해보면 좋을 것 같습니다!
Getter 사용 시 주의사항
List, Set, Map 등을 getter을 통해 반환할 때 final로 선언하더라도 함수 호출을 통해 내부를 변경할 위험이 있습니다.
public class Cars {
private final List<Car> cars;
..
public List<Car> getCars() {
return cars;
}
}
public class 외부 {
void 변경(Cars cars) {
//Cars의 변경 위험이 있음
cars.getCars().add(new Car());
}
}
Getter을 사용하는 경우 Collections의 unModifiable 메서드를 사용해서 변경을 불가능하게 만들면
외부에서의 내부 변경을 막을 수 있답니다!
public List<Car> geCars() {
return Collections.unmodifiableList(this.cars);
}
참고
객체지향 생활체조 원칙 규칙 9