Gidhub BE Developer

좋은 코드 vs 나쁜 코드 (Good Code vs. Bad Code)

2024-02-27
goodGid

The Importance of Good Names

Example 1

boolean processPayment(PaymentInstruction paymentInstruction) {
    Channel preferredChannel = paymentInstruction.getChannel();     
    channelFactory.getChannel(preferredChannel).send(paymentInstruction);
}
  • processPayment에서 process는 너무 Generic 한 표현이므로

    단순히 메서드 명만 봐서 이 메서드가 하는 게 정확히 무엇인지 알 수 없다.

  • 만약 알고 싶다면 메서드 내부 코드를 읽은 후에 파악을 할 수 있고

    코드를 보면 선호하는 Channel을 선정하고

    해당 Channel로 PaymentInstruction을 보내고 있음을 알 수 있다.

  • 그러면 메서드 명을 “sendPaymentToExternalChannel()”로 지정할 수 있지만 너무 많은 세부정보가 노출된다.

  • 대안으로 “startPayment()”는 내부 세부 정보를 노출하지 않고

    결제 흐름의 시작을 나타내므로 좋은 선택이 될 수 있다.


Example 2

String userId = PaymentInstruction.getUserId();
  • “userId”라는 변수명을 사용하기보단

    도메인에서 사용되는 개념으로 바꾸면 더 좋다.

String payerId = paymentInstruction.getPayerId();

Summary

  • 변수의 이름을 효과적으로 짓기 위해선 다음과 같은 조건을 충족해야 한다.
  1. 목적을 간결하고 정확하게 설명

  2. 비즈니스 도메인 용어 사용

  3. 구현 세부정보 노출 방지


The Perils of Copying Code

  • 새로운 요구 사항을 개발 시

    기존에 비슷한 기능의 코드가 있지만

    개발 시간이 부족하다면

    기존 코드 수정 후 테스트를 하기보다는

    코드 일부를 복사하여 새로운 요구 사항에 맞게 수정을 하고 싶은 유혹이 생긴다.

  • 예를 들어 UserService에 이미 비슷한 기능이 존재하지만

    수정 후 기존 기능에 영향을 주지 않는지 테스트를 하기에 시간이 부족하여

    필요한 코드를 복사하여 새로운 서비스를 만들고

    새로운 요구사항에 맞게 수정을 하였다.

  • 만약 사용자 관련 기능을 업데이트해야 한다면

    관리 포인트가 2곳으로 늘어나

    관리도 힘들고 혹시라도 한 곳을 놓치면 장애가 발생할 수 있어

    동일한 기능을 중복으로 구현하는 건 좋지 않다.


This violates the key "Don't Repeat Yourself" (DRY) principle of software development


  • static analyzers는 코드 중복 문제를 식별하는데 도움이 되지만

    기술 부채가 발생하지 않게 처음부터 잘 추상화된 구성 요소를 설계하는 것이 더욱 중요하다.

  • Deadline이 다가올수록 촉박한 상황에서

    서투른 지름길을 택하는 건 시간이 지남에 따라 기술 부채를 더 키우게 될 것이다.


Too Many Parameters

PaymentInstruction createPaymentInstruction(String payerId,
                                            String receiverId,
                                            String orderId,
                                            Channel preferredChannel, 
                                            String callbackUrl,
                                            Currency currency,
                                            Amount amount) {
   // create payment instruction
}
  • 매개변수가 너무 많으면 당연히 다루기 어려워진다.

    이럴 땐 단일 객체로 캡슐화 하여 관리를 손쉽게 할 수 있다.

public class PaymentInstructionParameters {
    private String payerId;
    private String receiverId;
    private String orderId;
    private Channel preferredChannel;
    private String callbackUrl;
    private Currency currency;
    private Amount amount; 
}
PaymentInstruction createPaymentInstruction(
    final PaymentInstructionParameters params) {
    // create payment instruction
} 
  • 객체로 캡슐화를 한 방법이 위 방법보다 좋지만

    String 타입의 여러 ID 값을 세팅하는데 실수가 발생할 수 있다.

    이럴 땐 Builder 패턴을 사용하면 좋다.

public class PaymentInstructionParameters {
  private String payerId;
  private String receiverId;
  private String orderId;
  private Channel preferredChannel;
  private String callbackUrl;
  private Currency currency;
  private Amount amount; 

  public PaymentInstruction newPaymentInstruction() {
    return PaymentInstruction.builder()
                             .payerId(payerId)
                             .receiverId(receiverId)
                             .orderId(orderId)
                             .preferredChannel(preferredChannel)
                             .callbackUrl(callbackUrl)
                             .currency(currency)
                             .amount(amount);
  }
}
  • 빌더 클래스를 사용하여 변수를 명시적으로 설정한다.

  • 복잡한 매개변수를 잘 정의된 객체로 캡슐화하면 실수 확률도 줄이고 코드를 읽기 편하게 구현할 수 있게 된다.


Avoiding Nested Conditional Logic

if (condition)  {
   //Nested if else inside the body of “if”
   if (condition2)  {
       //Statements inside the body of nested “if”
   }
   else {
      //Statement inside the body of nested “else”
   } 
} 
else  {
   //Statement inside the body of “else” 
}
  • 중첩된 if/else 문은 코드를 어렵게 만든다.

  • 적절한 해결책으로는

    조건이 실패하면 바로 return을 시키거나

    조건을 확인할 수 있는 함수 또는 메소드로 캡슐화한다.

    예를 들어 Google Guava는 전제 조건 유틸리티 클래스를 제공한다.

public static double sqrt(double value) {
     Preconditions.checkArgument(value >= 0.0, "negative value: %s", value);
     // calculate the square root
}

The Benefits of Immutable Data

  • 최신 IDE를 사용하면

    기본적으로 클래스에 대한 setter가 생성되어

    손쉽게 데이터 수정이 가능하여 버그 발생 가능성이 높아진다.

  • 이러한 setter를 없애고 Deep Copy를 통해 Immutable Data를 생성해보자.


Setter 제거

  • 값을 직접 수정하는 대신 API를 통해 상태 업데이트를 한다.

    ex) complete API를 호출하여 상태 변경 + 새로운 객체 할당

class PaymentInstruction {
    PaymentInstruction complete(CallBackResult result) {
        return new PaymentInstruction(..., result.getCode(), ...);
    }
}

Immutable Data 구조 장점

  • 여러 프로세스가 병렬로 접근하더라도

    충돌 위험 없이 데이터에 액세스 하고 조작할 수 있게 한다.

    즉 쉬운 유지 보수 + 병렬 처리 가능이라는 장점을 누릴 수 있다.


Managing Technical Debt

  • Bad Code는 시간이 지나면서 기술 부채로 쌓이게 된다.

    그러므로 기술 부채를 효과적으로 관리하는 것은 필수적이다.

    완벽하게 기술 부채를 제거할 순 없으니 적절하게 관리하는 것이 중요하다.

Quantify debt

  • SonarQube와 같은 정적 도구를 사용하여 기술 부채를 즉각적으로 확인한다.

    즉 부채를 정량화시켜 확인할 수 있다.

Allocate resources to pay down debt

  • 일정 주기마다 refactoring 작업을 진행하거나 서로 관리하는 시간을 갖도록 한다.

    이렇게 하면 부채가 너무 오래 머무르는 것을 방지할 수 있다.

Convey the urgency to management

  • 긴급한 상황에 놓이기 전에 미리 매니저에게 상황을 공유한다.

  • 기술 부채가 쌓이면 위험하고

    장기적으로 좋지 않다는 점을 지속적으로 공유하는 것이 중요하다.


Summary

  • 좋은 코드를 추구하지 않으면

    깨진 창문 이론처럼 점점 코드가 더러워지고

    돌아갈 수 없는 강을 건너게 된다.

  • 기술 부채는 지속적으로 관리하면서

    좋은 코드를 짤 수 있게 서로 노력하는 게 무엇보다 중요하단 생각이 든다.


Reference


Index