내가 싫어하는 코딩 스타일 중의 하나가 바로 다음과 같은 코딩 스타일이다.
if (BAR == foo) {
// whatever..
}
Java에서는 다음과 같이 쓰기도 한다.
if (BAR.equals(foo)) {
// whatever..
}
원래 이러한 코딩 스타일은 if 문 내에서 의도하지 않은 assignment가 일어나는 것을 방지하기 위한 것이다.
// XXX: assignment not intended
if (foo = BAR) {
// whatever..
}
Java의 경우에는 NullPointerException (이하 NPE)을 방지하기 위한 목적도 있다고 한다.
// XXX: when foo is null, throws NPE
if (foo.equals(BAR)) {
// whatever..
}
일단, 이런 스타일을 싫어하는 첫 번째 이유는 읽기가 힘들다는 것이다. ‘if foo is BAR’는 내 마음의 회로에서 바로 처리가 되지만, ‘if BAR is foo’는 위화감이 있다. 아마도 언어적인 이유이거나 (an student is not me?) 수학식을 읽는 방식 (when pi is x?) 을 기대하기 때문이라고 생각한다.
두 번째 이유는 이러한 스타일을 사용함으로써 얻으려는 이익은 다른 방법으로도 얻을 수 있기 때문이다. 단순히 잘못된 방식이라는 것이다.
일단 conditional에서의 의도하지 않은 assignments를 방지하기 위해서는 이미 오래 전부터 컴파일러가 보여 주고 있는 경고 (warning) 메시지를 이용하면 된다. 게다가 프로그래밍에 익숙해 지면 저런 실수는 흔치는 않은 일이 된다.
NPE의 방지는 방어적인 프로그래밍의 입장에서 일견 설득력이 있어 보이지만, Java의 스타일을 해치고, 결과적으로 오류 가능성을 감추어서 견고한 코드로 만드는 길을 막는다고 생각한다.
일반적인 프로그래밍에서 nullity는 말 그대로 invalidity를 의미하고, Java에서는 이러한 의미를 잘 살려서 코딩 할 수 있다.
Case 1
Foo foo1 = new Foo();
assert foo1 != null;
foo1.bar();
Foo foo2 = getValidFoo();
assert foo2 != null;
foo2.bar();
foo1과 foo2의 경우가 바로 그런 경우다.
foo1의 경우 Java의 new 키워드를 통해 valid하다는 것 즉, null이 아니라는 것이 보장된다.
getValidFoo()는 항상 valid한 Foo instance를 돌려준다라는 의미를 가지고 있다. 만약 getFooValid()가 null을 돌려준다면 assertion failure나 NPE가 발생할 것이다. 하지만, 그것은 getValidFoo()의 오류이므로, getValidFoo()를 사용하는 코드에서는 이 점을 무시할 수 있다. 그리고, getValidFoo()에서 제대로 Foo instance를 돌려줄 수 없는 상황이라면 exception을 throw할 것이다.
우리는 모든 코드에서 모든 변수의 validity를 체크할 수 있지만, 우리는 단순히 그렇게 하지 않는다. 만약 그렇게 한다면, 그것은 편집증 환자의 코드로 보일 뿐이다. 좋은 프로그래밍 언어는 그렇게 하지 않아도 되도록 하는 여러 장치들(new keyword, types, exception, assertion, …)을 가지고 있다.
만약 getValidFoo()가 믿을 만 하지 않아서 불안하다면 assertion을 사용해 명시적으로 오류를 발생시키면 된다.
Case 2
Foo foo3 = getFoo();
if (foo3 == null)
throw new RuntimeException("foo2 is null");
assert foo3 != null;
foo3.bar();
getFoo()가 semantic 상으로 null을 돌려줄 수 있을 경우가 있다. 이 경우 우리는 foo의 메서드를 호출할 예정이고 이후로도 다른 처리를 해야 하므로, 명시적으로 nullity 체크를 해서 적절한 처리를 하면 된다.
nullity는 위에서 얘기 한대로 invalidity를 의미하므로 이러한 상황에 대해서는 어떻게 대처할지 미리 준비가 되어 있어야 한다. 단순히 if (foo.equals(BAR))로 해결되었다고 착각하는 것은 위험하다. 그러한 코드가 invalidity 상황을 해결할 수 없다는 것이 아니다. 이러한 코딩 스타일이라는 것은 곧 invalidity 상황에 대해서 무시하는 습관을 들이는 것이랑 같다는 것이다.
한편, Case 1의 getValidFoo() 처럼, 원래의 의미는 validity를 보장해야 하나 실제로 그렇지 않을 경우, 안정성을 보장해야 하는 상황에서, 치명적인 결과를 가져오는 것이 걱정된다면, Case 2로 처리하라. 명시적으로 invalidity 상황을 처리하지 않고 위와 같은 코딩 스타일로 해결하고자 하는 것은 단순히 게으른 것이다.
Case 3
Foo foo4 = getFoo();
if (foo4 == null || foo4.equals(BAR) != true) {
// do A
}
else {
// do B
}
Foo foo5 = getFoo();
if (BAR.equals(foo5)) {
// do B
}
else {
// do A
}
// XXX: possible to throw NPE
foo5.bar();
마지막으로, foo4의 nullity가 invalidity라고 보기 힘든 경우가 있다. value object일 경우가 많을 텐데, 이 경우 null인 경우는 foo4의 상태 중 하나일 뿐인 것이다. 이 경우만이 겨우 위의 코딩 스타일이 약간이나마 빛을 발하는 경우라고 볼 수 있는데, 약간의 코드를 절약할 뿐, 충분히 명시적이지 못한 코드라고 생각한다. 만약 단순히 equality test만 있는 것이 아니라 메서드 호출도 필요한 상황이라면 실수할 여지가 있다.
이러한 경우라면 Null Object 패턴을 활용해 보라고 조언하고 싶다.
Foo foo6 = getValidFoo();
assert foo6 != null;
if (foo6.equals(BAR)) {
// do B
}
else {
// do A
}
foo6.bar();
즉, null 값으로 invalidity를 표현하지 말고 이를 표현하기 위한 객체를 만드는데, 이렇게 되면, null 값이 다시 invalidity를 의미하게 되므로, Case 1 또는 Case 2와 같은 방식으로 처리하면 된다. 그리고, 훨씬 Java 언어에 자연스러운 스타일이 될 것이다.
Closing
프로그래밍에 있어서 nullity 문제와 같이 사소한 코딩 상의 버그를 발생시키지 않는 비결은 의도를 발생할 수 있는 모든 경우를 명시적으로 처리하는 것이다. 코너 케이스들을 대충 해 놓는 경우들이 자주 보이는데 , 결국은 자신 또는 누군가가 그에 대한 비용을 치르게 될 것이란 점을 기억하라고 조언하고 싶다.