우아한테크코스 레벨2 장바구니 미션 정리

디우·2022년 6월 26일
0

1 단계 - 회원 기능

장바구니 미션의 경우 다른 미션과 달리 4~5명의 백엔드 크루와 2명의 프런트엔드 크루가 함께 미션을 진행하는 협업 미션이었다.
따라서 우리팀만의 API 명세를 도출하고, 협업 룰 및 일정 조율 등을 할 수 있었다.
장바구니 미션 Notion 페이지


1 단계 - 회원 기능

장바구니 1단계 저장소 링크

PR 링크

리뷰어: 로운

고민한 내용

API 명세

Front와 논의하여 도출한 API 명세에 대한 정리를 Notion Page에 해두었는데요, 혹시 몰라서 API 명세에 대한 부분은 따로 API.md 파일에 정리해두었습니다.

이번 1단계에서는 회원 기능 에 초점을 맞추어서 구현을 하였습니다.
따라서 Token 기반의 로그인, 회원가입, 회원 정보 수정, 회원탈퇴에 대한 API를 도출하였습니다.

API.md 파일에 API에 대한 명세 이외에도 어떤 값을 가지고 회원가입 및 로그인을 할지 그리고 형식은 어떻게 되는지를 함께 정리해두었습니다!

수정

수정 부분에서는 회원의 일반적인 정보(이름 등) 수정과 비밀번호 수정을 분리하였습니다.
Front에서 일반적인 정보를 수정하는 페이지와 비밀번호 수정 페이지를 별도로 두기로 하였고, 비밀번호 수정시에는 기존 비밀번호를 함께 입력받기로 하여 이 둘을 분리하였습니다. (비밀번호 수정과 일반 정보 수정의 입력과 응답이 다름)

이 때, Controller에서 어떤 메소드를 호출할지에 대해서는 URI의 query parameter를 통해서 분리하기로 하였습니다!!
(저희가 생각했을 때는 이게 제일 좋은 방법이라고 생각하여 이 방법을 선택하였는데, 혹시 더 좋은 방법이 있을지에 대해서는 계속해서 고민해보도록 하겠습니다.🤔)

Refresh Token

저희 팀은 이번 미션에서 Refresh Token에 대한 구현없이 진행하기로 하였습니다.
Refresh Token에 대한 구현도 함께 가자는 의견이 지배적이었으나, JWT Token 자체를 처음 접하는 크루들이 절반정도로 꽤나 많아서 이번 미션에서는 Token 기반의 인증이 어떻게 이루어지는지 만을 익혀도 좋을 것 같다는 결론이 나와 AccessToken만을 이용하여 진행하게 될 것 같습니다.

다음은 저희 고민 내용(왜 Refresh Token을 사용해야 하는지..)을 정리한 것입니다.

하지만 저도 제대로 Refresh Token을 사용해본 적이 없어 이번 기회에 사용해보고 싶었는데요, 아직 아쉬운 마음이 들어 한 번 정도는 더 어필할 생각이고, 2단계에서 구현을 진행하게 된다면 구현을 진행한 내용을 포함하여 PR 보내도록 하겠습니다!

로운 : 말씀하신 것처럼 token의 개념자체를 이번에 처음 학습하는데 refresh token까지 적용하는 것은 어려울 수 있어요.
다만, 시간이 되고 다른 협업하는 크루들의 동의를 모두 얻었다면 당연히 refresh token도 구현을 해보는 것을 추천드립니다.
해보면서 느끼고 학습하게 되는 점들이 많을 거라서요.
이 부분은 같이 협업하는 크루들과 많이 얘기를 나눠 보시고 결정하시면 좋을것 같아요 😄

HTTP DELETE

DELETE HTTP Method 에는 Body에 값이 존재하는 것이 물리적으로 불가능하지는 않지만 버전 등에 의해서 요청이 거절될 수 있고, 권장되는 방식이 아니라서 현재 DELETE 메소드 요청을 보내면서 Body에 password를 담는 것이 걸리는데요..
“사용자가 회원탈퇴 버튼 클릭 -> 비밀번호 확인을 위해 입력받음 -> 삭제 완료” 와 같은 시나리오에서 password 입력이 필요하다고 생각되고, POST로 변경할 경우 “행위”를 HTTP 메소드로 지정(행위는 URL이 아닌 Method로 지정하기)가 불가능해져서 우선은 DELETE 메소드 body에 password를 담아두었는데요. 단지 권장되지 않는다는 이유로 POST로 변경하는게 나을지에 대한 고민이 들었습니다. 혹시 로운은 이에 대해서 어떻게 생각하시는지 들어볼 수 있을까요..??

로운 : 이 부분에 대해서는 개인적인 의견을 말씀드릴게요ㅎㅎ
많은 library들에서 deprecated로 일정기간 제공을 하다가 더 나은 메서드나 기능이 제공되면서 유지보수를 하지 않는 것을 명시해주는 경우가 있습니다.
이러한 부분은 사용을 하다가 혹은 더 높은 버전으로 업그레이드 하면서 알게되는 경우가 많은데요.
이렇게 인지를 했을 때 필요하면 적용을 하게 되는것 같아요.
말씀하신 delete의 경우에도 권장되는 방법이지만 불가능한 방법이 아니고 답이 없는 문제이기 때문에, 우선 적용해보고 추후에 필요성을 느끼면 수정하는 것도 좋은 방법이라고 생각하고 저는 이러한 방향으로 작업을 합니다.
일을 하다보면 답이 없는 문제, 더 나은 방법을 고민하다가 생각보다 많은 시간을 보내는 경우가 있는데요. 작업에 대한 일정때문에 너무 많은 시간을 빼앗기면 안되기 때문에 우선 한가지를 적용하고 추후에 수정하는 방식으로 많이 하는 것 같아요!

: Delete Method에 대한 로운의 의견을 듣고, 조금은 고민에 대한 짐을 덜 수 있었습니다!! 감사합니다😁
지금과 같은 방법도 불가능한 방법도 아니니 향후에 필요성이 느껴지거나 회의중에 이야기를 한 번 다시 해보고, 수정하자는 의견으로 수렴되면 그 때 수정하는 것도 괜찮은 방법인 것 같습니다!!

커스텀 에러 코드 사용 이유

서버에서 던지는 예외 메시지가 뷰에 의존적이지 않게 한다. (서버에서 던지는 예외 메시지는 프런트의 참고용)
-> 그럼 어떻게 각 예외 메시지에 대한 구분을 해줄 수 있을까?
-> 우리만의 커스텀한 예외 코드와 함께 메시지를 보내도록 하자!
-> 프런트에서 우리가 던지는 예외를 이해할 수 있어야 하기 때문에 약속된 에러 코드인 커스텀 에러 코드를 만들자!
와 같은 이야기를 통해서 Custom Error를 사용하고 있습니다.
(로그인 관련: 1000번대, 입력관련: 4000번대 등)

로운 : 너무 많은 custom error를 만들게 되면 관리가 되지 않아서 큰틀에서 필요한 cumtom error만 만들어서 사용을 하게 되는데요.
이때 정말 특별하게 구분하여 확인이 필요한 경우에 대해 code와 메세지를 정의해서 사용하는 것 같고, 딱히 구분없이 에러의 발생여부 정도만 알면되는 경우에는 공통 code를 사용하면서 메세지만 다르게 하는 방식으로 많이 사용하는 것 같아요.
front쪽에서도 특별히 관리가 필요한 경우만 code를 바라보는 것 같아요. 너무 디테일하게 되면 지금과 같은 크기의 프로그램이 7, 8개인 프로젝트를 관리하기 힘들어 질 거라서요.
말씀하신 것처럼 어떠한 경우에 에러가 발생했는지 알려주는 경우는 생각이 안나네요.
그리고 dto의 검증에 대한 개인적인 의견을 드리면요.
저는 비지니스 로직을 모르고 dto에 대한 기본적인 검증(null check, blank check)과 필요한 service에게 비지니스 로직을 위임, dto로의 변환이 controller의 역할과 책임이라고 생각해요.
controller에서 이미 형식을 판별하는 것은 controller의 역할에 부합하지 않고 도메인의 역할과 책임에 부합한다고 생각해요.
그래서 이러한 기준에 맞춰 개발을 하고 있는데 이 부분에 대해서 고민해보고, 디우만의 controller, service, domain의 역할과 책임에 대한 기준을 세우면 좋을 것 같아요.

커스텀 에러 코드와 Valid

위와 같이 request DTO 에서 spring validation 을 통한 검사나 도메인에서 검사하는 내용이 동일하더라도 그 목적이 다르다고 생각해서 두 곳 모두에서 해주었습니다. 먼저 DTO에서 하는 검사는 서버로 요청이 들어올 때 검사하기 위한 목적이라고 생각하고, 도메인에서 하는 검사는 정말 도메인 객체에 대한 검사라고 생각합니다. 예를 들어서 요청이 들어오는 것과는 별개로 객체를 생성하는 경우를 생각해볼 수도 있을 것 같습니다.

하지만 이렇게 되다보니 커스텀 에러 코드에 대한 처리를 공통화 해야했습니다.
예를 들어서 비밀번호 입력 형식 불일치 라고 하면 이를 회원 가입시에 비밀번호 입력 형식 불일치, 또는 로그인 시에 비밀번호 입력 형식 불일치 와 같이 구분해주기가 어려웠습니다. (프런트에서는 분리해주길 원해서…)
결국 프런트와 상의 끝에 4000 번대 커스텀 에러 코드인 입력 형식과 관련된 부분을 분리해내게 되었습니다.
저는 “비밀번호 입력 형식 불일치” 라는 것에 초점을 맞추고 예외 메시지를 던져주면 된다고 생각하는데, 혹시 이렇게 어떤 상황에서 발생했는지를 구분지어주어야 하는 상황이 있을지 궁금합니다.

PasswordEncoder

예전에 Spring Security에서 지원하는 비밀번호 암호화 인터페이스인 PasswordEncoder를 사용해본 경험이 있는데요, 하지만 이해를 하고 썼다기 보다는 블로그 글들을 따라하면서 사용방법을 익힌 느낌이라서 조금이라도 이해를 한 후에 적용을 하고 싶어서 지금 바로 적용하지 않았습니다.
미션을 진행하면서 공부를 하고 어느정도는 이해가 된 상태(어떤 방식으로 동작을 하는지 정도라도..)에서 미션에 적용해보려고 합니다..!

피드백 내용

Q. 시나리오 테스트라는 것은 실제 사용자가 사용하는 흐름으로 진행하는 것을 테스트하는데요. 예외에 대한 것들도 테스트가 필요하지만 예외보다는 실제 사용자가 제대로된 흐름으로 동작을 했을 때 제대로 동작하는 지가 더 중요할 거라고 생각해요. 실제 사용자가 사용하는 흐름을 시나리오로 적거나 정리해보셨을까요?

Feature: 지하철 노선 관리
	Scenario: 지하철 노선 생성
    	When: 지하철 노선 생성을 요청하면
        Then: 지하철 노선 생성이 성공한다.
    Scenario: 지하철 노선 목록 조회
    	Given: 지하철 노선 생성을 요청하고
...
Feature: 지하철 노선 관리
	Scenario: 지하철 노선을 관리한다.
    	When 지하철 노선 n개 추가 요청을 한다.
        Then 지하철 노선이 추가 되었다.
        
        When 지하철 노선 목록 조회 요청을 한다.
        Then 지하철 노선 목록을 응답 받는다.
        And 지하철 노선 목록은 n개이다.


위의 두가지 경우처럼 나눌 수도 있고 더 다양하게 나누는 방법이 있을텐데요.
디우가 생각하는 시나리오를 한번 생각해보고 그것에 맞게 테스트를 작성해 보면 좋을 것 같아요!
시나리오로 모든 경우를 테스트할 수 없기 때문에 controller로 통합테스트를 진행하는 것이지 않을까요?

A. 저는 아래와 같은 정보라고 생각해볼 수 있을 것 같아요!! (말씀해주신 부분이 맞는지 모르겠습니다.ㅠ.ㅠ😅)
DisplayName도 레거시 코드 그대로여서 구체적으로 명시할 수 있도록 수정하였습니다..!!🙂

Feature: 회원기능
    Scenario: 회원 가입
        When: email, username, password를 가지고 요청하면
        Then: 고객 정보 생성에 성공한다.
    Scenario: 로그인
        When: 회원가입된 email과 password로 요청하면
        Then: 로그인에 성공한다.
    Scenario: 내 정보 조회
        When: 로그인 한 후 내 정보 조회를 하면
        Then: 내 정보(email, username)를 조회할 수 있다.
    Scenario: 일반 정보 수정
        When: 로그인하고 나서 내 정보 수정을 요청하면
        Then: 수정에 성공할 수 있다.
    Scenario: 비밀번호 변경
        When: 수정요청과 함께 현재 비밀번호와 새 비밀번호를 요청하면
        Then: 새 비밀번호로 변경된다.
    Scenario: 회원 탈퇴
        When: 로그인 한후 탈퇴 요청하면
        Then: 회원 정보를 삭제할 수 있다.

로운 : 네 맞습니다ㅎㅎ
저렇게 시나리오를 작성해서 진행을 하는 것이 atdd 테스트인데요.
개발자는 기능을 테스트 하는 것에 집중을 하지만 atdd는 프로젝트, 기획자분들과 함께 프로그램의 흐름에 집중을 해서 만드는 것이기 때문에 용도가 다르다고 생각해요.
그래서 개발자가 집중하는 기능 테스트는 controller 테스트로 진행하고, atdd는 흐름을 테스트하는 것 같아요 😄

atdd에서 @TestFactory를 활용할 수도 있는데 이 부분은 추후에 관심이 생기면 찾아보면 좋을 것 같구요ㅎㅎ
service test에서 spring boot test를 진행하는데 mock을 사용하는 단위테스트로 진행하는 것은 어떤지, controller와 service에서 각각 단위테스트와 통합테스트 중 어떤 테스트를 진행하면 좋을지 한번 고민해보면 좋을것 같아요 ㅎㅎ

https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/

: @TestFactory 에 대해서도 이전 미션 woowacourse/atdd-subway-path#175 (comment) 에서 적용해보려고 살짝..공부하고 적용해봐었는데요..아직 각 테스트가 독립적이라는 느낌이 들지 않고, 어떤 경우에 적절할지에 대해서는 답을 구하지 못한 상태라서 적용해보지 못했습니다...ㅠ.ㅠ

또한 우선 fake 객체를 사용한 테스트에 대한 저의 생각은 다음과 같습니다.
woowacourse/atdd-subway-path#261 (comment)

동일하게 mock을 사용하여 시나리오를 작성했을 때에도 해당 서비스가 논리적으로 동일하게 동작하는지를 검증을 해줘야한다고 생각합니다..(아직 사용해보지 않아서 느낌적으로는..ㅠ.ㅠ)

2단계 구현을 진행하면서 배포도 하다보니 조금 시간이 빠듯하다는 느낌이 있어서..최대한 학습해서 적용해보고 직접 서로의 장단을 느껴볼 수 있도록 하겠습니다..!!

로운 : 저의 개인적인 의견입니다! 참고만 하시면 될거 같아요!
통합테스트는 전체적으로 정상동작하는지 확인하는 것을 목적으로 하고, 단위테스트는 그 단위에서 진행해야하는 로직이 정상적으로 동작하는지 확인하는 것을 목적으로 한다고 생각해요.
예를 들어 주문을 하고 고객에게 카톡 알림을 보내고 업주에게 주문을 전달하는 로직이 진행된다고 하면, repository등 다른 class와의 연계 동작에 집중하기보다 실제로 주문을 받은 후에 고객과 업주에게 전달이 제대로 되는지를 테스트해야하지 않을까요?
다른 부분(repository 저장 등)은 정상적으로 동작한다고 가정하고 실제로 메서드내에서 동작해야하는 로직에 집중을 하는 것이 단위테스트인데, service에서는 어디에 집중하는것이 맞을까요?? 다른 테스트들의 역할도 함께 고민해보면 좋을것 같아요!

Q. 개인적인 궁금함인데요. 전체적으로 메세지를 영어로 한 이유가 있을까요?

    @ExceptionHandler
    public ResponseEntity<String> handle(RuntimeException e) {
        return ResponseEntity.badRequest().body("Unhandled Exception");

A. 어떤 구체적인 메시지 혹은 사용자에게 보여질 메시지를 전달하기 보다는 서버에서 던지는 예외 메시지는 Front와의 소통(?)을 위한 메시지라고 생각했습니다.
따라서 Front와 다음과 같은 예외 메시지들을 정리해두고, 이에 따라 Front에서 사용자에게 보여질 적절한 메시지를 보여주기로 하여 지금과 같이 정해진 영어 메시지를 전달해주고 있습니다.🙂

  • 에러 response(JSON)
    {
      "errorCode" : 1001,
      "message" : "No message available"
    }
  • 회원가입[400, Bad Request]
    - ErrorCode 1000번대는 회원가입
    • 이메일 중복 (Error Code: 1001, Message: Duplicated Email)
  • 로그인[400, Bad Request]
    - ErrorCode 2000번대는 로그인
    - 이메일 또는 패스워드가 맞지 않음 (Error Code: 2001, Message: Login Fail)
    ...
    (API.md에 작성해두었으므로 중간 생략)
    ...

그런데 현재 정의한 예외 이외의 상황에 대해서는 구체적으로 front팀과 함께 정의한 내용이 없어서 RuntimeException 뿐만 아니라 NotExistException에 대해서도 함께 정의한 내용이 없습니다..

따라서 현재 NotExistException에서는 임의로 기존의 레거시 코드의 EmptyResultDataAccessException에서 던지는 예외 메시지를 사용하도록 하고 있고, RuntimeException 에 대해서는 별도로 손보지 않은 상태입니다.😅

오늘 백엔드끼리 서로 회의를 진행하고 나서 내일(6/4 토요일) 에 front와 다시 회의를 하기로 하였으므로, 이 때 해당 case들에 대해서 어떻게 처리하면 좋을지 결정하고 개선해보도록 하겠습니다..!!!

로운 : 개인적인 의견입니다! 반영하지 않으셔도 돼요
front와 소통을 위한 경우에도 한글이 이해하기 더 편하지 않을까 하는 생각을 들었어서요ㅎㅎ
이 부분은 front와 협의를 통해 결정하면 될거 같구요.
참고용으로 예외적인 경우를 말씀을 드리면, application과 pc 프로그램의 경우에는 web(front, server)와 같이 수정이 빠르게 반영되지 않은 경우가 많고, 레거시라고 해서 이전 버전을 그대로 사용하는 경우들이 많아요.
그래서 이러한 경우에는 수정이 빠른 server에서 내려주는 메세지를 그대로 보여주어 추후의 수정에 용이하도록 하는 경우도 많다는 것을 알고만 계셔도 좋을 것 같습니다~ 😄

: 아하..!! 무조건 front에서 view를 관리하는 것은 아니고, 상황에 따라서는 server에서 내려주는 메시지를 그대로 사용하는 경우도 존재한다라고 이해해도 될까요??
그런데..레벨1에서 배운 것과 달리 그러면,,view에 대한 변경이 서버에도 영향이 가게 되는데..추후의 수정에 용이하도록이라는 말에 공감이 잘 안가는 것 같아요...혹시 조금만 구체적인 상황(?)을 들어주시면..조금 더 이해하기 편할 것 같습니다..!!ㅠ.ㅠ

로운 : 앱이나 pc 버전이 2.0에서 3.0으로 올라갔다고 했을때 메세지를 front에서 관리를 하게 되면 2.0과 3.0에서 다른 메세지가 보여지게 될거에요. 하지만 서버에서 관리를 하게 되면 메세지가 수정되면 2.0과 3.0에서 같은 메세지를 보여줄 수 있겠죠??
이 부분은 모바일 app과 pc client처럼 새로운 버전을 다운받거나 업그레이드해야하는 경우에 사용한다고 보시면 될거 같아요!

Q. 개인적인 의견입니다! 반영은 하지 않으셔도 돼요.
me라는 표현을 다른 곳에서 많이 보지 못했던거 같아서요.
저는 개인적으로 account를 쓰는데 다른 url를 참고하면서 더 나은 표현은 없는지 고민해보면 좋을 것 같아요!

    @GetMapping("/me")

A. 동의합니다! 저도 me 라는 표현을 다른곳에서 본 기억이 없습니다..

당연히 협업중에서도 이에 대한 의견이 많이 나왔었는데요..회의 중 나왔던 논의 내용을 다 정리한 것은 아니지만 me 라는 표현에 대해서 의견이 많이 나왔어서 이를 공유해드립니다..!! Notion Page

"/api/customers/me" vs "/api/auth/me"
"/api/customers/me" vs "/api/customers/{customer_username}"
"/api/me" vs "/api/customers/me"
등등 이와 관련하여 굉장히 많은 의견이 나왔었습니다.

우선 저희가 참고한 API 스펙은 이 사이트를 참고하였습니다.

우선 URI에 붙는 auth와 customers 에 대해서는 요청을 보낼 때 무엇을 더 중요하게 생각하냐로 구분해주었습니다.
"나를 인증해줘" 의 의미인 경우에는 auth, "내 정보를 조회해줘"등과 같은 의미가 큰 경우에는 customers를 붙여주었습니다.

말씀해주신 것과 같이 "me"라는 표현은 "내 정보조회", "내 정보수정", "내 정보 탈퇴" 와 같이 "나"와 관련된 요청이기 때문이 이렇게 해주었습니다.
예를 들어 장바구니 사이트에서 만약 다른 유저의 정보를 조회해볼 수 있다.(예를 들어 쿠팡에서 리뷰를 남긴 다른 사용자가 어떤 리뷰를 남겼는지를 볼 수 있다.와 같이...)라고 한다면 GET "/api/customers/{customer_username}" 과 같이 조회해볼 수도 있을 것 같습니다. (이 경우 username이 유니크하다는 요구사항이 추가되게 될 것이라고 생각함. 또는 username이 아니라 현재 유니크한 값을 가지는 email을 사용할 수도 있겠음.)

결론적으로 "me"라는 표현을 사용한 이유는 개인적인 "나"와 관련된 요청이기 때문에 "me"라는 표현을 붙여주었습니다.
또한 음...🤔 front에서 서버로 요청을 보내는 API의 URI와 우리가 어떤 사이트를 이용할 때 보이는 주소창의 URL에는 차이가 있다고 생각합니다..!! (예를 들어 "/api/customers/me" 로 요청을 보냈고, 응답을 받았다고 할 때 주소창에서는 "도메인/customers/myInfo"와 같이 나타내어 줄 수 있다고 생각합니다..!!)

로운 : 네 맞습니다ㅎㅎ api url과 페이지에 보이는 url은 다를 수 있을거라서요.
그래서 말씀드린 account는 페이지 url을 말씀드렸던 부분이구요.
me같은 api url 경우는 특정 유저가 아닌 전체 유저의 정보에 대한 url, 즉, users/{id} 혹은 유니크한 값을 사용하는 것으로도 개인정보를 가져올 수 있지 않을까?? 생각했습니다~
물론 이런 경우에 authorization을 통해 자신의 정보에 대한 접근과 다른 사람에 대한 접근에 따라 다른 정보를 주어야 할 수도 있을 거 같아요.
이러한 방법이 있을 수도 있겠구나라고 참고만 하시면 될 것 같습니다~

Q. 기본 생성자의 접근제어자가 private이여도 될거 같아요.

public class CustomerRequest {

    private static final String INVALID_EMAIL = "Invalid Email";
    private static final String INVALID_PASSWORD = "Invalid Password";
    private static final String INVALID_USERNAME = "Invalid Username";

    @NotBlank(message = INVALID_EMAIL)
    @Email(message = INVALID_EMAIL, regexp = "^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$")
    private String email;

    @NotBlank(message = INVALID_PASSWORD)
    @Pattern(message = INVALID_PASSWORD, regexp = "^.*(?=^.{8,12}$)(?=.*\\d)(?=.*[a-zA-Z])(?=.*[!@#$%^&+=]).*$")
    private String password;

    @NotBlank(message = INVALID_USERNAME)
    @Size(message = INVALID_USERNAME, max = 10)
    private String username;

    public CustomerRequest() {}
    
    ...
}

A. RestController에서 @RequestBody 바인딩을 Jackson 라이브러리의 ObjectMapper가 수행하고, Jackson은 JSON 필드의 이름을 Java 오브젝트의 getter 및 setter 메소드와 일치시켜 JSON 오브젝트의 필드를 매칭시킨다는 것을 확인하였습니다!
그리고 값에 대한 주입은 reflection을 사용해서 매핑해 주입해주므로 Setter는 필요하지 않다는 것을 알게되었습니다.
따라서 ObjectMapper로 바인딩할 때 Setter 혹은 Getter만 존재하면 됩니다. (reflection을 사용해서 필드 값들을 넣어주기 때문에...)
그리고 이렇게 reflection을 사용할 때 기본 생성자의 접근제어자는 상관이 없으므로 private으로 제한하여, 코드 상에서 직접 new XXX() 로 생성하는 경우를 방지해줄 수 있을 것 같습니다..!!😁
리플렉션은 접근 제어자와 상관 없이 클래스 객체를 동적으로 생성하는(런타임 시점) 자바 API

Q. service에서 dto를 반환하는데요. service에서 dto를 반환하는 것이 역할에 맞을까요??

    @Transactional(readOnly = true)
    public TokenResponse createToken(TokenRequest request) {

A. 제가 생각하는 controller, service, dao의 책임을 정리하면 다음과 같습니다.

Controller : 사용자로부터 요청을 받고, 이에 대한 처리를 한 후(직접 하는 것이 아니라 Service 계층으로 위임), 응답한다.
즉, Controller는 "요청을 받는다." 와 "응답을 보낸다."에 초점이 맞춰져 있다고 생각합니다.

Service : 트랜잭션 단위의 비즈니스 흐름에 대한 책임을 가진다고 생각합니다.
예를 들어 "회원을 저장한다.", "회원 정보를 조회한다." 와 같이 어떤 트랜잭션 단위의 비즈니스 흐름에 대한 책임을 가진다고 생각합니다.
따라서 DTO에 대한 변환에 대한 책임은 비즈니스 로직을 수행하고, 결과로써 나타난 DTO(response) 를 반환해주는 것이 적절하다고 생각합니다.
관련하여 Service단에서 응답에 대한 DTO를 만들어서 Controller 계층으로 반환해주는 것이 좋겠다라는 생각에 확신이 들 수 있었던 코치 '브리 강의'의 PPT 중 한 그림에 대해서 첨부합니다..!!

저는 위 그림에 대해서 Controller 와 Service 사이에서는 DTO를 사용하고, Service와 DAO 사이에서는 Domain 객체를 사용한다는 내용이라고 이해하였습니다.🙂

DAO : DAO의 책임은 말 그대로 데이터에 액세스하는 책임을 가진다고 생각합니다. 따라서 DAO 객체의 하나의 메소드에서는 하나의 쿼리를 보내는 책임을 가지며, 조회의 경우 이렇게 조회된 데이터를 domain 객체로 만들어 반환해주는 책임을 가진다고 생각합니다.

계층(Layer) 끼리도 객체처럼 결합도를 낮추고 단일 책임을 갖게 함으로써 응집도를 높이는 것이 좋다고 생각됩니다. 그런데 DAO에서 반환하는 도메인 객체를 만약 Controller까지 보내고 나서 Controller에서 변환을 한다라고 하면 서로간의 결합도가 높아지는 것 아닐까하는 생각이 듭니다. 또한 DAO에서 Domain이 아닌 DTO를 바로 만들어서 Service를 거쳐 Controller로 반환하는 것도 적절하지 않다고 생각이 듭니다..!!

질문주신 내용에 대한 답변이 되었을지 모르겠습니다...ㅎㅎ 저는 현재 각 계층의 책임과 역할을 위와 같이 생각하고 있는데...혹시 로운께서는 각 계층의 역할, DTO로의 변환 및 반환에 대한 책임이 어느 계층에 있는것이 적절하다고 생각하시는지 로운의 생각을 들어볼 수 있을까요...!! 로운의 생각을 공유해주시면 많은 도움이 될 것 같습니다!! 감사합니다!!😃

로운 : 네 이 부분은 개인의 기준을 제대로 정립하면 된다고 생각합니다ㅎㅎ 저만의 기준은 참고만 해주세요
service 부분은 지금 cartService에서 customerDao를 가지고 있는데 customerService를 가지는 방식을 토대로 말씀드린 것입니다~

controller

제가 생각하는 controller의 역할은 들어온 request의 기본적인 검증(null check, empty check, header 등)을 하고 넘어온 데이터를 변환하여 service에 넘겨준 후, service로부터 받은 데이터를 응답의 형태에 맞게 변환하여 전달하는 것이라고 생각하고 있어요.

service

service가 비지니스로직을 담당하기 때문에 db와 관련된 부분도 같이 관리를 한다고 생각해요.
실제로 db에 crud 하는 것들도 비지니스 로직에 포함된 부분이니까요.
그렇기 때문에 각 도메인을 담당하는 서비스가 생겼다면 그 도메인과 관련된 부분은 서비스를 통해 메세지를 전달하는 방식으로 해야하지 않을까요?
저는 개인적으로 service도 객체지향적으로 생각해야한다고 생각해요.
우리가 객체의 내부 데이터나 로직을 모른 상태로 메세지만 전달하여 로직을 수행하도록 하는 것처럼 service도 이와 같이 해야한다고 생각합니다!

저는 개인적으로 service에서는 도메인을 반환하고 controller에서 dto로 변환하는 방식을 많이 쓰는것 같은데요.
프로그램이 커지게 되면 service를 여러군데에서 사용하게 되는 경우가 많습니다.
이러한 경우 사용되는 곳에 맞게 같은 로직을 도는 메서드지만 응답형태만 변환해주는 메서드를 만들어 주어야 해요.
예를들어 LoginService라고 하면 UserService랑 AuthService등 여러 서비스에 대한 의존성을 가질 수 있을 텐데요.
UserService를 여러군데에서 가져서 사용할 텐데 현재 사용한 방식이면 UserController에 return한 UserResponse를 LoginService에서도 가져다가 사용하는 방식일까요?? controller로 return한 응답값 형태가 아닌 다른 UserLoginResponse처럼 새로운 응답값을 만들어서 사용하는 방식일까요?? dto를 사용하는데 필요하지 않은 정보를 넘길건가요?? 이러한 부분에 대한 고민도 하면 좋을것 같구요.

layer간에 이동을 위해 dto를 사용하면 서로 영향이 없어지기 때문에 좋지만 프로그램의 크기가 커졌을때 관리가 될까요?? 현재의 프로그램 수준의 모듈이 7, 8개가 된다고 하면 어떻게 될까요? 그리고 그 많은 모듈들이 공통 모듈을 서로 사용한다고 해서 dto가 모아진다고 하면 dto의 클래스만으로도 엄청나게 많은 클래스가 만들어지지 않을까요?

모든 방법에 장단점이 있을텐데요. 프로그램의 크기가 커지면서 관리를 조금이나마 용이하게 하기위해서 service에서 도메인을 반환하고 controller에서 api에 맞게 dto로 변환하는 방식으로 진행을 많이 하는것 같아요~ 이 부분은 어떤 게 더 나을지 고민해보고 기준을 정하시면 좋을것 같아요! 현재 구조가 맞다고 생각하시면 유지하셔도 좋습니다ㅎㅎ정답은 없으니까요

https://tecoble.techcourse.co.kr/post/2021-04-25-dto-layer-scope/

Q. AuthenticationPrincipalArgumentResolver package가 controller인데요. contoller보다는 config이지 않을까요?? 아니면 다른 package일 수 있다고 생각하는데요.
controller에 포함하게된 기준이 있을까요?

A. 앞서 정리했던 것과 같이 제가 생각하는 Controller란 "사용자로부터 요청을 받고, 응답을 보낸다." 라고 생각을 하고 보았을 때, 어떠한 요청이 왔을 때 요청으로 들어온 값으로부터 원하는 값으로 만드는 ArgumentResolver 의 경우 Controller 보다는 config 패키지에 존재하는 것이 더 적절한 것 같습니다!!
사실 어떤 기준으로 controller에 포함했다기 보다는 기존 레거시 코드에서 controller에 있던 ArgumentResolver에 대해서 "왜 Controller 패키지에 존재하지?" 에 대해서 생각해보지 못했습니다...😅

기존에 생각했던 것과 같이 어떤 요청을 받고, 그에 대한 처리를 한 후 응답을 보낸다라기 보다는 ArgumentResolver는 support 되는 parameter 를 가지는 요청이 Dispatcher Servlet을 거쳐 들어온 이후에 핸들러(컨트롤러)로 요청이 전달되기 이전에 이에 대해서 어떤 설정을 해준다고 보는 것이 적절하기 때문에 config 패키지에 두는 것이 적절해보입니다!

Q. 개인적인 의견입니다! 반영하지 않으셔도 돼요
코멘트에도 남겼는데요. 실제로 valid를 controller에서 하기 때문에 검증이 controller의 역할이 된다고 생각하는데요. 저는 개인적으로 형식에 대한 검증은 도메인에서 하는 것이 맞다고 생각합니다~ 저는 notblank만 할 거 같네요ㅎㅎ

public class ChangePasswordRequest {

    private static final String INVALID_PASSWORD = "Invalid Password";

    private String oldPassword;

    @NotBlank(message = INVALID_PASSWORD)
    @Pattern(message = INVALID_PASSWORD, regexp = "^.*(?=^.{8,12}$)(?=.*\\d)(?=.*[a-zA-Z])(?=.*[!@#$%^&+=]).*$")
    private String newPassword;
	
    ...
}

A. 크루들과 얘기해보니 정말 다양한 의견이 있더라구요..!!
(로운과 비슷한 의견을 가진 크루들도 많은 것 같았습니다!! 기본적인 검증은 DTO에서 하고, 비즈니스 로직에 종속적인(예를 들어, email 형식 등) 내용들은 도메인에서 검증을 하는게 분리가 잘 되어있는 것 같다라는 의견으로...)

그런데 저는 아직까지는 이전에 말씀 드린 것과 같이
request DTO 에서 spring validation 을 통한 검사나 도메인에서 검사하는 내용이 동일하더라도 그 목적이 다르다고 생각해서 두 곳 모두에서 해주었습니다. 먼저 DTO에서 하는 검사는 서버로 요청이 들어올 때 검사하기 위한 목적이라고 생각하고, 도메인에서 하는 검사는 정말 도메인 객체에 대한 검사라고 생각합니다. 예를 들어서 요청이 들어오는 것과는 별개로 객체를 생성하는 경우를 생각해볼 수도 있을 것 같습니다.
와 같은 의견이 강한 것 같아요..!!
로운의 의견을 공유해주셔서 감사합니다.😃

로운 : 네 참고만 해주시면 될거 같아요ㅎㅎ
디우처럼 한다고 했을때 로직을 잘 이해하고 있는 사람은 2군데에서 검증을 한다고 알고 있어서 정책이 바뀔 경우 두 군데 모두 수정을 할텐데요. 처음 온 사람이거나 로직에 익숙하지 못한 사람은 한 곳은 누락하고 한 곳만 수정해서 이슈가 발생하는 경우가 생길수도 있는 것 같아요.
그래서 중복으로 같은 동작을 하는 것은 최대한 만들지 않는 것 같고, 디우가 말한 경우에서는 정말 검증하는 것이 다른 경우에 검증을 추가하는 방식으로 진행하는 것 같습니다!


2 단계 - 장바구니/주문 API 변경하기

장바구니 2단계 저장소 링크

PR 링크

리뷰어: 로운

고민한 내용

API 명세

2단계 요구사항에 대해서 front와 논의하여 도출한 API 명세에 대한 정리를 Notion Page에 이전과 같이 정리해두었으며, API.md 파일에도 정리해두었습니다. 또한 Swagger 설정을 추가하여 문서자동화를 해보았습니다.(굉장히 편리하다는 느낌을 받았습니다.🙂)

에러 코드

이번 2단계 요구사항을 진행하면서 custom error code 또한 추가하게 되었는데요, 해당 과정에서 많은 부분을 느끼게 되었던 것 같습니다. Customer Error Code 정리 엑셀

가장 먼저 여기서 더 프로젝트의 규모가 커진다면 과연 우리가 해당 문서를 계속해서 유지보수 할 수 있겠는가? 라는 질문에 자신있게 "네!" 라고 말하지 못할 것 같다는 생각을 하였습니다. 현업에서는 더더욱 그러지 못할 것 같다는 생각을 하였습니다. 또한 신입사원이 공부해야할 내용도 많아지고 새로운 에러 상황이 이미 등록되어 있는지 없는지 체크하며 추가해나갈 자신이 없었습니다. 또한 이전에 말씀드렸던 것과 같은 장점도 무색해진다는 느낌을 받았습니다.

Password Encoder

이전에 자신있게 Spring Security에서 지원하는 비밀번호 암호화 인터페이스인 Password Encoder를 공부하고 적용해본다고 말씀드렸었는데요...배포까지 하려니 일정이 빡빡하고, 테코톡 발표도 하다보니 생각보다 여유가 생기질 않아서...아직 적용해보지 못했습니다..🥲 하지만 레벨3 프로젝트를 진행할 때에는 꼭 알고 있어야 하는 개념이라고 생각되므로 공부를 하고, 해당 branch에 적용하면서 익히도록 하겠습니다...!!

레거시 코드

레거시 코드를 개선해보는 경험을 이번 장바구니 미션에서 처음 해보게 되었는데요..생각보다 하나를 고쳤을 때 테스트 깨지는 부분들이 많아서 어떻게하면 좋을지에 대한 고민이 들었습니다..
예를 들어 Product 도메인에 대해서만 먼저 개선하고 싶은데, DAO, Service, Controller, 또 장바구니 같은 경우 다른 주문쪽 까지 고려하면서 리팩토링 해야 하더라구요...이러 부분에서 어려움을 겪었습니다.
그런데 현업에 가면 레거시 코드도 많이 보고 리팩토링하는 경험도 많이 가지게 될 것이고, 지금보다 규모가 더 커질 것으로 생각되는데요...혹시 로운만의 팁이 있을지..공유해주시면 너무 감사할 것 같습니다..!!

로운 : 리팩토링에 대한 저만의 팁을 말씀해주셨는데요;;; 솔직하게 말씀드리면 없습니다 😭 있으면 알고 싶네요.
이미 여러 클래스들과 관계를 맺고 있는 상황에서 한가지가 변경되면 다른 것들도 영향을 받을 수 밖에 없습니다.
이 상황에서 할 수 있는 선택은 2가지일 거 같은데요.

  1. 기존 테스트를 지우고 새로 작성
  2. 기존 테스트를 운영코드 리팩토링 하듯이 리팩토링

저는 기본적으로 2번째로 진행을 하는 것 같아요ㅎㅎ리팩토링의 과정에 운영코드 뿐만이 아니라 테스트에 대한 것도 포함이 되어 있다고 생각하기 때문에 어쩔 수 없는 부분이지 않나 생각합니다! 만약 구조가 현재에 너무 맞지 않다고 한다면 구조를 새로 설계하는 것도 한가지 방법이 될 수도 있겠죠?

application.yml

application.yml 을 분리해주었습니다. 운영환경과 로컬 개발 환경 그리고 테스트 환경을 분리해주었습니다.
테스트환경과 로컬 개발 환경이 동일하기는 하지만, 혹시나 다를 수도 있기 때문에 분리해주었습니다.
또 운영환경에서 사용될 설정에 대해서는 application-prod.yml을 사용하도록 하고 OS의 환경 변수를 이용하여 값을 넣어줄 수 있도록 설정을 구성하였습니다.

피드백 내용

Q. controller 테스트가 없는데 acceptance 테스트로 대체하기로 한건지 궁금하네요ㅎㅎ

A. 맞습니다...음..솔직히 이전까지 별도로 controller에 대한 테스트코드를 작성하지 않았었습니다. 왜냐하면 동일한 테스트를 두 번 작성하는 것과 같다는 생각이 들었기 때문입니다. 서비스 계층에 대한 테스트가 별도로 존재하기 때문에 인수테스트를 하면서 사용자 요청& 응답 처리에 관련된 부분에 대한 테스트를 진행한다고 생각했기 때문입니다.

하지만 해당 질문을 받고 나서 곰곰이 생각해보니, 테스트의 목적에서 차이가 날 수도 있다는 생각이 들었습니다.
구체적인 예를 들면 사용자 시나리오와는 별개로 입력값 validation에 대한 테스트가 있겠습니다. 상품의 가격은 음수일 수 없다.와 같은 테스트를 controller 테스트에서 진행해볼 수 있겠습니다!
그렇지만 이외에는 별도로 Contoller에 대한 테스튼가 존재해야하는 이유를 찾지 못했습니다...
그렇다고 어떻게 보면 내가 사용하는 외부의 기능(javax.validation.constraints패키지에서 제공)에 대한 테스트를 진행하는것이 맞는가? 하는 생각도 들었습니다.

혹시 로운이 생각하기에 acceptance 테스트와는 별개로 controller테스트가 존재해야하는 이유를 공유해주실 수 있을까요...??😅

로운 : 시나리오를 작성해서 진행을 하는 것이 atdd 테스트인데요. 개발자는 기능을 테스트 하는 것에 집중을 하지만 atdd는 프로젝트, 기획자분들과 함께 프로그램의 흐름에 집중을 해서 만드는 것이기 때문에 용도가 다르다고 생각해요. 그래서 개발자가 집중하는 기능 테스트는 controller 테스트로 진행하고, atdd는 흐름을 테스트하는 것 같은데요 😄
아마 이 부분은 기획자분들과 함께 일할 때 더 크게 느껴지지 않을까 싶어요.
기획자분들이 기획을 할 때는 사용자의 사용 흐름을 생각하고, 그 흐름에 맞춰 디자인 시안을 만드는 형식으로 진행을 하거든요.
사용자의 흐름에 맞춰 시나리오를 짤 때, 우리 api만 사용하는 것이 아닌 여러 팀의 api를 사용하는 경우에는 어떻게 진행을 해야할까요?
이러한 부분 때문에 atdd로 모든 경우를 테스트할 수 없어, controller 테스트를 진행하는 것이지 않나 생각합니다~

https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/

Q. swagger 도입 좋네요ㅎㅎ spring restdocs와 장단을 비교해봐도 좋을것 같아요!

A. 크루들 사이에서도 Notion과 같이 별도의 툴을 이용해서 API 명세를 관리하는 크루들도 있었고, README, Swagger, Spring REST Docs등 다양하였습니다.

이 둘의 장단을 비교해보면 다음과 같을 것 같습니다.

  • Swagger

    • 장점
      • GET, DELETE 등 HTTP 메소드에 따라 구분(색을통해)해주며 편리하게 사용할 수 있다.
      • API 문서화 이외에도 실제 API를 간단하게 테스트하는 기능을 제공한다. (본인은 이를 통해 테스트하기 보다는 postMan을 통해서 하는 것을 선호한다..토큰등을 실어보내는 방법을 몰라서😅)
      • 프로덕트 코드에 설정을 통해서 굉장히 간단하게 API 문서를 얻을 수 있다.
    • 단점
      • 프로덕트 코드에 Swagger 설정과 관련된 코드가 포함되게 된다.
      • 프로덕트 코드와 동기화가 안될 수 있다. (본인이 직접 느껴보진 못했지만, 다음의 링크를 참고하였습니다.) 참고 링크
  • Spring Rest Docs

    • 장점
      • 프로덕트 코드에 영향이 없다.
      • 테스트가 제대로 수행되지 않으면 문서가 작성되지 않는다는 장점이 있다.
    • 단점
      • 테스트를 작성해야하기 때문에 비용이 든다. (이 부분은 관점에 따라 다르다고 생각합니다. 앞서 참고한 링크를 보면 테스트가 성공해야 문서작성이 된다. 라는 점을 장점으로 꼽은 것으로 보면 꼼꼼한 테스트 작성이 가능해지기 때문에 장점으로 본 것 같습니다. 하지만 테스트 코드 작성도 결국 비용이라는 관점에서는 swagger와 비교했을 때 단점이라고 생각합니다..!)
      • 엔트포인트마다 수많은 코드를 추가해줘야한다. (그만큼 여러 메소드를 알아야하기 때문에 제대로 사용하기 위해서는 학습해야 할 양도 많다고 생각합니다.)

Spring Rest Docs는 직접 사용해보지는 못했어서..여러 블로그글들을 참고하며 적어보았습니다. 이렇게 정리를 하면서 장단을 비교해보고 나니, 음..🤔 저는 API 명세를 하나만으로 하는 것 보다는 2개정도의 툴을 이용하여 유지보수해나가도 괜찮지 않을까하는 생각이 들었습니다. (이 두 문서의 동기화 부분에서는 사람이 일일이 확인하는 것 이외에 아직 어떻게 하면 좋을지에 대한 생각이 들지는 않지만...)
왜냐하면 각각의 장단이 존재하고 이는 개인의 취향이라는 생각이 듭니다. 내가 속한 팀에서 모든 팀원의 의견을 만족하며 API 명세를 작성해나가기는 힘들다고 생각합니다. 또 예를 들어, Swagger 기반으로 도출된 API에 대한 테스트를 작성하여 Spring REST Docs를 도출한다와 같은 방법도 꼼꼼하게 명세를 도출해내는 방법이 될 수도 있을 것 같다는 생각이 들기 때문입니다!

로운 : 맞습니다 가자 장단점이 있고, 팀에서 논의 끝에 정한 것으로 문서를 작성하는데요.
추후에 어떤 것을 사용할지 정해야할 때, 지금 고민한 부분이 도움이 될 것 같구요.
참고로 말씀을 드리면 생각보다 테스트를 유지보수 하는 것이 쉽지 않고, api들이 굉장히 많아서, 2개 다 사용하는 것은 쉽지 않을 것 같습니다 😅

Q. 이런 경우에 정적 팩토리 메서드를 쓰는거지 않을까요?

    public ProductResponse(Product product) {
        this(product.getId(), product.getName(), product.getPrice(),
                product.getStockQuantity(), new ThumbnailImage(product.getImage()));
    }

A. 크게 고민하지 않고 생성자 체이닝 방식을 사용했던 것 같은데요, 로운의 말씀을 듣고 보니 Product 객체로부터 ProductResponse를 만든다는 의미도 "from"이라는 메소드 네임을 통해서 전달받을 수 있어서 좀 더 적절할 것 같다는 생각도 듭니다.
그리고 ProductResponse의 생성자에서 ThumbnailImage를 생성하고 있는데, 단순 "객체 생성"에 대한 책임만을 가지는 생성자에서 ThumbnailImage 객체 생성에 대한 책임도 가지고 있어 적절하지 못했다는 생각이 듭니다. 특히 ProductResponse에서 보다OrderResponse를 보면 기존 코드가 다음과 같습니다.

    public OrderResponse(Orders orders) {
        this.id = orders.getId();
        this.orderedProducts = orders.getOrderDetails().stream()
                .map(orderDetail -> ProductResponse.from(orderDetail.getProduct()))
                .collect(toList());
    }

위 코드를 보면 인자로 받은 Orders 객체에 대해서 ProductResponse 리스트로 변환하는 책임을 생성자에서 함께 가지고 있는데요, 이는 객체 생성과 관련된 최소한의 책임만을 가지는 생성자에 대해서 적절하지 않다는 생각이 들었고, 다음과 같이 정적 팩토리 메소드를 이용하여 확실하게 책임을 분리해줄 수 있다는 생각이 들었습니다.

    public static OrderResponse from(Orders orders) {
        final List<ProductResponse> orderedProducts = orders.getOrderDetails()
                .stream()
                .map(orderDetail -> ProductResponse.from(orderDetail.getProduct()))
                .collect(toList());

        return new OrderResponse(orders.getId(), orderedProducts);
    }

Q. 모든 product를 찾은 후에 업데이트를 하고 있는데요.
cart에 있는 모든 product를 한번에 업데이트할 수도 있지 않을까 하는 생각이 드네요ㅎㅎ

    private void reduceStockQuantity(List<Cart> carts) {
        for (Cart cart : carts) {
            final Product product = productDao.findById(cart.getProduct().getId());
            ...
        }
	}

A. 쿼리문에서 UPDATE문과 서브쿼리(SELECT)를 이용해서 카트의 productId와 같은 product들을 찾아와 product.stock_quantity - cart_item.quantity 로 얻은 값을 product에 update해주는 식으로 쿼리 하나로 한 번에 업데이트 해주도록 수정해주었습니다...!!

그런데 질문이 있습니다..음..현재 저는 Product 와 관련된 사항이 변경되는 것이기 때문에 ProductDao에 updateStockQuantity라는 메소드를 두었습니다.
그런데 어떻게 보면 "주문" 이라는 비즈니스 로직과 관련된 내용이기 때문에 해당 로직이 OrderDao에 존재해야하나하는 의문이 들었습니다..! 혹시 로운은 이와 관련하여 어떻게 생각하시나요..?? 혹시 이를 나누는 기준같은게 있을까요..??😅

로운 : dao 대신 service의 사용과 좀 겹칠 수 있을 것 같은데요.
service가 비지니스로직을 담당하기 때문에 db와 관련된 부분도 같이 관리를 한다고 생각해요.
실제로 db에 crud 하는 것들도 비지니스 로직에 포함된 부분이니까요.
그렇기 때문에 각 도메인을 담당하는 서비스가 생겼다면 그 도메인과 관련된 부분은 서비스를 통해 메세지를 전달하는 방식으로 해야하지 않을까요?
저는 개인적으로 service도 객체지향적으로 생각해야한다고 생각해요.
우리가 객체의 내부 데이터나 로직을 모른 상태로 메세지만 전달하여 로직을 수행하도록 하는 것처럼 service도 이와 같이 해야한다고 생각합니다!
서비스 혹은 layer도 객체로 본다면 각각의 역할을 무엇으로 생각했는지에 맞춰서 개발해야하지 않나 생각하거든요~
여기서 저는 db와 관련된 부분을 담당하는 service를 작은 단위의 service로 생각을 했던거 같아요.
entity가 필요한 모든 데이터를 가지고 있으면 비지니스 로직을 수행해도 괜찮다고 생각하지만 개인적으로 확실하게 entity와 domain이 일치한다고 생각하지 않아요.
도메인이 여러개의 entity를 필요로 한다면 큰 의미의 도메인이 된다고 생각할 수도 있지 않을까요? entity의 모든 column이 필요한 것이 아닌 A entity에서는 a,b,c column만 필요하고 B entity에서는 d,e만 필요한 도메인이 생길 수도 있지 않을까요? 이때 두 entity로 로직을 수행할지 도메인으로 변환해서 수행할지, entity를 완전한 domain으로 볼지 등등 개발자마다 의견이 다 다르다고 생각해요.

저의 의견으로는 order가 큰 의미의 도메인이고 product가 작은 의미의 도메인이 될텐데요.
해당 값이 product 도메인 내에서 관리를 하기 때문에 product service의 메서드를 호출하도록 했을 것 같습니다~
현재의 구조에서는 디우와 같이 productDao에서 하도록 했을 것 같네요

로운 : 개인적인 의견입니다~
저는 quantity계산은 server에서 했을 것 같아요.

Map<Long, Cart> productCarts;
List<Porduct> products = productDao.findIds();
for (Product product : products) {
    product.updateQuantity(quantity);
}
productDao.batchUpdate(products);

매우 간단하게 표현해봤는데요. 이런식으로 server에서 하지 않았을까 싶습니다~

Q. customerService가 있는데 customerDao를 사용한 이유가 있을까요?

public class CartService {

    private final CartItemDao cartItemDao;
    private final CustomerDao customerDao;
    private final ProductDao productDao;
    
    ...
}

A. 이 부분에 대해서도 크루들 사이에서 많은 의견이 오갔었습니다. 따로 정리하지는 않아서..ㅠㅠ 기억이 나는대로 내용을 정리해보면 다음과 같습니다!🙂

우선 Service에서 Service를 의존해도 된다는 입장의 가장 큰 이유는 동일한 비즈니스 로직을 작성해줄 필요가 없다였던 것으로 기억합니다.
예를 들어서 CartService에서 CustomerService에 의존하고 findByEmail를 호출하고 이 때 유저에 대한 검증이 필요하다면, 이메일 검증등과 같은 비즈니스 로직에 대한 처리를 또 다시 CartService에서 해줄 필요가 없어지게 됩니다. 물론 CartService의 책임도 아니라고 생각합니다. 하지만 Service계층 끼리의 순환참조등과 같은 부분에 대해서는 주의가 필요하다고 생각합니다.

반대로 Service에서 Dao에 의존하는 것은 우선 의존 관계의 일관성이 생깁니다. 모두 Service -> Dao로의 의존을 가지게 됩니다. 하지만 앞서 말씀드린 것과 같이 이미 작성되어있는 비즈니스 로직 처리에 대해서 또 다시 처리를 해주어야할 수 있습니다.

제가 DAO들에만 의존하도록 한 이유는 우선 의존 관계의 일관성에 있습니다. 순환 참조등에 대해서 고려할 필요가 없습니다. 또한 현재 미션들을 수행하면서는 하나의 트랜잭션과 관련된 복잡한 비즈니스 로직 처리가 필요한 경우보다는 단순 데이터 조회 혹은 저장과 과련된 부분이 많았다고 생각이 듭니다. 즉, 이번 미션을 포함하여 DAO에대한 의존을 하여 데이터 조회 혹은 저장 등과 같은 기능만을 필요로 하지, Service 계층에 대한 의존이 불필요했다고 생각합니다!

하지만 저도 이 둘에 대해서 정답은 없다고 생각되고, 만약 비즈니스 로직이 더욱 복잡해진다고 하고, 각 서비스 클래스 마다 중복되는 로직이 많아진다라고 하면 Service에 대한 의존도 고려해볼 것 같습니다.😁

profile
꾸준함에서 의미를 찾자!

0개의 댓글