// 이 파일이 하는 일: "레이어드 아키텍처와 관심사 분리" 코스 — 돌아가는 코드에서 오래 사는
// 코드로 넘어가는 첫 관문인 '계층'을 다룬다. 왜 나누는가(변경 격리)에서 출발해, 3계층의
// 책임 경계, 의존성 방향, 도메인 로직의 자리, 계층 간 데이터 이동, 마이크로서비스로의 확장,
// 안티패턴 자가진단까지 7개 섹션으로 안내하는 정적 학습 페이지.
// 프레이밍은 "서비스를 만들어 본 사람이 잘 만드는 사람이 되는 길" — 이미 CRUD를 돌려 본
// 수습이, 실제 사내 코드(traderbox의 web·domain·config 패키지 규칙)를 거울 삼아
// 자기 코드의 섞인 책임을 알아채고 갈라내는 눈을 기르게 한다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_SPAGHETTI = `// 계층이 없는 코드 — '일단 돌아가는' 컨트롤러의 흔한 최후
@RestController
class SignupController {

  @PostMapping("/api/auth/signup")
  ResponseEntity<Map<String, Object>> signup(@RequestBody Map<String, String> body) {
    // ① 입력 검증 (입출력의 일)
    String email = body.get("email");
    if (email == null || !email.contains("@")) { /* 400 */ }

    // ② 비즈니스 규칙 (규칙의 일) — 이미 있는 이메일? 금지 도메인?
    if (userRepository.existsByEmail(email)) { /* 409 */ }
    if (email.endsWith("@banned.example")) { /* 403 */ }

    // ③ SQL 냄새 나는 저장 (저장의 일)
    jdbcTemplate.update("insert into users ...", email, hash(body.get("pw")));

    // ④ 부수 효과 (또 다른 규칙의 일) — 가입 축하 알림, 감사 로그...
    kafkaTemplate.send("audit.events", "SIGNUP:" + email);

    return ResponseEntity.ok(Map.of("ok", true));
  }
}
// 한 메서드에 네 가지 관심사가 산다. 지금은 '짧아서' 문제가 안 보이지만,
// "가입 규칙이 바뀌었어요"가 오는 순간 ①~④를 전부 다시 읽어야 한다.`;

const CODE_PACKAGES = `# traderbox(사내 실전 코드)의 패키지 규칙 — 계층이 폴더로 보인다
click.axpdev.tdatalab.auth
├── web/        # 입출력: 컨트롤러, 요청/응답 DTO — HTTP를 아는 유일한 곳
├── domain/     # 규칙+저장: 서비스, 엔티티, 리포지토리 — HTTP를 모른다
├── config/     # 조립: 보안 설정, 빈 등록 — 앱을 '엮는' 코드
└── (특화 패키지) security/ token/ audit/ push/ ...

# 규칙은 단순하다:
#  - web은 domain을 부른다. domain은 web을 모른다. (한 방향)
#  - "이 코드는 어느 폴더?"에 3초 안에 답 못 하면, 책임이 섞인 것.`;

const CODE_THIN_CONTROLLER = `// '얇은 컨트롤러' — 번역과 위임만 한다
@RestController
@RequestMapping("/api/watchlist")
class WatchlistController {

  private final WatchlistService watchlistService;

  WatchlistController(WatchlistService watchlistService) {
    this.watchlistService = watchlistService;
  }

  @PostMapping
  WatchlistItemResponse add(@AuthenticationPrincipal AuthUser user,
                            @RequestBody AddWatchlistRequest request) {
    // 하는 일 딱 두 가지: HTTP → 자바(번역), 서비스 호출(위임)
    return watchlistService.add(user.id(), request);
  }
}

// 실패는 서비스가 던지고, 스프링이 HTTP로 번역한다
@Service
class WatchlistService {
  WatchlistItemResponse add(Long userId, AddWatchlistRequest request) {
    if (repository.countByUserId(userId) >= MAX_ITEMS) {
      throw new ResponseStatusException(
          HttpStatus.CONFLICT, "관심종목은 최대 30개까지 담을 수 있어요");
    }
    // ... 규칙 통과 후 저장
  }
}`;

const CODE_FORBIDDEN = `계층별 "절대 하지 말 것" 목록
──────────────────────────────────────────────
Controller가 하면 안 되는 것
  · if로 비즈니스 규칙 판정 (한도 검사, 권한 규칙, 상태 전이)
  · Repository 직접 호출 (서비스를 건너뛰는 지름길)
  · 엔티티를 그대로 반환 (섹션 3에서 왜인지 본다)

Service가 하면 안 되는 것
  · HttpServletRequest, @RequestHeader 등 HTTP 타입 만지기
  · JSON 직렬화 형태를 걱정하기 (그건 web의 일)

Repository가 하면 안 되는 것
  · 비즈니스 판단 (findActivePremiumUserForEvent 같은 규칙 범벅 쿼리)
  · 다른 서비스 호출, 알림 발송 (저장 외의 부수 효과)
──────────────────────────────────────────────
공통: 자기 위 계층의 이름을 아는 순간, 경계는 이미 무너진 것.`;

const CODE_DIRECTION = `의존성 방향 — 화살표는 언제나 바깥 → 안쪽
┌────────────────────────────────────────────────┐
│  web (바깥)          domain (안쪽)              │
│                                                │
│  Controller ──────▶ Service ──────▶ Repository │
│  요청/응답 DTO         엔티티·규칙        저장     │
│                                                │
│  ◀──── 이 방향 화살표가 하나라도 생기면 사고 ────   │
└────────────────────────────────────────────────┘

왜 이 방향인가?
· HTTP는 자주 바뀐다 (URL 개편, 필드 추가, v2 API...)
· 규칙은 천천히 바뀐다 ("관심종목 30개 제한"은 API가 바뀌어도 그대로)
· 자주 바뀌는 것이 천천히 바뀌는 것에 의존해야,
  바깥이 흔들려도 안쪽이 무사하다. 반대면 규칙까지 같이 흔들린다.`;

const CODE_DTO = `// DTO는 record로 — 계층 경계의 통행증 (traderbox 컨벤션)
// 요청: 바깥에서 안으로 들어올 때 '이것만 받겠다'는 선언
public record AddWatchlistRequest(String symbol, String market) {}

// 응답: 안에서 바깥으로 나갈 때 '이것만 보여 주겠다'는 선언
public record WatchlistItemResponse(Long id, String symbol,
                                    String market, Instant addedAt) {}

// 엔티티는 domain에 산다 — passwordHash처럼 밖에 내보이면 안 되는
// 필드, JPA 지연로딩 프록시 같은 '내부 사정'을 품고 있다.
// 엔티티를 컨트롤러까지 올려보내 JSON으로 찍는 순간:
//  ① DB 컬럼 이름이 곧 API 스펙이 된다 (컬럼 하나 바꾸면 앱이 깨진다)
//  ② 숨겨야 할 필드가 실수 한 번에 응답에 실린다
//  ③ 직렬화가 지연로딩을 건드려 예상 못 한 쿼리가 터진다
// record는 불변이고, 필드가 곧 전부라 '통행증'으로 꼭 맞는 그릇이다.`;

const CODE_ANEMIC = `// 빈약한 도메인 모델(Anemic Model) — getter/setter 덩어리
class Portfolio {
  private List<Holding> holdings;
  public List<Holding> getHoldings() { return holdings; }
  public void setHoldings(List<Holding> h) { this.holdings = h; }
}
// 규칙은 전부 서비스에 흩어진다. 어디서든 setHoldings로
// 아무 값이나 꽂을 수 있으니, "합계가 왜 안 맞지?"의 범인 찾기가 시작된다.

// 행동을 가진 객체 — 규칙이 데이터와 같은 집에 산다
class Portfolio {
  private final List<Holding> holdings = new ArrayList<>();

  void addHolding(Holding holding) {
    if (holdings.size() >= 100) {
      throw new IllegalStateException("보유 종목은 100개를 넘을 수 없어요");
    }
    holdings.add(holding);
  }

  BigDecimal totalValue() {   // 계산 규칙도 데이터 곁에
    return holdings.stream()
        .map(Holding::value)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
  }
}
// 이제 '100개 제한'을 우회할 문이 없다. 규칙이 한 곳에만 있으니까.`;

const CODE_RULE_HOME = `규칙의 집을 고르는 간단한 기준
──────────────────────────────────────────────
도메인 객체에 둘 규칙 — 그 객체 하나만 알면 판정 끝
  · "보유 종목 100개 제한"        → Portfolio 안
  · "수량은 0보다 커야"           → Holding 생성자 안
  · "총평가액 = 보유분 합"         → Portfolio.totalValue()

서비스에 둘 규칙 — 여러 객체·외부 세계를 엮어야 판정 가능
  · "중복 이메일 가입 금지"        → DB를 물어야 함 → Service
  · "매수 기록 후 감사 이벤트 발행" → Kafka라는 바깥세상 → Service
  · "프리미엄 시세는 로그인 회원만" → 사용자+정책 조합 → Service
──────────────────────────────────────────────
질문 하나로 요약: "이 판정, 객체 혼자 아는 것으로 되나?"
되면 객체로, 안 되면 서비스로.`;

const CODE_MAPPING = `// 매핑은 '경계를 아는 계층'인 서비스(또는 web의 매퍼)에서
@Service
class WatchlistService {

  WatchlistItemResponse add(Long userId, AddWatchlistRequest request) {
    WatchlistItem saved = repository.save(
        WatchlistItem.of(userId, request.symbol(), request.market()));
    return toResponse(saved);          // 엔티티 → 응답 DTO, 여기서 끝
  }

  private WatchlistItemResponse toResponse(WatchlistItem item) {
    return new WatchlistItemResponse(
        item.getId(), item.getSymbol(), item.getMarket(), item.getAddedAt());
  }
}
// 요청 DTO와 응답 DTO를 '하나로 합치고 싶은' 유혹이 온다. 참자.
//  · 요청엔 없어야 할 필드(id, addedAt)가 응답엔 필요하고
//  · 응답엔 없어야 할 필드(비밀번호 원문)가 요청엔 있다
// 하나로 합치면 "이 필드, 지금 들어오는 값이야 나가는 값이야?"를
// 코드를 읽는 모두가 매번 추리해야 한다. DTO 두 벌은 비용이 아니라 문서다.`;

const CODE_MSA = `# 계층 분리를 서비스 경계로 승격 — traderbox의 실제 지형
게이트웨이(8080)가 URL로 교통정리
  /api/auth/**    → service-auth   (8081)  회원·토큰·관심종목
  /api/market/**  → service-market (8083)  시세·호가
  /api/content/** → service-content(8084)  공시·아티클

데이터도 계층처럼 격리 — 스키마-퍼-서비스
  단일 Postgres 안에서 서비스마다 자기 스키마만 사용.
  남의 스키마에 조인하는 순간, 서비스 경계는 그림일 뿐이다.

서비스끼리 말 거는 채널 세 가지 — 채널 규율
  동기(즉답 필요)   → 게이트웨이 우회 /internal/** + 공유키
  비동기(감사·이력) → Kafka 토픽 audit.events
  실시간(시세 팬아웃)→ Valkey Pub/Sub market.ticks
  규율: 같은 데이터를 두 채널로 흘리지 않는다.`;

const CODE_SMELLS = `안티패턴 자가진단 체크리스트 — 하나라도 '네'면 리팩터링 후보
──────────────────────────────────────────────
□ 컨트롤러 메서드가 20줄을 넘고, if의 절반이 비즈니스 판정이다
□ 컨트롤러가 Repository를 import하고 있다
□ 응답 타입이 엔티티 클래스 그대로다
□ Repository 메서드 이름에 정책이 들어 있다
  (findEligibleUsersForPromotion — '자격'은 규칙인데 저장소가 안다)
□ 서비스 A가 B를 부르고, B가 다시 A를 부른다 (순환 의존)
□ 한 서비스 클래스가 800줄이고 이름이 CommonService다 (God Service)
──────────────────────────────────────────────
고치는 방향은 늘 같다:
  판정 로직 → 서비스나 도메인 객체로 내리고
  공용 로직 → 이름 있는 작은 서비스로 쪼개고
  순환 → 공통 조각을 셋째 클래스로 뽑아 둘 다 그것에 의존시킨다`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: '왜 나누나' },
  { n: 2, label: '3계층의 책임' },
  { n: 3, label: '의존성 방향' },
  { n: 4, label: '도메인 로직의 집' },
  { n: 5, label: '데이터 옮기기' },
  { n: 6, label: '서비스 경계로 확장' },
  { n: 7, label: '자가진단' },
];

export default function Lv3LayeredArchitecturePage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 고급 과정</div>
        <h1>레이어드 아키텍처와<br />관심사 분리</h1>
        <p>
          서비스를 한 번 만들어 봤다면 알 거예요 — <strong>돌아가게 만드는 것</strong>과{' '}
          <strong>오래 살게 만드는 것</strong>은 다른 기술입니다. 기능이 늘수록 코드가 서로를
          붙잡고 무너지는 이유는 대부분 하나, <strong>변경이 갇히지 않아서</strong>예요.
          계층(layer)은 변경을 한 곳에 가두는 가장 오래되고 확실한 담장입니다. 사내 실전
          코드(traderbox)를 거울 삼아, 그 담장을 세우는 법을 익혀 봅시다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 55분</span>
          <span className="chip">실습: 컨트롤러 리팩터링 스케치</span>
          <span className="chip">대상: 서비스를 만들어 본 사람</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="계층을 나누는 이유" sub="변경이 한 곳에 갇히도록">
        <p>
          <strong>관심사 분리(Separation of Concerns)</strong>는 거창한 말이 아니에요.
          "서로 다른 이유로 바뀌는 코드는, 서로 다른 자리에 두자" — 이게 전부입니다.
          URL이 바뀌는 이유(기획 개편)와 가입 규칙이 바뀌는 이유(정책 변경)와 DB가 바뀌는
          이유(성능·인프라)는 전부 달라요. 그런데 그 셋이 한 메서드에 살면, <strong>어느 하나가
          바뀔 때마다 셋 전부를 다시 읽고, 다시 테스트하고, 다시 두려워해야</strong> 합니다.
        </p>
        <Code>{CODE_SPAGHETTI}</Code>
        <p>
          계층 없는 코드가 커질 때 치르는 대가는 정확히 두 가지예요. 하나는{' '}
          <strong>스파게티</strong> — 모든 코드가 모든 코드를 만질 수 있으니, 수정 하나가 어디까지
          번질지 아무도 장담 못 하는 상태. 다른 하나는 <strong>중복</strong> — "가입 규칙"이
          컨트롤러 세 군데에 복사돼 있다가, 두 군데만 고쳐져서 생기는 버그. 스파게티는 변경을
          느리게 만들고, 중복은 변경을 <strong>틀리게</strong> 만듭니다.
        </p>
        <p>
          그래서 우리 회사의 실전 코드(traderbox)는 모든 서비스가 같은 규칙으로 패키지를
          가릅니다. <span className="icode">web/</span>(입출력),{' '}
          <span className="icode">domain/</span>(규칙과 저장),{' '}
          <span className="icode">config/</span>(조립). 폴더 이름이 곧 "이 코드가 바뀌는
          이유"의 선언이에요.
        </p>
        <Code>{CODE_PACKAGES}</Code>
        <div className="tip">
          <b>담장의 비유</b> 계층은 방화벽 담장이에요. 불(변경)이 안 나게 해 주진 못합니다.
          다만 불이 났을 때 <strong>옆 칸으로 번지지 않게</strong> 해 주죠. "API 필드명 바꿔
          주세요"라는 불은 <span className="icode">web/</span> 칸 안에서 꺼져야 정상이고,{' '}
          <span className="icode">domain/</span>까지 번진다면 담장 어딘가에 구멍이 있는 겁니다.
        </div>
      </Section>

      <Section n={2} title="3계층의 책임 경계" sub="Controller=입출력 · Service=규칙 · Repository=저장">
        <p>
          한 줄 정의부터요. <strong>Controller는 입출력</strong> — HTTP라는 바깥세상 말을 자바로
          번역하고, 결과를 다시 HTTP로 번역합니다. <strong>Service는 규칙</strong> — "관심종목은
          30개까지", "중복 가입 금지" 같은 업무 판단이 사는 곳. <strong>Repository는 저장</strong>{' '}
          — 데이터를 넣고 꺼내는 것, 그 이상도 이하도 아닙니다.
        </p>
        <p>
          여기서 나오는 원칙이 <strong>'얇은 컨트롤러'</strong>예요. 컨트롤러는 번역과 위임만
          하고, 판단은 전부 아래로 내려보냅니다. 실패했을 때는 서비스가{' '}
          <span className="icode">ResponseStatusException</span>을 던지고, 스프링이 그걸 HTTP
          상태 코드로 번역해 주죠 — traderbox 전 서비스가 따르는 컨벤션입니다.
        </p>
        <Code>{CODE_THIN_CONTROLLER}</Code>
        <p>
          "무엇을 해야 하나"만큼 중요한 게 <strong>"무엇을 하면 안 되나"</strong>예요. 경계는
          금지 목록으로 지켜집니다.
        </p>
        <Code>{CODE_FORBIDDEN}</Code>
        <div className="tip">
          <b>식당의 비유</b> Controller는 <strong>홀 직원</strong>(주문을 받아 주방 말로 전달),
          Service는 <strong>주방장</strong>(레시피와 판단), Repository는{' '}
          <strong>냉장고 담당</strong>(재료를 넣고 꺼냄)이에요. 홀 직원이 요리를 시작하거나,
          냉장고 담당이 "이 손님에겐 이 메뉴가 어울려"를 판단하는 식당을 상상해 보세요 —
          바쁜 날 반드시 사고가 납니다.
        </div>
      </Section>

      <Section n={3} title="의존성 방향이라는 규칙" sub="의존은 바깥→안쪽, 한 방향만">
        <p>
          계층을 나눴다면 이제 화살표를 관리할 차례예요. 규칙은 하나입니다 —{' '}
          <strong>의존은 바깥에서 안쪽으로(web → domain), 한 방향만.</strong> 컨트롤러는
          서비스를 알지만, 서비스는 컨트롤러의 존재조차 몰라야 해요. traderbox의{' '}
          <span className="icode">domain/</span> 패키지에서 <span className="icode">web/</span>을
          import하는 코드가 하나라도 보이면, 그건 담장에 뚫린 구멍입니다.
        </p>
        <Code>{CODE_DIRECTION}</Code>
        <p>
          이 방향이어야 하는 이유는 <strong>변경 속도의 차이</strong>예요. HTTP 스펙(URL,
          필드명, 버전)은 자주 바뀌고, 업무 규칙은 천천히 바뀝니다. 자주 바뀌는 쪽이 천천히
          바뀌는 쪽에 의존해야, 바깥이 아무리 출렁여도 안쪽 규칙은 흔들리지 않아요. 반대로
          안쪽이 바깥을 알면, API 필드명 하나 바꾸는 데 규칙 코드까지 수술해야 합니다.
        </p>
        <p>
          같은 이유로 <strong>엔티티를 컨트롤러까지 올려보내지 않습니다.</strong> 엔티티는
          안쪽(domain)의 주민이라 <span className="icode">passwordHash</span> 같은 내부 사정을
          품고 있어요. 그대로 JSON이 되는 순간 DB 컬럼이 곧 API 스펙이 되고, 숨겨야 할 필드가
          바깥으로 새어 나갑니다. 경계를 건널 때는 전용 그릇, <strong>DTO</strong>를 씁니다 —
          traderbox 컨벤션대로 자바 <span className="icode">record</span>로요.
        </p>
        <Code>{CODE_DTO}</Code>
        <div className="tip">
          <b>기억법</b> DTO는 <strong>공항의 세관 신고서</strong>예요. 나라(계층) 경계를 넘는
          짐은 정해진 양식에 적힌 것만 통과합니다. 집 안 살림(엔티티)을 통째로 들고 국경을
          넘겠다는 게 '엔티티 직접 반환'이고, 세관(경계)이 그걸 허용하는 순간 무엇이 오가는지
          아무도 통제할 수 없게 돼요.
        </div>
      </Section>

      <Section n={4} title="도메인 로직의 집은 어디인가" sub="빈약한 모델을 알아채고, 규칙에게 주소를 주기">
        <p>
          3계층을 잘 지켜도 남는 질문이 있어요 — <strong>"이 규칙, 서비스에 둬요? 객체에
          둬요?"</strong> 이 질문에 무감각하면 흔히 도착하는 곳이{' '}
          <strong>빈약한 도메인 모델(Anemic Domain Model)</strong>입니다. 엔티티는 getter/setter만
          가득한 데이터 봉지가 되고, 규칙은 전부 서비스에 흩어지는 상태죠. 겉보기엔 계층이
          멀쩡한데, 규칙의 <strong>주소가 없어서</strong> 같은 검증이 서비스 세 군데에 복사되는
          병이 생깁니다.
        </p>
        <Code>{CODE_ANEMIC}</Code>
        <p>
          아래 객체가 좋은 이유는 딱 하나예요 — <strong>'100개 제한'이라는 규칙을 우회할 문이
          없다</strong>는 것. setter가 열려 있으면 규칙은 "지켜 주길 바라는 부탁"이지만, 행동
          메서드 안에 있으면 "어길 수 없는 법"이 됩니다. 그럼 모든 규칙을 객체로 밀어 넣어야
          할까요? 아니요, 기준이 있습니다.
        </p>
        <Code>{CODE_RULE_HOME}</Code>
        <div className="tip">
          <b>자가진단 한 줄</b> 엔티티 파일을 열어 보세요. <strong>getter/setter 말고 동사
          메서드가 하나도 없다면</strong>, 그 도메인의 규칙들은 지금 어딘가를 떠돌고 있는
          거예요. 서비스 코드에서 같은 <span className="icode">if</span> 판정이 두 번 이상
          보인다면, 그 판정이 바로 집 잃은 규칙 — 객체 안으로 이사시킬 1순위 후보입니다.
        </div>
      </Section>

      <Section n={5} title="계층 사이 데이터 옮기기" sub="매핑의 자리, 그리고 DTO 두 벌의 값어치">
        <p>
          경계를 넘을 때 DTO를 쓴다면, <strong>엔티티↔DTO 변환(매핑)은 누가 하나요?</strong>{' '}
          답은 "경계를 아는 계층" — 보통 <strong>서비스</strong>(또는 web 쪽의 전용 매퍼)입니다.
          컨트롤러가 엔티티를 받아서 직접 변환하려면 엔티티가 web까지 올라와야 하니 탈락이고,
          리포지토리가 DTO를 만들면 저장소가 API 응답 모양을 알게 되니 역시 탈락이에요.
        </p>
        <Code>{CODE_MAPPING}</Code>
        <p>
          요청 DTO와 응답 DTO를 <strong>따로 두는 값어치</strong>도 여기서 나옵니다. 둘은 생김새가
          비슷해 보여도 방향이 달라요. 요청은 "받을 것만"(id 없음, 생성일 없음), 응답은 "보여줄
          것만"(비밀번호 없음). 하나로 합치는 순간 모든 필드가 "들어오는 값인지 나가는 값인지"
          모호해지고, 그 모호함은 코드 리뷰마다 세금처럼 걷혀 갑니다.
        </p>
        <p>
          반대로 매핑을 아무 데서나 하면 <strong>매핑 누수</strong>가 생겨요. 컨트롤러 A는
          서비스가 준 DTO를 쓰는데, 컨트롤러 B는 엔티티를 받아 직접 변환하고 있다면 — 응답
          모양을 바꿀 때 두 곳을 다르게 고쳐야 하고, 언젠가 한 곳을 빼먹습니다. 변환 규칙이
          여러 계층에 흩어지는 것, 그게 계층 사이의 <strong>숨은 결합</strong>이에요.
        </p>
        <div className="tip">
          <b>실무 감각</b> "DTO 만들기 귀찮은데 그냥 엔티티 쓰면 안 돼요?"는 모든 신입이 한
          번씩 하는 질문이에요. 답은 "지금은 되고, 석 달 뒤에 안 된다"입니다. 필드 5개짜리
          토이 프로젝트에선 못 느끼지만, 필드 30개·컨슈머 3팀이 되는 순간 DTO는 선택이 아니라{' '}
          <strong>API 스펙을 지키는 유일한 장치</strong>가 됩니다.
        </div>
      </Section>

      <Section n={6} title="모노레포·마이크로서비스로 확장" sub="계층 분리를 서비스 경계로 승격하기">
        <p>
          여기까지는 <strong>한 서비스 안</strong>의 이야기였어요. 그런데 관심사 분리의 원리는
          더 크게도 적용됩니다 — traderbox는 인증(auth)·시세(market)·콘텐츠(content) 같은{' '}
          <strong>관심사를 아예 별도 서비스로</strong> 가르고, 하나의 모노레포에 담아 운영해요.
          계층이 "패키지 담장"이라면, 마이크로서비스는 "프로세스 담장"입니다.
        </p>
        <Code>{CODE_MSA}</Code>
        <p>
          눈여겨볼 지점이 셋이에요. 첫째, <strong>스키마-퍼-서비스</strong> — 데이터베이스는
          하나여도 서비스마다 자기 스키마만 만집니다. 계층에서 "리포지토리만 DB를 안다"였던
          규칙이, 서비스 단위로 "내 스키마만 내가 안다"로 승격된 거예요. 둘째,{' '}
          <strong>게이트웨이 라우팅</strong> — 바깥세상은 게이트웨이 하나만 보고, URL 규칙으로
          각 서비스에 배달됩니다. 서비스는 자기 관심사만 지키면 되니 <strong>자율성</strong>이
          생기죠. 셋째, <strong>채널 규율</strong> — 서비스끼리 말을 걸 때 동기는{' '}
          <span className="icode">/internal/**</span>(공유키), 비동기·감사는 Kafka, 실시간
          팬아웃은 Valkey Pub/Sub. 그리고 <strong>같은 데이터를 두 채널로 흘리지 않는다</strong>는
          규칙이 채널판의 관심사 분리입니다.
        </p>
        <div className="tip">
          <b>순서가 중요해요</b> "그럼 처음부터 마이크로서비스로 짜면 되겠네요?"는 위험한
          결론이에요. 한 서비스 안에서 계층도 못 가르는 팀이 서비스를 가르면, 스파게티가{' '}
          <strong>네트워크를 타고 다니는</strong> 더 비싼 스파게티가 될 뿐입니다. 계층 분리가
          먼저 몸에 배야, 그 경계를 따라 서비스를 잘라 낼 자리도 보입니다.
        </div>
      </Section>

      <Section n={7} title="안티패턴 자가진단" sub="내 코드의 냄새를 맡는 체크리스트">
        <p>
          마지막으로, 배운 것을 <strong>탐지 도구</strong>로 바꿔 봅시다. 아키텍처가 무너지는 건
          대개 큰 결단이 아니라 "이번 한 번만"의 누적이에요. 그래서 냄새를 초기에 맡는 코가
          중요합니다.
        </p>
        <p>
          <strong>① 컨트롤러에 스며든 비즈니스 로직</strong> — 컨트롤러 안의{' '}
          <span className="icode">if</span>를 보세요. "값이 비었나?"(형식 검증)는 괜찮지만,
          "한도를 넘었나? 권한이 되나?"(업무 판정)가 보이면 규칙이 홀에 나와 요리하는 중입니다.{' '}
          <strong>② 리포지토리가 도메인 규칙을 아는 냄새</strong> — 메서드 이름에{' '}
          <span className="icode">eligible</span>, <span className="icode">valid</span> 같은{' '}
          <strong>정책 단어</strong>가 들어가기 시작하면, 냉장고 담당이 레시피를 외우고 있는
          거예요. <strong>③ 순환 의존과 God Service</strong> — A→B→A로 서로를 부르는 서비스
          쌍, 그리고 아무 일이나 다 하는 800줄짜리{' '}
          <span className="icode">CommonService</span>. 둘 다 처방은 같습니다 —{' '}
          <strong>공통 조각을 이름 있는 셋째 클래스로 뽑아내고</strong>, 둘 다 그것에 의존하게
          만드는 것.
        </p>
        <Code>{CODE_SMELLS}</Code>
        <div className="tip">
          <b>이 코스의 실습</b> 사내 코드 <span className="icode">service-auth</span>에서
          컨트롤러 하나를 골라 열어 보세요(예:{' '}
          <span className="icode">AuthController</span>). 메서드마다 "이 줄은 입출력인가,
          규칙인가, 저장인가"를 색칠해 보고, web·domain 책임이 섞인 부분을 하나 찾아{' '}
          <strong>리팩터링 스케치</strong>(어떤 메서드를 어느 계층으로 옮길지 화살표 그림)를
          그려 멘토에게 보여 주세요. 코드를 실제로 고치는 건 그다음 일 — 먼저{' '}
          <strong>보는 눈</strong>이 목표입니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🏛️ 여기까지 왔다면</h3>
          <p className="muted">
            이제 코드를 볼 때 "돌아가나?" 다음 질문이 생겼을 거예요 — <strong>"변경이
            갇히나?"</strong> 계층의 이유(변경 격리), 3계층의 금지 목록, 바깥→안쪽 한 방향
            의존, 규칙의 주소 정하기, DTO라는 통행증, 그리고 서비스 경계로의 승격까지 —
            오래 사는 코드의 뼈대를 손에 쥐었습니다. 그런데 계층 <strong>안에서</strong> 코드를
            잘 빚는 데는 또 다른 어휘가 필요해요. 반복되는 설계 문제에 선배들이 붙여 둔 이름,{' '}
            <Link to="/learn/lv3-design-patterns"><strong>디자인 패턴 실전</strong></Link> 코스에서
            그 어휘를 이어서 익혀 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
