카테고리 없음

[이커머스 백오피스] 프로젝트 시작과 설계 기록

mooncommit 2026. 4. 25. 03:54
프로젝트 시작 "이커머스 백오피스 서비스"

 

이번 프로젝트는 관리자 회원가입/인증, 관리자 관리, 고객 관리, 상품 관리, 주문 관리 기능을 포함하고 있고,

나는 관리자 부분을 맡았다. 프로젝트 초반에는 기능 구현보다 협업 방식과 공통 구조를 먼저 맞추는 일이 더 중요했고, 이번 글에서는 그 과정을 정리해보려고 한다.


✔️ 프로젝트를 시작하고 가장 먼저 한 일

가장 먼저 한 작업은 테이블 명세서, ERD, API 명세서를 정리하는 것이었다.

요구사항을 읽어보니 관리자, 고객, 상품, 주문까지 도메인이 여러 개였고 각 도메인이 서로 연결되어 있었다.

 

그래서 먼저 아래 순서로 전체 구조를 잡았다.

  1. 테이블 명세서 작성
  2. ERD 작성
  3. API 명세서 작성
  4. GitHub 저장소 생성 및 협업 세팅
  5. 공통 작업 진행
  6. 역할 분담 후 각자 브랜치에서 개발

이 과정을 거치면서 느낀 점은 구현 전에 명세를 먼저 정리하는 과정이 생각보다 훨씬 중요하다는 것이었다.
코드는 나중에 바꿀 수 있어도, 구조를 잘못 잡으면 기준이 명확하지 않아 수정 범위가 너무 커질 것 같다는 생각이 들었다.


📄 테이블 명세서, ERD, API 명세서

이 과정은 내가 참여하지는 못했지만 프로젝트를 진행하며 왜 첫 과정으로 했는지 이유를 알 수 있었다. 

 

🍬 관리자 테이블 명세서

  • 이름, 이메일, 비밀번호, 전화번호, 역할, 상태, 승인일시, 거부일시, 거부사유
  • 테이블 명세서로 정리되어 있으니, 어떤 필드가 필요한지 명확하게 보였다.

🍬 ERD

각 도메인이 어떤 관계를 가지는지 생각할 수 있다.

  • 상품은 등록한 관리자와 연결된다.
  • 주문은 고객과 연결된다.
  • CS 주문의 경우 주문 등록 관리자와도 연결된다.
  • 주문은 주문 상품과 연결된다.

🍬 관리자 API 명세서

  • 승인, 거부, 다건 조회, 단건 조회, 정보 수정, 역할 수정, 상태 수정, 삭제
  • 내 프로필 조회, 내 프로필, 비밀번호 변경

나중에 엔티티를 만들 때 "이 필드가 왜 필요한지", "이 상태값이 어느 API에서 쓰이는지" 쉽게 이해할 수 있었다.


📕 GitHub 저장소를 만들고 협업 방식을 맞춘 과정

명세 작업 이후 다시 합류 하여 GitHub 저장소를 만들고 협업 환경을 세팅했다.

 

기본 엔티티, enum, 예외 처리, BaseEntity 같은 공통 작업을 먼저 맞춘 뒤, 역할을 나누어 각자 브랜치를 생성하도록 진행했다.

이 방식이 중요했던 이유는 공통 기준 없이 각자 개발을 시작하면 나중에 코드 스타일과 구조가 서로 달라질 수 있기 때문이다.

 

예를 들어

  • 어떤 사람은 상태값을 문자열로 저장하고
  • 어떤 사람은 enum으로 만들고
  • 어떤 사람은 예외 RuntimeException으로 던지고
  • 어떤 사람은 ResponseEntity를 직접 반환하는 식이면

프로젝트 전체 코드가 통일되지 않게 된다.

 

😎 GitHub 기본 흐름

git init
git remote add origin <원격저장소주소>
git checkout -b master
git add .
git commit -m "init: 프로젝트 초기 세팅"
git push -u origin main

이후 각자 역할에 따른 브랜치를 만들어서 작업했다.

git checkout master
git pull origin master
git checkout -b feat/admin

작업 후에는 아래처럼 커밋하고 push한 뒤 PR을 생성하는 방식으로 진행한다.

git add .
git commit -m "feat : admin ... 추가"
git push -u origin feat/admin

📙 README에 정리한 협업 규칙

협업 과정에서 혼란을 줄이기 위해 README에 규칙을 정리했다.

  • master 브랜치에 직접 push 하지 않는다.
  • 각자 브랜치를 생성하여 작업한다.
  • 작업 완료 후 Pull Request로 병합한다.
  • 작업 전 최신 master 브랜치를 pull 받는다.
  • 커밋 타입은 feat, fix, refactor를 사용한다.
  • 커밋 메시지는 커밋타입 : 제목 형식으로 작성한다.
  • 커밋은 기능 단위로 나눈다.
  • 클래스 작명은 도메인 + 액션 + 요청/응답 + Dto 형식으로 작성한다.

👫 공통 엔티티 BaseEntity를 먼저 만든 이유

프로젝트를 하다 보면 거의 모든 테이블에서 공통으로 필요한 필드가 있다.

대표적으로 생성 시간, 수정 시간 → 모든 엔티티마다 반복해서 작성하지 않기 위해 공통 부모 클래스로 BaseEntity를 만들었다.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}
  • createdAt : 데이터가 처음 생성된 시간
  • updatedAt : 데이터가 마지막으로 수정된 시간
  • 여러 엔티티에서 공통으로 사용할 수 있는 시간 필드를 한 곳에서 관리

🤔 문자열 대신 enum을 사용한 이유

관리자 도메인을 설계하면서 역할과 상태를 문자열이 아니라 enum으로 분리했다.

 

🍬 관리자 역할 enum

public enum AdminRole {
    // 슈퍼 관리자
    SUPER_ADMIN,
    // 운영 관리자
    OPERATIONS_ADMIN,
    // CS 관리자
    CS_ADMIN
}

 

🍬 관리자 상태 enum

public enum AdminStatus {
    // 활성
    ACTIVE,
    // 비활성
    INACTIVE,
    // 정지
    SUSPENDED,
    // 승인대기
    PENDING,
    // 거부
    REJECTED
}
  • 허용된 값만 사용할 수 있다.
  • 오타를 줄일 수 있다.
  • 상태 비교가 더 명확해진다.
  • 나중에 비즈니스 로직을 작성할 때 가독성이 좋아진다.
// 예를 들면
if (admin.getStatus() != AdminStatus.ACTIVE) {
    throw new AdminNotApprovedException();
}

🤩 관리자 엔티티를 설계하면서 배운 점

이번 과제에서 관리자는 회원가입 후 바로 사용 가능한 것이 아니라 승인 과정을 거쳐야 한다.

참고해 아래와 같이 만들었다.

@Getter
@Entity
@Table(name = "admins")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Admin extends BaseEntity {

    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 관리자 고유 식별자

    @Column(nullable = false, length = 20)
    private String name; // 관리자 이름

    @Column(nullable = false, unique = true, length = 255)
    private String email; // 로그인에 사용하는 이메일

    @Column(nullable = false, length = 255)
    private String password; // 암호화된 비밀번호 저장

    @Column(nullable = false, length = 20)
    private String phone; // 전화번호

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AdminRole role; // 슈퍼 관리자 / 운영 관리자 / CS 관리자 구분

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private AdminStatus status; // 활성 / 비활성 / 정지 / 승인대기 / 거부 상태 관리

    private LocalDateTime approvedAt; // 관리자 승인 시각
    private LocalDateTime rejectedAt; // 관리자 거부 시간
    private String rejectionReason; // 관리자 거부 사유

    public Admin(String name, String email, String password, String phone, AdminRole role, AdminStatus status) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.phone = phone;
        this.role = role;
        this.status = status;
    }
}

⭐️ 예외 처리를 먼저 설계한 이유

이번에 가장 크게 배운 것 중 하나는 예외 처리도 설계의 일부 라는 점이었다.

 

관리자 로그인만 생각해봐도 케이스가 정말 많았다.

  • 이메일이 존재하지 않을 수 있다.
  • 비밀번호가 다를 수 있다.
  • 승인 대기 상태일 수 있다.
  • 거부된 상태일 수 있다.
  • 정지된 상태일 수 있다.
  • 비활성 상태일 수 있다.

서비스 로직은 항상 성공 케이스와 실패 케이스를 함께 고려해야 한다.

그렇기 때문에 공통 부모 예외를 먼저 만들고, 그 아래에 도메인별 예외를 세분화하는 구조를 잡았다.


🍎 예외 처리의 부모 클래스 : ServiceException

모든 비즈니스 예외의 부모가 되는 클래스이다.

@Getter
public class ServiceException extends RuntimeException {

    private final HttpStatus status;

    public ServiceException(HttpStatus status, String message) {
        super(message);
        this.status = status;
    }
}
  • 서비스 계층에서 발생하는 비즈니스 예외를 공통 형식으로 처리하기 위한 커스텀 예외 클래스
  • 부모 클래스

이 클래스를 만든 이유는, 예외가 발생했을 때 단순히 메시지만 전달하는 것이 아니라 HTTP 상태코드와 메시지를 함께 담기 위해서였다.

이 구조를 사용하면 하위 예외 클래스들은 각각 자신에게 맞는 상태코드와 메시지를 지정할 수 있다.


🍏 관리자 관련 예외 정리

관리자 도메인에서 필요한 예외를 다음과 같이 정리했다.

  • PasswordMismatchException → 400, "비밀번호가 일치하지 않습니다."
  • AdminNotFoundException → 404, "관리자를 찾을 수 없습니다."
  • DuplicateEmailException → 400, "이미 사용중인 이메일입니다."
  • InvalidPasswordException → 400, "이메일 또는 비밀번호가 올바르지 않습니다."
  • UnauthorizedException → 401, "로그인이 필요합니다."
  • TokenMissingException → 401, "인증 토큰이 없습니다."
  • TokenExpiredException → 401, "인증 토큰이 만료되었습니다."
  • AccessDeniedException → 403, "접근 권한이 없습니다."
  • AdminAlreadyProcessedException → 400, "이미 처리된 관리자입니다."
  • InvalidAdminInputException → 400, "입력값이 올바르지 않습니다."
  • AdminNotApprovedException → 401, "승인되지 않은 관리자입니다."
  • SelfRoleChangeNotAllowedException → 400, "자신의 역할은 변경할 수 없습니다."
  • InvalidRoleException → 400, "유효하지 않은 역할입니다."
  • InvalidAdminStatusException → 400, "변경할 수 없는 상태입니다."

코드 예시

public class AdminNotFoundException extends ServiceException {
    public AdminNotFoundException() {
        super(HttpStatus.NOT_FOUND, "관리자를 찾을 수 없습니다.");
    }
}

🥨 고객, 상품, 주문 관련 예외도 도메인별로 분리

예외는 관리자 도메인만 있는 것이 아니라 고객, 상품, 주문 도메인에도 각각 필요하다.

✔️ 고객

  • CustomerNotFoundException → 404, "고객을 찾을 수 없습니다."

✔️ 상품 관련

  • ProductNotFoundException → 404, "상품을 찾을 수 없습니다."
  • DiscontinuedProductException → 400, "단종된 상품은 주문할 수 없습니다."
  • SoldOutProductException → 400, "품절된 상품은 주문할 수 없습니다."
  • OutOfStockException → 400, "재고가 부족합니다."

✔️ 주문 관련

  • OrderNotFoundException → 404, "주문을 찾을 수 없습니다."
  • InvalidOrderStatusException → 400, "주문 상태를 변경할 수 없습니다."
  • OrderCancelNotAllowedException → 400, "준비중 상태에서만 취소할 수 있습니다."

이렇게 도메인별로 나누어서 정리하니 예외 이름만 봐도 어떤 상황인지 알 수 있었다.

같은 실패여도 이유는 각각 다르기 때문에 예외도 구체적으로 나눠야 한다는 걸 배웠다.


🔥 HTTP 상태코드도 함께 이해하기


배운 점

 

지금은 공통으로 사용하는 예외 처리, 엔티티, enum 같은 기본 구조를 잡는 단계까지 진행했다. 이 과정에서 개인 프로젝트와 팀 프로젝트는 시작 방식부터 다르다는 걸 많이 느꼈다. 개인 프로젝트에서는 테이블 구조를 대략만 잡고 바로 구현에 들어가도 괜찮았는데, 팀 프로젝트에서는 기능 구현 전에 전체 구조와 공통 규칙을 먼저 맞추는 게 훨씬 중요했다.

 

특히 예외 처리를 커스텀 방식으로 설계해본 건 처음이라 새롭게 배운 점이 많았다. 그리고 공통 구조를 먼저 잡아두니깐 아직 기능 코드를 시작하지 않았지만, 틀이 잡히는 느낌이어서 신기했다. 생각해야 하는 범위가 넓어져서 어렵기도 했지만, 이런 식으로 구조를 먼저 고민해본 경험 자체가 나한테는 의미 있고 성장할 수 있는 기회라고 생각한다.

 

정말 뜻깊은 과정이었다 ◡̈⋆*