Загружаем каталог…
Загружаем каталог…
4계층이라고 만든 배달 주문 백엔드가 사실은 3계층처럼 굴러가고 있었다. 여섯 번의 질문으로 Repository 위치, 의존 방향, 패키지 구성, 서비스의 역할, 도메인 모델 노출, 기술 추상화를 차례로 바로잡았다. 한눈에 보는 계층별 역할과 책임 큰 그림은 알고 있던 그대로였다. 달랐던 것은 규칙과 경계를 정확히 어디에 두고, 누가 소유하느냐 였다. 계층마다 역할과 책임을 정리하고, 이번에 새로 알게 된 것을 따로 적었다. presentation · 관문 시스템과 바깥(클라이언트)이 만나는 경계다. 요청값을 받아 안으로 전달하고 응답값을 돌려주는 관문 역할만 하며, 판단은 안쪽 계층에 맡긴다. 책임: HTTP 요청·응답, 입력 형식 검증( @Valid ), 상태 코드 결정 하지 않는 것: 비즈니스 판단, Repository 호출 새로 알게 된 것: Request를 그대로 넘기지 않고 application의 Command로 바꿔 넘긴다( toCommand() ). 응답은 application이 만든 DTO를 그대로 돌려준다. domain의 enum조차 import하지 않는다. application · 지휘자(Facade) 시스템이 해야 할 일(유스케이스)을 정의하고, 도메인 객체들이 그 일을 하도록 지휘하는 계층이다. 스스로 규칙을 갖지 않고 얇게 유지하며, 여러 도메인에 걸친 흐름을 Facade로 오케스트레이션한다. 그래서 비즈니스 규칙을 담은 서비스는 두지 않는다. 책임: 트랜잭션 경계, 도메인 서비스 호출 순서, 기술 처리(암호화·토큰), 엔티티 → 응답 DTO 변환 하지 않는 것: 상태 전이·금액 계산 같은 규칙, Repository 직접 호출 새로 알게 된 것: DTO는 변환만이 아니라 소유 도 application이다(Command·Response). 암호화·토큰 같은 기술도 구체 클래스가 아니라 application이 정한 인터페이스로 쓴다. (이 그림을 알고 있었는데도, 클로드 코드가 작성한 처음 코드는 Service 하나가 흐름과 조회·404·403·저장까지 모두 들고 있었다.) domain · 심장 비즈니스의 개념과 규칙, 상태를 표현하는 소프트웨어의 핵심이다. 비즈니스 규칙과 모델을 관리하며, 다른 계층이나 기술이 바뀌어도 흔들리지 않아야 한다. 책임: 엔티티는 규칙(상태 전이·총액 계산·소유 판단), 도메인 서비스는 조회·존재(404)·소유(403) 확인·저장, Repository 인터페이스는 "무엇이 필요한지"를 정한다 하지 않는 것: 트랜잭션, DTO, Security, DB 기술(Spring Data JPA) 새로 알게 된 것: 규칙 자체는 서비스가 아니라 엔티티 메서드 가 갖는다. 도메인 서비스는 Repository가 필요한 일을 맡고 엔티티에 일을 시킨다. Repository 인터페이스도 domain이 소유한다. infrastructure · 바깥으로 난 통로 다른 계층을 기술적으로 받쳐 주는 계층이다. DB, Redis, Kafka 같은 외부 인프라와의 연결(저장, 메시징, 외부 통신)을 맡는 통로다. 책임: Repository 구현(Spring Data JPA), JWT 발급·검증, Security 설정, 외부 인프라 연결 하지 않는 것: 비즈니스 규칙 새로 알게 된 것: 접근하는 구현 만 맡는다. 무엇이 필요한지는 안쪽 계층이 인터페이스로 정하고(domain의 Repository, application의 TokenProvider), infrastructure가 그 모양에 맞춘다. 정렬 컬럼이나 DB 예외 변환 같은 DB 세부도 여기에 둔다. 원칙 · 모델을 밖으로 노출하지 않는다 엔티티 같은 도메인 모델은 안쪽 계층에서만 다루고, 밖으로 나갈 때는 반드시 DTO로 바꾼다. 레이어드 아키텍처에서 가장 중요한 원칙 중 하나다. 이 원칙이 중요한 이유는 다섯 가지다. 내부를 고쳐도 API가 흔들리지 않는다. 모델이 곧 응답이면 필드 이름이나 enum 값 하나만 바꿔도 클라이언트가 받는 응답이 조용히 바뀐다. DTO가 사이에 있으면 바뀐 곳이 변환 코드에서 드러난다. 보여 주면 안 되는 값이 새지 않는다. 엔티티를 그대로 내보내면 비밀번호처럼 감춰야 할 필드까지 응답에 섞일 수 있다. DTO에는 보여 줄 값만 골라 담는다. 바깥에서 규칙을 우회할 수 없다. 요청을 엔티티로 바로 받으면 클라이언트가 상태나 금액 같은 필드를 직접 채워 보낼 수 있다. 이 프로젝트에서 총액과 결제 금액을 요청으로 받지 않고 서버가 계산하는 것도 같은 이유다. 기술 문제가 바깥으로 번지지 않는다. JPA 엔티티를 응답으로 직렬화하면, 트랜잭션 밖에서 지연 로딩이 터지거나( LazyInitializationException ) 연관관계를 따라 순환 참조가 생길 수 있다. 변환을 트랜잭션 안(application)에서 끝내면 이런 문제가 바깥에 닿지 않는다. 의존 방향이 지켜진다. presentation이 domain 모델을 알면 application을 건너뛴 의존이 생긴다. 모델을 안에 두면 presentation은 application의 DTO만 알면 된다. 새로 알게 된 것: enum도 모델이다. 응답 DTO에 domain enum이 들어가면 enum 이름이 곧 API 계약이 된다. 요청·응답의 역할·상태는 문자열로 주고받는다. 레이어드 아키텍처란 코드를 역할별 층(layer)으로 나누고, 위층만 아래층을 부르게 하는 구조 다. 규칙은 두 가지뿐이다. 층마다 맡는 일이 하나다. 의존은 한 방향이다. 아래층은 위층을 모른다. 식당으로 치면 홀 직원은 주문만 받아 주방에 넘기고, 주방은 요리만 하고, 창고는 재료만 꺼내 준다. 홀 직원이 창고에 직접 들어가거나, 창고가 손님 테이블을 알 필요가 없다. 그래서 한 층을 바꿔도 다른 층이 덜 흔들린다. 단, 4계층에서 infrastructure만은 화살표가 거꾸로다. domain이 "이런 게 필요하다"고 인터페이스를 정하면, infrastructure가 그 모양에 맞춰 구현한다. 그래서 DB를 바꾸거나 Redis·Kafka를 붙여도 규칙을 담은 domain은 그대로 남는다. 내가 알고 있던 레이어드 아키텍처 차세대 프로젝트를 하면서 몸에 익힌 기준은 이렇다. 층마다 한마디로 부를 수 있을 만큼 역할이 분명하다. 계층 한마디로 맡는 일 3계층에서는 presentation 관문 요청값을 받아 안으로 전달하고, 응답값을 밖으로 돌려준다 Controller application 지휘자 (Facade) 유스케이스와 여러 도메인에 걸친 흐름을 오케스트레이션한다. DTO 변환과 트랜잭션 관리도 여기서 한다 Service domain 심장 비즈니스 규칙(도메인 서비스)과 모델을 관리한다 Service 안에 섞여 있었다 infrastructure 바깥으로 난 통로 DB, Redis, Kafka 같은 외부 인프라에 접근한다 Repository 이번 프로젝트도 흔히 쓰는 3계층에서 출발해 이 기준대로 4계층으로 나눴다. 그런데 막상 만들고 보니 기준과 실제 코드 사이에 틈이 있었다. 출발점: 이름만 4계층이었다 회원·메뉴 기능까지 만든 뒤 점검해 보니, 패키지는 네 개였지만 실제 책임은 "Controller → Service → Repository" 3계층 그대로였다. infrastructure는 JWT와 Security 설정만 맡고, DB 접근은 domain에 있었다. 계층 실제로 들어 있던 것 문제 presentation Controller, Request DTO, Response DTO (응답 DTO가 presentation에 있었다) — application MenuService (트랜잭션, 조회, 404·403, 저장, DTO 변환) presentation의 DTO를 import, 유스케이스 흐름과 도메인 작업(조회·404·403·저장)이 한 클래스에 섞임 domain Menu , MenuRepository extends JpaRepository DB 기술(Spring Data JPA)에 묶임 infrastructure JwtProvider , SecurityConfig DB 접근을 맡지 않음 구조를 바꾸기 전에 테스트 104개가 모두 통과하고 있었다. 그래서 검증 내용은 그대로 두고 위치만 옮기면서, 단계마다 동작이 바뀌지 않았는지 바로 확인할 수 있었다. 고민 1. DB에 접근하는 코드는 어느 계층에 있어야 하나 결론은 인터페이스는 domain, 구현은 infrastructure 다. domain은 "무엇이 필요한지"만 정하고, "어떻게 가져올지"는 infrastructure가 맡는다. 질문은 이랬다. MenuRepository 가 JpaRepository 를 상속한 채 domain에 있으면, domain이 Spring Data JPA라는 DB 기술을 안다. 비유하면 주방장 수첩에 특정 창고 회사 전용 양식이 붙어 있는 셈이다. 두 가지 안을 비교했다. 인터페이스는 domain, 구현은 infrastructure Repository를 통째로 infrastructure로 모양 domain/MenuRepository (순수 인터페이스) ← infrastructure/MenuRepositoryImpl → MenuJpaRepository infrastructure/MenuRepository extends JpaRepository Service가 아는 것 domain 인터페이스만 DB 기술을 직접 추가 클래스 도메인마다 2개 없음 선택한 안에서 따라온 작은 결정들: domain 인터페이스 메서드는 도메인 언어로 지었다. findByIdExcludingDeleted 는 domain에, findByIdAndDeletedAtIsNull 같은 Query Method 이름은 infrastructure에만 둔다. "최신 등록순" 정렬은 Service가 아니라 MenuRepositoryImpl 로 옮겼다. createdAt , id 같은 컬럼 이름은 DB 쪽 정보이기 때문이다. 동시 가입 때 나는 UNIQUE 제약 위반( DataIntegrityViolationException )을 409로 바꾸는 코드도 UserRepositoryImpl 로 옮겼다. domain이 DB 예외를 모르게 하기 위해서다. 페이징 결과인 Page (Spring Data Commons)는 domain 인터페이스에서 허용했다. DB 기술이 아니라 결과를 담는 타입이고, 직접 만드는 비용이 얻는 것보다 컸다. 고민 2. application이 presentation의 DTO를 알아도 되나 결론은 application이 입력·출력 DTO를 소유한다 다. 그래야 의존이 presentation → application 한 방향이 된다. 처음에는 MenuService 가 presentation 패키지의 MenuRequest 를 받고 MenuResponse 를 돌려줬다. import를 세어 보니 application → presentation이 7곳이었다. Controller는 Service를 알고 Service는 Controller 쪽 DTO를 아는, 서로를 아는 구조였다. 바꾼 방식: presentation의 MenuRequest 는 HTTP 형식과 @Valid 검증만 맡는다. Controller가 request.toCommand() 로 application의 MenuCommand 를 만들어 넘긴다. application이 MenuResponse 를 만들어 돌려주고, Controller는 그대로 응답한다. 비유하면 손님의 주문서(Request)를 창구가 받아 형식을 확인하고, 직원이 쓰는 작업 지시서(Command)로 옮겨 적어 넘기는 것이다. 직원은 손님 주문서 양식을 몰라도 된다. 포기한 것은 Request를 Service에 그대로 넘기는 간결함이다. 응답 DTO가 application으로 가면서 @JsonFormat (Jackson 어노테이션)이 application에 남는 것은 허용했다. 고민 3. 패키지는 도메인 먼저인가, 계층 먼저인가 결론은 도메인 먼저, 그 안을 계층별로 나누는 지금 구조를 유지하는 것이다. 앞의 두 문제는 폴더 배치가 아니라 도메인 안의 계층 경계 문제라, 어느 방식을 써도 똑같이 생긴다. 1 도메인 먼저 (선택) 2 계층 먼저 menu/ presentation/menu/ ├─ presentation/ presentation/order/ ├─ application/ application/menu/ ├─ domain/ application/order/ └─ infrastructure/ domain/menu/ ... order/ ... 기준 1 도메인 먼저 2 계층 먼저 기능 하나를 고칠 때 한 폴더 안에서 끝남 4개 폴더를 오감 계층 구조가 보이는 정도 도메인마다 반복돼 덜 두드러짐 최상위에 바로 보임 도메인 간 의존 확인 order → menu import가 바로 드러남 같은 계층 안에 섞여 잘 안 보임 나중에 모듈로 분리 폴더째 떼어내기 쉬움 흩어진 조각을 모아야 함 브랜치와 TDD가 기능 단위( feat/menu , feat/order )로 진행되는 것도 1을 고른 이유다. 포기한 것은 "4계층"이 최상위 폴더에서 바로 보이는 명확함이다. 고민 4. Service는 domain에 가야 하나 결론은 application에는 Facade, domain에는 도메인 서비스 를 둔다. 비즈니스 규칙 자체는 계속 엔티티에 남긴다. 이 고민은 두 번에 걸쳐 정리됐다. 첫 질문: "Service는 도메인 레이어에 가야 맞지 않나?" 당시 Service가 쓰던 것을 하나씩 보니 @Transactional , Request·Response DTO, JwtProvider , PasswordEncoder 처럼 domain에 두면 안 되는 것들이었다. 그대로 옮기면 방금 고친 문제(domain이 기술에 묶임)가 다시 생긴다. 여기서 서비스에 두 종류가 있다는 걸 알게 됐다. 애플리케이션 서비스 (application) 도메인 서비스 (domain) 하는 일 유스케이스 진행 순서 지휘 도메인 단위 작업, 엔티티 하나에 넣기 어색한 규칙 다루는 것 트랜잭션, DTO, 인증·암호화 같은 기술 엔티티, 값, 자기 도메인의 Repository 인터페이스 3계층에서는 Service가 비즈니스 로직을 직접 담아서 "Service = 비즈니스 계층"으로 익히게 된다. 4계층에서는 그 로직이 엔티티로 내려가고, 애플리케이션 서비스에는 지휘 역할만 남는다. 두 번째 질문: "application에는 유스케이스 흐름과 도메인 서비스들을 조율하는 파사드가 와야 하지 않나?" 여기서 구조를 바꿨다. 결정을 밀어준 것은 곧 만들 주문·결제였다. 주문 생성은 메뉴·회원·주문이, 결제는 주문·결제가 엮인다. 애플리케이션 서비스 하나로 가면 "없는 메뉴면 404"를 MenuService 와 OrderService 가 각자 구현하게 된다. Facade (application): 트랜잭션, 도메인 서비스 호출 순서, 암호화·토큰 같은 기술, DTO 변환 도메인 서비스 (domain): 조회, 존재 확인(404), 소유 확인(403), 저장, 엔티티에 일 시키기 엔티티: 상태 전이, 총액 계산 같은 규칙 자체 쓰면서 정한 세부 사항: 로그인에서 회원 없음·탈퇴 검사는 도메인 서비스가, 비밀번호 대조·토큰 발급은 Facade가 맡는다. domain이 Security를 모르게 하기 위해서다. 이때 Facade는 아직 JwtProvider 구체 클래스를 직접 썼는데, 이후 인터페이스로 바꿨다. 도메인 서비스는 자기 도메인의 Repository만 쓴다. PaymentService.pay(order, method) 는 주문을 직접 조회하지 않고 Facade에게서 받는다. Facade 단위 테스트는 여러 도메인을 조율하거나 기술을 엮는 것만 쓴다. 단순 위임을 테스트하면 "호출했는지"만 보는 구현 검증이 된다. 포기한 것은 클래스 하나로 끝내는 간결함이다. 메뉴 수정 같은 단순 CRUD에서는 Facade가 도메인 서비스를 그대로 부르기만 하는 통과용 코드가 생긴다. 또 도메인 서비스에 규칙이 쌓이면 엔티티가 빈 껍데기가 될 수 있어서, 규칙은 엔티티에 둔다는 선을 지켜야 한다. 고민 5. 도메인 모델은 어디까지 밖으로 나가도 되나 결론은 엔티티뿐 아니라 domain enum도 application 밖으로 내보내지 않는다 다. 요청·응답의 역할·상태는 문자열로 주고받는다. "도메인 모델이 밖으로 노출되는 것 같다"는 느낌에서 시작해 코드를 점검했다. 엔티티는 새지 않았다. Facade가 항상 XxxResponse 로 바꿔 돌려줬다. 대신 enum이 새고 있었다. presentation의 SignupRequest 가 domain의 UserRole 을 import했다. presentation이 application을 건너뛰고 domain을 직접 알고 있었다. 응답 DTO에 UserRole , MenuStatus 가 그대로 들어 있어, domain enum이 곧 API 계약이었다. 비유하면 손님 영수증에 주방 내부 코드표를 그대로 찍어 주는 셈이다. 주방에서 코드 이름을 바꾸면( SOLD_OUT → OUT_OF_STOCK ) 영수증 양식도 모르는 사이에 바뀐다. 바꾼 방식: 받을 때: SignupRequest.role 은 String + @Pattern("CUSTOMER|OWNER") 으로 검증하고, Facade가 UserRole.valueOf() 로 바꾼다. 내보낼 때: MenuResponse.from() 이 menu.getStatus().name() 으로 문자열을 넣는다. 덤으로 얻은 것도 있다. 잘못된 역할( "ADMIN" )을 보내면 전에는 JSON 변환 실패로 fieldErrors 가 빈 400이었는데, 이제 fieldErrors 에 role 이 담긴다. enum ↔ 문자열 변환용 공통 메서드도 고민했다. 내보낼 때는 name() 자체가 이미 모든 enum의 공통 메서드라, 래퍼를 만들어도 짧아지지 않는다. 받을 때는 @Pattern 허용값과 enum이 어긋나면 valueOf() 가 실패해 500이 날 수 있어, 안전망으로 EnumParser 같은 걸 둘 수도 있었다. 결국 공통 메서드 없이 가기로 했고, "enum 값을 바꾸면 @Pattern 도 함께 고친다"는 규칙으로 대신했다. 하나는 일부러 남겨 두었다. Security가 토큰으로 만든 AuthUser.role ( UserRole )을 Controller가 Facade로 넘기는 흐름이다. Controller는 UserRole 을 import하지 않고 값을 전달만 한다. 이것까지 문자열로 바꾸면 JWT와 Security 설정까지 손대야 해서, 얻는 것에 비해 범위가 커진다. 고민 6. application도 기술 구현을 몰라야 하나 결론은 기술도 쓰는 쪽(application)이 인터페이스를 정하고, infrastructure가 구현한다 다. Repository에 적용한 원칙을 토큰 발급에도 똑같이 적용했다. 주문 기능까지 만든 뒤 의존성 역전 원칙(DIP)으로 다시 점검했다. domain → infrastructure import는 0곳이었지만, UserFacade (application)가 JwtProvider (infrastructure) 구체 클래스를 직접 import하고 있었다. 토큰 방식을 바꾸면 Facade 코드도 함께 바뀌는 구조였다. 바꾼 방식: application에 TokenProvider 인터페이스( createToken , getExpirationSeconds )를 둔다. 쓰는 곳이 UserFacade 하나라 user/application 에 두었다. JwtProvider 가 TokenProvider 를 구현한다. 토큰 검증( parse )은 Security 필터만 쓰므로 인터페이스에 넣지 않았다. UserFacade 는 TokenProvider 만 안다. Facade 테스트도 인터페이스만 Mock한다. PasswordEncoder 는 그대로 두었다. 이미 인터페이스라 Facade가 BCrypt 구현을 모른다. Spring Security가 정한 인터페이스라 완전한 DIP는 아니지만, 표준 추상화로 보고 허용했다. 포기한 것은 구체 클래스를 바로 쓰는 간결함이다(인터페이스 1개 추가). 결정 정리 여섯 번의 고민 모두 "더 간결한 쪽"을 포기하고 경계를 분명하게 하는 쪽을 골랐다. 고민 선택 포기한 것 DB 접근은 어디에? 인터페이스는 domain, 구현은 infrastructure JpaRepository 상속 인
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
이름만 4계층이었다: 레이어드 아키텍처를 다시 배운 기록. 4계층이라고 만든 배달 주문 백엔드가 사실은 3계층처럼 굴러가고 있었다. 여섯 번의 질문으로 Repository 위치, 의존 방향, 패키지 구성, 서비스의 역할, 도메인 모델 노출, 기술 추상화를 차례로 바로잡았다. 한눈에 보는 계층별 역할과 책임 큰 그림은 알고 있던 그대로였다. 달랐던 것은 규칙과 경계를 정확히 어디에 두고, 누가 소유하느냐 였다. 계층마다 역할과 책임을 정리하고, 이번에 새로 알게 된 것을 따로 적었다. presentation · 관문 시스템과 바깥(클라이언트)이 만나는 경계다. 요청값을 받아 안으로 전달하고 응답값을 돌려주는 관문 역할만 하며, 판단은 안쪽 계층에 맡긴다. 책임: HTTP 요청·응답, 입력 형식 검증(…
Открыть источник