코드 냄새 Katas — 우아한 예외 처리

컴퓨터 프로그래밍에서 코드 냄새는 더 깊은 문제를 나타낼 수 있는 프로그램 소스 코드의 특성입니다. 코드 냄새인지 여부를 결정하는 것은 주관적이며 언어, 개발자 및 개발 방법론에 따라 다릅니다. - 위키백과

코드 냄새는 종종 잘못된 디자인 또는 구현 선택의 증상입니다.



이 블로그 게시물에서 Code Smell kata를 살펴보겠습니다.

문서가 있는 경우 문서를 업데이트하거나 문서가 없는 경우 새 문서를 생성하기 위해 서비스에 HTTP 호출을 해야 하는 비즈니스 시나리오를 고려하십시오.

아래 클래스에는 하나의 공개 메서드createOrupdateDocument만 있고 다른 모든 메서드는 비공개이며 다른 클래스에 대한 가시성이 없습니다.

public class CodeSmellHttpClient {
  public static final Logger LOG = LoggerFactory.getLogger(CodeSmellHttpClient.class);
  private final String baseUrl = "https://some.service.com/documents/";

  private HttpClient httpClient = HttpClients.createDefault();

  private String bearerToken = "some bearer token";

  public void createOrUpdateDocument(String requestBody, String documentId) {
    if (this.isDocumentExistingWithId(documentId)) {
      this.updateDocument(requestBody, documentId);
    } else {
      this.createDocument(requestBody);
    }
  }

  private void createDocument(String requestBody) {
    try {
      HttpPost httpPostRequest = new HttpPost(baseUrl);
      StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

      httpPostRequest.setEntity(requestEntity);
      httpPostRequest.addHeader(getAuthorizationHeader());

      HttpResponse response = httpClient.execute(httpPostRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private void updateDocument(String requestBody, String documentId) {
    final String updateDocumentUrl = baseUrl.concat(documentId);

    HttpPatch httpPatchRequest = new HttpPatch(updateDocumentUrl);
    StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

    httpPatchRequest.setEntity(requestEntity);
    httpPatchRequest.setHeader(getAuthorizationHeader());
    try {
      HttpResponse response = httpClient.execute(httpPatchRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private boolean isDocumentExistingWithId(String documentId) {
    final String getTemplateUrl = baseUrl.concat(documentId);

    HttpGet httpGetRequest = new HttpGet(getTemplateUrl);
    httpGetRequest.setHeader(getAuthorizationHeader());
    HttpResponse response = null;

    try {
      response = httpClient.execute(httpGetRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }

    if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
      LOG.info(documentId + "already exists");
      return true;
    }

    LOG.info(documentId + "does not exists");
    return false;

  }

  private Header getAuthorizationHeader() {
    return new BasicHeader(HttpHeaders.AUTHORIZATION,
        "Bearer " + bearerToken);
  }

}


위 코드 : https://gist.github.com/akhil-ghatiki/98339ea96a6a39876aa3745f9cf185cb#file-codesmell1-java

지금은 코드 냄새에서 약간 벗어나 위의 클래스를 살펴보겠습니다. 이 클래스가 HttpClient 생성에 주의를 기울여야 하는 이유는 무엇입니까? 단위 테스트를 위해 이 클래스를 어떻게 테스트할 수 있습니까? 단위 테스트를 위해 무엇을 테스트할 수 있습니까?

HttpClient 생성을 다른 클래스로 이동하고 아래처럼 생성자 매개변수로 이 클래스에 전달하거나 Spring 프레임워크를 사용하는 경우 autowired bean으로 전달하는 것을 고려하십시오.

private HttpClient httpClient;

public CodeSmellHttpClient(HttpClient httpClient, String bearerToken) {
  this.httpClient = httpClient;
  this.bearerToken = bearerToken;
}


이렇게 하면 get, post 및 patch 요청이 개인 메서드를 통해 만들어질 때 HttpClient 생성을 조롱하고 각각 httpClient.execute(httpGetRequest) , httpClient.execute(httpPostRequest) , httpClient.execute(httpPatchRequest) 호출을 확인하는 데 도움이 됩니다.

ProTip: If one is following test driven development, one will not end up into most of the code smells like in above implementation. Test driven development forces you to write cleaner code in many ways. All hail TDD !



이제 위의 코드에서 isDocumentExistingWithId(String documentId) 메서드를 살펴보고 잠시 시간을 내어 그 안에 있는 코드 냄새를 나열해 보세요.

예외가 있으면 어떻게 됩니까?

행복한 시나리오에서는 모든 것이 원활하게 작동합니다. 그러나 예외가 발생한 경우 메서드는 예외를 포착하여 기록하고 catch 이후의 if 문은 이에 대한 응답으로 상태 코드를 확인합니다. 하나는 예외가 발생할 때 콘솔에서 NullPointerException의 거대한 빨간색 로그 라인을 보는 것과 그리 멀지 않은 형태입니다.

자, 이제 이 메서드에 예외가 있는 예외 시나리오를 해결하기 위해 일부 변경을 수행하겠습니다. 방법을 아래와 같이 변경하면 어떨까요? — if 조건을 try 블록으로 이동합니다.

private boolean isDocumentExistingWithId(String documentId) {  final String getTemplateUrl = baseUrl.concat(documentId);

  HttpGet httpGetRequest = new HttpGet(getTemplateUrl);
  httpGetRequest.setHeader(getAuthorizationHeader());
  HttpResponse response = null;

  try {
    response = httpClient.execute(httpGetRequest);
    LOG.info(response.toString());    if(response.getStatusLine().getStatusCode()==HttpStatus.SC_OK){
      LOG.info(documentId + "already exists");
      return true;
    }
  } catch (IOException e) {
    LOG.error(e.getMessage());
    e.printStackTrace();
  }
  LOG.info(documentId + "does not exists");
  return false;
}


이것은 우리가 얼마 전에 논의했던 문제를 완화시키는 것 같습니다. 하지만 잠깐, 이 메서드의 호출자 메서드를 살펴봤나요? 코드 예제에서 createOrUpdateDocument(String requestBody, String documentId)를 살펴보십시오. (페이지를 너무 많이 스크롤하게 해서 죄송합니다 :p)
createDocument(newRequest)isDocumentExistingWithId(String documentId)에서 예외가 포착될 때 false를 반환하고 이것이 createDocument 메서드를 호출하기 때문에 실행됩니다. 우리는 그런 일이 일어나기를 원하지 않습니다.

지금 잠시 시간을 내어 이 문제를 어떻게 처리할 수 있을지 생각해 보십시오! 생각하기 전에 대답하기 위해 아래로 스크롤하지 마십시오.

.

.

.

.

.

.

.

.
예외를 던지는 것이 뇌의 회랑에 울려 퍼지는 것이라면 네, 맞습니다!

예외를 처리하는 대신 isDocumentExistingWithId(String documentId)에서 예외를 발생시킵니다. 호출자 메서드가 해당 예외에 대해 수행할 작업을 결정하도록 합니다. 이것이 제가 예외를 정상적으로 처리하는 것입니다. 구현에서 예외를 일급 시민처럼 취급하십시오. 그들을 무시하지 마십시오. 함수형 프로그래밍은 예외를 일급 시민으로 취급하는 놀라운 일을 합니다. 함수형 프로그래밍에 대해서는 이 블로그 게시물의 범위를 벗어나므로 더 이상 논의하지 않습니다.

ProTip: Treat exceptions like first class citizens in your implementations.



아래는 논의된 대로 예외를 던지는 구현입니다.

public class CodeSmellHttpClient {

  public static final Logger LOG = LoggerFactory.getLogger(CodeSmellHttpClient.class);
  private final String baseUrl = "https://some.service.com/documents/";

  private HttpClient httpClient;

  private String bearerToken = "some bearer token";

  public CodeSmellHttpClient(HttpClient httpClient, String bearerToken) {
    this.httpClient = httpClient;
    this.bearerToken = bearerToken;
  }

  public void createOrUpdateDocument(String requestBody, String documentId) {
    boolean isDocumentExisting;
    try {
      isDocumentExisting = isDocumentExistingWithId(documentId);
    } catch (IOException e) {
      LOG.error("Unable to get template details", e);
      throw new RuntimeException("Unable to get template details", e);
    }
    if (isDocumentExisting) {
      this.updateDocument(requestBody, documentId);
    } else {
      this.createDocument(requestBody);
    }
  }

  private void createDocument(String requestBody) {

    try {

      HttpPost httpPostRequest = new HttpPost(baseUrl);
      StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

      httpPostRequest.setEntity(requestEntity);
      httpPostRequest.addHeader(getAuthorizationHeader());

      HttpResponse response = httpClient.execute(httpPostRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private void updateDocument(String requestBody, String documentId) {
    final String updateDocumentUrl = baseUrl.concat(documentId);

    HttpPatch httpPatchRequest = new HttpPatch(updateDocumentUrl);
    StringEntity requestEntity = new StringEntity(requestBody, ContentType.APPLICATION_JSON);

    httpPatchRequest.setEntity(requestEntity);
    httpPatchRequest.setHeader(getAuthorizationHeader());
    try {
      HttpResponse response = httpClient.execute(httpPatchRequest);
      LOG.info(response.toString());
    } catch (IOException e) {
      LOG.error(e.getMessage());
      e.printStackTrace();
    }
  }

  private boolean isDocumentExistingWithId(String documentId) throws IOException {
    final String getTemplateUrl = baseUrl.concat(documentId);

    HttpGet httpGetRequest = new HttpGet(getTemplateUrl);
    httpGetRequest.setHeader(getAuthorizationHeader());

    HttpResponse response = httpClient.execute(httpGetRequest);
    LOG.info(response.toString());

    if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
      LOG.info(documentId + "already exists");
      return true;
    }
    LOG.info(documentId + "does not exists");
    return false;
  }

  private Header getAuthorizationHeader() {
    return new BasicHeader(HttpHeaders.AUTHORIZATION,
        "Bearer " + bearerToken);
  }

}


위 코드 : https://gist.github.com/akhil-ghatiki/37594c5852e7293a2d97b2e0028aa6b3#file-codesmell2-java

예외를 잘 다루면 그들은 당신을 더 잘 대할 것입니다.

의도적으로 몇 가지 코드 냄새를 해결하지 않은 상태로 두었습니다. 알아낸 것이 있으면 댓글에 남겨주세요.

이 문서는 원래 게시된 위치: https://akhil-ghatiki.github.io/#/code-smell-katas-graceful-exception-handling

좋은 웹페이지 즐겨찾기