티스토리 뷰
※ 본 글은 원문 Return Early Pattern을 번역 및 요약한 내용입니다.
Nested if
public String returnStuff(SomeObject argument1, SomeObject argument2) {
if (argument1.isValid()) {
if (argument2.isValid()) {
SomeObject otherVal1 = doSomeStuff(argument1, argument2)
if (otherVal1.isValid()) {
SomeObject otherVal2 = doAnotherStuff(otherVal1)
if (otherVal2.isValid()) {
return "Stuff";
} else {
throw new Exception();
}
} else {
throw new Exception();
}
} else {
throw new Exception();
}
} else {
throw new Exception();
}
}
- 중첩된 조건문은 가독성을 저하함
- 아래 두 개의 안티 패턴을 포함함
- Else Considered Smelly
if (condition()) {
ifBlock();
}
else { /* 조건에 주어진 이름이 없다 */
elseBlock();
}
if
if
if
if
do something
endif
endif
endif
endif
Return Early
- 기대하는 결과 값은 메소드 마지막에 return
- 조건을 충족하지 못하는 경우 중간에 return 또는 exception 발생시킴
public String returnStuff(SomeObject argument1, SomeObject argument2){
if (!argument1.isValid()) {
throw new Exception();
}
if (!argument2.isValid()) {
throw new Exception();
}
SomeObject otherVal1 = doSomeStuff(argument1, argument2);
if (!otherVal1.isValid()) {
throw new Exception();
}
SomeObject otherVal2 = doAnotherStuff(otherVal1);
if (!otherVal2.isValid()) {
throw new Exception();
}
return "Stuff";
}
- 한 레벨의 들여쓰기만 있어 가독성 좋음
- 기대하는 result가 무엇인지 금방 알 수 있음
- fail-fast 방식은 TDD와 유사해서 테스트하기에도 용이
- 에러 발생 시 바로 함수가 종료되기 때문에 의도하지 않은 코드가 실행되는 것을 방지할 수 있음
Design Patterns
Fail Fast
"return early" 규칙의 기본. 코드 실행이 종료될 수 있는 조건을 찾는데 중점을 두기 때문에 코드가 안전해진다.
Guard Clause
사전 조건 체크를 통해 발생 가능한 에러를 미리 인식하고 return 또는 exception 발생 시키는 것을 의미한다. 코드가 선형적으로 읽히고 happy path를 보장한다.
Bouncer Pattern
특정 조건을 검증하여 return 또는 exception 발생 시키는 메소드를 따로 두는 것을 의미한다. 검증 코드가 복잡할 때 유용하다. "return early" 패턴을 보완하는 패턴이다.
private void validateArgument1(SomeObject argument1){
if(!argument1.isValid()) {
throw new Exception();
}
if(!argument2.isValid()) {
throw new Exception();
}
}
public void doStuff(String argument1) {
validateArgument1(argument1);
// do more stuff
}
쟁점
위와 같은 장점에도 불구하고 return early가 갖고 있는 몇 가지 쟁점들이 있다.
"함수는 한 개의 exit point만을 가져야 한다"
이 코딩 규칙은 Dijkstra’s structured programming로 거슬러 올라간다. Single Entry, Single Exit (SESE) 개념은 C나 어셈블리어처럼 명시적으로 자원을 관리하는 언어에서 비롯되었는데, 이것은 자동으로 자원을 관리하는 (일반적으로 최근에 만들어진) 언어에서는 큰 의미가 없다. SESE는 오히려 코드를 복잡하게 만들기도 한다.
리소스 해제 이슈
Java나 C같은 high-level 언어에는 가비지 콜렉션이 있지만 일부 자원은 여전히 수동으로 관리가 되어야 할 수 있다. 하지만 감사하게도, 다음과 같은 컨셉이 추가되었다.
- "Try, catch, and finally" 구문은 자원을 활성화 하면서 발생할 수 있는 exception을 처리하고 memory leak이 발생하지 않도록 자원을 반환한다.
- "using" 구문은 block 안 쪽에서만 자원을 사용 가능하도록 하고 그것을 모두 다 사용한 이후에는 (중간에 함수가 종료되더라도) 자동으로 반환한다.
이 컨셉들은 "return early" 규칙이 함수가 종료되었을 때 자원을 해제할 수 있도록 해준다.
로깅 및 디버깅 이슈
쟁점 중의 하나는 "return" 문이 하나여야 (하나의 breakpoint 또는 하나의 log 문만 있으면 된다는 관점에서) 디버깅과 로깅이 쉽다는 것이다. 하지만 이것은 사실이 아니다.
"return early"는 에러 발생의 원인을 명확하게 하기 때문에 디버깅을 더 쉽게 만든다. 로깅 또한 메소드 종료 시점마다 로그를 남기는 것이 개발자에게 더 많은 정보를 주며, 만약 많은 "return" 문에서 모든 로그가 필요하다면 각 함수에서 값을 가져온 후에 로깅을 하면 된다.
"많은 exit point는 가독성을 저하한다"
200 줄 코드에 많은 "return" 문이 파편화되어 나타난다면 이건 좋은 프로그래밍 스타일이 아니고 가독성도 떨어질 것이다. 하지만 이러한 함수에서 "return" 문을 제거한다고 해서 그 코드가 이해하기 쉬워지는 것은 아니다. Bouncer Pattern과 Extract Method Pattern은 함수의 사이즈를 적절히 유지해줄 수 있다.
"코드 스타일은 주관적인 것이다"
디자인 패턴은 소프트웨어 설계에서 반복적으로 발생하는 문제에 대한 보편적이고 솔루션이다. 이것들은 개발자의 작업을 더 용이하게 하는 규악이므로 적절히 사용되어야 하지만, 프로그래밍에는 주관적인 측면도 있다.
public String returnStuff(SomeObject argument) {
if(!argument.isValid()) {
return;
}
return "Stuff";
}
public String doStuff(SomeObject argument) {
if(argument.isValid()) {
return "Stuff";
}
}
- 첫 번째 접근 방식은 두 번째 방식에 비해 코드의 양이 많고 복잡하다. 이것은 향후 함수의 변경 가능성을 염두하여 "return early" 방식이 적용되었지만, KISS(Keep It Simple Stupid) 및 YANGI(You Aren’t Gonna Need It) 규칙에는 어긋난다. 나중에 필요할 때 "return early" 패턴을 적용하는 것은 쉽다.
- 두 번째 방식은 더 간단하고 읽기 쉽다. 개인적으로는 이 방식을 선택할 것이다. 그러나 어떤 사람이 첫 번째 방식을 사용한다고 하더라도 그것은 납득이 되는 선택이다. "정답"은 없다.
결론
"return early" 패턴은 함수가 혼란스러워지는 것을 예방하는 훌륭한 방법이다. 그러나 이 방법이 모든 상황에 적용될 수 있다는 것을 의미하는 것은 아니다. 경우에 따라 복잡한 비즈니스 로직으로 인해 중첩된 조건문이 불가피할 수도 있다.
중요한 것은 팀원 간에 서로 협력하고 지식을 공유하고 적절한 패턴을 결정하고 동일한 마인드셋을 갖는 것이다. 모두에게 도움이 되기 위해서 코드를 쓰는 것보다 읽는 것에 더 많은 시간을 할애해야 한다.
'공부' 카테고리의 다른 글
Transition from Java 8 to Java 11 (자바 8 vs 11 비교) (0) | 2021.08.20 |
---|
- Total
- Today
- Yesterday