getter를 써야 하나 말아야 하나

Yeseong31·2023년 10월 22일
0

객체 지향을 공부하면서 데이터를 다른 곳에서 변경하지 못하도록 접근 제어자를 private으로 설정하고, getter/setter를 사용해야 한다고 배운 적이 있다.

최근에 읽은 <자바 코딩의 기술>에서는 gettersetter가 이미 표준화가 잘 되어 있어서 자바 빈 명세까지 만들어졌다고 설명한다.

상태 값을 가지는 객체는 상태 값을 외부에서 변경하지 못하도록 캡슐화하고 메서드만 노출시키도록 설계하는 것이 일반적이다. 이때 주로 사용하는 것이 바로 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를 지울 수 있다. 또한 MemberAddress가 어떤 데이터를 가지고 있는지 외부에서는 더 이상 알지 못하게 된다.

또 다른 예시를 살펴보자.

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 값이 제일 큰 자동차를 구한다. 이 메서드에서는 CargetPosition()을 통해 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 내부에서 자동차들을 비교하도록 변경하였다. Carposition 값은 Car 내부에서만 처리하고, 결괏값만을 외부에 전달하여 데이터의 은닉성을 향상시켰다.


정리하면

gettersetter는 지양하자

무분별한 gettersetter의 사용은 객체 지향의 핵심인 정보 은닉을 해치는 원인이 될 수 있다. 이로 인해 외부에서 객체의 상태를 알게 되거나 객체의 상태를 그대로 수정하게 되고, 객체의 상태가 변경되면 의도하지 않은 동작을 수행하므로 문제가 될 수 있다.

setter

  • setter는 값을 바꾸는 이유를 명확하게 알 수 없다.
  • setter를 사용하면 해당 객체가 해야 할 일(책임)을 다른 객체가 하게 된다.

getter

  • getter는 조회로 끝나지 않는 경우가 많다.
  • getter를 통해 조건을 검사하면 변경에 취약하다. (객체가 본인의 일을 책임지지 않는 문제)

대안

  • setter 대신 명확한 의도를 가진 메서드를 사용하자.
  • getter로 조건을 검사하지 말고 결과를 반환하게 하자.

물론 getter를 아예 쓰지 말자는 것은 아니다

getter에 대한 안 좋은 이야기만 했지만, getter의 사용이 완전히 잘못되었다는 것은 아니다. 값의 출력이나 순수 값 프로퍼티 조회에는 getter가 여전히 유용하게 사용된다.

다만 getter를 사용해야 한다면 외부에서 값을 유추하거나 변경할 수 없도록 더욱 신경써야 한다. 예를 들어 Collectiongetter를 함께 사용하는 경우, 외부에서 쉽게 값을 변경할 수 있으므로 Unmodifiable Collection을 사용하여 이를 방지해야 한다.

// Bad
public List<Car> getCars() {
	return cars;
}

// Good
public List<Car> getCars() {
	return Collections.unmodifiableList(cars);
}

참고

getter를 사용하는 대신 객체에 메시지를 보내자
[OOP] 디미터의 법칙(Law of Demeter)
getter 쓰지 말라고만 하고 가버리면 어떡해요

profile
역시 개발자는 알아야 할 게 많다.

0개의 댓글