2024. 10. 30. 03:19ㆍ우아한테크코스
2주 차 과제인 자동차 경주 게임을 진행하며, 매 라운드(이동 시도)마다 자동차의 위치를 출력해야 한다는 요구 사항이 있었다.
이를 반영하면서 생긴 고민들을 기록해보고자 한다.
매 라운드마다 자동차의 위치를 출력하라.
처음엔, 직관적으로 라운드가 끝날 때 Cars 클래스(일급 컬렉션)의 멤버 변수인 Car 리스트를 순회하며, 각 Car 클래스에서 OutputHandler 클래스의 출력 메서드를 호출하는 방법을 생각했다.
하지만 이 경우 Car 클래스가 OutputHandler 클래스에 의존하는 것이 불편하게 느껴졌다. OutputHandler를 정적 클래스로 두지 않고 객체로 관리하고 싶었기 때문에 이러한 의존성이 형성되는 것은 피할 수 없었다.
그렇다면 반대로 OutputHandler 클래스가 Cars 클래스의 Car 리스트를 순회하며 위치를 가져와 출력하는 방법도 있을 것이다.
사실 지금 생각해보면 이 방법이 더 적절했을지도 모른다. 하지만 그 순간 머릿속을 스친 옵저버 패턴!
옵저버 패턴
옵저버 패턴이란 ?
소프트웨어 설계와 엔지니어링에서 사용되는 디자인 패턴 중 하나로, Subject 라고 불리는 객체가 자신의 상태 변화를 Observer 라는 의존 객체들에게 알리는 구조입니다. Subject는 Observer들의 목록을 유지하고, 상태가 변할 때마다 Observer의 메서드를 호출하여 알림을 전달합니다.
사용 예시
움직임 상태 변경 시 멤버로 관리/등록해둔 옵저버에게 알림을 보낸다.
public class Car {
private final String name;
private int position = 0;
private final List<CarObserver> observers = new ArrayList<>();
public Car(String name) {
this.name = name;
}
public void addObserver(CarObserver observer) {
observers.add(observer);
}
public void removeObserver(CarObserver observer) {
observers.remove(observer);
}
private void notifyObservers() {
for (CarObserver observer : observers) {
observer.onMoved(this);
}
}
public void move(int randomValue) {
if (randomValue >= 4) {
position++;
}
notifyObservers(); // 움직임 상태 변경 시 알림
}
...
}
옵저버 인터페이스
public interface CarObserver {
void onMoved(Car car);
}
옵저버 구현체 (상태 변경, 즉 move를 한 Car로부터 알림을 받고 올바르게 출력한다.)
public class CarMovePrinter implements CarObserver {
private final OutputHandler outputHandler;
public CarMovePrinter(OutputHandler outputHandler) {
this.outputHandler = outputHandler;
}
@Override
public void onMoved(Car car) {
outputHandler.displayCarPosition(car);
}
}
옵저버 등록 헬퍼
public class CarObserverHelper {
private CarObserverHelper() {
}
public static <T extends CarObserver> void addObserverToCar(Car car, Class<T> observerClass, Object... dependencies) {
if (car == null || observerClass == null) {
throw new IllegalArgumentException("Car와 observerClass는 null이 될 수 없습니다.");
}
if (dependencies == null || Arrays.stream(dependencies).anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("dependencies 배열에 null 값이 포함될 수 없습니다.");
}
try {
// 생성자 매개변수를 기반으로 인스턴스를 생성
Constructor<T> constructor = findMatchingConstructor(observerClass, dependencies);
T observerInstance = constructor.newInstance(dependencies);
car.addObserver(observerInstance);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new IllegalStateException("옵저버 인스턴스를 생성할 수 없습니다.", e);
}
}
private static <T extends CarObserver> Constructor<T> findMatchingConstructor(Class<T> observerClass, Object... dependencies) throws NoSuchMethodException {
for (Constructor<?> constructor : observerClass.getConstructors()) {
// 매개변수의 수와 타입이 일치하는 생성자를 찾음
if (isParameterCountMatching(constructor, dependencies) && areParameterTypesMatching(constructor, dependencies)) {
return (Constructor<T>) constructor;
}
}
throw new NoSuchMethodException("적절한 생성자를 찾을 수 없습니다.");
}
// 생성자의 파라미터 개수가 dependencies의 개수와 일치하는지 확인
private static boolean isParameterCountMatching(Constructor<?> constructor, Object[] dependencies) {
return constructor.getParameterTypes().length == dependencies.length;
}
// 생성자의 각 파라미터 타입이 dependencies의 타입과 호환되는지 확인 (상속 관계 고려)
private static boolean areParameterTypesMatching(Constructor<?> constructor, Object[] dependencies) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
if (!parameterTypes[i].isAssignableFrom(dependencies[i].getClass())) {
return false;
}
}
return true;
}
}
리플렉션을 이용해 옵저버의 생성자를 찾아내고, 입력받은 의존성과 비교하여 적합한 옵저버 인스턴스를 생성 및 추가하도록 구현했다.
옵저버 패턴 자체는 간단하게 구현했지만, 이를 더 유연하게 관리할 수 있는 설계를 고민하다 보니 과제의 범위를 넘어서는 작업이 되었다.
확장성과 안정성을 고려해서 리플렉션을 적용하긴 했지만, 이 방식이 과연 유지보수나 가독성 면에서 좋다고 할 수 있을지 의문이다.
오히려 코드의 흐름을 복잡하게 바꾼 것 같기도 하다.
마무리
자바스크립트를 처음 배울 때, 버튼에 이벤트를 추가하려고 onClick 같은 키워드를 사용했던 기억이 난다. 그때는 단순히 “클릭하면 동작이 실행된다” 정도로만 이해했지, 이것이 사실 옵저버 패턴의 일종이라는 생각은 전혀 하지 못했다. 심지어 옵저버 패턴을 나중에 배우고 나서도 onClick과 연결 지어 생각하지 못했는데, 이제서야 하나로 연결된다.
지금 돌아보면, 자동차의 위치 출력을 더 간단하고 효율적인 코드로 구현할 수 있었을 것 같은데, 오히려 복잡하게 구현하려고 한 것 같다. 물론 학습 목적이 있었기에 여러 가지 방법을 시도해본 건 좋은 경험이지만, 앞으로는 조금 더 깊이 고민하고, 단순하고 효과적인 해결책을 선택할 필요가 있다는 생각이 든다.
'우아한테크코스' 카테고리의 다른 글
[우테코 7기 프리코스 2주 차] 회고 (0) | 2024.11.01 |
---|---|
[우테코 7기 프리코스 2주 차] TDD (0) | 2024.10.31 |
[우테코 7기 프리코스 2주 차] NsTest (0) | 2024.10.24 |
[우테코 7기 프리코스 1주 차] 피드백 (0) | 2024.10.22 |
[우테코 7기 프리코스 1주 차] 객체 지향 (3) | 2024.10.21 |