- Chapter 1 프로젝트 피드백
- 정적 팩토리 메서드 및 빌더 세팅
- Stream과 for
- UserDetails
- 프로젝트 기본 구성
- 컨벤션
- Page 정보를 이용한 요청
- Page 객체를 이용한 DTO 리턴
- restful api 설계
- 디렉토리 구성
- 마무리
Chapter 1 발표 이후, 튜터님께 피드백을 받은 내용에 대해 정리하고 어떻게 리팩토링을 진행할 수 있는가에 대해 생각해 보았다.
의도 파악을 원활히 하고 및 필드 입력 순서 휴먼에러를 줄이기 위해 정적 팩토리와 빌더패턴을 사용하시는 것은 좋은 방향
responseDTO는 현재처럼 하되, entity에 static fromDTO를 두는 것보다는 requestDTO에 toEntity를 두는 것이 좋음
- 다양한 DTO가 늘어날수록 Entity 내부 코드 복잡성이 증가.
Entity는 도메인 로직과 데이터 검증에 집중해야 하며, DTO는 변환 및 전달 역할에 충실- 레이어 또는 레벨의 위치에 따른 계층 간 의존성을 위반할 수 있음
서비스가 컨트롤러를 알 필요가 없지만 반대로 컨트롤러는 서비스에 의존
즉, 의존성 방향은 Presentation/DTO → Service → Domain(Entity)으로 유지해야 계층 구조가 깨지지 않으며, 이를 통해 설계가 더 일관되고 유지보수가 쉬워짐
로우 레벨에 있는 Repository와 Entity는 복잡하지 않고 간결한 것이 좋음- 직관적 설계가 가능
DTO에서 public ArticleEntity toEntityWith(UserEntity userEntity) {}
위와 같은 방식으로 구성하여 서비스에서 사용할 경우,
articleRepository.save(dto.getArticle().toEntityWith(userEntity));
위와 같이 사용된다.
'DTO를 Entity로 변환하고 Repository를 통해 저장할거야' 라는 의도를 주석 없이 명확하게 표현할 수 있다.
현재 대부분의 entity에 from~ 정적 팩토리 메서드가 존재한다. Entity 코드의 복잡성이 확실히 높다고 볼 수 있다. 이것보다는 dto에 toEntity 메서드를 적용할 수 있도록 리팩토링이 필요하다.
계층 간 의존성의 문제도 있다. Entity와 Repository는 최하위 계층이다. 이들은 상위 계층들의 존재를 전혀 알 필요가 없다. 따라서 계층 간 데이터를 전달하는 역할인 dto에 대해서도 알 필요가 없는 것이다. (의존성 방향은 Presentation/DTO → Service → Domain(Entity))
그러므로 toEntity 메서드를 requestDto에 추가하는 것이 더 합리적이다.
네이밍을 통해 의도를 드러내는 것을 중요하게 생각했는데, toEntity~를 위시한 방식으로 로직을 수정하는 것이 의도를 드러내는 것 뿐만 아니라 여러 방향에서 더 유리하다고 생각된다.
for문이 복잡한 로직을 자유롭게 구성할 수 있고 오버헤드가 적고(대부분 성능이 좋고), break, continue 등으로 반복제어도 가능한 장점이 있음
그럼에도 불구하고 웹 및 앱 백엔드 실무에서 Stream을 많이 사용
- 게임 같이 빠른 처리 및 응답이 중요한 경우, 성능 효율성을 크게 따져야 함
- 하지만 일반적인 웹 데이터를 주고받는 경우 네이버 메인페이지를 열었을 때 조금 늦게 뜬다고 문제 되지 않는 것 처럼 0.1초가 걸리든 0.101초가 걸리든 크게 문제가 되지 않음
- 그것보다는 협업과 유지보수에 중요한 가독성, 함수형 프로그래밍의 장점인 불변성, 연산 체이닝을 통한 Lazy Evaluation, parallelStream을 통한 병렬처리 등등 장점이 더 많기 때문에 일반적인 경우 Stream을 사용
- 물론, Stream은 단순한 반복문이 아니기 때문에, 상황에 따라 for문을 선택할 수 있는 유연성도 필요
이 부분은 기존에도 리뷰를 나눴던 부분이다. 내 코드에서는 stream을 사용하지 않고 for문으로 처리한 부분이 있다. 이 부분을 stream으로 리팩토링 하는 방향이 나아보인다. stream은 단순 반복만을 위한 것이 아니고, 피드백을 주셨듯 여러 장점이 존재한다. 상황에 따라 선택해야겠지만, stream 대신 for문을 선택해야 할 정도의 상황은 없는 듯 하다.
UserDetails를 커스텀하여 사용 시, UserEntity나 UserDTO 등을 필드로 포함하여 편리하게 사용하는 경우가 많을 것임
- UserDetails에 UserEntity를 둔다고 하더라도 그것은 영속성이 끊어진 단순 객체
- Controller에서 Service로 넘겨서 사용한다고 하더라도 수정을 한다던가, 다른 엔티티의 저장시 필드로 포함한다던가하면 문제가 발생
- Service에서 단순히 본인 밸리데이션을 하는 경우(id값 비교)나 권한확인 등을 목적으로 UserDetails를 사용
- 기타 영속화된 UserEntity가 필요한 경우에는 서비스에서 다시 UserDetails로 UserEntity를 쿼리하여 사용하도록 기준을 정하는 게 좋음
현재는 controller에서 service로 UserDetails의 getusername()을 통해 username을 넘기고, 이걸 통해 repository에서 user를 찾는 방식으로 모든 로직이 통일되어 있다.
이 부분에 대해서는 고민을 많이 했던 부분인데, 튜터님께서 피드백을 주신 대로 단순 본인 검증의 경우 userDetails를 직접 사용하는 부분이 좋지 않나 생각하고 있었다. 이 방향으로 리팩토링을 진행해야겠다.
그 외에 정말로 User Entity가 필요한 부분은 쿼리를 사용하는(repository에서 호출) 방식을 그대로 유지하는 것이 좋을 것 같다.
- 큰 회사에서는 보통 CTO 또는 팀장급의 설계자들이 뼈대를 거의 설계하신 뒤 서비스 등의 기능 구현만 하위 직원이 진행하는 경우가 더 많을 것
- 신입사원이 프로젝트를 설계하는 일은 스타트업이나 작은 회사의 테스트성 프로젝트일 가능성이 높음. 그렇기 때문에 이러한 과정에서 프로젝트를 구성해보고 시행착오를 겪어보는 것도 좋은 경험이라고 생각
- 개발을 하는 것만큼 공을 들여야하는 것이 기획과 설계이며, 분신이 아닌 한 처음부터 딱딱 나눠서 원활하게 진행되기가 어렵고 속도가 나기 어려움. 오히려 혼자 작성하는 것보다 느릴 수 있음.
- 차후 프로젝트에서는 기본 설계 및 구성까지는 팀원들이 다 같이 모여서 논의하며 의견 정리를 잘하는 한명의 사람이 화면공유를 하고 코딩을 하여 뼈대를 구성해두고 이후에 각자 분업을 하시는 것을 추천
- 서로 의견이 합치된 상태에서 작성한 코드를 모두 알고 있는 상태에서 나눠서 진행하게 된다면 뼈대 구성까지는 조금 느릴 수 있으나 이후에는 진행에 속도가 붙을 수 있음
정말 뼈저리게 느꼈던 부분이다. 특히 뼈대가 될 코드까지 구성해두라는 피드백이 참 인상깊다. 튜터님께서 추천해주신 대로 기본 설계 및 구성까지는 팀원들이 다 같이 모여서 논의하며 의견 정리를 잘하는 한명의 사람이 화면공유를 하고 코딩을 하여 뼈대를 구성해두고 이후에 각자 분업을 하는 방향이 좋아보인다. 이번 프로젝트에서는 기본 설계 및 구성까지는 다 같이 모여서 하긴 했지만 각자 맡은 도메인에 대해서는 따로 설계하고 큰 이상이 없어보이면 그대로 진행했었는데, 이 부분이 문제가 된 적이 참 많았다. 이후엔 제대로 된 방식으로 진행해야겠다.
- 협업 중 비슷한 로직이어도 구현 방식의 차이로 작업 속도가 지연되었다고 하셨는데, 회사에서도 각자 관점, 스타일, 구현 방식 등이 선호하는 방식이 모두 다른 것은 마찬가지
- 그렇기 때문에 '이번 프로젝트에서는 이러한 규칙을 미리 정해서 프로젝트를 진행하겠습니다'라는 내용의 가이드 문서가 있어야 함
- 완전히 마음에 들지 않더라도 서로 합의가 가능한 선에서 진행하여 비슷한 코드가 나오도록 해야, 추후 해당 프로젝트를 담당하는 직원이 바뀌어서 원래 개발했던 개발자들이 모두 회사에 없는 상태에서도 유지가 될 것
- 프로젝트를 진행하는 도중에 생기는 미리 정하지 못한 규칙도 회의를 통해 조율하고 결과를 컨벤션에 추가로 정리해두어 문서로 남기는 것이 좋음
이전 파트에서 이어지는 부분이라고 생각한다. 가이드 문서가 없었던 부분이 정말 큰 문제가 됐던 것 같다. 뼈대가 될 코드를 구성해 두고, 이를 바탕으로 규칙을 미리 정한 가이드 문서를 작성한다면 비슷한 코드가 나오게 될 것이다. 그렇게 되면 유지보수성이 매우 올라갈 것이다.
- 컨트롤러 매개변수에서 페이지 번호, 사이즈, 정렬기준 등을 매개변수로 쭉 받아서 처리하는 내용이 있음
- 이렇게 사용해도 무방하나,
@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable 와 같은 어노테이션 및 객체를 이용해서 처리하는 것이 좋음- 서비스에서 JpaRepository에 요청 시 pageable 객체만 추가해 주시면 페이징 처리되어서 리턴됨
처음에는 이 부분을 인지하지 못했었는데, 프로젝트를 진행하면서 알게 되었고 '나중에 처리해야겠다' 라고 생각만 하고 적용을 못 했다. @PageableDefault로 기본값을 설정해 주는 것이 좋을 것이다. 이 방향으로 리팩토링을 진행해야겠다.
- 다건 조회 등에서 Page 객체를 조회한 뒤 리턴하고 있음
- Page나 PageImpl 객체를 이용해서 리턴할 경우, json이 생성이 안되거나, 필요없는 pagination 정보가 많이 포함됨
- 스프링에서 권장하는 PageModel 객체를 이용하거나 상속해서 DTO를 만들어 보는 것을 추천
- 꼭 필요한 pagination 정보만 출력되어 깔끔함
PageModel의 존재에 대해서는 알고 있었지만, 페이징 처리에 대해 익숙하지 않다보니 이런 문제가 발생했던 것 같다. PageModel에 대해 학습을 진행해 보고, 이것을 활용하는 방향으로 리팩토링 하는 것이 좋을 것 같다.
경로를 restful하게 구성하려고 노력한 부분이 보이지만, 아쉬운 부분도 있음.
- GET /api/v1/users/search
-> GET /api/v1/users?쿼리스트링
search 없이 조회 하거나
-> GET /api/v1/search/users
따로 도메인을 구성하는 것이 좋음.- POST /api/v1/users/signup
-> POST /api/v1/auth/signup
따로 도메인을 구성하는 것이 좋음.- GET /api/v1/orders/stores/{storeId}
-> GET /api/v1/orders?storeId=ㅁ
주문이 주 관심사면 이렇게
-> GET /api/v1/stores/{storeId}/orders
가게가 주 관심사면 이렇게 구성 할 수 있음.- GET /api/v1/reviews/user
-> GET /api/v1/reviews?username=ㅁㅁ
-> GET /api/users/{username}/reviews- PUT /api/v1/orders + DTO
-> PUT /api/v1/orders/{orderId} + DTO
위에서 예를든 몇가지 뿐만 아니라 다른 부분도 확인해보길 바람.
솔직히 restful하게 설계되지 않은 부분이 굉장히 많다고 생각한다. URI만으로도 그 의도를 충분히 드러낼 수 있어야 하는데, 그렇지 못한 부분이 굉장히 많다. 이 부분은 Chapter 1 팀원들과 API 명세서를 전체적으로 확인하며 리팩토링하는 시간을 가져보아야 할 것 같다.
model 디렉토리에 각 엔티티를 기준으로 entity, repository 디렉토리를 구성해서 파일을 배치하고,
domain 디렉토리에 각 도메인을 기준으로 controller, dto, service 디렉토리를 구성해서 파일을 배치했으면 더 좋지 않았을까라는 생각이 듦.
- domain
├── user
│ ├── controller
│ │ └── UserController.java
│ ├── dto
│ │ ├── UserRequestDto.java
│ │ └── UserResponseDto.java
│ └── service
│ └── UserService.java
│
└── order
├── controller
│ └── OrderController.java
├── dto
│ ├── OrderRequestDto.java
│ └── OrderResponseDto.java
└── service
└── OrderService.java- model
├── user
│ ├── entity
│ │ └── User.java
│ └── repository
│ └── UserRepository.java
└── order
├── entity
│ └── Order.java
└── repository
└── OrderRepository.java
이 부분에 대해서도 고려하고 있었는데, 의견만 남기고 진행하지 못했다. 점점 클래스들이 늘어가면서 원하는 클래스를 찾는 게 너무 힘들었다. 패키지 구조를 위와 같이 나누면 원하는 클래스를 찾는 것이 쉬워질 것으로 기대된다.
프로젝트에 엉망인 부분이 참 많았는데 정말 열심히 피드백을 해주신 튜터님께 감사의 말씀을 드리고 싶다. 피드백을 주신 내용과, 발표 중 다른 조에게 피드백을 주신 내용을 바탕으로 팀원들과 스터디를 해 보고, 결론을 내리고 튜터님께 한번 더 검토를 받은 후 리팩토링을 진행할 수 있도록 해야겠다.