패키지 구조 표준은 도메인 안을 네 겹으로 나누고 들어오는 칸을 api라고 불렀다. 그때는 컨트롤러가 한 종류라고 전제하고 있었다.
그런데 컨트롤러는 두 종류다. JSON을 내보내는 API 컨트롤러가 있고, 템플릿 엔진으로 HTML을 렌더해 내보내는 Web 컨트롤러가 있다. 둘은 같은 자리에 살지만 적용되는 규칙이 정반대다.
// 안 A — 지금 구조. 둘이 같은 패키지에 산다
order/api/OrderApiController.java // HttpApiResponse로 감싼다
order/api/OrderWebController.java // 감쌀 수 없다. HTML이다
// 안 B — 출력 형식으로 자리를 가른다
order/api/OrderController.java
order/web/OrderWebController.java
걸려 있는 건 이름의 예쁨이 아니다. 이 저장소의 규칙은 applyTo 글로브로 폴더를 겨눠서 붙는다. 자리가 안 갈라지면 “응답은 HttpApiResponse로 감싼다”가 Web 컨트롤러에도 그대로 붙는다. 그리고 아무 신호도 안 난다.
먼저: 계층 이름이 어긋나는 자리는 두 표면에 있었다
이 글은 원래 다른 문제에서 출발했다. 「아직 정하지 않은 것」 장부의 2번 — 같은 자리를 도메인 드리븐은 api라 부르고 3계층은 Controller라 부른다는 모순이다.
인증 주체 표준의 원칙 1은 “LoginUser는 api에서 죽는다”인데, 이 글은 3계층 탭에도 올라가 있다. 3계층 프로젝트에는 api 폴더가 없다. 시키는 대로 하려는 순간 만들 폴더가 없어서 막힌다.
범위는 재놨었다. 양쪽에 걸치는 글 중 계층 이름을 쓰는 건 2편, 총 17군데. 그런데 그건 산문만 센 것이었다.
# coding-standards/.github/instructions/authenticated-user-standard.instructions.md
applyTo: "**/api/**/*.java, **/common/**/*.java"
이 줄은 산문이 아니라 폴더를 찾는 패턴이다. 3계층 프로젝트에 api 폴더가 없으면 이 규칙은 아무 파일에도 안 붙는다. 에러도 경고도 없이 그냥 없는 것처럼 동작한다. layered를 단 표준 9편 중 4편이 이 상태였다.
| 표준 | applyTo |
3계층에 있는 폴더인가 |
|---|---|---|
| authenticated-user | **/api/**, **/common/** |
없다 |
| swagger-documentation | **/api/** |
없다 |
| service-assert | **/application/** |
없다 |
| exception-taxonomy | **/infra/**, **/common/** |
없다 |
세는 방법을 한 번 틀렸다. 처음엔 본문 전체에서 layered를 grep해 15편이 나왔는데, 그건 layered-dto-naming-standard라는 링크 문자열까지 센 것이다. 소속은 front matter의 architectures:에만 있다. 실제로는 9편이고 그중 4편이었다.
산문은 얼버무릴 수 있지만 글로브는 못 얼버무린다. 실재하는 폴더 이름을 대야 한다. 그래서 이 모순은 “글을 어떻게 쓸 것인가”가 아니라 “3계층의 폴더 이름을 무엇으로 정할 것인가” 를 먼저 답해야 닫히는 문제였다.
원칙 1. 컨트롤러는 출력 형식으로 갈라 api와 web 형제 패키지에 둔다
3계층 폴더 이름을 정하려던 자리에서 컨트롤러가 두 종류라는 사실이 나왔고, 그게 api라는 이름 자체를 흔들었다. SSR 컨트롤러도 api/ 안에 산다면 api는 자기 내용물을 틀리게 부르는 이름이다.
정해야 했던 건 이거였다 — 컨트롤러 두 종류를 패키지 구조의 어디서 가를 것인가.
안 A — api 안에서 한 겹 더 나눈다
order/api/rest/ OrderRestController, XxxRequest, XxxResponse
order/api/web/ OrderWebController
얻는 것은 기존 구조를 안 건드린다는 점이다. api라는 계층 이름이 그대로 살고 패키지 구조 표준의 “네 겹” 서술도 그대로다. 글로브는 **/api/rest/**로 좁히면 정확해진다.
버린 이유는 api/web이 자기모순이라서다. “API인데 웹”이 무슨 뜻인지 6개월 뒤에 안 읽힌다. api가 계층 이름과 종류 이름 두 역할을 겸하는 순간 생기는 문제고, 겹의 개수를 늘려도 그 겸직은 안 풀린다.
안 B — 계층 이름을 presentation으로 바꾸고 그 아래서 나눈다
order/presentation/api/
order/presentation/web/
얻는 것은 이름이 전부 맞는다는 것이다. 계층 이름(presentation)과 종류 이름(api/web)이 서로 겹치지 않는다.
버린 이유는 파급이 제일 큰데 얻는 게 이름의 정확함뿐이라서다. **/api/** 글로브 3편, 패키지 구조 표준의 구조도와 mermaid, _data의 계층 id, api를 계층 이름으로 부르는 본문 전부가 바뀐다. 그리고 3계층이 노리는 게 단순함인데 order/presentation/api/는 도메인 아래 세 겹이다.
안 C — 형제 패키지 둘로 뽑는다
order/api/ JSON을 내보낸다
order/web/ HTML을 내보낸다
order/application/
order/domain/
order/infra/
얻는 것은 자리 이름이 곧 출력 형식이라는 점이다. 폴더를 보면 무엇이 나가는지 알고, 글로브도 **/api/** 하나로 정확히 겨눈다. 덤이 하나 더 있는데, 3계층도 들어오는 칸을 같은 이름으로 쓸 수 있다는 것이다.
order/api/ order/api/
order/web/ order/web/
order/application/ order/service/
order/domain/ order/repository/
order/infra/
들어오는 두 칸의 이름이 양쪽에서 같아진다.
안 D — 나누지 않고 클래스 이름으로만 구분한다
order/api/OrderApiController.java
order/api/OrderWebController.java
얻는 것은 구조 변경이 0이라는 점이다.
버린 이유는 글로브가 못 가른다는 것이다. applyTo: "**/api/**"는 두 클래스를 똑같이 집는다. Copilot이 Web 컨트롤러를 편집할 때 “응답은 HttpApiResponse로 감싼다”를 그대로 먹인다. 규칙이 틀린 자리에 붙는데 아무 신호가 없다 — 3계층에서 글로브가 조용히 안 붙는 고장과 정확히 대칭이다.
골랐다 — 안 C. 안 A와 안 B는 종류를 계층 안에 숨겨서 글로브가 한 겹 더 파고들게 만들고, 안 D는 글로브를 아예 못 쓰게 만든다. 안 C만 자리와 규칙 적용 대상이 1:1로 맞는다. 대신 패키지 구조 표준의 “네 겹”이 더 이상 모든 도메인에 맞지 않는 것을 감수한다.
원칙 2. web은 새 계층이 아니라 들어오는 계층의 두 번째 패키지다
형제 패키지로 뽑기로 했으니 그 형제가 계층인지 패키지인지를 정해야 한다. 이걸 안 정하면 3계층의 “책임을 셋으로만 나눈다”가 깨지는지 아닌지부터 답이 안 나온다.
bulk와 batch를 뽑을 때의 근거를 먼저 확인했다. 그때는 “부를 수 있느냐” 가 기준이었다 — bulk를 infra에 두면 같은 패키지의 Repository 구현체가 벌크 Mapper를 주입받을 수 있고, Service는 못 부르는데 구현체는 부를 수 있는 구멍이 생긴다.
그 근거는 여기 안 온다. API 컨트롤러와 Web 컨트롤러는 서로 부를 일이 없다. 둘 다 application의 UseCase를 부를 뿐이고 의존 방향이 완전히 같다. 다른 건 나가는 것이 JSON이냐 HTML이냐뿐이다.
그래서 이렇게 정한다.
Controller, Request, Response"] --> APP["application
UseCase, Service"] WEB["web
WebController, View 모델"] --> APP APP --> DOM["domain
Domain, Repository 인터페이스"] INF["infra
Entity, Mapper, 구현체"] --> DOM style API fill:#2d3748,stroke:#4299e1,stroke-width:2px,color:#e2e8f0 style WEB fill:#2d3748,stroke:#9f7aea,stroke-width:2px,color:#e2e8f0 style APP fill:#2d3748,stroke:#48bb78,stroke-width:2px,color:#e2e8f0 style DOM fill:#1a202c,stroke:#ed8936,stroke-width:3px,color:#e2e8f0 style INF fill:#2d3748,stroke:#f56565,stroke-width:2px,color:#e2e8f0
계층은 그대로다. 들어오는 계층이 패키지를 둘 가질 수 있을 뿐이다. 그러면 셋으로 나눈 3계층도 여전히 셋이고, 도메인 드리븐도 여전히 넷이다.
패키지 개수는 bulk·batch 때와 같은 문장으로 처리한다 — 기본형은 넷이고, SSR을 쓰는 도메인만 web을 더 갖는다. JSON만 내보내는 도메인에 빈 web/ 폴더를 만들지 않는다.
원칙 3. 3계층도 도메인을 먼저 나누고 그 안에 계층을 둔다
3계층의 폴더 구조는 이렇게 간다.
order/
├── api/ Controller (JSON)
├── web/ WebController (HTML)
├── service/ Service
└── repository/ Repository, DAO, Mapper
도메인 드리븐과 패키징 원칙이 같다 — 도메인을 먼저 나누고, 계층은 그 안에 둔다. 차이는 그 안이 몇 겹이냐뿐이다. 3계층에는 domain이 없고, 그래서 application/infra로 의존을 뒤집는 일도 없다. 호출 방향과 의존 방향이 그냥 같다.
여기서 선택지는 하나였다. 흔한 3계층은 최상위를 계층으로 먼저 나누지만(controller/order, service/order), 그 안은 검토하지 않았다. 도메인 드리븐에서 이미 도메인 먼저로 정했고, 두 구조를 오가는 사람이 매번 반대로 생각해야 할 이유를 못 찾았기 때문이다. 검토를 안 했다는 사실을 여기 적어둔다 — 나중에 3계층 프로젝트에서 이 순서가 걸리면 그때 다시 연다.
3계층에서 노리는 것은 단순함이다. 계층을 더 쪼개거나 인터페이스로 뒤집지 않는다. 그래서 domain을 전제하는 표준(Repository 설계 표준, Domain 영속화 표준)은 3계층에서 문장 자체가 성립하지 않는다. 이 둘은 이미 ddd만 달고 있어서 고칠 것이 없다 — 소속 기준이 폴더를 정하기 전부터 이미 걸러주고 있었다.
다만 그 기준이 글을 통째로 걸러낸다는 점은 남는다. Repository 설계 표준의 원칙 다섯 중 셋은 3계층에서도 그대로 통하는데, 나머지 둘이 domain을 쓰기 때문에 글 전체가 도메인 드리븐 전용이 된다. 이건 이 글에서 닫지 않고 열린 항목으로 남긴다.
원칙 4. 양쪽이 다르게 부르는 자리는 계층 이름 대신 클래스 이름으로 쓴다
원칙 1과 3을 정하고 나니 api가 들어간 자리는 저절로 닫혔다. 3계층에도 api 폴더가 생겼으므로 “LoginUser는 api에서 죽는다”가 양쪽에서 그대로 맞는 문장이 된다. 헤딩도 안 고친다.
남은 건 application 5회와 infra 2회, 총 7군데다. 이건 여전히 3계층에 없는 이름이다.
안 A — 중립어로 다시 쓴다. “application으로는” → “유스케이스를 조율하는 자리로는”. 양쪽에서 읽히지만 중립어가 사람마다 달라지면 헤딩과 본문이 곧 색인이라 grep이 깨진다.
안 B — application(Service)처럼 병기한다. 양쪽이 자기 이름을 보지만 서술이 두 벌 되고, 컨트롤러가 두 종류라는 걸 안 지금은 부정확한 이름을 두 벌로 늘리는 셈이다.
안 C — 계층어를 쓰는 글에서 layered를 뺀다. grep으로 자동 검사까지 되지만 3계층에서도 통하는 규칙을 3계층이 못 받게 된다. 인증 주체 표준은 3계층에서도 그대로 맞는 규칙이다.
안 D — 글을 안 고치고 대응표를 한 곳에 둔다. 수정이 0이지만 검색으로 글에 바로 들어온 사람은 그 표를 안 거친다. 이 저장소가 「소속은 글이 든다」로 이미 한 번 기각한 형태다.
안 E — 3계층 폴더도 application/infra로 부른다. api처럼 이름을 맞춰버리면 7군데를 안 고쳐도 된다. 버린 이유는 infra가 “domain의 반대편”이라는 뜻으로 지은 이름이라서다. 3계층에는 domain이 없다. 짝이 없는 이름을 가져오면 6개월 뒤에 “여기 domain은 어디 있지”를 찾게 된다. application도 같은 이유로 domain과 짝일 때만 의미가 산다.
골랐다 — 안 A. 다만 중립어를 지어내지 않는다. 그 자리에 사는 클래스 이름은 이미 양쪽이 같다.
| 지금 | 도메인 드리븐 | 3계층 | 바꿔 쓸 말 |
|---|---|---|---|
application |
application/XxxService |
service/XxxService |
Service |
infra |
infra/XxxRepositoryImpl |
repository/XxxRepository |
Repository 구현체 |
// 지금 — 3계층에 없는 폴더 이름이다
`LoginUser`는 `api`에서 죽는다. `application`으로는 `Long userId`만 간다
// 바꾸면 — 양쪽 다 맞는다
`LoginUser`는 `api`에서 죽는다. `Service`로는 `Long userId`만 간다
이게 안 A의 약점을 지운다. 중립어가 표류할 걱정이 없는 이유는 Service·Repository가 지어낸 말이 아니라 표준이 이미 못 박아둔 클래스 이름이기 때문이다. 대가는 계층을 가리키던 말이 클래스를 가리키게 되면서 “어느 패키지에 두라”는 정보가 그 문장에서 빠진다는 것이다. 그건 패키지 구조 표준이 답할 일이라 여기서 겹쳐 말하지 않는다.
원칙 5. applyTo 글로브는 두 아키텍처의 실제 폴더를 모두 적는다
3계층 폴더 이름이 정해졌으니 글로브가 겨눌 과녁이 생겼다. 남은 건 한 파일에서 양쪽을 어떻게 겨누느냐다.
안 A — 글로브를 **/*.java로 넓힌다. 어느 구조에서든 붙는다. 버린 이유는 그러면 규칙이 전혀 상관없는 파일에도 붙는다는 것이다. Repository 구현체를 고치는데 Controller 규칙이 따라온다. 지금 글로브를 좁게 쓰는 이유가 그거였다.
안 B — 아키텍처별로 파일을 두 벌 만든다. 각자 자기 경로만 겨눈다. 버린 이유는 같은 규칙이 두 파일이 되면서 한쪽만 고치는 순간 어긋난다는 것이다. 이 저장소가 글·SKILL.md·instructions.md를 항상 같은 커밋에 넣는 이유와 같은 문제를 하나 더 만든다.
골랐다 — 양쪽 경로를 한 줄에 나열한다. applyTo는 쉼표로 여러 패턴을 받으므로 파일을 늘리지 않고 양쪽을 겨눌 수 있다.
# 지금 — 3계층에서 아무 파일에도 안 붙는다
applyTo: "**/application/**/*.java"
# 바꾸면 — 양쪽에서 붙는다
applyTo: "**/application/**/*.java, **/service/**/*.java"
대응은 이렇게 된다.
| 도메인 드리븐 | 3계층 |
|---|---|
**/api/** |
**/api/** (같다) |
**/web/** |
**/web/** (같다) |
**/application/** |
**/service/** |
**/infra/** |
**/repository/** |
**/domain/** |
없다 |
**/domain/**만 겨누는 표준은 3계층에 붙을 자리가 없다. 그건 글로브로 해결할 문제가 아니라 소속(architectures)을 확인하라는 신호다. Repository 설계 표준과 Domain 영속화 표준이 그 경우인데, 둘 다 이미 ddd만 달고 있어서 경로를 늘릴 일이 없었다. 글로브와 소속이 어긋나면 둘 중 하나가 틀린 것이고, 먼저 보는 쪽은 소속이다.
이 규칙으로 실제로 고친 것은 service-assert와 exception-taxonomy 둘이다. swagger-documentation과 authenticated-user는 **/api/**만 겨누고 있어서 원칙 1이 3계층에 api 폴더를 만든 것만으로 저절로 맞았다.
본문 한 벌과 글로브 양쪽 경로는 짝이다. 파일을 두 벌로 가르면 각 파일이 자기 아키텍처 이름을 쓰면 되니 원칙 4가 할 일이 없어지고, 계층 호칭 문제가 같이 되살아난다. 하나만 되돌리는 게 아니라 둘이 함께 무너진다.
대가는 글로브 줄이 길어지고, 아키텍처를 새로 열 때마다 몇 편이 아니라 전 편의 applyTo를 다시 훑어야 한다는 것이다. 아키텍처가 둘인 지금은 감당되는 비용이다. 셋째가 생기는 시점이 파일을 가를지 다시 판단할 자리다 — 그때는 본문 계층 호칭도 함께 봐야 한다.
판단 기준 정리
| 질문 | 답 | 결론 |
|---|---|---|
| JSON 컨트롤러는 어디에? | {도메인}/api |
양쪽 아키텍처 공통 |
| HTML(SSR) 컨트롤러는 어디에? | {도메인}/web |
형제 패키지. 쓰는 도메인만 만든다 |
web은 새 계층인가? |
아니다 | 의존 방향이 api와 같다. 계층 수는 안 변한다 |
| 3계층의 폴더는? | api web service repository |
도메인 먼저, 계층은 그 안 |
글에서 application이라고 써도 되나? |
안 된다 | Service로 쓴다 |
글에서 infra라고 써도 되나? |
안 된다 | Repository 구현체로 쓴다 |
글에서 api라고 써도 되나? |
된다 | 양쪽에 같은 이름의 폴더가 있다 |
applyTo에 무엇을 적나? |
양쪽 경로를 쉼표로 나열 | 파일을 두 벌 만들지 않는다 |
**/domain/**만 겨누는 표준은? |
소속에서 layered를 뺀다 |
글로브로 못 메운다 |
이 표준을 정하기까지
시작은 못 고르고 멈춰 있던 장부 2번이었다. 같은 자리를 도메인 드리븐은 api, 3계층은 Controller라 부르는 모순이었고, 안을 넷까지 뽑아놓고 그중 하나를 고르지 못한 채로 남아 있었다. 이번에 다시 열었다.
첫 발견은 범위를 잘못 쟀다는 것이었다. 장부에는 “17군데”라고 적혀 있었는데 그건 산문만 센 것이다. instructions.md의 applyTo 글로브가 별개의 표면이었고, 거기 DDD 패키지 경로가 박혀 있었다. 배지가 어긋나는 것보다 나쁜 고장이다 — 배지는 사람이 보고 이상하다고 느끼지만, 글로브는 아무 파일에도 안 붙고 아무 신호도 안 낸다.
그리고 그 범위도 한 번 더 틀렸다. 처음 센 “15편 중 7편”은 본문 전체에서 layered를 grep한 결과였는데, 거기에 layered-dto-naming-standard라는 링크 문자열이 섞여 있었다. front matter만 세니 9편 중 4편이었다. 범위를 잘못 재서 시작한 문제를 다시 잘못 쟀다는 게 이 대화에서 제일 뼈아픈 대목이다. 그래서 지금은 소속을 셀 때 본문이 아니라 architectures: 블록만 본다.
그리고 그 글로브는 안 A~D 중 무엇을 골라도 안 닫힌다는 걸 알았다. 산문은 중립어로 얼버무릴 수 있지만 글로브는 실재하는 폴더 이름을 대야 한다. 그런데 3계층의 폴더 이름을 정한 적이 없었다. 모순을 닫으려면 먼저 폴더를 정해야 했다. 문제가 한 칸 뒤로 밀렸다.
폴더를 정하려다 컨트롤러가 두 종류라는 사실이 나왔다. JSON을 내보내는 API 컨트롤러와 템플릿 엔진으로 HTML을 내보내는 Web 컨트롤러다. 그리고 이 구분은 3계층만의 것이 아니라 도메인 드리븐에도 똑같이 있다. 그 순간 api는 “양쪽이 다르게 부르는 같은 자리”가 아니라 한쪽에서도 정확하지 않은 이름이 됐다. 병기(안 B)가 여기서 죽었다 — 부정확한 이름을 두 벌로 늘리는 일이 되기 때문이다.
자리를 나눌 근거를 기존 결정에서 찾아봤는데 안 맞았다. bulk를 형제로 뽑은 근거는 “같은 패키지면 부를 수 있다”였다. API 컨트롤러와 Web 컨트롤러는 서로 부를 일이 없으니 그 근거는 안 온다. 대신 다른 근거가 있었다 — 적용되는 규칙이 갈린다. 공통 봉투·ErrorCode·조건부 GET은 전부 JSON 쪽 규칙이고, 이 저장소에서 규칙은 글로브로 폴더를 겨눠 붙는다. 자리가 안 갈라지면 글로브가 못 겨눈다. 안 D가 여기서 죽었다.
형제 패키지(안 C)를 고르자 처음 문제의 절반이 저절로 닫혔다. 3계층에도 api 폴더가 생기니 “LoginUser는 api에서 죽는다”가 양쪽에서 맞는 문장이 됐다. 모순을 풀려고 고른 게 아니라 컨트롤러를 가르려고 고른 것인데 모순이 따라 닫혔다. 이건 예상 못 한 수확이었다.
남은 7군데의 중립어는 지어낼 필요가 없었다. application과 infra를 무슨 말로 바꿀지 고민하다가, 그 자리에 사는 클래스 이름이 이미 양쪽에서 같다는 걸 알았다 — XxxService와 XxxRepository다. 안 A의 유일한 약점이 “중립어가 표류하면 grep이 깨진다”였는데, 표준이 이미 못 박아둔 클래스 이름을 쓰면 표류할 데가 없다.
3계층 폴더를 application/infra로 부르는 길(안 E)도 잠깐 열렸다. 이름을 맞춰버리면 7군데를 안 고쳐도 된다. 버린 이유는 infra가 “domain의 반대편”이라는 뜻으로 지은 이름이라서다. 3계층에는 domain이 없다. 짝 없는 이름을 가져오면 없는 짝을 찾게 된다.
마지막에 이 결정이 만든 빈틈을 확인했다. web/이라는 자리는 생겼는데 그 안에서 무엇을 하라는 규칙이 하나도 없다. SSR·템플릿 엔진은 표준 24개와 SKILL.md 20개 전체에서 0건이다. 공통 봉투는 HTML을 못 감싸고 @RestControllerAdvice는 View를 못 돌려준다. 이건 여기서 닫을 수 없어서 열린 항목으로 남긴다.
정리
- 컨트롤러는 출력 형식으로 가른다. JSON은
{도메인}/api, HTML은{도메인}/web web은 새 계층이 아니다.api와 의존 방향이 같은 형제 패키지다. 쓰는 도메인만 만든다- 3계층도 도메인을 먼저 나눈다.
apiwebservicerepository.domain이 없으므로 의존을 뒤집지 않는다 - 글에서는
application·infra를 쓰지 않는다.Service·Repository구현체로 쓴다. 클래스 이름은 양쪽이 이미 같다 api는 그대로 쓴다. 양쪽에 같은 이름의 폴더가 생겼다applyTo는 양쪽 경로를 쉼표로 나열한다. 파일을 두 벌 만들지 않는다**/domain/**만 겨누는 표준은 소속이 틀린 것이다. 글로브로 못 메운다
AI 코드 어시스턴트에 바로 적용하기
coding-standards/.claude/skills/controller-package-split-standard/SKILL.md
---
name: controller-package-split-standard
description: 컨트롤러를 출력 형식으로 갈라 api·web 패키지에 두는 규칙과, 두 아키텍처가 계층을 부르는 이름 규칙. 패키지를 만들거나 표준 문서를 쓸 때 반드시 적용한다.
---
# 컨트롤러 패키지 분리 표준
컨트롤러는 두 종류다. JSON을 내보내는 것은 `{도메인}/api`, 템플릿 엔진으로 HTML을 내보내는 것은 `{도메인}/web`에 둔다.
## 패키지 배치
- JSON을 반환하는 Controller와 그 `XxxRequest`·`XxxResponse`는 `{도메인}/api`에 둔다.
- 템플릿을 렌더하는 Controller와 View 모델은 `{도메인}/web`에 둔다.
- `web`은 SSR을 쓰는 도메인만 갖는다. 빈 `web/` 폴더를 만들지 않는다.
- `api`와 `web`은 형제다. 서로를 참조하지 않는다.
- 두 패키지 모두 `application`의 UseCase만 부른다. 의존 방향이 같다.
- 한 클래스가 JSON과 HTML을 둘 다 반환하지 않는다.
## 3계층 아키텍처의 패키지
- 도메인을 먼저 나누고 계층은 그 안에 둔다. `order/api`, `order/web`, `order/service`, `order/repository`.
- `domain` 패키지를 만들지 않는다. Repository 인터페이스로 의존을 뒤집지 않는다.
- 호출 방향과 의존 방향이 같다. `api`·`web` → `service` → `repository`.
## 표준 문서를 쓸 때의 계층 호칭
- `application`이라고 쓰지 않는다. `Service`로 쓴다.
- `infra`라고 쓰지 않는다. `Repository` 구현체로 쓴다.
- `api`·`web`·`common`은 그대로 쓴다. 양쪽 아키텍처에 같은 이름의 패키지가 있다.
- `domain`을 쓰는 문장은 도메인 드리븐 전용이다. 그 글의 `architectures`에서 `layered`를 뺀다.
## instructions.md의 applyTo
- 양쪽 아키텍처의 경로를 한 줄에 쉼표로 나열한다. 파일을 두 벌 만들지 않는다.
- `**/application/**`을 적으면 `**/service/**`도 적는다.
- `**/infra/**`를 적으면 `**/repository/**`도 적는다.
- `**/api/**`, `**/web/**`, `**/common/**`은 그대로 둔다.
- `**/domain/**`만 겨누는 표준은 경로를 늘리지 않는다. `architectures`에서 `layered`를 뺀다.
coding-standards/.github/instructions/controller-package-split-standard.instructions.md
---
applyTo: "**/*.java"
---
# 컨트롤러 패키지 분리 표준
- JSON을 반환하는 Controller와 그 `XxxRequest`·`XxxResponse`는 `{도메인}/api`에 둔다.
- 템플릿을 렌더하는 Controller와 View 모델은 `{도메인}/web`에 둔다.
- `web`은 SSR을 쓰는 도메인만 갖는다. 빈 `web/` 폴더를 만들지 않는다.
- `api`와 `web`은 서로를 참조하지 않는다. 둘 다 `application`의 UseCase만 부른다.
- 한 클래스가 JSON과 HTML을 둘 다 반환하지 않는다.
- 3계층은 도메인을 먼저 나누고 그 안에 `api`·`web`·`service`·`repository`를 둔다.
- 3계층에는 `domain` 패키지를 만들지 않는다. 호출 방향과 의존 방향이 같다.
- 표준 문서에서는 `application` 대신 `Service`, `infra` 대신 `Repository` 구현체로 쓴다.
- `applyTo`에 `**/application/**`을 적으면 `**/service/**`도, `**/infra/**`를 적으면 `**/repository/**`도 함께 적는다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기