⌨️ 인프런 워밍업 클럽 2기 미션 Day-15
이번 미션은 Layerd Architecture의 각 레이어별로 어떤 특징이 있고, 어떻게 테스트하면 좋을지 나만의 언어로 정리하는 것이다.
1. 레이어 별 특징
레이어를 3단계 혹은 4단계로 나누는데 나는 3단계로 나누어서 작성토록 하겠다.
1.1 Persistence Layer
✅ Data Access의 역할
✅ 비즈니스 가공 로직이 포함되어서는 안 된다. Data에 대한 CRUD에만 집중한 레이어
DB의 연결만을 위한 Layer라고 생각한다. 간혹 비즈니스 레이어 (서비스 레이어)에 DB의 연결과 쿼리까지 있는 경우가 있는데,
유지 보수하기도 힘들었던 기억이 있다.
크게는 두 분류의 프레임워크를 사용하는데 ORM과 SQL Mapper다.
간혹 ORM이 SQL Mapper보다 최신 기술이고 뛰어나다고 생각하고 또 초반에 그렇게 잘못 배우는 경향이 있는데
두 기술은 서로 지향하고 장-단점이 다른 것이지 비교 우위의 관계에 있진 않다고 생각한다.
⭐ ORM, SQL Mapper
ORM
- 객체 지향 패러다임과 관계형 DB 패러다임의 불일치
- 이전에는 개발자가 객체의 데이터를 한 땀 한 땀 매핑하여 DB에 저장 및 조회
- ORM을 사용함으로써 개발자는 단순 작업을 줄이고, 비즈니스 로직에 집중할 수 있다.
SQL Mapper
- 객체와 SQL의 필드를 맵핑하여 데이터를 객체화하는 기술
- ORM처럼 객체와 테이블 간의 관계를 매핑한 것이 아닌,
SQL을 직접 작성하고 쿼리 수행 결과를 어떠한 객체에 매핑하여 줄지 바인딩하는 방법으로 SQL에 의존적
1.2. Business Layer
✅ 비즈니스 로직을 구현하는 역할
✅ Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.
✅ 트랜잭션을 보장해야 한다.
우리 서비스의 메인 비즈니스가 전개되는 공간이기 때문에, 정말 소중히 다뤄야 할 클래스라고 생각한다.
뭔들 안 소중하겠냐마는.. 그래도 Core가 어디냐 묻면 나는 이 Layer라고 말하고 싶다.
🤔 요즘 내가 활용하는 구현법
Business Layer를 한번 더 쪼개서 Business Layer와 Implements Layer를 구분하는 것이다.
( Service - ServiceImpl의 전략 패턴을 말하는 것이 아님)
그렇게 하는 이유는 구현 영역과 비즈니스가 전개되는 서비스영역을 구분하기 위함이다.
이렇게 함으로써 장점은 비즈니스 영역의 코드가 간결해지고 우리의 서비스를 한눈에 명확히 알 수 있고, 순환 참조를 예방할 수 있다.
반면에 복잡도(패키지와 클래스의 수 증가)가 증가하기에 적절한 관리 전략이 필요하다.
1.3. Presentation Layer
✅ 외부 세계의 요청을 가장 먼저 받는 계층
✅ 파라미터에 대한 최소한의 검증을 수행한다.
클라이언트와 소통을 하는 공동현관 같은 곳이다.
공동현관에서 세대를 호출하듯이, 우리 서비스에 접근할 수 있는 엔드포인트를 제공한다.
그러므로 이 영역에서는 비즈니스 로직이 들어가기보다 넘겨온 값들에 대한 검증을 하는 것이 중요하다고 생각한다.
파라미터의 값의 길이 제한 등
파라미터의 값의 길이를 제한하는 것이 비즈니스 영역인지 통신의 영역인지를 잘 고민해 보는 것을 추천함
2. 레이어 별 테스트 전략
2.1 Persistence Layer
✅ @DataJpaTest
Data JPA 컴포넌트들(JPA에 의해 자동 생성되는 Proxy 객체)을 테스트할 수 있는 환경을 만들어준다.
Data Jpa 컴포넌트만 불러오기 때문에 다른 객체를 가져오려고 하면 에러가 발생한다.
@DataJpaTest 어노테이션에는 기본적으로 @Transactional이 들어가 있기 때문에 모든 테스트가 롤백된다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {...}
✅ @SpringBootTest
위에서 설명했다시피 @DataJpaTest는 Data Jpa 컴포넌트만 지원하기 때문에 그 외의 다른 컴포넌트를 테스트하려면 다른 방법을 사용해야 한다. 이때 사용할 수 있는 게 @SpringBootTest 어노테이션이다.
예를 들어, QueryDsl을 사용해 만들어진 Repository 구현체는 Data Jpa 컴포넌트가 아니기 때문에 @DataJpaTest 어노테이션으로는 주입받을 수 없는 대신 @SpringBootTest를 사용할 수 있다.
🤔 나는 SpringBootTest를 선호한다.
여러 가지 이유들이 있지만, Persistence Layer를 한번 더 추상화해서 구현하는 것을 좋아한다.
사실 그럴 일은 많이 없지만 그렇게 하지 않으면 Layer가 JPA에 강결합되기 때문이다.
내가 원하는 것은 save()를 했을 때 DB에 데이터가 저장이 되는 것이지 그 사이에서 어떠한 일이 일어나는지는 중요하지 않다.
즉 Data Jpa가 아닌 경우에도 Repository의 구현체를 갈아 끼울 수 있고, 그럼에도 테스트는 정상 동작해야 한다는 생각이다.
2.2 Business Layer
✅ @Transactional에 대해
테스트코드에서 @Transactional 사용에 대해
@Transactional이 없으면 변경 감지 기능이 사용이 안 됨
- 테스트 코드에서 사용하려면 부작용에 대해 잘 알고 사용하자
이 레이어에선 Transactional 사용이 많이 갈릴 것 같다. 관련해서 몇 개 링크를 주자면
https://youtu.be/mB3g3l-EQp0?si=Yr2n5UWMqx-N9wWE
https://youtu.be/-961J2c1YsM?si=z5aijIBBtjlXQqsO
https://jojoldu.tistory.com/761
등등이 있다.
위의 Persistence Layer에서도 DataJpaTest 보다 SpringBootTest를 선호한다고 말했는데, DataJpaTest에서는 내부적으로 @Transactional이 있기 때문이다
주의해서 사용해야 하기 때문에 모든 팀원들이 꼼꼼히 공유되지 않았다면 문제를 일으킬 가능성이 있다.
✅ 복잡한 로직에 대한 검증?
테스트를 짜면서 가장 힘들었던 부분이고 비즈니스는 복잡하기 때문에 이 부분에 대해 고민을 많이 했다.
내가 내린 결론 그리고 전략은 분할 정복이다.
한 번에 결과를 도출해 내기 힘들다면 부분 부분으로 개발자가 검증할 수 있고 예측가능한 결과를 받는 모듈로 쪼개어 계속해서 검증된 결과를 다음 테스트에서 활용하는 것이다.
첫째로, 한 번에 큰 기능을 수행하는 메서드는 테스트가 힘들기 때문에 테스트가 힘들면 프로덕트의 구조나 코드가 아쉬운 부분이 있을 것이다라는 피드백을 반영했고
두 번째로는 내가 계산할 수 없는 영역은 안에서부터 차근차근 테스트를 통해 그 결괏값으로 다음 assert를 추측해 나가는 것이 어떨까? 하는 접근이었다.
나는 그래서 비즈니스 테스트 코드에서 리팩토링이 굉장히 많이 발생하는 타입이며, 코드 퀄리티를 높일 때 이 부분을 중점적으로 꼼꼼히 테스트하려고 한다.
2.3 Presentation Layer
⭐기본적으로 Data의 기본적인 validation은 필수!!!
- 나의 경우 비즈니스 로직이 아닌 Data 형태가 응당 가져야 할 형태가 올바르게 통신되는지 확인함
정말 순순하게 잘 들어왔고 기대한 것으로 잘 나가는 지를 확인한다.
거기에 중요한 부분이 곁들여진다면,
request header, 그런데 이제 token을 곁들인
그런 부분을 꼼꼼히 검증한다.
또한 file이나 어떤 blob 한 데이터가 오고 가야 한다면, 내가 생각하는 용량 혹은 네트워크 트래픽의 수준을 상회하진 않는지 검증한다.
즉 header - payload에 대해 꼼꼼히 검증하고
response도 원하는 대로 나가는지 검증할 것이다.
❓정리
각 레이어에 대해 테스트 전략을 어떻게 해야 할지 조금 깊게 생각해 보는 경험이 됐다.
아직 소화가 덜 되어 여기에 다 녹이진 않았지만,
이외에도 더 많은 것들을 고민하고 있으니 향후 다시 한번 회고에 정리해서 올려야겠다.
🌟REFERENCE
인프런 : 박우빈 - 읽기 좋은 코드를 작성하는 사고법
유튜브 : 토비의 스프링 제미니의 개발 실무
블로그 : 향로님 블로그 - https://jojoldu.tistory.com/761