Today I../Today I Learned

[Coding Style] Return Early Pattern

HJChung 2021. 6. 19. 23:50
본 글은 Leonel Menaia의 “Return Early Pattern” 글을 번역한 것입니다.
Return Early Pattern

 

프로그래밍에 대해 배우고 함수를 만들 때의 기본적인 사고방식은 '함수를 만들 때, 어떤 결과를 도출하기까지 어떤 조건에 맞는지 검사'해나가는 것이었다. 

public String returnStuff(SomeObject argument, SomeObject argument2){
	if(argument1.isValide()){
        if(argument2.isValide()){
        	SomeObject otherVal1 = doSomeStuff(argument1, argument2)
            
            if(otherVal1.isValid()){
            	someObject otherVal2 = doAnothreStuff(otherVal1)
                
                if(otherVal2.isValide()){
                	return "Stuff";
                 }else{
                 	throw new Exception();
                 }
            }else{
            	throw new Exception();
            }
        }else{
        	throw new Exception();
        }
    }else{
    	throw new Exceptioon();
    }
}

위의 접근에서 어떤 사항을 알 수 있는가?

  • 중첩된 조건문으로 인한 코드의 nonlinear한 흐름 때문에 따라가기가 힘들다.(가독성이 좋지 않다. 부드럽게 읽히지 않는다.)
  • if조건문의 block이 길 경우 각 if에 대한 else를 파악하기가 힘들고, 읽기가 힘들기 때문에 에러 헨들링도 쉽지 않다. 
  • 예상되는 결과를 찾기 위해서는 중첩된 if를 따라가면서 코드의 흐름을 따라가야만 한다. 
  • 위의 예에서 exception이 else 구문에서 발생한다. 만약 else가 실행문을 종료시키지 않는다면 나머지 코드를 실행시킬 것이고, 이로인해 불필요한 에러가 발생할 수 있다.

그 외에도 anti-patterns도 있다. 

  • 'Else Considered Smelly()': 조건문이 복잡하면 else인 경우를 찾는 것이 두 배로 힘들다. 개발자는 조건을 뒤집어서 생각해야하는데 if문이 너무 길면 해당 조건을 기억하기는 쉽지 않기 때문이다. 즉, if와 else는 개발자를 혼란스럽게 한다. 
  • 'Arrow Anti Pattern': 조건문과 반복문의 중첩으로 인해 코드 모양이 화살처럼 된다. 

Return Early

위와 다른 사고방식으로 코드를 리펙토링 해보자 

Return early 는 함수나 메소드를 작성하는 방식으로, 예상되는 긍정 결과가 함수의 끝에서 리턴되게 하고 조건이 맞지 않는 경우 나머지 코드는 (예외를 return 하거나 throw해서) 실행을 종료한다.

이러한 방식은 if 조건문으로 에러 처리나 적절한 예외를 반환하거나 throwing하여 함수의 실행문을 끝내는 방식으로 수행된다. 

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";
}

위의 접근에서 알 수 있는 몇 가지 사항

  • 위의 코드에는 1번의 indentation만 있다. 그래서 linearly하게 읽을 수 있다. 
  • 예상되는 긍정 결과는 함수의 끝에서 빨리 찾을 수 있다. 
  • 이런 사고 흐름으로 구현하면 에러를 먼저 잡고 그 다음 불필요한 버그를 피하면서 안전하게 비지니스 로직을 구현할 수 있게 된다. 
  • fail-fast 사고방식은 Test-Driven Development 과 유사해서 테스트에도 용이하다. 
  • 함수가 에러 발생시 즉시 종료되기 때문에 의도되지 않은 코드가 실행될 가능성을 피할 수 있다. 

 

디자인 패턴

Fail Fast

Jim Shore과 Martin Fowler는 2004년에 Fail Fast라는 개념을 만들었다. 이 개념은 return early 규칙의 기본이 된다. failing fast할 때 코드는 더 강력해지는데, 초기에 코드의 실행이 종료될 수 있는 조건을 먼저 찾는데 중점을 두기 때문이다. 

 

Guard Clause

guard clause는 return문이나 예외문 등으로 함수를 즉시 종료하도록 체크하는 것이다.

(guard clause 란?

사전 조건이 거짓이라면 ( 다음 단계로 진행하기 위한 올바른 실행 조건이 아니라면) 예외처리를 하여 더이상 다음 단계가 실행되지 않도록 하는 것이다.) 

 

함수의 happy path는 에러를 발생시키지 않는 유효성 검사 규칙으로 인해 함수의 실행이 끝까지 문제 없이 성공적으로 계속되어서 긍정적인 응답을 내는 것이다.

Return early 접근방식을 사용하면 코드가 linnearlly하게 읽히고, happy path하게 작동한다. 

 

Bouncer Pattern

bouncer pattern은 예외를 반환, 발생시켜 특정 조건을 확인하는 방법이다. 이는 특히 유효성 검사 코드가 복잡할 때나 muultiple scenarios에 사용하기 유용하다. 

이 패턴은 return early 패턴을 보완해준다. 

private void validateArgument1(SomeObject argument1) {
	if(!argument1.isValid()){
    	throw new Exception();
    }
    
    if(!argument2.isValid()){
    	throw new Exception();
    }
}

public void doStuff(String argument1){
	validateArgumentt1(argument1);
    
    // do more stuff
 }

 

단점

return early 방식은 긍정적인 측면도 있지만 몇 가지 비판적인 부분도 있다. 

 

함수에는 exit point가 1개만  있어야 한다. 

이런 코딩 규칙은 이 코딩 규칙은 Dijkstra’s structured programming으로 거슬러 올라간다 . 이러한 Single Entry, Single Exit (SESE) 개념은 C 및 Assembly와 같은 명시적인 리소스 관리를 사용하는 언어에서 비롯된다.

 

리소스가 manually하게 관리되지 않거나 관리ㅣ되어서는 안되는 언어일 경우 이러한 규칙을 지키는 것이 거의 중요하지 않다. SESE가 코드를 더 복잡하게 할 때도 있다. 이는 C를 제외하고는 요즘 사용되는 대부분의 언어에 잘 맞지 않고, 코드의 이해를 더 방해할 뿐이다. 

 

Resource Cleaning

Java나 C#과 같 고급언어에는 garbage collection이 있다 그러나 때로는 일부 리소스를 manually하게 관리해야할 필요도 있다. 

다행히도 최신 언어에는 다음과 같은 개념이 있다. 

  • Try, catch, and finally 구문을 사용하면 가능한 예외를 잡고, final block에서 리소스를 해제하여 메모리 낭비가 없도록 확인한다. 
  • using 문을 사용하면 block 내의 리소스 사용을 허용하고, 사용한 후에 자동으로 처리해준다. (조기 종료가 발생하더라도)

이를 통해 return early 규칙을 사용하더라도 함수의 실행이 종료되었을 때 리소스를 해제할 수 있다. 

 

Logging and Debugging

하나의 return이 하나의 breakpoint만 있는 것이기 때문에 디버깅에도 쉽고 마지막 로그만 필요하므로 로깅 하기에도 쉽다는 주장이 있다. 그러나 이게 반드시 사실인 것은 아니다. 

return early 방식을 통해 예외를 올바르게 처리할 수 있다. 디버깅도 코드가 실패하는 이유를 명확히 알면 더 쉬워질 수 있다. 로그는 개발자에게 더 나은 정보를 주기 위해 각 종료 지점 전에 추가되리 수도 있다. 만약 많은 return 문에서 모든 log가 필요하다면 각 함수에서 값을 가져온 후 로그를 하면 된다. 

 

Multiple exit points affect readability

200줄이넘는 함수에 여러 return 문이 무작위로 있다면 읽기 쉽지 않을 것이고 좋은 코딩 스타일도 아닐 것이다. 그러나 이러한 함수는 return 문 들이 없다하더라도 읽고 이해하기 어려울 것이다. 이 경우 bouncer pattern 과 extract method pattern 을 통해 함수의 사이즈를 합리적인 한도 내로 유지해야 한다. 

 

Code style is subjective

design pattern은 소프트웨어 디자인에서 일반적으로 발생하는 문제에 대한 반복적인 해결방법이다. 이는 개발자들이 그들의 작업을 용이ㅣ하게 하는 규칙이며 적절한 경우에 사용해야한다. 아래의 예시를 보자. 

public String returnStuff(SomeObject argument) {
  if(!argument.isValid()) {
    return;
  }
  
  return "Stuff";
}

public String doStuff(SomeObject argument) {
  if(argument.isValid()) {
    return "Stuff";
  }
}
  • 첫 번째 방식에서는 두 번째 방식에 비해 작성된 코드도 많고 복잡하다. 그러나 향후 개선을 위해 return early 사고방식을 적용하여서 이에 대응하였다. 그러나 이러한 사고 방식은 KEEP It Simple Stupid(KISS) 및 You Aren’t Gonna Need It(YAGNI) 규칙에 맞지 않는다. 그냥 나중에 필요할 경우 코드를 "return early"패턴으로 쉽게 변경하면 된다.
  • 두 번째 방식은 훨씬 더 간단하고 읽기 쉽다. 이게 포인트가 되어 내 경우에도 이러한 방식을 선택할 것이다. 

결론

"return early" 패턴은 함수가 혼잡해지는 것을 막는 훌륭한 방법이다. 그러나 이 방식이 매번 적용될 수 있다는 의미는 아니다. 

때때로 복잡한 비즈니스 로직 중에 다른 함수로 코드를 빼었더라도 nested if를 사용하는 것이 불가피할 때도 있다. 

 

더 나은 접근 방식은 각 팀과 협력하고, 지식을 공유하고, 각 경우에 사용할 패턴을 결정하여 모든 사람이 프로그래밍 할 때 비슷한 사고 방식을 갖도록하는 것이다. 개발자는 코드를 작성하는 것보다 읽는 데 더 많은 시간을 쓰므로 모두에게 도움이 될 수 있도록 하는 것이 중요하다.