나를 기록하다
article thumbnail
반응형

getter를 지양하라

클래스 내의 public 변수 선언시 주의사항

숫자 야구 게임 - User 클래스

우아한테크코스 프리코스를 진행하면서 사용자 클래스를 생성했는데, userNumbersstatic으로 선언하여 외부 클래스에서도 접근하여 볼, 스트라이크를 구하고 유효성 검사를 수행이 가능하도록 설계했다. 하지만 이때 경고 밑줄이 등장하면서 내가 잘못 설계한 두 가지 오류를 알려준다.하나씩 설명하겠다.


Utility classes should not have public constructors

위 오류는 이전 발행 글에 기재하였다. static으로 선언하여 인스턴스를 생성하지 않고 바로 사용할 수 있도록 설계하였기에, 혹시 모를 실수를 방지하려면 설계 시 private 생성자를 이용하여 인스턴스 생성을 막아둬야 한다. 자세한 내용은 아래 글을 참고하기 바란다.

[Java] private 생성자의 사용 이유, final, 자바 메모리 구조

 

[Java] private 생성자의 사용 이유, final, 자바 메모리 구조

sonarlint의 코드리뷰 프리코스를 진행하면서 내 코드를 자동으로 리뷰해주는 IDE 확장 플러그인인 sonarlint의 도움을 많이 받았다. sonarlint는 코드에 문제가 있다고 판단되면 해당 코드에 물결표 밑

prao.tistory.com

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> userNumbersprivate static List<Integer> userNumbers로 변경하고, getter를 생성하여 값을 불러오는 것이다.

private으로 선언 후 getter를 통해 값을 받음

이렇게 하니 오류 문구가 사라졌다.

하지만 프리코스를 진행하면서 자주 마주친 문구가 머릿 속을 스쳐 지나갔다.


Getter의 사용을 지양하라

왜 getter의 사용을 지양해야 할까?

이런 의문을 가졌다.

하지만 곰곰히 생각해보면, 객체지향의 원칙과 밀접한 연관이 있다.

객체지향의 원칙 중 하나는 정보 은닉(Information Hiding), 즉 객체의 중요한 정보를 외부에 노출하지 않는 것이다.

이러한 원칙을 지키기 위해 자바에서 클래스 작성 시 모든 필드를 private로 숨기고 public 메서드를 통해 간접적으로 필드를 다룬다.

엄밀히 말하자면 필드를 private로 은닉하고 gettersetter를 사용한다면 정보 은닉이라고 할 수 없다.

필드를 숨겼지만 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/

 

getter를 사용하는 대신 객체에 메시지를 보내자

getter는 멤버변수의 값을 호출하는 메소드이고, setter는 멤버변수의 값을 변경시키는 메소드이다. 자바 빈 설계 규약에 따르면 자바 빈 클래스 설계 시, 클래스의 멤버변수의 접근제어자는 private

tecoble.techcourse.co.kr

https://colabear754.tistory.com/173

 

[OOP] Getter와 Setter는 지양하는게 좋다

목차 들어가기 전에 얼마 전 사내에서 Getter와 Setter를 함부로 사용하면 안되는 이유에 대한 세미나가 있었다. Setter에 대한 이야기는 워낙 많이 알려져있었지만 Getter에 대한 이야기는 잘 하지 않

colabear754.tistory.com

https://dkswnkk.tistory.com/687

 

디미터 법칙 (Law of Demeter)이란?

최근 클린코드를 읽던 중 디미터 법칙에 대해 알게 되었습니다. 용어 자체는 생소하긴 한데 법칙의 내용 자체는 크게 어렵지 않고, 평상시 개발에서 흔히 마주칠 수 있는 내용을 다룬 법칙이기

dkswnkk.tistory.com

 

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...