Files
invyone/docs/REST_API_ARCHITECTURE.md
T
johngreen db06c95724
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m8s
docs: REST API 아키텍처 문서 추가
backend-architecture-summary.md 는 옛 Node 백엔드(VEX 1세대) 기준이라 현재 Spring 백엔드의 컨벤션이 문서화돼있지 않음. 신규 작업자가 합의된 패턴(3-layer, Map<String,Object>, 네이밍 규칙, 표준 응답, common SQL 단편) 을 한 곳에서 빠르게 파악할 수 있게 정리.

다른 아키텍처 문서(MULTI_TENANCY_ARCHITECTURE / DOMAIN_MAPPING) 와 상호 링크.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:46:24 +09:00

7.2 KiB

INVYONE REST API 아키텍처

3-layer (Controller → Service → XML), DTO 없음, Map<String, Object> 일원화. 로우코드 ERP 특성상 테이블/컬럼이 런타임에 결정되므로 사전 정의 DTO를 두지 않고 모든 데이터를 키-값 맵으로 흘린다.


1. 기술 스택

언어         Java 21
프레임워크    Spring Boot 3.x
DB 접근      MyBatis (XML) + sqlSession 직접 호출
DB           PostgreSQL 16 (DB-per-tenant)
인증         JWT (Authorization: Bearer …)
빌드         Gradle 8.10

멀티테넌시·서브도메인 라우팅은 별도 문서: MULTI_TENANCY_ARCHITECTURE.md.


2. 3-layer 구조

[HTTP 요청]
   ↓
@RestController            ← 입력 추출, 검증, 응답 래핑
   ↓ Map<String, Object>
Service extends BaseService ← 비즈니스 로직, sqlSession 호출
   ↓ namespace.queryId
mapper/{module}.xml        ← SQL (UPPER_SNAKE), 동적 OGNL
   ↓
PostgreSQL (테넌트 자동 라우팅)

절대 규칙

  • @Mapper 인터페이스 만들지 않는다. Service 가 sqlSession.selectList("namespace.queryId", params) 직접 호출.
  • DTO·엔티티 클래스 만들지 않는다. 파라미터·응답 모두 Map<String, Object> 또는 List<Map<String, Object>>.
  • 유일한 DTO는 ApiResponse (성공/실패 + payload 래퍼).
  • Service 는 반드시 BaseService 상속 + @Slf4j + @Autowired CommonService.

3. 네이밍 컨벤션 (전 영역 일관)

위치 컨벤션
Java 코드 (변수/메서드/클래스) camelCase getOrderList(), String companyCode
Map 키 (params.put, row.get) snake_case params.put("company_code", ...)
MyBatis #{파라미터} snake_case #{company_code}, #{table_name}
SQL 키워드/테이블/컬럼 UPPER_SNAKE SELECT COMPANY_CODE FROM TEMPLATES
SELECT 쉼표 앞에 , COLUMN_NAME
XML 파일명 소문자, Mapper 접미사 없음 meta.xml, template.xml
XML namespace 파일명과 동일 namespace="meta"
OGNL test 속성 바깥 작은따옴표 test='company_code != "*"'

Java는 camelCase, DB·맵은 snake_case. 경계는 Service 안에서만 발생 — Controller·XML 내부엔 변환 코드 없음.


4. 메서드·엔드포인트 패턴

조작 메서드명 HTTP
목록 get{Module}List GET /api/{module}/list
카운트 get{Module}ListCnt GET /api/{module}/list/count
단건 get{Module}Info GET /api/{module}/{id}
등록 insert{Module} POST /api/{module}
수정 update{Module} PUT /api/{module}/{id}
삭제 delete{Module} DELETE /api/{module}/{id}

List API는 반드시 Count 쿼리를 동반한다. getXxxList + getXxxListCnt 한 세트로 페이지네이션을 보장.


5. 표준 응답 (ApiResponse)

// 성공
ResponseEntity.ok(ApiResponse.success(data));
ResponseEntity.ok(ApiResponse.success(data, "메시지"));

// 실패
ResponseEntity.badRequest().body(ApiResponse.error("사용자 ID와 비밀번호를 입력해주세요."));

JSON:

{
  "success": true,
  "message": "조회 완료",
  "data": [...]
}

프론트는 항상 success + data만 보면 충분.


6. common 레이어 (공용 SQL 조각)

mapper/common.xml<sql> 단편을 모든 모듈에서 <include>.

<select id="getTemplateList" resultType="map">
  SELECT
    T.TEMPLATE_CODE,
    T.TEMPLATE_NAME
  FROM TEMPLATES T
  WHERE 1=1
  <include refid="common.companyCodeFilter"/>
  <include refid="common.dynamicOrderBy"/>
  <include refid="common.pagination"/>
</select>
  • companyCodeFilter — 멀티 회사 데이터 분리 (COMPANY_CODE = #{company_code}).
  • dynamicOrderBy — 프론트에서 받은 정렬 필드/방향 동적 적용.
  • paginationLIMIT #{page_size} OFFSET #{offset}.

7. 인증 (JWT)

  • 로그인: POST /api/auth/login body { "user_id": "...", "password": "..." }.
  • 응답에 access_token 포함. 이후 요청은 Authorization: Bearer {token} 헤더 필수.
  • 검증은 JwtAuthenticationFilter 가 일괄. 토큰 페이로드의 company_code·user_id·roleSecurityContext 로 흐름.
  • SubdomainResolverFilter 다음에 동작 → 테넌트 컨텍스트 + 사용자 컨텍스트 모두 갖춘 채 컨트롤러 진입.

화이트리스트(공개 엔드포인트): /api/auth/login, /api/auth/refresh, 정적 리소스.


8. 예시: 템플릿 목록 조회

컨트롤러

@RestController
@RequestMapping("/api/template")
@RequiredArgsConstructor
public class TemplateController {
    private final TemplateService templateService;

    @GetMapping("/list")
    public ResponseEntity<ApiResponse> getTemplateList(@RequestParam Map<String, Object> params) {
        return ResponseEntity.ok(ApiResponse.success(templateService.getTemplateList(params)));
    }
}

서비스

@Service
@Slf4j
public class TemplateService extends BaseService {
    @Autowired CommonService commonService;

    public Map<String, Object> getTemplateList(Map<String, Object> params) {
        params.put("company_code", currentCompanyCode());     // BaseService 헬퍼
        List<Map<String, Object>> list = sqlSession.selectList("template.getTemplateList", params);
        Integer total = sqlSession.selectOne("template.getTemplateListCnt", params);
        return Map.of("list", list, "total", total);
    }
}

매퍼 (resources/mapper/template.xml)

<mapper namespace="template">
  <select id="getTemplateList" resultType="map">
    SELECT
        T.TEMPLATE_CODE
      , T.TEMPLATE_NAME
      , T.CREATED_AT
    FROM TEMPLATES T
    WHERE 1=1
    <include refid="common.companyCodeFilter"/>
    <if test='keyword != null and keyword != ""'>
      AND T.TEMPLATE_NAME ILIKE '%' || #{keyword} || '%'
    </if>
    <include refid="common.dynamicOrderBy"/>
    <include refid="common.pagination"/>
  </select>

  <select id="getTemplateListCnt" resultType="int">
    SELECT COUNT(*)
    FROM TEMPLATES T
    WHERE 1=1
    <include refid="common.companyCodeFilter"/>
    <if test='keyword != null and keyword != ""'>
      AND T.TEMPLATE_NAME ILIKE '%' || #{keyword} || '%'
    </if>
  </select>
</mapper>

9. 프론트 연동 (요약)

  • 호출은 항상 frontend/lib/api/client.ts (axios 래퍼) 또는 모듈별 클라이언트 (flow.ts, dashboard.ts 등). fetch 직접 사용 금지.
  • 응답 타입은 별도 인터페이스 정의 없이 Record<string, any> 사용. (예외: frontend/types/invyone-component.ts 의 시스템 핵심 계약 타입만 명시 정의.)
  • 베이스 URL 분기 (*.invyone.comlocalhost) 는 클라이언트가 호스트 헤더 보고 알아서 해결 → 컴포넌트는 상대경로만 신경 쓰면 됨.

10. 관련 문서

  • MULTI_TENANCY_ARCHITECTURE.md — 서브도메인 라우팅, DB-per-tenant, 회사 프로비저닝 흐름.
  • DOMAIN_MAPPING.md — 환경별 도메인·포트.
  • notes/gbpark/2026-04-08-lowcode-platform-spec.md — 플랫폼 SPEC (왜 DTO를 두지 않는지의 배경).