본문 바로가기
프로그래밍/기타

팻 핑거와 테스트 코드

by 카프카뮈 2021. 5. 17.

최근에 투자에 관심이 생겨서 주식 관련 유튜브를 종종 보고 있다.

그러다 보니 주식 시장에서의 재미있는 에피소드도 알게 되었는데, 

그중 인상깊은 사건을 먼저 하나 소개하고 싶다.

 

[황당 실수로 한맥투자증권 파산까지… 증시 강타한 ‘팻핑거’]

 

황당 실수로 한맥투자증권 파산까지… 증시 강타한 ‘팻핑거’

케이프증권은 올 2월 62억 손실 獨·日서도 주문실수로 주가 출렁삼성증권의 우리사주 배당 사고로 인해 이른바 ‘팻핑거’ 오류가 주목받고 있다. 증시 거래 담당자들이 주문을 넣으면서 실수

www.seoul.co.kr

2013년, 한맥투자증권은 선물 옵션 만기일에 이자율을 계산하고 있었다.

그 과정에서 직원이 타이핑 실수(아마도 칸을 비운 채 입력을 완료한 것이 아닐까?)를 했고,

잔여일을 365가 아닌 0으로 나누면서 무한대의 값이 들어가 엄청난 물량의 매도/매수 주문이 실행됐다.

이로 인해 462억의 손실을 본 한맥투자증권은 결국 파산하고 만다.

 

이런 실수를 팻 핑거라고 한다. 두꺼운 손가락 탓에 자판을 잘못 눌러 생기는 실수라는 뜻이다.

그러면 이를 단순히 직원의 실수 내지 해프닝 정도로 봐야 할까? 지금도 팻 핑거는 반복되고 있다.

[증권사 무너뜨리는 팻 핑거의 저주]

 

증권사 무너뜨리는 '팻 핑거'의 저주

팻 핑거 실수가 대형 사고로 번지는 데는 3가지 법칙이 있다. 이 3가지가 맞물리면 팻 핑거 오류는 여지없이 대형 사고로 번졌다. 김상봉 한성대 경제학과 교수는 "입력 실수 사고는 회사 규모,

news.joins.com

해당 뉴스를 보면, 참 가지가지 한다 싶을 정도이다.

61만엔 1주 구매를 1엔 61만주로 입력해서 400억엔 손실, 60억 달러 착오 입금,

심지어 2018년에 큰 이슈였던 삼성증권의 110조원대 유령 주식 유통까지,

잊을 만하면 계속 발생하는 것이 팻 핑거 사고이다.

 

그렇다면 팻 핑거를 어떻게 막을 수 있을까?

갑작스럽게 이런 고민이 들어서, 만약 내가 개발자였다면 어떻게 했을지 고민해보게 되었다.


0. 운영 환경 가정

먼저, 개발 환경을 임의로 가정해 보겠다.

증권사 내부망을 통해 운영되는 증권 웹 어플리케이션이 있다고 해보자.

그리고 해당 페이지에서 특정 값을 입력한 뒤 실행 내지 전송 버튼을 누르면,

REST API로 서버에 요청이 가고 해당 요청을 서버에서 실행한 뒤 잘 수행되었다는 response를 준다고 해보자.

서버는 내가 사용하는 기술 스택에 맞춰서 SpringBoot + JPA로 운영되고 있다고 가정한다.

이 때 어디에서 잘못된 값을 검증할 수 있을까?


1. 프론트엔드에서 입력값 검증

우리가 로그인을 수행할 때, 이메일 칸에 형식에 맞지 않는 값을 넣으면 어떻게 될까? 혹은 최소 8자 이상이 요구되는 패스워드 칸에 7글자짜리 암호를 입력하면 어떻게 될까?

그 값은 서버에 넘어가도 정상적으로 로그인 처리가 되지 않을 것이고, 서버에서는 이에 대해 예외를 발생시켜 해당 내용을 프론트엔드에 반환할 것이다. 이것을 알고 있음에도 잘못된 이메일이나 패스워드를 서버에 보내는 것은, 분명한 시간 낭비이다.

그렇기 때문에, 대다수의 페이지에서는 저런 상황이 생기면 로그인 창 아래에 "올바른 이메일이 아닙니다"나 "패스워드의 길이가 올바르지 않습니다"와 같은 에러 메시지를 출력한다. 또한, 로그인 버튼을 눌러도 요청을 보내지 않거나 아예 버튼을 비활성화시켜 사용자의 잘못된 행동을 미연에 방지한다.

예전에 진행한 프로젝트에서 구현한 모습. 테스트 중 캡쳐.

위의 이미지는 작년에 프로젝트를 진행하면서 프론트엔드의 검증 로직을 구현한 예시이다.

당시에 나는 신고 기능을 백엔드/프론트엔드에 구현하고 있었는데, Vue로 만든 페이지 테스트 과정에서 신고 분류를 선택하지 않아도 신고 요청을 보낼 수 있음을 알게 되었다. 그래서 카테고리를 선택하지 않을 경우에는 버튼을 비활성화하고, 관련된 알람 문구를 보여주도록 설정했다.

 

물론 이 방법이 능사는 아니다. 사용자는 임의로 페이지를 조작해서 허용되지 않은 값을 넣고 전송할 수 있다는 점을 우리는 유의해야 한다. 게다가 보안 취약점을 노리고 세션 등을 탈취해서 잘못된 요청을 보내는 경우도 고려해야 하기 때문에, 프론트엔드의 검증은 최소한의 검증이라고 꼭 이야기하고 싶다. (보안의 영역보다는, 사용자 편의와 불필요한 요청 감소의 영역으로 보는 것이 맞다는 뜻이다.)


2. Bean Validation을 통한 DTO에서의 검증

이에 대해서는 이전에 게시물을 쓴 적이 있어서, 링크를 남긴다. 궁금하시다면 꼭 읽어보시길.

 

5. Request DTO의 Validation / 예외 테스트와 ParameterizedTest

오늘은 저번 글에 이어서, DTO 및 비즈니스 로직의 검증과 그 테스트에 대해 다뤄보려고 한다. 4. TDD 개발 : Read/delete 기능 만들어보기 kafcamus.tistory.com/7 이전 포스팅에 이어서, 오늘은 남은 기능들

kafcamus.tistory.com

SpringBoot로 서버를 구축하면서, DTO를 통해 뷰에서 요청한 값을 컨트롤러 레이어에 전달한다.

그런데 그 과정에서, DTO에는 다음과 같은 에러가 발생할 수 있다.

  • DTO 직렬화 중 이름을 찾을 수 없어 특정 값에 null이 대입되는 경우
  • 이메일 등 특수한 양식을 띄는 문자열이, 기준을 충족하지 않은 채 대입되는 경우
  • 특정 정수 값에 음수나 지나치게 큰 값 등 의도하지 않은 값이 들어가는 경우

Bean Validation을 활용하면, 위의 여러 상황에 대한 어노테이션을 필드에 명시할 수 있다.

그리고 만약 해당 어노테이션 조건이 충족되지 않을 경우, 예외를 발생시킬 수 있다.

 

이를 통해 치명적인 오류 값이 모델에 전달되고 DB에 들어가는 등의 치명적인 상황을 미연에 방지할 수 있다.

또한 서비스 레이어에 중복되는 검증 코드를 넣는 번거로운 작업을 줄일 수 있다.


3. Bean Validation을 통한 Repository에서의 검증

만약 DTO에서 문제 없이 값이 통과되었더라도, 엔티티 클래스를 생성/조작하는 과정에서 잘못된 값이 들어갈 수 있다.

이 값이 DB에 들어가면 치명적인 문제를 만들 수도 있기 때문에, 이 역시 Bean Validation으로 막고자 한다.

 

JPA의 Repository 인터페이스는 Entity를 저장할 때,Validation Annotaion을 검사하고 이를 만족하지 못하는 경우 ConstraintViolationException을 발생시킨다. 그렇기 때문에, 나는 프로젝트를 진행하면서 엔티티 클래스에도 어노테이션을 달아 검증을 추가로 수행하도록 구현하였다.

 

이에 대한 게시글 링크는 다음과 같다. 

 

[JPA] nullable=false와 @NotNull 비교, Hibernate Validation

오늘은 다음의 고민 때문에 글을 작성하게 되었다. JPA에서 DDL을 자동으로 생성할 수 있는데, 이 때 not null 옵션은 어떻게 붙이나? JPA의 엔티티 객체에 @NotNull 검증 어노테이션을 주면 어떻게 되나

kafcamus.tistory.com

또한 이 글 역시 참고하여 구현하였다.

 

Spring Boot에서의 Bean Validation (1)

Bean Validation은 Java 생태계에서 유효성(Validation) 검증 로직을 구현하기위한 사실상의 표준이다. Bean Validation은 Spring과 Spring Boot에도 잘 통합되어 있다.

jongmin92.github.io

이 부분에 대해서는 현업에서 어떤 식으로 구현하는지 잘 몰라서, 혹시 잘 아시는 분은 댓글을 꼭 부탁드립니다.


4. 예외 테스트를 통한 검증

위의 경우들을 통해 우리는 임의의 조건을 충족하는 값만이 프론트엔드에서 서버를 거쳐 DB까지 순차적으로 들어오도록 설정하였다. 하지만 그럼에도, 여전히 에러의 위험은 우리 코드에 암약하고 있다.

 

앞서 소개한 팻 핑거 기사에서 어떤 사례들을 방지했는지 생각해 보자.

  • [연도]칸에 0을 입력한 것은 프론트엔드 검증 및 Bean Validation으로 막을 수 있다.
  • 60억 달러를 한번에 입금하는 경우, 금액이 너무 큰 만큼 Bean Validation에 값을 설정해 막을 수도 있다.

하지만 다음과 같은 사례는 어떻게 막아야 할까?

  • 61만엔에 주식 1주를 매도하려고 했는데, 실수로 61만개의 주식을 1엔에 매도했다.
  • 주당 1000원에 배당해야 하는 주식을 실수로 주당 1000주씩 입고했다.

위의 경우는 값의 상한가/하한가를 설정해서 막을 수도 있지만, 실제로 작동하는 과정에서 들어올 수 있는 값들이기에 무조건 막는것이 올바르다고 할 수는 없다. 언젠가는 1엔에 주식을 매도하거나 1000주의 주식을 입고할 수도 있지 않겠는가?

이러한 경우에 대해서는 서버에서 다른 서버나 DB를 통해 해당 주문이 유효한지 검증하는 것이 필요할 것이다. 예를 들어 매도하려는 주식을 실제로 보유중인지, 총 결산 금액이 잔고보다 많지는 않은지 등등...

 

이 때, 해당 방어 로직이 올바르게 작동하는 것은 어떻게 확인할 수 있을까?

혹은, 올바른 로직 수행 시 방어 로직이 작동하지 않는 것을 어떻게 확인할 수 있을까?

방법은 테스트 코드!!!

@DisplayName("Book 개별 조회 요청 시 올바르게 수행된다.")
@Test
void getBookTest() {
	Book book = BookGenerator.createBook();
	when(bookRepository.findById(도서_ID_1)).thenReturn(Optional.of(book));

	BookResponseServiceDto foundBook = bookService.getBook(도서_ID_1);

	assertThat(foundBook.getId()).isEqualTo(도서_ID_1);
	assertThat(foundBook.getName()).isEqualTo(도서_이름_1);
	assertThat(foundBook.getIsbn()).isEqualTo(도서_ISBN);
}

@DisplayName("예외 테스트 : Book 개별 조회 요청 시 해당 도서가 없다면 예외가 발생한다.")
@Test
void getBookWithExceptionTest() {
	when(bookRepository.findById(도서_ID_1)).thenReturn(Optional.empty());

	assertThatThrownBy(() -> bookService.getBook(도서_ID_1))
		.isInstanceOf(BookNotFoundException.class)
		.hasMessage(도서_ID_1 + "에 해당하는 도서를 찾을 수 없습니다.");
}

위의 테스트 코드는 내 이전 게시물에서 사용한 코드이다.

책의 목록을 조회하는 API에 대한 서비스 테스트 코드로, 정상적인 상황과 예외적인 상황에 대해 작성했다.

위 테스트 코드를 통해, 나는 다음의 상황을 확인할 수 있었다.

  • 도서 개별 조회 기능이 올바르게 작동하는지 여부
  • 존재하지 않는 도서를 id로 조회할 때 예외가 발생하는지 여부

또한 도메인에 Bean Validation을 설정해 두고 해당 Validation이 잘 작동하는지를 테스트했기 때문에, 각각의 값이 의도하지 않은 값(음수인 책 가격, 비어있는 저자 이름 등등)인 경우에 예외가 발생함을 알 수 있었다.


우리가 위에서 이야기한 팻 핑거의 사례들도 이러한 예외 테스트로 막을 수 있다.

만약 서비스 레이어에 "주식 매도 전 해당 주식의 보유를 확인하는 로직"이 있다고 가정해 보자.

그렇다면 우리는, 다음과 같은 테스트 코드를 작성해 해당 기능을 검증할 수 있다.

  • 올바른 값이 들어왔을 때 작동하는지 여부
  • 보유한 주식과 같은 개수의 매도 요청이 들어왔을 때 작동하는지 여부(경계값)
  • 0 혹은 그 미만의 개수를 매도 요청할 때 예외가 발생하는지 여부
  • 보유한 주식 이상의 매도 요청이 들어왔을 때 예외가 발생하는지 여부
  • 매도하려는 주식을 아예 보유하지 않고 있을 때 예외가 발생하는지 여부
  • 기타 의도하지 않은 동작들...

또한 테스트 코드를 작성한 경우, 주기적으로 테스트 코드를 실행하고 또 추가하게 된다.

그렇기 때문에 특정한 로직 변경에 따라 다른 코드가 영향을 받더라도, 테스트를 통해 빠르게 발견할 수 있다.

테스트 코드의 좋은 점은 끝도 없으므로...추후 이에 대한 글을 써보려고 한다.


결론: 손가락은 죄가 없다

팻 핑거에 대한 기사를 읽다 보니, 해당 실수를 저지른 직원에 대해 다음과 같은 언급이 있었다.

수억 원의 손실이 발생하더라도 업계와 당국은 사고 예방에 크게 관여치 않는 모양새다. 삼성증권의 팻핑거 담당자 역시 손해배상 책임이 없었다. 업계 관계자는 "증권사에서도 실수인 점을 감안해 심각한 손해를 끼친 게 아닌 이상 직원을 바로 해고하거나 하지는 않는다"고 말했다.

출처 : http://sateconomy.co.kr/View.aspx?No=1418802

이에 대해 처음에는 "회사가 막대한 손해를 입었는데, 그럼 누가 책임을 지는 거지?" 라는 생각을 했다. 그래서 이 글을 쓰며 생각을 정리해 본 것이고.

내가 내린 결론은 다음과 같다: "잘못한 것은 개발자이다. 실무자는 죄가 없다!" 

 

어떤 게임이 개발자가 의도하지 않은 방향으로 클리어가 가능하다고 할 때, 누구도 유저를 탓하지 않는다. 사용자가 몰려들어 어플리케이션의 서버가 다운되더라도, 누구도 유저를 탓하지 않는다. 서비스를 제공하고 해당 서비스에 대한 책임을 진 이상, 개발자는 다양한 예외 상황을 고려하고 더 안전하게 서비스를 만들 수밖에.


삼성증권의 유령주식 사태 이후, 증권업계에서는 특정한 금액 이상의 주문이 들어올 경우 안내 메시지를 보여주거나 잠시 주문을 보류하는 기능을 추가하겠다고 발표했다. 이는 법적 구속력이 없는 '모범 규준'이기에 논란이 있었지만, 그 이전에 팻 핑거를 잘 막을 수 있을지 우려가 되는 기준이기도 하다.

 

조심스럽게 금융업계에 테스트/검증 코드 가이드라인을 만들어보는건 어떨지 권하고 싶다. 물론 다른 업계도 마찬가지고. 내가 잘 해서 조금이나마 인식을 바꿔나갈 수 있을지, 막막한 마음에 영 코드가 안나오는 밤이다.

반응형

댓글