본문 바로가기
멘토링/Codesoom

가장 많은 것을 배운 주차

by Alex_beom 2024. 3. 9.
이 글은 필자의 이전 블로그인 https://velog.io/@beomdrive/CodeSoom-5에서 핵심 내용을 중심으로 요약한 후에, 내용을 추가 보완하였습니다.

 

이번 주에는 지난주에 개발했던 장난감(Product) API에 요청 값 검증(validation) 기능과 회원 생성, 수정, 삭제 API를 TDD로 구현하는 미션이었다.

이제 코드숨의 절반이 지나는 5주차로 막 절반이 지나고 있는 현시점에도 종립 멘토님과 영환 멘토님 덕분에 많은 것을 배우고 있다는 것을 느끼면서, 코드숨 시작 전의 내 모습과 지금의 내 모습을 볼 때 꽤 성장한 것 같아 보여 뿌듯함을 느꼈다.

 

이번 주에는 지금까지의 과정 중에 가장 많은 것을 깨달았던 주차이기도 하다.

그중에서 이번 글에는 객체 지향과 DTO <-> Entity의 새로운 시야를 넓히게 된 계기와 TDD에 대해 다시 한번 생각해 본 것을 기록할 예정이다.

(모든 내용이 궁금하다면 https://github.com/CodeSoom/spring-week5-assignment-1/pull/80에 있는 70개의 코멘트들을 쭉 한번 읽어보는 것을 추천한다 🫠)


Exception과 HTTP를 분리하기

먼저 해당 작업은 기존에 스스로 개발해 보았던 Exception 핸들링에 관한 부분을 객체지향적으로 개선하는 과정이다.

기존에는 2주차 때 코드숨 2주차- Exception Handling에서 작성한 내용대로, 전역적으로 발생하는 Exception에 대해서 핸들링할 수 있도록 구현해 놨었다. 이에 대해 종립 멘토님은 아래와 같은 피드백을 주셨다.

https://github.com/CodeSoom/spring-week5-assignment-1/pull/80#discussion_r1017869921

여기서 아래와 같은 의문이 들었다

  • Exception에서 HTTP 관련 의존성을 떼어낸다면 어떤 것이 좋을까?
  • 객체지향에서 의존이란 무엇일까?
  • 떼어낸다면 어떻게 떼어내야 할까?

해당 고민들에 대한 결과를 요약해 보면 Exception 객체는 유의미한 예외 정보를 리턴하는 책임을 가지고 있고, HTTP는 여러 네트워크 통신 방법 중에 하나일 뿐이다. 즉, HTTP 통신이 아닌 상황에서는 해당 Exception 객체를 재사용할 수 없게 된다.
대부분의 현업에서는 HTTP 통신을 위주로 하기 때문에 Exception 객체에서 HTTP를 관리하는 것이 실용적일 수는 있지만, 객체 지향적인 관점에서는 관심사를 분리해 주는 것이 바람직해 보였다.

 

관련 내용을 상세하게 따로 작성해 놓은 글을 아래 링크로 첨부하겠다.


트레이드오프

5주차 강의에서는 Dozer Mapper라는 라이브러리를 사용해 객체 간 매핑을 진행했다.

하지만 라이브러리 공식문서를 확인해 보니 아래와 같이 deprecated 될 예정이니 사용하는 걸 권장하지 않는다는 말이 떡하니 있었다.

The project is currently not active and will more than likely be deprecated in the future. If you are looking to use Dozer on a greenfield project, we would discourage that. If you have been using Dozer for a while, we would suggest you start to think about migrating onto another library, such as:
- mapstruct
- modelmapper

 

 

또한 Dozer와 ModelMapper는 내부적으로 리플렉션을 사용하여 성능이 비교적 좋지 않다는 것을 아래 문서에서 발견했다.

 

물론 크리티컬한 성능 저하가 아닌 마이너한 수준이긴 하지만, 사용성 또한 MapStruct와 ModelMapper 둘 다 편하고 레퍼런스도 많기 때문에 MapStruct를 사용하기로 결정을 하고, 종립 멘토님께 선 코멘트를 남겼더니 아래와 같은 답변을 해주셨다.

https://github.com/CodeSoom/spring-week5-assignment-1/pull/80#discussion_r1016237314

회사에서 동료들을 설득하기 위한 MapStruct의 침투율과 제거 시나리오에 대한 보고서를 작성해보았고, 내용을 요약하면 다음과 같다.

침투율: 해당 기술이 어느 깊이까지 침투(사용)할 것 같은지 / 추후 걷어낼 때의 비용을 고려하기 위함

MapStruct는 매핑 라이브러리이기 때문에 Presentation Layer의 DTO 영역에서만 사용될 예정입니다.
RequestDTO -> Entity & Entity -> ResponseDTO

또한, MapStruct의 매핑할 때 생성할 Mapper 인터페이스는 각 dto 패키지에 별도 패키지로 관리하고 실제 MapStruct 사용 코드는 기존 DTO 객체의 매핑 메서드에 사용을 할 예정입니다.
이렇게 관리한다면 만약 추후 MapStruct를 걷어낼 때 각 인터페이스들을 삭제하고, DTO 객체의 매핑 메서드만 수정한다면 실제 사용하는 쪽에는 코드 수정이 일어나지 않아서 변경을 최소화할 수 있습니다.

 

종립 멘토님은 정말 다양하고 넓은 범위로 트레이닝을 해주시는 덕분에, 많은 것을 생각해 보면서 배울 수 있는 것 같아서 매우 좋다 😁


ResponseDTO has a Entity

나는 개발을 하다가 아래의 내용이 궁금해서 종립 멘토님께 질문을 드렸다.

모든 도메인에서 동일하게 RequestDto -> Command -> Entity -> ResponseDto 변환(매핑)이 진행할 것으로 보이는데, 너무 많은 보일러플레이트 코드가 생기는 것 같습니다.
따라서 매핑 기능을 공통 객체로 추상화를 해보고 싶은데, 어떤 식으로 진행해야 될지 모르겠습니다 😂

 

종립 멘토님은 아래와 같이 감사하게 본인의 노하우를 대방출해주셨다.

https://github.com/CodeSoom/spring-week5-assignment-1/pull/80#discussion_r1016577933

 

그중에서 ResponseDto가 Entity 객체를 몰래 가지고 있는 방법은 생각하지 못했던 방법이라 신기해하면서 학습했다.

ResponseDto has a Entity는 아래와 같은 코드로 짤 수 있다.

public class CreateProductResponseDto {

    @JsonIgnore // 몰래 가지고 있기
    private Product product;

    public CreateProductResponseDto(Product product) {
        this.product = product;
    }

    public Long getId() { // 응답에 필요한 값만 꺼내기
        return this.product.getId();
    }

    public String getName() { // 응답에 필요한 값만 꺼내기
        return this.product.getName();
    }
}

 

이렇게 내부적으로 Entity를 가지고 있으면 일종의 소규모의 repository 역할을 하게 되고, DTO는 원본 데이터를 특수하게 보여주는 View의 역할이 된다.

 

 

근데 여기서 나는 한가지 의문이 들었다. "변수가 없는데 어떻게 클라이언트로 반환이 되는 거지?"

이에 대한 문제 상황 - 원인 - 해결 - 참고자료 순으로 분석 결과를 따로 기록해 놓았다.

요약하자면 일반 @Controller는 응답 반환 시 ViewResolver로 전달되고, @RestController 또는 @Controller + @ResponseBody가 붙어있다면 MessageConverter로 전달된다.
그리고 MessageConverter는 알아서 반환 타입에 맞는 Converter로 위임 하는데, 여기에서 나는 CreateProductResponseDto를 통해 객체에 데이터를 담고, application/json 콘텐츠 타입으로 반환하고 있으므로 MappingJackson2HttpMessageConverter로 응답이 위임된다.

이때 Jackson이 데이터를 매핑할 때 Java의 프로퍼티(getter, setter)를 통해 매핑하므로, 별다른 변수를 선언하지 않아도 getter만 존재한다면 정상적으로 반환되는 것이었다.
또한, getter 사용 대신 멤버 변수를 사용하고 싶다면 @JsonProperty를 지정해 주면 된다.

public class CreateProductResponseDto {

    @JsonIgnore
    private Product product;
    
    @JsonProperty
    private Long id;
    
    @JsonProperty
    private String name;

    public CreateProductResponseDto(Product product) {
        this.product = product;
        this.id = product.getId();
        this.name = product.getName();
    }
}

TDD 리마인드

마지막으로 평소에 헷갈렸던 TDD에 관한 내용을 정리해보려고 한다.

 

TDD와 MVC 계층(Repository/Service/Controller)의 개발 순서는 별개이다

TDD 방법론은 단순히 프러덕션 코드를 작성하기 전, 테스트를 먼저 작성하는 방법이다.

따라서 TDD는 일반 MVC 계층 구조의 서비스 레이어인 (R/S/C)와는 완전히 별개이므로, 분리해서 생각하여야 한다.

 

나는 TDD에서는 Classicist TDD와 Mockist TDD를 통해 MVC 계층 구조를 Inside-Out 방식 혹은 Outside-In 방식을 사용한다는 것에 집중했었는데, 본질을 제대로 이해해야 될 것 같다.

TDD는 단순히 테스트를 먼저 작성하는 개발 방법론일 뿐이고, 그 위에 MVC 같은 계층 구조에서의 개발 방법이 Inside-Out 방식 혹은 Outside-In 방식으로 도출되는 것인 것 같다.

 

일반적인 TDD 방법론에 따른 개발 순서는 다음과 같다.

  1. 테스트를 먼저 작성해서 테스트가 실패할 수밖에 없는 상황을 만든다. (프러덕션 코드가 없기 때문)
  2. 테스트 코드의 피드백을 받으며 테스트가 성공되도록 코딩한다.
  3. 테스트가 성공되게 프러덕션 코드를 작성했다면, 리팩토링을 진행한다.
  4. 1 ~ 3 반복

 

테스트 케이스 작성 순서에 대한 코드숨 추천 방법은 아래와 같으니 참고만 해보자.

  1. 모든 게 올바르게 동작하는 것을 가정하는 Happy path에 대한 테스트를 먼저 작성한다.
  2. 테스트가 통과되도록 구현 후에 리팩토링을 진행한다.
  3. 예외 케이스에 대한 테스트를 작성하고 구현, 리팩토링을 진행한다.

이런 순서를 추천하는 이유는 처음부터 너무 많은 기능과 예외 케이스를 생각하다 보면 복잡성에 압도되어 구현하기 어려워지기 때문이라고 한다.

하지만 개인적으로 테스트 코드 작성은 성공 케이스든, 실패 케이스든 제일 먼저 생각나는 테스트 케이스부터 만들면 된다고 생각한다.

 

TDD 관련해서 개념들이 헷갈렸는데 이번 주차에서 종립 멘토님이 제대로 잡아주셔서 다시 한번 정리하게 된 것 같다 😊

https://github.com/CodeSoom/spring-week5-assignment-1/pull/80#issuecomment-1312718333

 

1년 반이 지난 시점에서 현재 필자는 TDD를 선호하지 않는다. 정확히 얘기하면 TDD가 익숙하지 않고 어렵다.
보통 실무에서의 요구사항은 매우 복잡하다. 그래서 나는 개인적으로 비즈니스 로직을 짜면서 이해도를 높이곤 한다.
따라서 프러덕션 코드를 짜기 전에는 머릿속이 백지상태라 무엇을 어떻게 테스트로 검증해야 할지 생각이 잘 안 난다.
그러기에 나는 프러덕션 코드를 우선 작성하면서 떠오르는 검증 사항들을 모두 적어놓은 후에, 로직을 모두 작성하면 그 후에 테스트 코드로 검증 사항들을 모두 녹여낸다. 내가 작성한 로직이 잘 돌아가는지 확인하는 느낌으로 말이다.

사실 나는 이 정도로도 충분하다고 생각한다.
무조건 프러덕션 코드보다 테스트 코드를 먼저 짜야되나? 솔직히 나는 TDD를 실무에서 잘 적용하는 사람들을 많이 보지는 못했다. (당연히 아예 없진 않다😅)
물론 검증해야 할 요구 사항들을 테스트 코드로 미리 다 짜놓은다면, 프러덕션 코드를 짤 때 그 테스트가 등대 역할을 하여 오버 엔지니어링을 하지 않고 깔끔하게 코드를 짤 수는 있을 것이다. 또한, 사내의 업무 체계가 확실하게 잘 잡혀있는 경우 테스트 케이스는 PO 측에서 내려올 테니 좀 더 수월하게 테스트 코드를 먼저 짤 수도 있을 것이다.

결론적으로 필자가 하고 싶은 얘기는 다음과 같다.
- 요구 사항이 비교적 쉽거나 이해가 잘 될 경우에는 TDD를 시도해 본다.
- 요구 사항이 어렵다면 무조건 테스트 코드를 먼저 짜야 된다는 강박으로 퍼포먼스가 떨어지면서 스트레스받을 바에는, 프러덕션 코드를 먼저 짜면서 검증할 요소들을 생각하고 테스트 코드를 나중에 짠다.
- 테스트 코드를 먼저 짜든, 나중에 짜든 우리한테 중요한 건 퍼포먼스가 너무 떨어지지 않도록 비즈니스 요구 사항을 개발해 나가고 배포 나가기 전에만 핵심 로직들을 여러 검증 요소들을 통해 테스트 코드로 커버해 나간다면 이미 충분하다고 생각한다.