[북스터디]스프링 부트 핵심 가이드(ch10)를 공부해 보았다.(8편)ch10 유효성 검사와 에러 처리(feat.ch7)

Wang_Seok_Hyeon·2023년 4월 15일
0
post-thumbnail

주저리

이번 장의 경우, 유효성 검사라는 부분을 다루는데 해당 부분은 값 자체의 형태를 조정하고 통제 함에 있어 최대한 적절한 형태의 값과 기능이 동작하도록 지원한다.
반면 테스트 코드의 경우 메서드의 기능을 테스트하는 것을 의미한다.
로직을 잘못 짜거나 의도하지 않은 값을 잘못 넣는 경우 등의 실수를 방지하는 차원이 테스트 코드이고,
기본적인 데이터가 잘못 들어가는 것을 방지하는게 유효성 검사에 해당하는 것이다.

유효성 검사의 개념이 많이 지협적이기 때문에 조금씩 포괄적인 개념인 테스트 개념의 형태도 함께 소개하며 이번 핵심인 ch10을 정리하고 ch7도 함께 알아 보자!

테스트 코드 작성이란

ch7의 시작에는 테스트 코드를 작성하는 이유!라는 부제가 있는데 뭐 뻔한 내용이 담겨 있다.

  • 개발 과정에서 문제를 미리 발견할 수 있다!
  • 리팩토링의 리스크가 줄어든다.
  • 애플리케이션을 가동해서 직접 테스트 하는 것 보다
    테스트를 빠르게 진행할 수 있다.
  • 하나의 명세 문서로서의 기능을 수행한다.
  • 몇 가지 프레임 워크에 맞춰 테스트 코드를 작성하면 좋은 코드를 생산할 수 있다.
  • 코드가 작성된 목적을 명확하게 표현할 수 있으며, 불필요한 내용 추가를 방지할 수 있다.

그렇다면 10장의 시작은? 위와 같은 이론적인 내용이 빠져있다. 즉 7장과 중복된다는 것이다.
다만 유효성 검사를 제공하는 프레임워크인 Bean Validation을 소개하며 시작한다.

즉, 구체적이고 세세한 방식이 뒤에서는 후술된 것이며,
7장에서는 개괄적으로 해당 내용을 이해하기 위한 전반이 구술된 셈이다.

물론 7장에서는 테스트 코드의 전반을 다루기 때문에

  • 단위 테스트(unit test) : 애플리케이션의 개별 모듈을 독립적으로 테스트
  • 통합 테스트(integration test) : 애플리케이션을 구성하는 다양한 모듈을 결합해 전체 로직이 의도한 대로 동작하는지 테스트 하는 방식.
    V-model Test Image

단위 테스트와 통합 테스트라는 단어에서도 감이 오지만, 단위 테스트가 더 작은 형태로 테스트를 쪼갠 것이기 때문에 더 빠르게 동작한다.

통합 테스트의 경우 테스트 하나만 하는데도 오랜 시간이 소모 될 수 있기 때문에 테스트의 경우
단위 테스트를 작성하는 것이 지향해야 할 방법인 셈.

물론 통합 테스트가 불필요한 것은 아니다. 외부 자원을 포함해 모든 것을 사용하기 때문에 유의미하지만 앞서 말한 시간이 많이 소모되며 일의 세계에서 시간은 굉장히 큰 자산이다.

그럼 유효성 검사를 한 번 알아 보자.

구체적인 어노테이션으로 자바 Spring 진영은 2009년부터 Bean Validator를을 제공한다.
이러한 부분은 AOP 관점지향과 관련이 있다고 할 수 있다.
테스트 코드를 작성하는 것은 좋지만 테스트 코드의 경우 각각의 계층마다,
클래스 마다 작성하게 되면 겹치는 게 생길 수도 있기 때문에 그러한 부분 중
통합할 수 있는 것을 통합해 테스트하는 것. 이것이 관점지향 AOP의 개념이지 않을까?

Bean Validator를을 통해 유효성 검사, 즉 테스트가 DTO 와 같은 도메인 모델과 묶어서 각 계층에서 사용하게 된다.

이에 따라 테스트 코드의 작성 자체도 한편 더 간결해 지는 것이다.
이중, Hibernate Validator를는 Bean Validataon의 명세 구현체로, 스프링 부트는 Hibernate Validator를 채택해서 사용하며
Hibernate Validator를를 JSP-303(?)명세의 구현체로서 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능하게 도와준다.

의존성의 경우 스프링 부트 2.3 이후는 spring-boot-starter-validation으로 추가할 수 있다고 한다. 해당 키워드로 maven과 gradle 추가 방법을 찾아 보자.
2.3 이후면 이제 모두 추가해 주는 게 맞을 것이다. 현재 버전이 2023년 4월 11일 기준으로 3.0.4까지가 IntelliJ에 나와 있으니 말이다.

총 9가지의 큰 도메인 기반 즉 엔티티의 DTO의 유효성을 검증하는 9가지의 큰 틀과 구체적인 다양한 @어노테이션을 제공한다.

1. 문자열 검증

  • @Null : null 값만 허용
  • @NotNull : null 허용 하지 않음. ""와 " "는 허용(공백이 있는 것입니다.)
  • @NotEmpty : null과 ""를 허용하지 않음 " "는 허용(공백이 있는 것입니다.)
  • @NotBlank : null,""," " 모두 허용하지 않음 가장 엄격하네요.

최댓값/최솟값 검증

  • BigDecimal, BigInteger, int, long 등의 타입을 지원
  • @DemicalMax(value = "$numberString"):$numberString보다 작은 값 허용
  • @DemicalMin(value = "$numberString"):$numberString보다 큰 값 허용
  • @Min(value = "$number"):$number 이상의 값을 허용
  • @Max(value = "$number"):$number 이하의 값을 허용

값의 범위 검증

  • BigDecimal, BigInteger, int, long 등의 타입을 지원
  • @Positive :양수 허용
  • @PositiveOrZero: 0 포함한 양수 허용
  • @Negative : 음수 허용
  • @NegativeOrZero :0 포함한 음수 허용

시간에 대한 검증

  • Date, LocalDate, LocalDateTime 등의 타입을 지원
  • @Future : 현재보다 미래의 날짜 허용
  • @FutureOrPresent : 현재를 포함한 미래의 날짜 허용
  • @Past : 현재보다 과거의 날짜 허용
  • @PastOrPresent : 현재를 포함한 과거의 날짜 허용

이메일 검증

  • @Email:이메일 형식을 검사합니다.""는 허용합니다.

자릿수 범위 검증

  • BigDecimal, BigInteger, int, long 등의 타입을 지원
  • @Digits(integer = $number1, fraction = $number2):$number1의
    정수 자릿수와 $number2의 소수 자릿수를 허용

Boolean 검증

  • @AssertTrue : true인지 체크, null 값 체크 X
  • @AssertFalse : false인지 체크, null 값 체크 X

문자열 길이 검증

  • @Size(min = $number1, max = $number2) :
    $number1 이상$number2 이하의 범위를 허용

정규식 검증

  • @Pattern(regexp = "$expression"): 정규식을 검사.
    정규식은 자바 java.util.regex.Pattern 패키지 컨벤션 따름
    + 추가 항목 : 정규식에 관한 내용.
    정규식은 카카오 테스트에서도 자주 보인다. 물론 해당 내용을 몰라도
    코드를 짜서 만들 수 있지만 해당 내용을 안다면 훨씬 담백한 코딩이 가능하다.
    그래서 해당 정규식(Regular Expression)을 정리해 보고 가자!
    
    이후 이하에 나오는 테스트 코드를 작성하는 개괄적인 내용을 이해하면 더 좋다!
    - ^ : 문자열의 시작 
    - $ : 문자열의 종료
    - . : 임의의 '한' '문자'
    - * : '앞' 문자가 '없거나' '무한정' 많음
    - + : '앞' 문자가 '하나 이상'
    - ? : '앞' 문자가 '없거나' '하나' 존재
    - [, ] : 문자의 '집합'이나 범위, 두 문자 사이는 기호 '-'로 범위 표현
    ex. a-z(a부터 z까지)
    - {, } : 횟수 또는 범위를 의미
    - (, ) : 괄호 안의 문자를 하나의 문자로 인식
    - | : 패턴 안에서 'OR' 연산 수행
    - \ : 정규식에서 역슬래시는 확장문자로, 역슬래시 다음 특수문자 오면 문자로 인식 
     ex.\\역슬래시 인식 \" "를 문자로 인식
    - \b : 단어의 경계
    - \B : 단어가 아닌 것에 대한 경계
    - \A : 입력의 시작 부분
    - \G : 이전 매치의 끝
    - \Z : 종결자가 있는 경우 입력의 끝
    - \z : 입력의 끝
    - \s : 공백 문자
    - \S : 공백 문자가 아닌 나머지 문자(^\s와 동일)
    - \w : 알파벳이나 숫자
    - \W : 알파벳이나 숫자가 아닌 문자(^\w와 동일)
    - \d : 숫자[0-9]와 동일하게 취급
    - \D : 숫자를 제외한 모든 문자(^0-9, ^\d와 동일)
    위의 내용 중 여러 번 본 것도 있고 하지만 책에서 이렇게 많이 정리된 경우는
    처음 봐서 한 번 같이 내용을 정리하기 위해 공유했다.
    이외에도 책에서는 https://regexr.com/ 과 https://regex101.com/을 통해 
    학습을 권장하고 있다.

위와 같은 어노테이션이 꽤나 큰 도움이 될 거 같다!
정말 특히나 Pattern에 관한 학습을 하고 해당 코드를
클론 코딩 했음에도 해당 내용이 뭔지 모르고 타이핑 했던 기억이 난다.
그런 부분의 전반을 정리하는 건 마치 프로그래밍 언어에서
삼항 연산자를 처음 보고 ++prefix, postfix++과
단항 연산자를 보는 그런 느낌이다. 낯설지만 이런 것들이 익숙해지면
더 없이 편리하겠구나!
이런 느낌이 나는 요소이다.

다만, 실무에서는 위와 같은 기본 내용 이외에도 커스텀
Validation을 추가해야하는 상황이 발생할 수 있으며,
이를 추가해 사용하는 방법 역시 알아 둘 필요가 있다.

이런 경우 새로운 Validation 클래스를 선언하고
이때 ConstarintValidator라는 interface를 구현해서 사용한다.

해당 값들이 있으면 테스트 코드의 작성이 훨씬 간결해 질 것 이다.

테스트 코드의 구조적인 부분을 조금 알아 보자.

위와 같이 값들이 아무리 잘 작성 되어도
우리는 오타를 내거나, 여러 레포지터리를 사용하다 보니 값을
잘못 넣는 실수를 할 수도 있기 때문이다.

테스트 코드의 형태(작성법)와 속성

1. Given - When - Then
@Test어노테이션과 함께
//given
//when
//then 형태를 자동완성으로
intellij에서 만들어 작성하는 만큼
해당 부분은 매우 주요하다. 각각의 영역 별 역할에 대해 알아 보자.

  • given
    테스트를 수행하기 전에 테스트에 필요한 변수를 정의하거나 Mock 객체를 통해 특정 상황에 대한 행동을 정의
  • when
    테스트의 목적을 보여주는 단계, 실제 테스트 코드가 포함되며, 테스트를 통한 결과값을 가져옴.
  • then
    테스트의 결과를 검증하는 단계 일반적으로 When 단계에 나온 결과값 검증하는 작업 수행. 결괏값이 아니더라도 이 테스트를 통해 나온 결과에서 검증해야 하는 부분이 있다면 이 단계에 포함

위와 같은 Given-When-Then 패턴은 테스트 주도 개발에서 파생된 BDD (Behavior-Driven-Development;행위 주도 개발)를 통해 탄생한 테스트 접근 방식이다.

단위테스트 보다, 인수 테스트 사용에 적합하다고 알려져 있다고 한다. 저자는 단위 테스트에서도 유용하게 활용할 수 있다고 생각한다고 하는데 실제로 많이 써 오는 걸 보았기에 단위 테스트용인지 알았다...ㅎ

그 이유에 대해서, 단위 테스트의 작은 단위에서 코드가
불필요하게 길어질 수 있다고 하는데 이 부분은 꽤 공감이 된다.

다음으로는 좋은 테스트 작성하는 5가지 속성이다.

좋은 테스트 코드 작성 5가지 속성(F.I.R.S.T)
1.빠르게(Fast)

빠르게 수행돼야 한다. 
테스트가 느리면 코드를 개선하는 작업이 느려져 코드 품질이 떨어질 수 있다. 
테스트 속도 절대적인 기준은 아니지만, 목적을 단순하게 설정해 작성, 
외부 환경을 사용하지 않는 단위 테스트를 작성하는 것 등을 빠른 테스트라고 함.

2.고립된, 독립된(Isolated)

하나의 테스트 코드는 목적으로 여기는 하나의 대상에 대해서만 수행. 
만약 하나의 테스트가 다른 테스트 코드와 상호작용하거나 관리할 수 없는
외부 소스를 사용하게 되면 외부 요인으로 인해 테스트가 수행되지 
않을 수 있음

3.반복 가능한(Repeatable)

테스트는 어떤 환경에서도 반복 가능하도록 작성해야 함. 
Ioslated 규칙과 유사한 의미.
테스트는 개발 환경의 변화나 네트워크의 연결 여부(외부 리소스)와 상관없이 수행 가능해야 함.

4.자가 검증(self-Validating)

테스트는 그 자체만으로도 테스트의 검증이 완료 돼야 함. 
테스트 성패 확인 가능한 코드를 함께 작성.
만약 결괏값과 기댓값을 비교하는 작업을 코드가 아니라
개발자가 직접 확인하고 있다면 좋지 못한 코드임.

5.적시에(Timely)

테스트 코드는 테스트 하려는 애플리케이션 코드를 구현하기 전에 완료돼야 함.
너무 늦게 작성된 테스트 코드는 정상적인 역할을 수행하기 어려움.
또한 테스트 코드로 인해 발견된 문제를 해결하기 위해 소모되는 개발비용도 커지기 쉬움.
다만, 이 개념은 테스트 주도 개발의 원칙을 따르느느 테스트 작성 규칙.
텍스트 주도 개발을 기반으로 애플리케이션을 개발하는 것이 아니면,
이 규칙은 제외하기 진행하기도 한다.

위와 같은 개괄적인 속성들을 유념하며 테스트 코드를 작성해야 좋은 테스트 코드를 짤 수 있다.
그리고 추가로 TDD라는 개념도 같이 이해해 두자!

테스트 주도 개발(TDD)

TDD란 'Test-Driven Development'의 줄임말로 '테스트 주도 개발'이라고 번역된다.
반복 테스트를 이용한 소프트웨어 개발 방법론으로서
테스트 코드를 먼저 작성한 후, 테스트를 통과하는 코드를 작성하는 과정을 반복하는
소프트웨어 개발 방식이다.
애자일 방법론 중 하나인 익스트림 프로그래밍(extream Programming)의 Test-First 개념에
기반을 둔, 개발 주기가 짧은 개발 프로세스로 단순한 설계를 중시함.

테스트 주도 개발의 개발 주기 3단계

  • 실패 테스트 작성(Write a failing test) : 실패하는 경우의 테스트 코드를 먼저 작성
  • 테스트를 통과하는 코드 작성(Make a test pass) : 테스트 코드를 성공시키기 위한 실제 코드 작성
  • 리팩토링(Refactor) : 중복 코드를 제거하거나 일반화하는 리팩토링 수행

테스트 주도 개발의 효과

디버깅 시간 단축

테스트 코드 기반으로 개발이 진행되기 떄문에 문제가 발생했을 때 어디에서 잘못됐는지 확인이 쉬움

생산성 향상

테스트 코드를 통해 지속적으로 애플리케이션 코드의 불안정성에 대한 피드백 받기 때문에
리팩토링 횟수가 줄고 생산성이 높아짐

재설계 시간 단축

작성돼 있는 테스트 코드를 기반으로 코드를 작성하기 때문에 재설계가 필요할 경우
테스트 코드를 조정하는 것으로 재설계 시간을 단축

기능 추가와 같은 추가 구현이 용이

테스트 코드를 통해 의도한 기능을 미리 설계해 코드를 작성.
목적에 맞는 코드를 작성하는 데 비교적 용이함.

이제 테스트 코드 작성에 대한 전반적인 이해도도 올라간 상태일 것이다.
그렇다면, 이러한 유효성 검사, 테스트 검사를 수행할 때
우리가 제일 많이 마주하는 것은 뭘까?
그것은 다름 아닌 바로 exception! 예외!이다!

코드를 작성할 때 이러한 예외처리가 무엇보다 중요한 요소가 된다!

예외 처리

소프트웨어의 관점에서 예외와 에러는 엄연히 다른 용어에 해당한다고 한다.
에러의 경우 소프트웨어에서 관리할 수 없는 형태의 것.(주로 하드웨어 문제)
예외의 경우 소프트웨어에서 실수가 나서 수정해 고쳐야 하는 것.을 말한다고 볼 수 있다.

예외 클래스의 상속 구조 역시 전체를 살펴보면 굉장히 다양한 것을 가지고 있다.

위의 이미지만 봐도 꽤나 복잡한 구조라는 것을 알 수 있다.
하지만 우리가 대부분 만나는 경우가
IOException과 RuntimeException에 속한다는 부분을 먼저 인지해 두자.

예외 처리의 경우 통상적으로 try catch문을 사용하거나 throws를 통해 해당 경우를 넘겨주는 경우도 있지만,

실제 환경에서는 이 보다 더 많은 서비스에 특화되고 규격화된 예외를 발생시켜야 하기 때문에
Custom Exception을 만드는 방법에 대해 알아둬야 한다.

이를 활용하기 위해서 Spring에서는
@(Rest)ControllerAdivice와 @ExceptionHandler를 통해 모든 예외를 처리 합니다.(RestControllerAdvice 는 JSON 형태로 값을 반환하기 때문에 가시성이 높다!)
@ ExceptionHandler는 특정 컨트롤러의 예외를 처리할 수 있다.

CustomExetion

우선 기본적으로 CustomException을 만들 때는
상위의 클래스를 상속 받아야 한다. Exception으로
포괄적으로 받아올 수 있지만,
이렇게 되면 너무 많은 것들을 가져오기 때문에
문제가 발생했을 때 해당 부분을 파고 들어가
분석하는데 어려움도 있다.

그렇다고 너무 지협적으로 하게 되면
또한 너무 많은 커스텀 Exception을 만들어야 하므로
이 역시 좋지 않다.
서적에서는 Exception을 상속하고 있는데
내가 들어온 강의들에서는 대체로 그 하위인
RuntimeException을 상속받아
CustomException을 구현해 왔다.

그리고 해당 클래스에서 오는 Http 요청을 받을 수 있는
HttpStatus 변수를 활용해
해당 요청값을 받아오고,
해당 요청에 맞게 반환하고 싶은 요청값을
변경할 수도 있다.

여기서 message를 주는 것도 커스텀 해서
해당 문제가 어떤 문제인지 커스텀하게 작성하는 경우는
Enum을 활용해 제작할 수 있다.

예외 처리의 경우, 상당히 간단하게 이야기 하고 있는데
실제 구현코드가 길지 않기 때문도 있지만

해당 부분은 처음 접하면 너무 방대해 보이지만
또 한편으로 이후에 다시 접하게 되면
굉장히 심플하면서도 중요하다는 걸 알 수 있기 때문이다.
(유효성 처리와 예외처리만으로는 분량이 안 나올 거 같아 ch7을 가져온)

위와 같은 방식을 통해 코드 작성을 더 규격화하고 고도화 할 수 있게 된다!

profile
하루 하루 즐겁게

0개의 댓글