JPA 테이블 별 공통 함수 및 인터페이스 개발 방향

개요

befw-lib-core 라이브러리 내 JPA Service 계층의 공통화 모듈 개발 방향을 정의한다. 반복적인 DB 접근 코드를 줄이고, 일관된 에러 처리 및 테넌트 격리를 자동화하는 것이 목표다.

관련 모듈

  • 라이브러리: befw-lib-core
  • 베이스 패키지: com.tsh.starter.befw.lib.core.data.orm
  • 현재 구조:
    • BasicAuditBaseModel → 실제 엔티티
    • BaseJpaRepository → 엔티티별 Repo
    • CrudServiceAbstractCrudService → 엔티티별 Access

개발 아이템

아이템 목록

번호아이템상태
1UK별 CRUD 자동 생성설계확정
2DB 에러 처리 공통화설계확정
3Tenant 격리 자동화설계확정

개발 순서

아이템 3 (Tenant 격리 자동화)
        ↓
아이템 2 (에러 처리 공통화)  ← Tenant 에러도 여기서 처리
        ↓
아이템 1 (UK별 CRUD 자동 생성)  ← 에러 처리 + Tenant 격리 위에서 동작

순서 이유

상위 아이템이 하위 아이템의 기반이 되기 때문에 Tenant 격리 → 에러 처리 → UK CRUD 순서로 개발한다.


아이템 1. UK별 CRUD 자동 생성

목적

엔티티의 Unique Key(UK) 조합으로 조회 / 수정 / 삭제하는 경우, 매번 각 Access 클래스에 중복 메서드를 작성하지 않도록 공통화한다.

확정 설계

항목확정 내용
UK 선언 방식@Table(uniqueConstraints) 재활용 (별도 파일/어노테이션 없음)
복합 UK 여러 개name 속성으로 구분하여 각각 CRUD 생성
조회 실패 시EntityNotFoundException throw
Update 방식Model 객체로 받되 null 필드는 무시, Audit 자동 유지
DDL 전략dev: ddl-auto: update / prod: 추후 Flyway 도입 검토

UK 선언 방식

기존 JPA 표준 어노테이션인 @UniqueConstraint를 재활용한다. 별도 어노테이션이나 설정 파일 없이 UK 변경 시 @UniqueConstraint만 수정하면 CRUD도 자동으로 따라간다.

// 예시: GnMsgSrvConnModel
@Table(
    name = GlobalTableName.GN_MSG_SRV_CONN,
    uniqueConstraints = {
        @UniqueConstraint(name = "uk_msg_srv", columnNames = {"env", "host", "port"})
    }
)
public class GnMsgSrvConnModel extends BaseModel {
    ...
}

DDL 자동화 전략

UK 추가/변경 시 코드 어노테이션만 수정하면 DB에 자동 반영된다.

# befw-config-repo 환경별 설정
 
# local / dev
spring:
  jpa:
    hibernate:
      ddl-auto: update
 
# prod (추후 Flyway 도입 후 변경)
spring:
  jpa:
    hibernate:
      ddl-auto: validate

주의사항

ddl-auto: update는 UK 추가는 자동 반영되지만 UK 삭제/변경은 자동 반영되지 않는다. prod 환경 전환 시 반드시 Flyway 도입을 검토한다.

자동 생성되는 메서드 구조

AbstractCrudService가 런타임에 @UniqueConstraint를 읽어서 아래 메서드를 자동으로 처리한다.

findByUk(ukName, params)    → UK로 단건 조회
updateByUk(ukName, params, model)  → UK로 조회 후 부분 업데이트
deleteByUk(ukName, params)  → UK로 조회 후 Soft Delete

Update 정책

  • 호출부에서 Model 객체를 넘긴다.
  • null인 필드는 업데이트에서 제외한다.
  • BasicAuditmodifiedAt, modifiedBy@LastModifiedDate, @LastModifiedBy로 자동 유지된다.

아이템 2. DB 에러 처리 공통화

목적

JPA에서 발생하는 다양한 예외를 서비스 계층에서 일관된 방식으로 처리하고, 디버깅에 필요한 파라미터/쿼리 정보를 함께 로깅한다.

확정 설계

항목확정 내용
에러 형태공통 DataErrorResponse DTO 반환
로깅 수준일단 동일 수준, 추후 심각도별 구분 검토
Optimistic Lock 재시도최대 2회, 총 100ms 초과 금지
파라미터 로깅실행 시 넣은 파라미터 + 시도한 쿼리 정보 함께 출력

DataErrorResponse DTO 구조

DataErrorResponse {
    String errorCode;    // "UK_DUPLICATE", "NOT_FOUND", "LOCK_CONFLICT", "DB_UNAVAILABLE"
    String message;      // 사람이 읽을 수 있는 설명
    String entity;       // 어떤 엔티티에서 발생했는지
    Object params;       // 실행 시 넣은 파라미터
    String query;        // 시도한 쿼리 정보
    LocalDateTime occurredAt;  // 발생 시각
}

에러 코드 정의

errorCode원인 JPA 예외설명
NOT_FOUNDEntityNotFoundExceptionPK/UK로 조회했는데 없음
UK_DUPLICATEDataIntegrityViolationExceptionUK 중복 저장 시도
INVALID_REQUESTConstraintViolationException필수값 누락 등
LOCK_CONFLICTOptimisticLockingFailureException동시 수정 충돌
DB_UNAVAILABLEJpaSystemException연결 실패 / 타임아웃

Optimistic Lock 이란?

Optimistic Lock 개념

DB에 실제 락을 걸지 않고, 저장 시점에 버전 번호로 충돌을 감지하는 방식이다.

예시 시나리오

  1. Agent A가 설정값을 읽음 (version = 1)
  2. Agent B도 같은 설정값을 읽음 (version = 1)
  3. Agent A가 먼저 저장 → DB version = 2
  4. Agent B가 저장 시도 → DB는 version 2인데 B는 version 1 기준으로 수정
  5. 충돌! OptimisticLockException 발생

이를 방지하려면 BasicAudit@Version 필드 추가가 필요하다.

Optimistic Lock 재시도 정책

최대 재시도 횟수: 2회
총 허용 시간: 100ms 초과 금지

1차 시도 실패
    → 대기 후 2차 시도
        → 2차 시도 실패
            → LOCK_CONFLICT 에러 반환

주의사항

총 재시도 시간이 100ms를 초과하면 즉시 실패 처리한다.


아이템 3. Tenant 격리 자동화

목적

개발자가 tenant 파라미터를 수동으로 넘기지 않아도 자동으로 주입되도록 하여, 실수로 tenant 조건을 빠뜨려 다른 테넌트 데이터가 노출되는 리스크를 방지한다.

확정 설계

항목확정 내용
주입 방식ThreadLocal 기반 TenantResolver 인터페이스
tenant 없는 요청예외 처리 (배치 포함 무조건 tenant 필수)
적용 대상전체 엔티티
확장 전략추후 JWT/Security 방식으로 교체 가능하도록 인터페이스로 추상화

TenantResolver 인터페이스 구조

각 시스템이 TenantResolver 인터페이스만 구현하면 자동으로 연결된다. 현재는 ThreadLocal 방식으로 구현하며, 추후 JWT/Security 방식으로 교체 가능하다.

TenantResolver (인터페이스)
        ↓
ThreadLocalTenantResolver (현재 구현체)
        ↓         (추후 교체 가능)
JwtTenantResolver / SecurityTenantResolver

Tenant 주입 흐름

요청 진입 (HTTP / Solace 메시지 / 배치)
        ↓
TenantResolver.set(tenant)  ← 각 시스템이 진입점에서 세팅
        ↓
Hibernate Filter 자동 활성화
        ↓
모든 JPA 쿼리에 WHERE tenant = ? 자동 추가
        ↓
요청 완료 후 TenantResolver.clear()  ← ThreadLocal 반드시 정리

Tenant 없는 요청 처리

TenantResolver.get() 호출
        ↓
tenant == null 또는 blank
        ↓
TenantMissingException throw
        → DataErrorResponse { errorCode: "TENANT_MISSING" } 반환

주의사항

ThreadLocal은 요청 처리 완료 후 반드시 clear()해야 한다. 특히 Thread Pool 환경에서 clear 누락 시 다른 요청에 이전 tenant가 묻어나올 수 있다.

확장 전략

현재는 백엔드 서비스 개발에 집중하고, 서비스 운영 단계에서 아래 방식으로 전환을 검토한다.

단계방식비고
현재 (개발)ThreadLocal시스템 간 약속으로 tenant 세팅
추후 (운영)JWT 클레임 기반HTTP 요청에 자연스러움
추후 (운영)Spring Security ContextSecurity 의존성 추가 필요

현재 구조와의 연결

BasicAudit
    └── BaseModel  ←  Hibernate Filter로 tenant 자동 주입 (아이템 3)
            └── GnMsgSrvConnModel
                    └── @UniqueConstraint  ← UK CRUD 자동 생성 (아이템 1)

AbstractCrudService
    ├── 에러 처리 공통화 (아이템 2)
    ├── Tenant 격리 (아이템 3)
    └── UK별 CRUD (아이템 1)

관련 문서


변경 이력

날짜내용작성자
2026-04-13최초 작성 - 3개 아이템 설계 확정David