brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Jan 24. 2021

스프링부트 백엔드 프로그래밍 (5)

5. 예외 처리 및 테스트 코드 작성

"스프링부트 백엔드 프로그래밍"이라는 주제로 약 8주간 글을 작성할 예정입니다. 스터디가 잘못된 방향으로 가지 않도록, 의견 및 조언을 아낌없이 해주시길 부탁드립니다. 많이 부족합니다.


지난 주에는,  스프링 프레임워크, 스프링부트의 기본 개념인 IoC, DI, AutoConfiguration에 대해서 설명하였습니다. 이번 주에는 자바 예외 처리 및 테스트 코드에 대해서 공부합니다. 


어떻게 설명하면 취준생들이 이해하기 쉽게 설명할 수 있을지 고민했습니다만, 역시 누군가에게 지식을 전달하는 것은 너무 어려운 일입니다. 일단, 스터디 일정으로 인해서 오전에 정신없이 글을 작성해서 빠르게 먼저 글을 공개합니다.


깔끔하지 못한 이 글은, 나중에 시간이 생기면 다시 수정하겠습니다...




전체 목차

1주차 - 스프링부트란 무엇인가?, 간단한 API 서버 만들어보기

       1. 스프링부트란 무엇인가?

       2. 간단한 API 서버 만들어보기 (커리큘럼 소개)

2주차 - 스프링 프레임워크 기본 개념 이해하기

       3. 스프링 프레임워크 IoC, DI(의존성주입)

       4. [이전글]스프링부트 AutoConfiguration

3주차 - Rest API, 테스트 코드 작성하기, 예외 처리하기

       5. [지금글]예외 처리 및 테스트 코드 작성하기

       [나중에 다시] Rest API (HTTP 기본 개념)    

4주차 - 캐싱

       6. 스프링 캐시 추상화

       7. Redis 연동하기

5주차 - MQ, Pub/Sub


[미정] 6주차 - 보안(인증)

[미정] 7주차 - 병렬, 비동기 프로그래밍

[미정] 8주차 - Spring Cloud

[미정] JPA, Spring Data, Spring Session 등



이번 스터디의 샘플 코드입니다. 제 코드를 그대로 따라하지 마시고, 각자 좋은 코드로 개선해보세용~

https://github.com/sieunkr/spring-study-group/tree/master/3-1


5. 예외 처리 & 테스트 코드


예외 처리와 테스트코드는 별개의 주제로 글을 작성할려고 했습니다만... 

어쩌다보니 두 개의 주제가 짬뽕이 되었네요......


5.1 단위테스트 코드 예습하기

이번 주 스터디를 진행하기 전에, 단위 테스트에 대해서 예습이 필요합니다.

https://brunch.co.kr/@springboot/536


- 영화 평점 순 정렬이 잘 되는지 검증

- 평점이 0 인 데이터는 제외하는지 검증

아래와 같이 검증을 잘 했습니다. 


쿼리로 영화를 검색하면 기본적으로는 평점이 높은 순으로 정렬이 되어서 제공하며, 평점이 0인 데이터는 제외하는 로직입니다. 위 소스를 기반으로 예외 처리를 추가해보겠습니다. 



5.2 예외 처리하기 

영화 검색 기능 외에 추가 기능을 구현해봅니다. 추가 기능은, 기획자가 지정한 키워드를 검색어로 영화를 추천해서 제공해주는 기능입니다. 기획자는 쿼리를 변경할 수 있어야 하며, 쿼리를 검색해서 나온 영화 중 평점이 제일 높은 영화를 제공해주는 기능입니다. 


우선, 평점이 제일 높은 영화를 검색하는 메서드를 아래와 같이 작성하였습니다. 기존 평점 순 정렬 기능을 활용해서, 정렬 된 리스트의 첫번째 데이터를 사용하면 됩니다. 

그리고, 아래와 같이 응용서비스 레이어에서 추천영화를 제공하는 메서드를 작성합니다. 기획자의 요구에 따라서 추천 영화는 기획자가 지정한 쿼리에 해당하는 평점이 가장 높은 영화를 제공합니다. 

Controller 를 만들어서 호출해봅니다. 

잘 동작합니다. 단, Null 처리가 신경쓰이네요.. Null 처리는 조금 이따 다시 다루기로 하고일단 테스트 코드를 먼저 작성해볼까요?

 stub 데이터는 아래와 같습니다.

테스트 코드를 실행해봅니다. 


하지만, 영화 검색 결과가 없으면 어떻게 될까요? 

위와 같이 변경하고 애플리케이션을 실행해봅니다. 크롬에서는 아래와 같이 에러가 발생하네요.

Postman 으로 호출해도 동일하게 오류가 발생합니다. 단, 에러 포맷은 크롬에서 보는 것과는 다릅니다.



데이터가 없을 때에 대한 예외 처리를 추가하겠습니다. 그리고, 예외 처리에 대한 테스트 코드를 같이 작성하면 좋겠습니다. 가장 쉽게 생각나는 방법은 Null 을 반환해주는 것입니다.     

메서드를 호출하는 곳에서는 Null 체크를 해야합니다. 

Null 을 반환하는 방법은 좋은 방법인 것 같지는 않습니다. 



자바 프로그래밍 언어에서는 "데이터가 있거나, 또는 없을 수도 있는 경우" 를 처리하기에 괜찮은 방법으로는 Optional 입니다. 자바8 에서 처음 등장하였으니 나온지 10년이 되었네요. 이 글을 읽고있는 코틀린 경험자 분들께서는.... 제 글이 아주 우습게 생각이 되시겠지만, 초보자를 위해 작성한 이 글에서는 Java 의 Optional 을 사용해서 코드를 개선하겠습니다. 아래와 같이 getHighestRatingMovie 메서드의 반환을 Optional<T> 로 수정하였습니다.  

데이터가 존재한다면 Optional 객체에 데이터를 포함해서 전달합니다. 반면에, 데이터가 없다면, Optional empty 를 반환합니다. 메서드를 호출하는 곳에서는, Optional 이 Empty 인 경우에 대해서 예외 처리를 해줘야 합니다. orElse 를 사용해서, 데이터가 없는 경우에는 기본영화 데이터를 제공하도록 정의하였습니다. 

검색 데이터가 없을 경우에는 에러가 발생하지 않고, 아래와 같이 기본 영화가 제공됩니다. 

물론, 검색 결과가 있다면 당연히 정상적으로 검색 결과 상위 1개의 데이터를 제공하겠지요...


기획자와 개발자는, 검색 결과가 없는 경우에 기본 데이터를 제공하도록 협의를 잘 하였습니다. 기본 데이터를 제공하는 로직이 잘 동작되는지, 위와 같이 크롬에서 확인할 수도 있지만, 좀 더 좋은 방법은 테스트 코드를 작성하는 것입니다. 새로 합류한 개발자는, 테스트 코드를 먼저 읽으면, 예외 처리를 어떻게 하였는지에 대해서 이해할 수 있습니다. 자, 그럼 테스트 코드를 작성해봅니다. Mock 객체(MovieRepository) 가 쿼리 검색 시 빈 리스트를 반환하도록 합니다. 

테스트를 실행하면, 아래와 같이 성공을 하게 됩니다. 

물론, 기존 테스트 코드 역시 성공해야 합니다. 평점 순으로 기본 정렬하던 기능은 그대로 잘 동작해야 합니다. 그리고, 테스트는 매우 빠르게 실행이 되어야 합니다. 6ms 정도 걸렸네요.


단위테스트는 빠르게 실행할 수 있도록 작성해야 합니다. 






갑작스러운 요구사항 변경

갑작스럽게 비즈니스 전략이 변경되었습니다. 기획자는, 검색 결과가 없을 때 기본 영화를 제공하지 않고, 별도로 프론트에서 다른 로직을 수행하기로 결정하였습니다. 백엔드에서는 제공하는 API 에서 에러가 발생했다는 것을 프론트에 전달하는게 좋을 것 같습니다. 


전략 변경

검색 데이터가 없을 시 기본 데이터 제공 --> 검색 데이터가 없을 시 예외 전달 및 프론트에서 별도 처리


우선, 예외 메시지를 정의할 Enum 클래스를 정의합니다.

그리고, 아래와 같이 RuntimeException 를 상속하는 Exception 클래스를 하나 정의합니다. 

그리고, Optional 데이터를 호출하는 곳에서, orElse 대신에 orElseThrow 로 변경합니다. 


데이터가 없다면, 필자가 정의한 ClientNoContentRuntimeException 예외가 발생합니다. 호출하면 아래와 같이 500 에러를 응답합니다. 

브라우저에서는 아래와 같이 다르게 나오네요. 

지난 스터디에서도 설명하였지만, 스프링 부트 내부 로직에서는 유입하는 레퍼러에 따라서 에러를 다르게 처리해줍니다. (자세한 내용은 생략) 참고로, 에러 상태 코드를 204 로 변경하면...

204 로 응답하게 됩니다. 



어쩃든, 데이터가 없는 경우에 대한 예외 처리를 잘 수행하였습니다. 하지만, 우리는 예외 발생 시 스프링부트에서 처리해주는 예외 응답을 사용하지 않고, 커스텀하게 예외를 처리하도록 하겠습니다. 



5.3 스프링부트 예외 처리하기

커스텀하게 예외를 처리할 수 있도록 개선하겠습니다. 먼저, 예외 처리 시 응답할 커스텀 클래스를 정의합니다. 

프로젝트의 전체 예외를 핸들링할 수 있도록 ExceptionHandler 클래스를 정의합니다. 이때 반드시 @RestControllerAdvice 어노테이션을 정의해줘야 합니다. 


처리해야 하는 예외를 아래와 같이 정의해줍니다. ClientNoContentRuntimeException 예외가 발생하면 아래 로직을 수행하게 됩니다. 

크롬에서 호출하면 아래와 같이 커스텀하게 정의한 예외 포맷을 응답합니다. 


포스트맨에서도 동일하게 응답합니다. 


자... 이제 테스트 코드를 작성해야하지만, 작성하기 전에


한바탕 소스를 수정하였으니... 테스트 코드를 전체적으로 한번 돌려봅시다. 

역시 실패하였네요. 테스트 코드의 목적은, 우리가 원하는 대로 잘 구현하였는지 검증하는 것입니다. 원하는 구현이 변경이 되었다면... 어쩔수 없이 테스트 코드 역시 변경을 해야 합니다. 



테스트 코드에서는 아래와 같이 변경되어야 합니다.

[AS-IS 변경 전] 검색 데이터가 없을 때 기본 영화를 제공하는지 검증

[TO-BE 변경 후] 검색 데이터가 없을 때 예외 발생시키는지 검증



테스트 코드를 아래와 같이 수정하였습니다.

자, 이제 변경된 테스트는 성공합니다. 


또한, 중요한 사실은... 다른 테스트 코드 역시 잘 성공한다는 것입니다. 



5.5 테스트 코드 심화

아쉽지만, 이 글에서 테스트 코드에 대해서 모든 내용을 설명할 수는 없습니다. 저는, 그럴 실력도 되지 않습니다. 간략하게만 소개합니다. 테스트 코드는 몇년동안 꾸준히 작성해야지 됩니다.


5.5.1 테스트 코드를 작성하는 의미

테스트 코드를 작성하는 것은, 어떤 의미를 가질까요? 개발자는, 기획자의 요구사항에 맞춰서 수많은 기능을 구현해야 합니다. 그 많은 기능이 잘 동작하는지를 우리는 개발자가 매번 검증할 수 없습니다. (사실 저는 불안한 마음에 매번 검증합니다..) 신규 개발자 또는 이직한 개발자는 업무 히스토리를 잘 모르기 때문에, 요구사항에 대응하는 것이 쉽지 않습니다. 그리고, 인수인계는 문서는 제대로 되어있지 않습니다. 


갑작스런 상황이 발생합니다.

기획자는 추천영화에 대한 로직을 변경하고 싶어합니다. 평점이 제일 높은 영화를 추천하는 것이 아니라, 평점이 2번째 영화를 추천하고 싶어합니다. 신규 개발자는 잠시 고민하더니, 정렬 기능에서 평점이 제일 높은 걸 제외하면 되겠다고 아주 단순하고 멍청하게 생각합니다. 이렇게 바보같이 생각하는 개발자가 없을 것 같지만, 저는 자주 이런 실수를 합니다. 매우 간단합니다. 소스 한 줄만 추가하면 됩니다. 

테스트 코드 역시 작성해봅니다. 

잘, 성공합니다. 


신규 개발자는 프로젝트를 전부 파악하고 있지 않기 때문에, 정렬 기능이 다른 곳에서 사용하고 있는지 모를 수 있습니다. 정말 바보같지만.. 흔히 발생합니다. 급하게 서비스에 배포하고, 서비스에는 알 수 없는 장애가 발생합니다. 기존에 제공하던 평점 순 정렬 기능에서 이상한 점이 발생한 것입니다. 




사실... 단위테스트를 전체적으로 한번만 돌려봤다면, 최악의 상황을 피할 수 있습니다. 



또한, 단위테스트를 작성하면서, 정말 중요한 사실을 깨닫게 됩니다. 바로 설계에 대한 고민을 많이 할 수 있습니다. 단위 테스트를 작성할 때, 의존성이 많으면 많을수록 테스트 코드를 작성하기 어렵다는 것을 깨닫게 됩니다. 의존성이 많을 수록 Mock 객체를 만들어야 하는 수가 많아지기 때문입니다. 단위 테스트 작성을 하다보면, 의존성에 대해서 고민하게 되고, 소프트웨어 설계에 대해서 같이 고민할 수 있습니다. 



5.5.2 단위 테스트 vs 통합 테스트


테스트 코드를 작성하기 전에 항상 생각해야 합니다. 단위테스트를 할 것인지, 통합 테스트를 할 것인지에 대해서!!! 



기본적으로 단위 테스트는 아래와 같은 5가지 원칙을 따릅니다. 


F - Fast  (테스트 코드를 실행하는 일은 오래 걸리면 안된다.)

I - Independent (독립적으로 실행이 되어야 한다.)

R - Repeatable (반복 가능해야 한다.)

S - Self Validating (매뉴얼 없이 테스트 코드만 실행해도 성공,실패 여부를 알 수 있어야 한다.)

T - Timely (바로 사용 가능해야 한다. )


https://dzone.com/articles/writing-your-first-unit-tests


하지만, 통합테스트는 조금 다릅니다. 독립적으로 실행이 되지 않습니다. 스프링부트에서의 통합테스트는 스프링 인프라 위에서 동작해야 합니다. 스프링부트에서 제공하는 테스트를 살펴볼게요. @SpringBootTest 를 사용하면, 아주 간편하게 스프링 애플리케이션을 테스트 할 수 있습니다. 


여기서 중요한 사실은 바로, @SpringBootTest 어노테이션에 의해서, ComponentScan 이 동작하며, 애플리케이션의 모든 Bean 을 등록해준다는 사실입니다. 즉, 테스트 코드를 실행하는 것은, 마치 애플리케이션을 실행하는 것과 같게 동작한다는 것입니다. 


그리고, 위 코드에서는, MovieService 에서 의존하고 있는 MovieRepository 구현체에 대한 어떤 설정도 없고, MovieService 에 MovieRepository 의 구현체인 MovieRepositoryImpl 이 주입이 잘 됩니다. 그리고, 실제로 네이버 오픈 API 를 통신하는 구문입니다. 



하지만, 네이버 오픈 API 통신 결과는 언제나 변경 될 수 있습니다. 스프링 부트에서는 통합테스트를 수행할 때 Mock 객체를 편하게 사용할 수 있는 기능을 제공하는데, 바로 @MockBean 어노테이션입니다. 

그리고,  @SPringBootTest 어노테이션에 특정 classes 를 지정할 수 있습니다. 

클래스를 지정하기 전에는 모든 Bean 이 스프링 컨테이너에 등록될 것입니다. 하지만, 해당 테스트는 MovieService 만 필요한 테스트이기 때문에 위와 같이 클래스를 지정해서 선언해봅니다. MovieService 의 Bean 만 스프링 컨테이너에 등록됩니다. 불필요하게 모든 Bean 을 등록해서, 느린 테스트를 수행할 필요는 없습니다. 

위와 같이 통합테스트를 작성하면 됩니다. 참고로, classes 를 지정해서 테스트 하는 것이 통합테스트가 아니라 단위테스트라고 말씀하시는 개발자를 꽤 많이 봤습니다. 저도 정답을 모르겠습니다. 통합테스트인지, 단위테스트인지 따지는 것보다는, 테스트 코드가 개발자에게 주는 의미에 대해서 생각해보는게 더 소중할 것 같습니다. 어쨋든, 스프링부트에서 제공하는 기능을 우리는 잘 알고 사용해야 합니다. 


이 글은 스프링부트의 통합테스트에 대해서 100분에 1도 설명하지 않은 글입니다. 그만큼 내용이 방대하며, 공부해야할 내용이 많습니다. 필자가 기존에도 글을 몇번 작성하였지만, 사실 많이 부족합니다. 각자 앞으로 잘 공부하시길 바랍니다. 

https://brunch.co.kr/@springboot/418

https://brunch.co.kr/@springboot/207



5.6 예외 처리 심화

예외 처리에 대해서, 조금만 더 알아보겠습니다. 네이버 오픈 API 인증이 실패한 경우에 대해서 예외 처리를 해보겠습니다. secret 인증키를 임의로 잘못된 키로 변경한 이후에, 아래와 같이 예외 처리를 해보았습니다. 

아래와 같이 크롬에서 401 에러가 발생합니다. 

포스트맨에서는 아래와 같이 응답합니다. 

위에서 배운대로, 예외 처리를 커스텀하게 처리하기 위해서 아래와 같이 수정하였습니다. 

자, 이제 아래와 같이 커스텀하게 예외를 응답합니다. 

포스트맨에서도 동일하게 응답합니다. 



사실, 예외 처리는 이렇게 글 하나로 정리할 수 있는 내용이 절대 아닙니다...자세한 내용은 생략하며, 각자 공부해보길 바랍니다. 



5.7 Optional 심화

Optional 은 남용해서 사용하면 절대로 안됩니다. Effective Java 3판에 Optional 에 대한 글을 꼭 읽어보길 바랍니다. 책이 없다면, 아래 글이라도 먼저 읽어보길 바랍니다.

https://dzone.com/articles/using-optional-correctly-is-not-optional

코틀린 또는 다른 언어와 비교해봐도 좋을 것 같습니다. 자바 언어의 Optional 이 생각보다는 별로이지만, 그래도 많이 사용하기 때문에 취준생 및 신입 개발자들은 꼭 숙지해야 합니다. 




5.8 마무리

다음 주에는 백엔드 프로그래밍에서 가장 중요한 캐싱에 대해서 다룹니다. 참고로, 시간이 없어서 Rest API & HTTP 기본 개념에 대한 내용은 패스하게 되었는데요. 각자 공부하시면 될 것 같습니다. 스터디 진행하면서 여유가 생기면 다시 다루겠습니다.












과제

이번 주 과제는 아래와 같습니다. (과제가 이해가 안되시면 꼭 말씀 부탁드려요...)
이번 주 과제는 좀 별로이긴 하네요..ㅠㅠ

1. 사용자에 의한 키워드 검색 시 네이버 Open API 영화 검색 결과를 애플리케이션 내부에 정의한 자료구조에 저장해놓기(캐싱 역할로 사용)

   (Map or Set or 커스텀한 자료구조 등 자유롭게 정의해보세용)
   예) Map 을 사용해서, Key 에는 검색어를 Value 는 검색 결과를 저장하시면 됩니다.

2. 사용자에 의한 키워드 검색 시 특정 키워드에 해당하는 데이터가 자료구조에 이미 저장되어있다면, 네이버 오픈 API 호출하지 않고, 저장된(캐싱된) 데이터를 사용하도록 구현 (없으면 네이버 Open API 호출 후 자료구조 저장)

3. 관리자에 의한 캐시 데이터를 강제로 업데이트(갱신)하는 RestController API 만들기
(RestAPI 의 갱신해주는 API를 호출하면, 네이버 오픈 API 를 호출 후 캐시에 저장)


즉, 자료구조에 저장하게 되는 케이스는 아래와 같이 두가지 케이스가 됩니다.

 - 사용자에 의한 검색 요청하는 경우

 - 관리자에 의해서 결과를 강제로 업데이트 해주는 경우



(선택)4. 자료구조에 저장한 데이터는 10분이 지나면 서버에서 자동으로 지우기 

(왠지 이건 어려울 것 같네요.. 하실 수 있으면 하시고, 너무 어려우면 하지 마세요)



위 주제와는 별개로 준비하시면 됩니다. 

- Redis 준비하기 

   방법1:가상 머신의 Linux에 설치

   방법2:윈도우에 레디스 설치   

   방법3: MacOS 에 brew 로 설치

   방법4: AWS Elastic Cache 등 편한 방법으로 준비하시면 됩니다.

   방법5: GCP 인스턴스 또는 AWS EC2 에 레디스 설치하고 포트 오픈하기

등등.. 아무 방법으로 자유롭게.. 준비하시면 됩니다. 

- redis-cli 접속해서 콘솔에서 이것저것 해보기

   (데이터 저장, 삭제, 만료시간 확인 등)



그리고, 이번주부터는 코드리뷰를 요청해주시기 바랍니다. PR 방법은 아래와 같습니다. 


1) 각자 개인 github 프로젝트에 Collaborator 에 제가 알려드린 계정을 추가합니다.



2) collaborator 수락을 기다립니다. 

3) develop 브랜치를 생성합니다. 

4) develop 브랜치에서 feature/step01 브랜치를 생성합니다.  

5) feature/step 01 에서 개발을 합니다. 

6) feature/step01 --> develop 으로 PR 을 요청합니다. 이떄 반드시 리뷰어를 등록합니다. 


7) 코드리뷰를 기다립니다.

8) 리뷰에 대한 수정사항을 반영합니다.

9) 최종 머지 합니다. 

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari