이 주제에 대해서 "엘레강트 오브젝트-조영호 번역" 책에서 다루고 있습니다. 이론상으로만, 이해하고 있었습니다. 그러나, 최근 사내 서비스를 트러블 슈팅하며, 다른 분의 코드를 리뷰하며 이 주제에 대해서 말할 수 있었던 기회가 있었습니다. 그러나, 역시 이론과 코드에는 거리감이 있었고, 그 상황에서 저또한 명확하게 설명하기 힘들었습니다. 저 스스로는 빠르게 실패하기 를 실천하기 위해서 코드 상에서 노력하고 있는데, 이것을 누군가에게 설명하려니 잘 안되더군요. 그래서 팀원분에게 이 부분을 설명해주면서 헀던 이야기를 공유합니다.
먼저 빠르게 실패하기와 안전하게 실패하기에 대해서 이론으로 살펴보고 난 뒤에, 코드로서 살펴보겠습니다.
이 두 가지는 적절한 상황에 잘 활용하는 것이 중요하지만-, 만약 코딩 중에 둘 중 어느것을 선택해야 한다면, 저 스스로 판단하기로는 빠르게 실패하는 것에 조금더 지지하는 입장입니다.
안전하게 실패하기(fail safe)
먼저 '안전하게 실패하기' 부터 살펴봅시다. 예외 처리라는 것이 만들어진 이유가 즉, 안전하게 실패하기에 적절한 예시일 수 있습니다.
try {
...
} catch (Exception e){
...
}
버그, 입출력 문제, 메모리 오버플로우 등이 발생한 상황에서도 소프트웨어가 계속 실행될 수 있도록 하는 것이 안전하게 실패하기
빠르게 실패하기(fail fast)
빠르게 실패하는 것은 위 방법의 반대입니다. 문제가 발생하면 곧바로 실행을 중단하고 최대한 빨리 예외를 던집니다.
assert False;
계약에 의한 설계와 연관이 있습니다. 프로덕트 환경에서 소프트웨어가 만약 실패하게 된다면- 모든 실패 지점을 명확하고 문서화되어 있다면 추후 유지보수 관점에서 테스트를 쉽게 추가할 수 있을 것입니다.
코드로서 이해하기
이론적으로 쉽게 '안전하게 실패하기'와, '빠르게 실패하기' 에 대해서 이해할 수 있었습니다. 그럼 이제 코드로서 살펴봅시다.
private void setMasterInform(Community community) {
User master = community.getMasterUser();
if (Objects.nonNull(master)) {
masterUser = master.getName();
masterPosition = master.getPositionName();
}
}
처음 이슈가 제기되었던 코드는 위 코드였습니다. 만약 master 가 Null 이라면, setMasterInform 은 어떠한 일도 일어나지 않을 것입니다. 이와 비슷한 형태가 try catch 일텐데, 이또한 안전하게 실패하기 의 한 사례라고 볼 수 있었습니다.
이 코드에 대해서, "Null 이면 예외를 던지는게 맞다. 그러나, NullPointerException 이 아니라, 우리가 알아볼 수 있는 예외로 변환시켜 던지는 것이 좋다" 라고 코멘트를 남겼습니다.
그러나, 발생했던 이슈는 'Null 일 때 문제가 발생하는거고 히스토리 추적이 불가해서, null 을 무시하게 해야 한다.' 마지막 해결카드이기 때문에, 저는 그러면 try catch 를 사용하여 로그를 남기는 것이 좋지 않을까요? 라고 말씀드렸습니다.
이렇게 말하면서 위 코드를 작성했던 분에게 마침 제가 해결했던 이슈 PR에 대해서 설명하면서 빠르게 실패하기와 안전하게 실패하기에 대한 설명을 드렸습니다. 아래는 마침 해결했던 이슈 PR에 대한 코드입니다.
@Transactional
@Override
public AppletBaseModel saveBaseConfig(Long appletId, AppletBaseModel model) throws IOException, URISyntaxException {
Applet applet = appletService.getApplet(appletId);
authorityHelper.checkAppletAdmin(applet, getActor());
applet = entityConverter.apply(applet, model);
////// before //////
Long companyId = applet.getCompany().getId();
if (model.getIconUrl() != null && !model.getIconUrl().isEmpty()) {
applet.setIcon(attachFileHelper.buildAttachFile(makeTempIcon(model.getIconUrl()), companyId,
SiteAttachableApps.WORKS));
----
////// after //////
Company company = applet.getCompany();
Long companyId = company.getId();
if (Objects.isNull(companyId)){
throw new NotFoundCompanyException(ExceptionMessageKey.NOT_FOUND_COMPANY);
}
if (StringUtil.isNotEmpty(model.getIconUrl())) {
applet.setIcon(
attachFileHelper.buildAttachFile(
makeTempIcon(model.getIconUrl()), companyId, SiteAttachableApps.WORKS)
);
}
----
return appletModelConverter.toAppletBaseModel(applet);
}
변경 전 후에 추가된 것은 Long companyId = applet.getCompany().getId(); 이 부분에 대한 예외를 발생시키는 부분을 추가시켰습니다. 레퍼런스 타입의 Long 받는 것은 즉, Null 을 허용될 수 있다는 잠재적인 요소가 존재하고, NullPointerExecption 은 프리미티브 타입의 long 이 존재하는 attachFileHelper.buildAttachFile() 에서 발생하게 됩니다.
비록, 위 코드의 원초적인 이슈는 위에서 말한 부분은 아닙니다만, 코드의 신뢰성이 깨진 상태(잠재적인 Null 이 존재)에서 코드를 수정하기란 마치, 코드에 희망을 갈구하는 기분입니다.
Company company = applet.getCompany();
Long companyId = company.getId();
if (Objects.isNull(companyId)){
throw new NotFoundCompanyException(ExceptionMessageKey.NOT_FOUND_COMPANY);
}
이슈가 발생할 것 같다면, 빠르게 실패하는 것의 좋은 예시가 아닐까? 싶어, 공유드렸고, 다행히 수긍해주셔서 다시한번 빠르게 실패하기와 안전하게 실패하기에 대해서 코드로서 상기시킬수 있는 기회가 되었습니다.
[더 알아보기]
엘레강트-오브젝트 책 내용 - github.com/LenKIM/Book/blob/master/Elegant-object/_05_retire.md
'가치관 쌓기 > 개발 돌아보기' 카테고리의 다른 글
[우아한테크코스Pro]로또 구현(테스트 주도 개발)[1/9] (1) | 2021.05.28 |
---|---|
우아한테크코스Pro 프리코스 후기 (1) | 2021.05.17 |
동시성에 대해서 생각해보자. (0) | 2021.02.09 |
JPA 연관 관계를 명시(사용)하는 이유는 무엇일까? (0) | 2021.01.16 |
테스트 주도 설계를 실천한다는 것 (0) | 2020.12.18 |
댓글