inblog logo
|
LifeLog, DevLog
    TIL

    TIL 20260113~20

    이커머스 백오피스 API의 구현 프로젝트, KPT
    KYJTHEYJ's avatar
    KYJTHEYJ
    Jan 15, 2026
    TIL 20260113~20
    Contents
    팀 프로젝트의 시작끝KPTKEEPPROBLEMTRY1. Security, JWT

    팀 프로젝트의 시작

    이커머스 백오피스 API의 구현을 파트별로 담당하여 진행하는 팀 프로젝트를 시작했다

    프론트와의 연결은 아직 하지 않고, 더미 사이트로 참조만 하는 형식으로 진행

    답답한 점은 실제 DB와 연동된다면 다들 쿼리로 실 데이터를 만져볼 수 있어
    SQL도 경험 할 수 있었을 텐데, 그 부분은 로컬로 진행해야해서 데이터 저장이 없어
    테스트 데이터 성으로 개인이 매번 작업하는 경우가 종종 생겨 아쉽다

    이커머스의 도메인을 가져오다보니,
    관리자, 상품, 주문, 리뷰, 통계로 크게 5가지의 기능을 구현해야 한다

    팀장 역할로 있다보니 프로젝트의 시작이 상당히 고민이 되었었다

    브랜치 전략은 Dev Stage Prod 로 정했었다
    Dev 에서 필수 영역까지의 통합테스트를 진행할 것이고
    Stage 는 도전 영역의 구현이 끝나면 Dev 에서 소스를 취합하여 Dev에서 반영,
    Stage에서 2차 통합테스트를 진행하고 main 으로 배포하여 프로젝트를 마칠 것이다

    프로젝트는 필수 구현 (기능 별 기본적 CRUD) 과
    도전 (주문 도메인 구현 + 재고 관리 요소, 통계 데이터 구현, JWT, Spring Security) 으로 나뉘어져 있고 전부 다 구현하는 것이 목표긴 하지만
    모두가 아직 배우는 입장이라 속도만 중시하기엔 개인의 성취감이
    제일 커야하니 그게 제일 조절하기 어려웠다

    워터폴로 진행하고 있지만
    파트 배분에 있어선 최대한 관심 영역을 먼저 물어보는 등 개인의 흥미나
    구현 정도가 균등한가를 중심으로 진행하고 있다, 배우는 입장이기 때문이다

    공통적인 틀이 있는 상태에서 안정적으로 시작을 원해
    필수영역의 엔티티의 기본적 구현과 틀은 먼저 팀장으로써 구현했는데
    차라리 유틸적 측면도 (전역 처리 등) 같이 구현해 둘 것을 하는 아쉬움이 있다

    여하튼 작성중인 15일 오늘은 필수영역에 대해 통합 테스트를 진행했고
    세부적 수정사항 외에 기능이 틀어져버리거나 하는 등은 없었다


    끝

    아쉽다, 끝까지 코드의 구현점을 요구사항 만큼 높히지 못했다
    개인적으로는 혼자 다 하려다 망친 느낌이 좀 든다
    시큐리티와 JWT도 학습하고 싶었고 차차 정리하며 더 계획적으로 프로젝트를
    운용하고 싶었다, 완성도에 신경을 많이 썼지만, 결과는 참담하다 생각한다

    새벽에 발견한 N+1 조회 내역, 글로벌 처리로 빠져버리는 어중간한 예외처리
    , 조회 조건 미작동 등 더는 생각도 하기 싫다, 통합테스트를 잘못했나보다

    여하튼 민망한데 뭐.. 종료되었다

    다음 팀 프로젝트 때는 그 때도 팀장을 하고 있을런지는 모르겠다
    다음에도 팀장이라면 이렇게까지 이타적이진 않을 것 같다

    아쉽고 화나는 게 천지지만, 개인적으로 좋은 것은
    내 수준에서 전반적인 이해가 된 갖고 놀기 좋은 코드뭉치가 생긴 것이다

    포크하여 개인적으로 못해본 시큐리티와 JWT, 테스트 코드,
    N+1 조회 해결과, 더 나아갈 수 있다면 헥사고날 전환까지 천천히 혼자 해보려 한다


    KPT

    KEEP

    1. 브랜치 전략

    • 각자의 개발 브랜치 feature

    • 개발 내역 통합 및 테스트용 브랜치 dev

    • 통합 내역 기록용 (버전 개념) 브랜치 stage

    • 메인 배포 브랜치 main or prod

    Reason

    dev 위에 브랜치를 하나 올려 놓는다면 버전 개념처럼 활용하여
    관리에 유용하게 사용할 수 있다

    이번 프로젝트에서도 제출 전 핫픽스를 할 때도 stage 를 당겨 급하게 수정했다
    또한 코드 리팩토링도 배포 전 버전 별로 진행할 수 있다
    그럼 버전별로 코드의 변화를 파악하기 쉬우니 파악이 쉬울 것이다

    이 브랜치 방식은 다음 프로젝트 진행이나
    혼자 개발할 때도 가져가면 좋은 방식이라고 생각한다

    2. 중앙화된 에러 처리 관리

    @Getter
    public enum ErrorEnum {
        ERR_NOT_ADMIN_STATUS_WAIT(HttpStatus.CONFLICT, MSG_ADMIN_STATUS_NOT_WAIT)
        , ERR_LOGOUT_DUPLICATED(HttpStatus.UNAUTHORIZED, MSG_LOGOUT_DUPLICATED)
        , ERR_NOT_LOGIN_ACCESS(HttpStatus.UNAUTHORIZED, MSG_NOT_LOGIN_ACCESS)
        , ERR_NOT_FOUND_CUSTOMER(HttpStatus.NOT_FOUND, MSG_NOT_FOUND_CUSTOMER)
        , ERR_DUPLICATED_EMAIL(HttpStatus.CONFLICT, MSG_DUPLICATED_EMAIL)
        , ERR_DUPLICATED_PHONE(HttpStatus.CONFLICT, MSG_DUPLICATED_PHONE)
        , ERR_WRONG_EMAIL_PASSWORD(HttpStatus.BAD_REQUEST, MSG_WRONG_EMAIL_PASSWORD)
        , ERR_WAIT_ADMIN_ACCOUNT_LOGIN(HttpStatus.FORBIDDEN, MSG_WAIT_ADMIN_ACCOUNT_LOGIN)
        , ERR_DENY_ADMIN_ACCOUNT_LOGIN(HttpStatus.FORBIDDEN, MSG_DENY_ADMIN_ACCOUNT_LOGIN)
        , ERR_SUSPEND_ADMIN_ACCOUNT_LOGIN(HttpStatus.FORBIDDEN, MSG_SUSPEND_ADMIN_ACCOUNT_LOGIN)
        , ERR_IN_ACT_ADMIN_ACCOUNT_LOGIN(HttpStatus.FORBIDDEN, MSG_IN_ACT_ADMIN_ACCOUNT_LOGIN)
        , ERR_UNAUTHORIZED_ACCOUNT_LOGIN(HttpStatus.FORBIDDEN, MSG_UNAUTHORIZED_ACCOUNT_LOGIN)
        , ERR_ONLY_SUPER_ADMIN_ACCESS(HttpStatus.FORBIDDEN, MSG_ONLY_SUPER_ADMIN_ACCESS)
        , ERR_NOT_FOUND_ADMIN(HttpStatus.NOT_FOUND, MSG_NOT_FOUND_ADMIN)
        , ERR_NOT_FOUND_ADMIN_ROLE(HttpStatus.BAD_REQUEST, MSG_NOT_FOUND_ADMIN_ROLE_ERR)
        , ERR_NOT_FOUND_ADMIN_STATUS(HttpStatus.BAD_REQUEST, MSG_NOT_FOUND_ADMIN_STATUS_ERR)
        , ERR_NOT_FOUND_PRODUCT(HttpStatus.NOT_FOUND, MSG_NOT_FOUND_PRODUCT)
        , ERR_NOT_FOUND_PRODUCT_CATEGORY(HttpStatus.BAD_REQUEST, MSG_NOT_FOUND_PRODUCT_CATEGORY_ERR)
        , ERR_NOT_FOUND_PRODUCT_STATUS(HttpStatus.BAD_REQUEST, MSG_NOT_FOUND_PRODUCT_STATUS_ERR)
        , ERR_DELETED_ADMIN_SELF(HttpStatus.FORBIDDEN, MSG_NOT_DELETE_ADMIN_SELF)
        , ERR_SAME_OLD_PASSWORD(HttpStatus.BAD_REQUEST, MSG_SAME_OLD_PASSWORD)
        , ERR_NOT_MATCH_PASSWORD(HttpStatus.BAD_REQUEST, MSG_NOT_MATCH_PASSWORD)
        , ERR_DENY_CUSTOMER_ACCOUNT_DELETE(HttpStatus.BAD_REQUEST, MSG_DENY_CUSTOMER_ACCOUNT_DELETE)
        , ERR_NOT_FOUND_CUSTOMER_STATUS(HttpStatus.BAD_REQUEST, MSG_NOT_FOUND_CUSTOMER_STATUS_ERR)
        , ERR_ORDER_TO_DISCONTINUE(HttpStatus.BAD_REQUEST, MSG_ORDER_TO_DISCONTINUE_ERR)
        , ERR_ORDER_TO_SOLD_OUT(HttpStatus.BAD_REQUEST, MSG_ORDER_TO_SOLD_OUT_ERR)
        , ERR_ORDER_TO_QUANTITY_OVER(HttpStatus.BAD_REQUEST, MSG_ORDER_TO_QUANTITY_OVER_ERR)
        , ERR_NOT_FOUND_ORDER(HttpStatus.NOT_FOUND, MSG_NOT_FOUND_ORDER)
        , ERR_NOT_FOUND_ORDER_STATUS(HttpStatus.BAD_REQUEST, MSG_NOT_FOUND_ORDER_STATUS_ERR)
        , ERR_ORDER_PROCESSING_DELIVERED_FORBIDDEN(HttpStatus.FORBIDDEN, MSG_ORDER_PROCESSING_DELIVERED_FORBIDDEN)
        , ERR_ORDER_STATUS_INVALID_TRANSITION(HttpStatus.BAD_REQUEST, MSG_ORDER_STATUS_INVALID_TRANSITION)
        , ERR_ORDER_ALREADY_CANCELLED(HttpStatus.BAD_REQUEST, MSG_ORDER_ALREADY_CANCELLED)
        , ERR_NOT_FOUND_REVIEW(HttpStatus.NOT_FOUND, MSG_NOT_FOUND_REVIEW);
    
        private final HttpStatus status;
        private final String message;
    
        ErrorEnum(HttpStatus status, String message) {
            this.status = status;
            this.message = message;
        }
    }
    

    Reason

    핸들러에서 이 ErrorEnum 하나로 status와 message 를
    출력하기에 충분하기 때문이다

    메세지도 상수로 관리하면 한눈에 정리되어 파악하기 쉽다
    주석을 달아 도메인 별 사용이 구분되면 더 좋을 것

    3. Session Check Interceptor

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LoginSessionCheck {
    }
    
    @Component
    public class LoginSessionAccessInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
            if(handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                LoginSessionCheck loginSessionCheck = handlerMethod.getMethodAnnotation(LoginSessionCheck.class);
    
                if (loginSessionCheck == null) {
                    return true;
                }
    
                HttpSession session = request.getSession(false);
                if (session == null || session.getAttribute(ADMIN_SESSION_NAME) == null) {
                    throw new ServiceErrorException(ERR_NOT_LOGIN_ACCESS);
                }
    
                return true;
            }
    
            return true;
        }
    }
    
    @Configuration
    @RequiredArgsConstructor
    public class WebMvcConfig implements WebMvcConfigurer {
        private final LoginSessionAccessInterceptor loginSessionAccessInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginSessionAccessInterceptor);
        }
    }
    

    Reason

    이 인터셉터 하나로 커스텀 어노테이션이 붙어 있다면, 메서드 레벨의 경우에 작동하여 세션이 유지되고 있는지 체크할 수 있다 시큐리티와 JWT를 사용한다면 의미가 바래지지만 그래도 인터셉터의 PreHandle을 사용해보는 좋은 예제 라고 생각한다

    PROBLEM

    1. 주문 번호 커스텀, 다수 활용시 필요해질 동시성 제어

    @Getter
    @Entity
    @Table(name = "ordering_seq")
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class OrderingSeq {
        @Id
        private String orderingSeqId = "ORDER";
    
        @Column(nullable = false)
        private Long currentOrderSeq = 0L;
    
        public void update() {
            this.currentOrderSeq = this.currentOrderSeq + 1;
        }
    
        public String getNextOrderNo() {
            update();
            return String.format("%s%s%03d", this.orderingSeqId, "-", this.currentOrderSeq);
        }
    
        public static OrderingSeq register() {
            OrderingSeq orderingSeq = new OrderingSeq();
            orderingSeq.orderingSeqId = "ORDER";
            orderingSeq.currentOrderSeq = 0L;
    
            return new OrderingSeq();
        }
    
    }
    

    Reason

    자동 순번을 통한다면 작성되는 주문번호는 1, 2, 3… 순으로 등록이 되는데
    이러면 의미가 담기지 않았다

    보통 커머스 시스템이든 자체 판매 시스템이던 간에
    주문번호를 단순하게 이렇게 보관하지 않는다
    연월이 대다수 기본이고 어떤 상품의 키나 고객의 키 등
    여러가지가 섞인 채번 시스템이 있기 때문이다

    물론 작성한 채번 자체도 의미가 적은 단순한 로직이지만, 하는데 의의를 두었다

    문제는 다수 주문이 발생을 고려하면 동시성 제어를 해두는 것이 가장 좋을 텐데
    인지하고 있으나 마땅히 처리 방법에 뭐가 옳은지 판단을 못했다
    결국은 잘 모르기 때문이 아닌가 싶고 추후 개인적으로 개선해볼 것이다

    2. N+1 발생

    Reason

    코드는 발생의 경위가 제각기 너무 많아 발생 이유와 해결법만 서술한다
    먼저 해결법은 Fetch Join 을 통해 해결하던
    직접 native Query 를 사용해 Join문으로 해결하던
    여러가지 방법으로 가능 할 것이다

    뒤늦게 발견되는 상황마다 급하게 수정을 거쳤지만
    테스트 때도 API 결과를 중시해서 발생 자체를 생각하지 않았던 것이
    프로젝트의 패착점이라 본다

    TRY

    1. Security, JWT

    TRY Reason

    프로젝트의 구현점이였지만 달성하지 못했다
    적용을 해보거나 공부해보지도 못한 영역이라 사용해보고 싶다

    2. Test Code

    TRY Reason

    테스트 코드 작성법만 좀 제대로 알았어도 사용법을 공유하고
    코드로 개발자답게 테스트 했을 것이다

    Share article
    Contents
    팀 프로젝트의 시작끝KPTKEEPPROBLEMTRY1. Security, JWT

    LifeLog, DevLog - https://github.com/KYJTHEYJ

    RSS·Powered by Inblog