[사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #1 - 초기 DB 설계와 프로젝트 셋팅

1. 들어가며

현재 사이드 프로젝트로 크리에이터 디지털 굿즈(포토카드) 플랫폼을 개발하고 있다. 오늘은 개발의 첫 단추이자 가장 중요한 단계 중 하나인 DB 모델링(ERD)과 Spring Boot 프로젝트 초기 설정을 진행했다. 단순히 테이블을 만들고 서버를 띄우는 것이 아니라, 확장성데이터 무결성을 최우선으로 고려하며 설계한 과정과 그 이유를 기록한다.


2. DB 모델링: 왜 이렇게 설계했는가? 

전체 도메인을 회원(Auth), 상점(Store), 결제(Commerce), 게임(Synthesis), 전시(Exhibition), 운영(Operation) 6개 영역으로 모듈화하여 총 22개의 테이블을 설계했다. 이 과정에서 고민했던 핵심 기술 포인트는 다음과 같다.

① PostgreSQL JSONB의 활용 RDBMS를 사용하지만 일부 데이터는 유연성이 필요했다.

  • 상황: 크리에이터마다 사용하는 SNS(유튜브, 인스타, 틱톡 등)가 제각각이다. 이를 별도 테이블로 1:N 관계를 맺기에는 오버헤드가 크다고 판단했다.
  • 결정: PostgreSQL의 강력한 기능인 JSONB 타입을 사용하여 SNS 링크 정보를 저장하기로 했다. 추후 PG사의 비정형 결제 응답 데이터(Raw Data)를 저장할 때도 이 방식을 사용하여 스키마 변경 없이 유연하게 데이터를 적재할 예정이다.

② 결제 로그의 불변성 (Immutability) 돈과 관련된 데이터는 수정되어서는 안 된다.

  • 고민: 주문(Orders) 테이블의 상태(status) 값만 변경하는 방식은 부분 취소나 수수료 변동 이력을 추적하기 어렵다.
  • 해결: Payment_logs라는 별도의 원장 테이블을 설계했다. 이 테이블은 절대 UPDATE 하지 않고, 환불이나 변경 사항이 발생하면 음수 금액 등을 포함한 새로운 로그를 INSERT 하는 방식으로 설계하여 정산의 무결성을 보장했다.

③ 유저와 크리에이터의 데이터 분리 모든 유저가 크리에이터는 아니다. Users 테이블에 정산 계좌 같은 크리에이터 전용 컬럼을 넣으면 NULL 값이 불필요하게 많아진다. 따라서 Users와 Creators 테이블을 1:1 식별 관계로 분리하여 데이터 희소성(Sparsity) 문제를 해결했다.

ERD 설계


3. Spring Boot 초기 설정: 생산성을 높이는 패턴 적용

본격적인 비즈니스 로직 구현에 앞서, 반복되는 코드를 줄이고 협업 효율을 높이기 위한 공통 클래스들을 구현했다.

① JPA Auditing을 이용한 생성/수정일 자동화 (BaseTimeEntity) DB 테이블마다 공통적으로 들어가는 created_at, updated_at 컬럼을 매번 수동으로 setter를 통해 넣는 것은 번거롭고 실수하기 쉽다. JPA Auditing을 적용하여 이를 자동화했다.

@Getter
@MappedSuperclass // 상속받는 자식 엔티티에게 매핑 정보만 제공
@EntityListeners(AuditingEntityListener.class) // JPA Auditing 활성화
public abstract class BaseTimeEntity {

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

Tip: 이 기능이 동작하려면 메인 애플리케이션 클래스에 @EnableJpaAuditing 어노테이션을 반드시 추가해야 한다.

② 통일된 API 응답 포맷 (ApiResponse) 프론트엔드와의 원활한 통신을 위해 성공/실패 여부에 관계없이 항상 동일한 JSON 구조를 반환하도록 래퍼(Wrapper) 클래스를 만들었다.

@Getter
private final String status;  // "SUCCESS" or "ERROR"
    private final String message; // 응답 메시지
    private final T data;         // 실제 데이터 (없으면 null)

    // 성공 시 호출
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>("SUCCESS", "요청이 성공적으로 처리되었습니다.", data);
    }

    // 성공 시 메시지 커스텀
    public static <T> ApiResponse<T> success(String message, T data) {
        return new ApiResponse<>("SUCCESS", message, data);
    }

    // 실패 시 호출
    public static <T> ApiResponse<T> error(String message) {
        return new ApiResponse<>("ERROR", message, null);
    }

    private ApiResponse(String status, String message, T data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

이제 컨트롤러에서는 return ApiResponse.success(dto); 형태로만 반환하면 되므로, API 일관성이 크게 향상될 것이다.


4. 마치며 & 다음 계획

단순한 CRUD 게시판이 아닌, 실제 비즈니스 요구사항(정산, 조합 등)을 수용할 수 있는 DB를 설계하는 과정에서 많은 것을 배울 수 있었다. 다음에는 설계한 도메인별 패키지 구조를 잡고, 실제 JPA Entity 클래스들을 구현하여 애플리케이션의 뼈대를 완성할 예정이다.

그리고 오늘 설계하고 구현했던 것이 무조건 맞지 않다는 것을 알고 있다. 문제가 있거나 다른 방식으로 더 좋은 방향으로 수정이 가능한 부분을 계속 찾아보고 적용해서 이 프로젝트를 실제 론칭하는 목표를 달성하도록 노력할 것이다.