[JAVA] 객체 생성시 유효성 검사에 관하여

20312 단어 JavaJava

객체 생성시, 그 멤버 변수들에 대해 유효성 검사를 진행해야 하는 상황을 자주 마주친다. 이와 관련하여 했던 고민들을 정리하고, 결론적으로 어떤 방식으로 유효성 검사를 진행해야 하는가에 대한 생각을 정리한다.

메소드로 추출해라

public class Car {
    private static final int CAR_NAME_LENGTH_MAX = 5;
    private String name;

    public Car(String name) {
        if (name.length() > CAR_NAME_LENGTH_MAX) {
            throw new IllegalArgumentException( );
        }
        this.name = name;
    }
}

위 Car의 예시를 보면, name.length()CAR_NAME_LENGTH_MAX보다 크면 에러를 띄우는 간단한 로직으로 구현되어있다. 객체가 조금만 복잡해지더라도, 훨씬 더 많은 유효성 검증 로직이 추가되게 되고, 이에 따라 생성자의 길이도 증가하게 된다. 이에 대한 간단한 해결책으로 아래 예시에서 볼 수 있듯이 검증 로직을 메소드로 추출하는 방법이 있다.

public class Car {
    private static final int CAR_NAME_LENGTH_MAX = 5;
    private String name;
    
    public Car(String name) {
        validateNameLength(name);
        this.name = name;
    }
    
    private void validateNameLength(String name) {
        if (name.length() > CAR_NAME_LENGTH_MAX) {
            throw new IllegalArgumentException();
        }
    }
}

메소드 추출의 의문점

생성자객체를 생성하는 로직인데, 생성되지도 않은 인스턴스의 메소드생성자에서 사용하는 것이 정당한가?

이는 객체의 생성 과정에 대한 잘못된 이해에서 비롯됐다.

객체의 생성 과정

Car car = new Car("name");new를 실행한 시점에, Car 객체는 메모리에 할당이 되어 있는 상태이다. 그렇기 때문에, 그 인스턴스 메소드인 validateNameLength를 실행하는 데, 문제가 발생하지 않는다.
(부끄럽지만, 저자는 생성자가 로직 수행이 완료되고 나서야 객체가 메모리에 할당된다고 생각했었다..)

실제 바이트 코드를 봤을 때, java/lang/Objcet.<init>을 실행한 시점에서 메모리에 할당되고, 그 이후에 validateNameLength가 실행이 되는 것을 확인할 수 있다.

메소드 추출의 위험성 - Override

메소드 추출의 경우 편리한 방법이지만, 하나의 위험성을 가지고 있다. 만약 Car를 상속하는 Sonata클래스를 생각해보자. Sonata클래스에 경우 name.length() <= 5라는 다른 이름의 유효성 검증 로직을 진행한다고 하면 다음과 같이 구현할 수 있다.

public class Car {
    private static final int CAR_NAME_LENGTH_MAX = 5;
    private String name;
    
    public Car(String name) {
        validateNameLength(name);
        this.name = name;
    }
    
    //기존의 private의 경우 상속이 불가하기에 protected로 접근자 변경
    protected void validateNameLength(String name) {
        if (name.length() > CAR_NAME_LENGTH_MAX) {
            throw new IllegalArgumentException();
        }
    }
}

public class Sonata extends Car {
    private static final int CAR_NAME_LENGTH_MIN = 5;
    public Sonate(String name) {
        super(name);
    }
    
    @Override
    protected void validateNameLength(String name) {
        if (name.length() <= CAR_NAME_LENGTH_MIN) {
            throw new IllegalArgumentException();
        }
    }
}

이처럼 구현을 하게 되면, Sonata클래스는 부모 클래스과의 일관성이 깨지는 결과를 초래하게 된다.

위험성을 막기 위해서는 어떻게 해야하는가?

해결책은 간단하다. validation메소드를 상속하지 못하도록 접근자를 제어해주면 된다.

  • private 접근 제어자 사용
  • default(package-private) + final 접근 제어자 사용
    패키지 다른 부분에서 validation 로직을 사용하게 될 경우 이와 같은 방식을 사용

사실 대부분의 경우, private으로 validation로직을 구현하기에 이러한 실수를 경험할 일이 적겠지만, 한번 쯤 생각해 볼만한 주제라 생각한다.

유효성 검사의 책임은 누가져야 하는가?

메소드 추출을 활용하여, 생성자의 로직을 간결하게 표현할 수 있었다.
이러한 validation 로직들이 많아지면 생성자가 다음과 같아진다.

public Car(String name, ...) {
        validateNameLength(name);
        validateA(...);
        validateB(...);
        validateC(...);
        validateD(...);
        this.name = name;
}

이 경우에는 어떻게 할 것인가?
이 또한 이 validation 로직들을 모두 포함하는 메소드로 추출할 것인가?

public Car(...) {
        validateCar(...);
        this.name = name;
}

public validateCar(...) {
        validateNameLength(name);
        validateA();
        validateB();
        validateC();
        validateD();
        ...
}

유효성 검증만을 위한 수많은 메소드들...

유효성 검증 로직이 너무 많을 때에는, 객체에 너무 많은 책임이 주어진 것이 아닌가 고민해봐야 한다.

당장 이름의 경우에도, 이름의 길이를 Car 객체가 가지고 있는 것이 좋을지, 별개로 Name 클래스로 구분하여 관리하는 것이 좋을지에 대해 말이다.

책임의 이전

public class Car {
    private Name name;

    public Car(String name) {
        this(new Name(name));
    }

    public Car(Name name) {
        this.name = name;
    }
}


public class Name {
    private static final int CAR_NAME_LENGTH_MAX = 5;
    private final String name;

    public Name(String name) {
        validateNameLength(name);
        this.name = name;
    }

    private void validateNameLength(String name) {
        if (name.length() > CAR_NAME_LENGTH_MAX) {
            throw new IllegalArgumentException( );
        }
    }
}

이름 길이에 유효성 검사에 대한 책임을 Name으로 이전한 코드의 모습이다. 분리하고 나서야 책임에 대한 분리가 명확하게 보인다.

결론

객체 생성에 대해 고민은 많이 했지만, 결론은 다소 뻔했다.

메소드를 추출하고, 책임을 분리하라.

객체 생성과 관련하여 추가로 정리했으면 하는 주제들을 남겨두어 미래에 나에게 맡기겠다.

  • 객체 생성시 유효성 검사는 언제 이루어 져야 하는가?
    변수 할당 이전 vs 변수 할당 이후

  • 정적팩토리 메소드 패턴을 사용

  • 팩토리 메소드 패턴 사용

참고 사이트

Can I call methods in constructor in Java?

좋은 웹페이지 즐겨찾기