100%를 한번에 바꾸는건 어려워도 1%를 100번 바꾸는건 쉽다.

생각정리 자세히보기

나의 일상/생각정리

[생각정리] 어떤 리더가 되어야 하는가? (프로젝트 회고록)

dc-choi 2024. 4. 2. 21:41
반응형

어느덧 학원에서 시작하였던 프로젝트도 마무리가 되면서 자연스럽게 회고록을 작성해야지! 라는 생각으로 글을 작성하게 되었습니다.

 

길다면 길고 짧다면 짧았던 1월 초부터 3월 말까지의 여정을 한번 다시 돌아보는 시간을 가져보려고 합니다.

 

우선 뜬금없는 이야기지만, 제 개발자 커리어의 최종 도착지는 CTO입니다. 제가 생각하는 CTO란 기술적으로 사람들을 이끌 수 있는 사람이라고 생각합니다. CTO는 기술적으로 많은 것을 아는 것도 중요하지만 사람들을 이끌 수 있는 사람이여야 합니다. 그러기 위해서는 최대한 기술적으로 많은 경험을 해보고 어느정도 사람들을 리드를 할 수 있는 환경을 경험하고 싶었습니다.

 

사실 초기 프로젝트를 시작하면서 CTO라는 자리를 받은게 아니라 프로젝트의 기획을 하면서 어느순간 사람들을 리드하게 되었고 자연스럽게 리드를 하는 사람들이 생겼습니다. 하지만 필요에 의해 리더들의 역할을 세분화하기로 하였고 저는 기술적인 부분을 팀에 기여하게 되었습니다. 그래서 제가 원하는 대로 이번 프로젝트안에서 자연스럽게 CTO? 같은 역할을 맡게 되었고 본의 아니게 체험판으로 CTO라는 자리의 무게를 맛보게 되었습니다. 간단하게 먼저 느낀점을 요약하자면 성장이라는 것이 꼭 기술적인 성장만 있는 것은 아니다. 라는 생각이 들었습니다.

 

이 부분에 대한 이야기는 밑에서 자세하게 이야기해보겠습니다! 지금부터 1월 초부터 3월 말까지의 여정을 제가 이 프로젝트에서 잘한점, 아쉬웠던 점으로 요약해서 정리해보겠습니다!

이번 프로젝트에서 잘한 점

이번 프로젝트에서 잘했던 부분은 크게 3가지로 나누어 보았습니다.

1. 프로젝트 기획 및 설계

초기 기획에 어느정도 기여를 하였습니다. 보통 주니어의 경우 직접 요구사항을 정의하지 않고 이미 만들어진 요구사항을 받아서 그것을 구현하기에 급급합니다. 하지만 이번 프로젝트에서는 처음부터 만드는 일이기에 직접 기획부터 차근차근 하게 되었습니다. 프로젝트를 처음부터 만드는 경험을 하는 것은 정말 흥미로운 경험이였습니다. 하지만 그만큼 디테일에 대해서 생각할 점도 많았던거 같습니다. 이 부분에 대한 내용은 아쉬웠던 점에 대해 이야기할때 더 자세히 말씀드리겠습니다.

 

기획을 주도적으로 맡다보니 자연스럽게 DB 설계를 담당하게 되었습니다. 최대한 RDB에 맞도록 정규화를 진행하였고 어느정도 기본적인 설계를 하게되었습니다. 물론 추후에 프로젝트의 추가 기능이 나오게 되면서 어느정도 추가 되긴 했지만, 기본적인 DB설계와 좋은 설계가 무엇인지에 대해서는 다시 한번 리마인드 할 수 있게 되는 계기가 되었습니다.

 

최종적으로 ERD와 SQL은 다음과 같습니다.

 

DROP TABLE IF EXISTS `member`;
CREATE TABLE `member` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`email`	VARCHAR(50)	NOT NULL	UNIQUE	COMMENT '이메일',
	`provider`	VARCHAR(20)	NULL	COMMENT 'oauth2 가입 제공자',
	`name`	VARCHAR(50)	NULL	COMMENT '이름',
	`nickname`	VARCHAR(50)	NULL	COMMENT '별명',
	`role`	VARCHAR(50)	NULL	COMMENT '회원의 권한',
	`phone`	BIGINT	NULL	COMMENT '전화번호',
	`certify_at`	DATE	NULL	COMMENT '성인인증을 한 날짜',
	`agreed_to_service_use`	BOOLEAN	NULL	COMMENT '이용약관 동의',
	`agreed_to_service_policy`	BOOLEAN	NULL	COMMENT '개인정보 수집 이용 안내 동의',
	`agreed_to_service_policy_use`	BOOLEAN	NULL	COMMENT '개인정보 활용 동의',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `address`;
CREATE TABLE `address` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`is_primary`	BOOLEAN	 NULL	COMMENT '대표주소 여부',
	`address`	VARCHAR(200)	NULL	COMMENT '주소',
	`address_detail`	VARCHAR(200)	NULL	COMMENT '주소 상세',
	`postcode`	VARCHAR(50)	NULL	COMMENT '우편번호',
	`recipient`	VARCHAR(50)	NULL	COMMENT '배송 받는 사람',
	`phone`	BIGINT	NULL	COMMENT '배송 받는 사람 연락처',
	`request`	MEDIUMTEXT	NULL	COMMENT '배송시 요청사항',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`category_id`	BIGINT	NULL	COMMENT 'category fk',
	`maker_id`	BIGINT	NULL	COMMENT 'maker fk',
	`name`	VARCHAR(50)	NULL	COMMENT '제품 이름',
	`price`	DECIMAL(64, 3)	NULL	COMMENT '제품 원가',
	`distribution_price`	DECIMAL(64, 3)	NULL	COMMENT '유통 가격',
	`quantity`	BIGINT	NULL	COMMENT '재고 수량',
	`alcohol`	DOUBLE	NULL	COMMENT '술 도수',
	`ingredient`	VARCHAR(1000)	NULL	COMMENT '제품 재료',
	`sweet`	BIGINT	NULL	COMMENT '술 단맛',
	`sour`	BIGINT	NULL	COMMENT '술 신맛',
	`cool`	BIGINT	NULL	COMMENT '술 청량감',
	`body`	BIGINT	NULL	COMMENT '술 바디감',
	`balance`	BIGINT	NULL	COMMENT '술 밸런스',
	`incense`	BIGINT	NULL	COMMENT '술 향기',
	`throat`	BIGINT	NULL	COMMENT '술 목넘김',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`category_class_id`	BIGINT	NULL	COMMENT 'catogory_class fk',
	`last_name`	VARCHAR(50)	NULL	COMMENT '소분류',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `maker`;
CREATE TABLE `maker` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`name`	VARCHAR(50)	NULL	COMMENT '제조사 이름',
	`address`	VARCHAR(200)	NULL	COMMENT '제조사 주소',
	`detail`	VARCHAR(200)	NULL	COMMENT '제조사 상세 주소',
	`region`	VARCHAR(50)	NULL	COMMENT '술 제조지역',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `item`;
CREATE TABLE `item` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`category_id`	BIGINT	NULL	COMMENT 'category fk',
	`type`	VARCHAR(20)	NULL	COMMENT '상품 유형',
	`name`	VARCHAR(200)	NULL	COMMENT '상품 이름',
	`price`	DECIMAL(64, 3)	NULL	COMMENT '가격',
	`info`	MEDIUMTEXT	NULL	COMMENT '상품 설명',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `item_product`;
CREATE TABLE `item_product` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`item_id`	BIGINT	NULL	COMMENT 'item fk',
	`product_id`	BIGINT	NULL	COMMENT 'product fk',
	`quantity`	BIGINT	NULL	COMMENT '제품의 재고를 없애는 수량',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant`;
CREATE TABLE `restaurant` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk / 사장 ID',
	`category`	VARCHAR(50)	NULL	COMMENT '레스토랑 분류',
	`name`	VARCHAR(200)	NULL	COMMENT '레스토랑 이름',
	`address`	VARCHAR(200)	NULL	COMMENT '레스토랑 주소',
	`address_detail`	VARCHAR(200)	NULL	COMMENT '레스토랑 상세 주소',
	`postcode`	VARCHAR(50)	NULL	COMMENT '우편번호',
	`location`	POINT	NULL	COMMENT '위도, 경도',
	`contact`	BIGINT	NULL	COMMENT '가게 연락처',
	`menu`	JSON	NULL	COMMENT '메뉴',
	`time`	JSON	NULL	COMMENT '영업시간',
	`provision`	JSON	NULL	COMMENT '레스토랑의 편의시설',
	`business_name`	VARCHAR(50)	NULL	COMMENT '사업자 등록증에 등록된 사업장명',
	`business_number`	VARCHAR(50)	NULL	COMMENT '사업자 번호',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_stock`;
CREATE TABLE `restaurant_stock` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`product_id`	BIGINT	NULL	COMMENT 'product fk',
	`restaurant_id`	BIGINT	NULL	COMMENT 'restaurant fk',
	`price`	BIGINT	NULL	COMMENT '제품 판매 단가',
	`quantity`	BIGINT	NULL	COMMENT '레스토랑 재고 수량',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `ncp_file`;
CREATE TABLE `ncp_file` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`s3_files`	JSON	NULL	COMMENT 'file 정보가 저장된 json',
	`entity_id`	BIGINT	NULL	COMMENT 'entity의 pk',
	`entity_type`	VARCHAR(20)	NULL	COMMENT '파일이 저장되는 entity 이름',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `order_detail`;
CREATE TABLE `order_detail` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`item_id`	BIGINT	NULL	COMMENT 'item fk',
	`order_id`	BIGINT	NULL	COMMENT 'orders fk',
	`item_price`	DECIMAL(64, 3)	NULL	COMMENT '상품 단가',
	`quantity`	BIGINT	NULL	COMMENT '주문 수량',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '상품의 총 금액',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `payment`;
CREATE TABLE `payment` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`order_id`	BIGINT	NULL	COMMENT 'orders fk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`payment_no`	VARCHAR(200)	NULL	COMMENT '결제 고유번호',
	`status`	VARCHAR(20)	NULL	COMMENT '결제 상태정보',
	`method`	VARCHAR(20)	NULL	COMMENT '결제 수단',
	`provider`	VARCHAR(20)	NULL	COMMENT 'easyPay_간편결제사 코드',
	`card_type`	VARCHAR(20)	NULL	COMMENT 'card_카드 종류',
	`owner_type`	VARCHAR(20)	NULL	COMMENT 'card_카드의 소유자 타입',
	`issuer_code`	VARCHAR(20)	NULL	COMMENT 'card_카드 발급사',
	`acquirer_code`	VARCHAR(20)	NULL	COMMENT 'card_카드 매입사',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '결제 총 금액',
	`requested_at`	DATETIME	NULL	COMMENT '결제 요청일',
	`approved_at`	DATETIME	NULL	COMMENT '결제 승인일',
	`currency`	VARCHAR(20)	NULL	COMMENT '결제 통화',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`order_no`	VARCHAR(200)	NULL	COMMENT '주문 고유번호',
	`status`	VARCHAR(20)	NULL	COMMENT '주문 상태정보',
	`price`	DECIMAL(64, 3)	NULL	COMMENT '주문 상품 총 금액',
	`delivery_price`	DECIMAL(64, 3)	NULL	COMMENT '배송 금액',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '배송비 포함 주문 총 금액',
	`recipient`	VARCHAR(50)	NULL	COMMENT '배송받는 사람',
	`phone`	BIGINT	NULL	COMMENT '배송받는 사람의 연락처',
	`address`	VARCHAR(200)	NULL	COMMENT '배송지 주소',
	`address_detail`	VARCHAR(200)	NULL	COMMENT '배송지 상세주소',
	`description`	MEDIUMTEXT	NULL	COMMENT '배송시 주의사항',
	`postcode`	VARCHAR(50)	NULL	COMMENT '배송지 우편번호',
	`cancel_reason`	VARCHAR(200)	NULL	COMMENT '주문 취소,환불 사유',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `cart`;
CREATE TABLE `cart` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `cart_detail`;
CREATE TABLE `cart_detail` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`cart_id`	BIGINT	NULL	COMMENT 'cart fk',
	`item_id`	BIGINT	NULL	COMMENT 'item fk',
	`quantity`	BIGINT	NULL	COMMENT '장바구니 수량',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `question`;
CREATE TABLE `question` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`title`	VARCHAR(200)	NULL	COMMENT '제목',
	`content`	MEDIUMTEXT	NULL	COMMENT '문의 내용',
	`status`	VARCHAR(20)	NULL	DEFAULT '미완료'	COMMENT '답변 여부',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `answer`;
CREATE TABLE `answer` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`question_id`	BIGINT	NULL	COMMENT 'question fk',
	`content`	MEDIUMTEXT	NULL	COMMENT '답변 내용',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `review`;
CREATE TABLE `review` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`item_id`	BIGINT	NULL	COMMENT 'item fk',
	`order_detail_id`	BIGINT	NULL	COMMENT 'order_detail fk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`score`	DOUBLE	NULL	COMMENT '별점',
	`content`	MEDIUMTEXT	NULL	COMMENT '리뷰 내용',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `notice`;
CREATE TABLE `notice` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`title`	VARCHAR(200)	NULL	COMMENT '공지사항 제목',
	`content`	MEDIUMTEXT	NULL	COMMENT '공지사항 내용',
	`status`	VARCHAR(20)	NULL	DEFAULT '작성 중'	COMMENT '작성 상태',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `category_class`;
CREATE TABLE `category_class` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`first_name`	VARCHAR(50)	NULL	COMMENT '카테고리 대분류',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `refresh_token`;
CREATE TABLE `refresh_token` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`token`	VARCHAR(512)	NULL	COMMENT '리프레시 토큰',
	`expiry_date`	DATETIME	NULL	COMMENT '리프레시 토큰 만료 일시',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성 일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정 일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제 일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_order`;
CREATE TABLE `restaurant_order` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`restaurant_id`	BIGINT	NULL	COMMENT 'restaurant fk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`status`	VARCHAR(20)	NULL	COMMENT '주문 상태 정보',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '주문 총 금액',
	`address`	VARCHAR(200)	NULL	COMMENT '배송지 주소',
	`address_detail`	VARCHAR(200)	NULL	COMMENT '배송지 상세 주소',
	`description`	MEDIUMTEXT	NULL	COMMENT '배송시 주의사항',
	`postcode`	VARCHAR(50)	NULL	COMMENT '배송지 우편번호',
	`recipient`	VARCHAR(50)	NULL	COMMENT '배송 받는 사람',
	`phone`	BIGINT	NULL	COMMENT '배송 받는 사람의 연락처',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_order_detail`;
CREATE TABLE `restaurant_order_detail` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`restaurant_order_id`	BIGINT	NULL	COMMENT 'restaurant_order fk',
	`product_id`	BIGINT	NULL	COMMENT 'product fk',
	`quantity`	BIGINT	NULL	COMMENT '주문 수량',
	`price`	DECIMAL(64, 3)	NULL	COMMENT '주문 시 제품 1개당 가격',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '총 주문 금액',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_order_refund`;
CREATE TABLE `restaurant_order_refund` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`restaurant_id`	BIGINT	NULL	COMMENT 'restaurant fk',
	`restaurant_order_id`	BIGINT	NULL	COMMENT 'restaurant_order fk',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '환불 총 금액',
	`owner_reason`	VARCHAR(200)	NULL	COMMENT '주문자 환불 사유',
	`admin_reason`	VARCHAR(200)	NULL	COMMENT '관리자 환불 반려 사유',
	`status`	VARCHAR(20)	NULL	COMMENT '환불 상태 정보',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_order_refund_detail`;
CREATE TABLE `restaurant_order_refund_detail` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`restaurant_order_refund_id`	BIGINT	NULL	COMMENT 'restaurant_order_refund fk',
	`product_id`	BIGINT	NULL	COMMENT 'product fk',
	`quantity`	BIGINT	NULL	COMMENT '제품 당 주문 환불 수량',
	`price`	DECIMAL(64, 3)	NULL	COMMENT '제품 1개당 환불 가격',
	`total_price`	DECIMAL(64, 3)	NULL	COMMENT '제품 당 총 환불 가격',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_order_cart`;
CREATE TABLE `restaurant_order_cart` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`restaurant_id`	BIGINT	NULL	COMMENT 'restaurant fk',
	`member_id`	BIGINT	NULL	COMMENT 'member fk',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS `restaurant_order_cart_detail`;
CREATE TABLE `restaurant_order_cart_detail` (
	`id`	BIGINT	AUTO_INCREMENT  NOT NULL    PRIMARY KEY COMMENT 'pk',
	`restaurant_order_cart_id`	BIGINT	NULL	COMMENT 'restaurant_order_cart fk',
	`product_id`	BIGINT	NULL	COMMENT 'product fk',
	`quantity`	BIGINT	NULL	COMMENT '매장 장바구니 제품 당 수량',
	`created_at`	DATETIME	NULL	DEFAULT CURRENT_TIMESTAMP	COMMENT '생성일자',
	`updated_at`	DATETIME	NULL	COMMENT '수정일자',
	`deleted_at`	DATETIME	NULL	COMMENT '삭제일자'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 프로젝트 개발 과정

개발자로서 참여했던 프로젝트이다보니 아무래도 이 부분에서 조금 더 이야기할게 많을 것 같습니다.

1. 테스트 코드

저는 테스트코드를 굉장히 중요하게 생각합니다. 개발자는 단순히 코드를 치는 사람이 아닌 현실의 복잡함을 가상세계에 넣는 사람이라고 생각합니다. 개발자가 복잡한 현실을 어느정도 이해하기 위해서는 그에 맞는 비즈니스 로직을 작성해야하고 그 로직을 테스트 해보는 것이 굉장히 중요하다고 생각합니다.

 

그것을 일일히 UI를 통해서, Postman, Thunder Client를 통해서 테스트를 해볼 수 있지만 그것은 테스트 코드를 치는 것보다 비효율적이고 테스트 케이스에 대해서 생각해볼 수도 없는 것이라고 생각합니다. 개발자가 어느정도 테스트 케이스에 대해서 생각해야 그 비즈니스에 대해서 이해할 수 있다고 생각이 듭니다.

 

이런 테스트 코드를 프로젝트 초기에 도입하여 API 테스트를 통합 테스트처럼 사용하였고, 단위 테스트를 Mock를 사용하여 진행하도록 하였습니다. 최대한 팀원들이 테스트 코드에 대한 러닝커브를 줄일 수 있도록 직접 기능에 대한 테스트를 작성하였고, 그것을 레퍼런스로 삼게 하였습니다. 그래서 현재 팀원들은 각 기능을 개발할 때마다 테스트 코드를 작성하게 되었습니다.

 

다들 처음에는 테스트 코드에 대한 막연한 두려움이 있었지만 지금은 테스트 코드를 너무 잘 작성하고 테스트에 대해서 너무 잘 이해하고 있습니다. 테스트 커버리지는 인텔리제이의 기능을 사용해서 측정하였고 다음과 같습니다.

 

테스트 커버리지가 모든 코드의 품질을 보장할 수는 없지만 최소한의 코드 품질을 보장할 수 있는 장치라고 생각하였습니다. 코드의 품질에 대해서는 리뷰를 통해서도 같이 검증이 되어야 한다고 생각합니다. 코드 품질을 향상시키고 성장까지 할 수 있는 리뷰에 대해서는 뒤에서 다시 이야기 하도록 하겠습니다.

2. 전체 아키텍처 구성

또 전체적인 아키텍처 구성을 맡게 되었습니다. 그래서 배포에 관련된 모든 부분을 신경썼습니다. 이 부분에 대해서 많은 고민을 하였고 결론적으로 클라우드에 배포하는 방식을 사용하기로 했습니다. 클라우드는 NCP를 선택하게 되었습니다. NCP도 은근 많은 기능을 가지고 있었고 학원에서 제공해주는 무료 NCP 크레딧을 사용할 수 있었기에 NCP를 선택하였습니다. (전 개인적으로 AWS를 더 좋아합니다.)

 

프로젝트를 진행했었던 최종 아키텍처는 다음과 같습니다.

 

그 다음으로 배포시 사용했던 모든 기술스택의 경우는 다음과 같습니다.

 

Ncp Server의 경우 AWS의 EC2 같은 컴퓨팅 자원을 제공해주는 서비스입니다. 도커기반이긴 하였으나, NCP의 ECS라고 불리는 Kubernetes Service를 도입하기에는 Kubernetes 러닝커브가 있고 지금 도입하는 것은 오버엔지니어링이라는 생각을 하게 되었습니다. 그래서 Ncp Server를 사용하여 Docker Host를 만들고 그 안에 Docker Container를 띄우는 방식으로 배포하게 되었습니다.

 

Ncp Object Storage의 경우 적극적으로 도입을 하였는데 팀원중에 직접 사용해보신 분이 계셨고, 다들 이 서비스를 사용하는 것에 동의하셨습니다. Ncp Object Storage같은 버킷을 사용하는 이유는 크게 두가지라고 생각했습니다.

 

첫번째는 확장성입니다. 이미지 파일이 기본적으로 어느정도 큰 용량을 차지하기에 서버에 직접 이미지를 저장하게 되는 경우 서버 저장공간에 대한 고민을 같이 해야합니다.

 

두번째는 내구성입니다. 이미지 파일을 서버에 직접 저장하게 되는 경우 이미지가 유실되는 경우를 대비해야 합니다. Ncp Object Storage를 사용하면 99.99999%로 이미지 손실을 방지할 수 있습니다.

 

그래서 Ncp Object Storage는 이미지 파일이나 도커 이미지를 저장하는 용도로 사용하려고 하였습니다. 도커 이미지의 경우 NCP Container Registry에서 이미지를 저장할 공간을 Ncp Object Storage 사용하기에 전용 Bucket을 생성해주었습니다.

 

NCP Container Registry는 Docker Image를 저장할 수 있는 서비스입니다. 위에서 설명드렸다시피 Docker 기반으로 배포를 진행해서 Ncp Server에 배포하는 방식을 사용하였습니다. 하지만 Ncp Server의 성능이 도커 이미지를 빌드하기에는 충분하지않다고 판단해서 미리 빌드된 도커 이미지만을 사용하는 방법을 생각하게 되었습니다.

 

Docker Hub를 사용할 수 있는 방법도 있겠지만 NCP Container Registry를 사용하면 빌드된 도커 이미지를 pull 받게 하는 권한을 설정할 수 있어서 도커 이미지를 private하게 사용할 수 있다고 생각하였습니다.

 

결론적으로는 클라우드 서비스를 사용하면 직접 서버를 관리해야하는 수고를 어느정도 덜어낼 수 있다고 생각했습니다.

3. CI/CD

프로젝트를 진행하면서 어느정도 CI/CD도 고민을 하였습니다. 이 부분에 있어서는 크게 Jenkins와 GitHub Actions 두개중에서 고민하였고 GitHub Actions를 선택하였습니다. GitHub Actions를 선택한 이유는 팀에서 GitHub를 사용하고있어서 Jenkins를 직접 설치하는 것보다 빠르게 CI/CD를 적용할 수 있을 것이라고 생각하였습니다. GitHub Actions의 스크립트는 다음과 같습니다.

 

ci.yaml

name: ci

on:
  pull_request:
    branches:
      - dev

jobs:
 test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: create application.yml
        run: |
          touch ./application.yml
          echo '${{ secrets.TEST_APPLICATION_YML }}' >> ./application.yml

      - name: create application-file.yml
        run: |
          touch ./application-file.yml
          echo '${{ secrets.TEST_APPLICATION_FILE_YML }}' >> ./application-file.yml

      - name: create application-secret.yml
        run: |
          touch ./application-secret.yml
          echo '${{ secrets.TEST_APPLICATION_SECRET_YML }}' >> ./application-secret.yml

      - name: create application-test.yml
        run: |
          touch ./application-test.yml
          echo '${{ secrets.TEST_APPLICATION_TEST_YML }}' >> ./application-test.yml

      - name: Set up Java
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'

      - name: Use Gradle 7.4.2
        uses: gradle/actions/setup-gradle@v3
        with:
          gradle-wrapper-file: 'gradlew'
          distribution: 'gradle-7.4.2'
          arguments: -i test

      - name: Check test results
        uses: actions/github-script@v1
        with:
          script: |
            if (github.event && github.event.pull_request) {
              if (github.event.pull_request.head.sha !== github.sha) {
                throw new Error('The pull request head SHA does not match the workflow run SHA.')
              }
            
              const testResults = await getTestResults()
              if (!testResults.success) {
                throw new Error('The tests failed.')
              }
            
              console.log('The tests passed.')
            } else {
              // Handle non-Pull Request event
            }

 

cd.yaml

name: cd

on:
  push:
    branches:
      - dev

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: create application.yml
        run: |
          echo '${{ secrets.DEV_APPLICATION_YML }}' > src/main/resources/application.yml

      - name: create application-dev.yml
        run: |
          touch src/main/resources/application-dev.yml
          echo '${{ secrets.DEV_APPLICATION_DEV_YML }}' >> src/main/resources/application-dev.yml

      - name: create application-file.yml
        run: |
          touch src/main/resources/application-file.yml
          echo '${{ secrets.DEV_APPLICATION_FILE_YML }}' >> src/main/resources/application-file.yml

      - name: create application-secret.yml
        run: |
          touch src/main/resources/application-secret.yml
          echo '${{ secrets.DEV_APPLICATION_SECRET_YML }}' >> src/main/resources/application-secret.yml

      - name: Login ECR
        run: |
          docker login -u ${{ secrets.NCP_ACCESS_KEY }} -p ${{ secrets.NCP_SECRET_KEY }} ${{ secrets.NCP_ECR_URL }}

      - name: Make Docker Image
        run: |
          docker build --build-arg PROFILE=dev -t alcohol-friday:${GITHUB_SHA::8} .

      - name: Tag Docker Image
        run: |
          docker tag alcohol-friday:${GITHUB_SHA::8} ${{ secrets.NCP_ECR_URL }}/alcohol-friday:${GITHUB_SHA::8}

      - name: Docker Image Push To ECR
        run: |
          docker push ${{ secrets.NCP_ECR_URL }}/alcohol-friday:${GITHUB_SHA::8}

  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: NCP Login And Deploy Script Run
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.NCP_HOST }}
          username: ${{ secrets.NCP_USERNAME }}
          password: ${{ secrets.NCP_PASSWORD }}
          port: ${{ secrets.NCP_PORT }}
          envs: GITHUB_SHA
          script: |
            cd /usr/local/docker
            ./deploy-server.sh ${GITHUB_SHA::8}

 

deploy-server.sh

#!/bin/sh

if [ $# -ne 1 ]; then
        echo 'Check arguments'
        exit -1
fi

ACCESS_KEY=""
SECRET_KEY=""
URL=""
IMAGE_NAME=""

docker pull nginx

docker login -u $ACCESS_KEY -p $SECRET_KEY $URL
docker pull $URL/$IMAGE_NAME:$1
docker tag $URL/$IMAGE_NAME:$1 $IMAGE_NAME:dev

docker compose down

docker compose up -d

docker compose ps

docker image prune -a -f

 

CI의 경우 PR이 생성될 경우 test를 실행하도록 하였고 CD는 dev 브랜치로 merge되는 경우에 실행되도록 하였습니다. Docker라는 도구가 굉장히 좋다는 것을 알고는 있었지만 학습을 해볼 기회가 많이 없었습니다. 하지만 이번 프로젝트에서 Docker 기반으로 배포를 진행하면서 ECR도 함께 사용하는 경험을 쌓을 수 있었고 기존에는 Jenkins만 사용했었는데 GitHub Actions를 새로 배울 수 있는 좋은 시간이 되었던거 같습니다.

3. 협업 문화

일단 협업툴을 선택하는 과정에서 고민을 좀 했었습니다. Jira와 Confluence를 도입하여 협업 문화를 세우고 싶었습니다. Jira를 통해 이슈 트래킹을 진행할 수 있고 Confluence로 문서화가 필요한 부분에 있어서는 문서화를 진행하였습니다.

 

단 Jira와 Confluence를 사용한다고 해서 온전한 협업 문화를 가질 수 없다고 생각하였습니다. 그래서 Jira와 GitHub에 뭔가 이벤트가 생길 때마다 Discord로 알림을 받도록 하였습니다. 그렇게 해서 알림에 대해서 어느정도 신경쓰고 서로 어떤 작업을 진행하는지에 대한 정보를 알 수 있었습니다.

 

이번 프로젝트에서는 Discord가 팀 메신저같은 역할을 하게 되었습니다. Discord와 GitHub의 연동은 어렵지 않았으나 Jira와 Discord의 연동은 쉽지 않았습니다. 이 부분은 Jira의 Automation을 사용하여 알림을 받을 수 있도록 하였습니다.

 

Automation의 구현 내역은 다음과 같습니다.

 

위 사진처럼 총 4개의 이벤트를 등록하고 이벤트가 등록되면 Discord Webhook을 통해 메시지를 보내도록 하였습니다.

 

세부 설정은 다음과 같습니다.

 

{
    "content": "이슈가 갱신되었습니다.\n이슈명: [{{issue.key}} {{issue.summary}}]({{baseUrl}}/jira/software/projects/AF/boards/1?selectedIssue={{issue.key}})\n이슈상태: {{issue.status.name}}\n담당자: {{issue.assignee.displayName}}\n보고자: {{issue.reporter.displayName}}\n이슈 설명: {{issue.description}}\nㅤ"
}

 

Jira와 Discord의 연동의 경우 Automation을 사용하게 되었습니다. 이 부분은 뒤에서 자세하게 다루도록 하겠습니다.

 

개인적으로 이런 협업툴을 잘 다루는 것도 중요하지만 결국 협업을 잘하기 위해서는 각자 개인의 태도가 굉장히 중요하다고 생각합니다. 그래서 프로젝트 후반으로 가면서 Jira, Confluence를 사용하는 것에 그치지 않고 무엇인가 변경사항이 생기면 그 내용을 공유할 수 있도록 Discord를 적극 활용하였습니다.

 

위 사진에서 확인하시는 것처럼 질문과 소통이라는 채널을 만들어서 이 곳에 변경사항이나 기타 궁금한 내역들에 대한 질문을 이곳에서 주고 받았습니다.

 

결론적으로 Jira와 Confluence를 사용해서 이슈 추적, 문서화에 좀 더 힘을 실었고 Discord로 소통에 오류가 없도록 추가적인 장치를 마련하였습니다.

 

다음으로 테스트 코드만큼이나 중요하다고 생각했던 것은 코드 리뷰였습니다. 코드의 품질을 어느정도 보장하기 위해서는 리뷰가 필연적이였습니다. 하지만 팀원들이 리뷰를 공격으로 인식하면 안된다고 생각하였습니다. 결국 우리가 코드 리뷰를 하면서 나아가야할 점은 성장이라고 생각하였습니다. 그래서 이런 부분에 있어 좋은 자료를 팀원들에게 공유하였고 리뷰 문화에 대해 다시 한번 생각하게 되었습니다.

 

참고한 자료는 다음과 같습니다.

 

https://youtu.be/mJj2MfomkvU?si=ysuBja1FwUCWDUxe

 

https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/

 

효과적인 코드리뷰를 위한 리뷰어의 자세

안녕하세요, 톡FE파트에서 톡명함 서비스를 개발하고 있는 Kay입니다.저는 2022년 신입 공채 기술 온보딩 교육의 코드 리뷰어로 활동을 했는데요, 이를 통해 얻었던 경험과 효과적인 코드 리뷰를

tech.kakao.com

코드 리뷰는 개인적으로 비용이 많이 드는 일이라고 생각을 하였습니다. 코드를 이해하고 그 의도를 파악하는 것과 내가 남긴 코멘트가 내 의도와 다르게 상대에게 전달되는 것에 대한 두려움이 조금 있다고 생각하였습니다.

 

이 부분을 어떻게 해소할 수 있을까에 대해서 생각을 하였고 뱅크샐러드에서 사용한 Pn룰을 적용하기로 하였습니다.

 

https://blog.banksalad.com/tech/banksalad-code-review-culture/#커뮤니케이션-비용을-줄이기-위한-pn-룰

 

코드 리뷰 in 뱅크샐러드 개발 문화 | 뱅크샐러드

안녕하세요, 뱅크샐러드 BanksaladX iOS Engineer…

blog.banksalad.com

이 부분으로 인해 저의 의도를 확실하게 전달할 수 있는 수단을 어느정도 확보했고 팀원분들도 적극적으로 따라주셨습니다.

 

성숙한 협업 문화에 대해서 많은 고민을 했었고 아직 부족하지만 이번 프로젝트를 통해서 어느정도 얻어갈 수 있는 시간이 되었던 거 같습니다!

이번 프로젝트에서 아쉬웠던 점

사람이 항상 잘할 수 없고 매번 후회가 남습니다. 프로젝트를 잘하기 위해서 아쉬웠던 점을 기록해보려고 합니다.

1. 기획의 누락

이번 프로젝트에서 제일 커다란 부분이였습니다. 우리 프로젝트는 기획자라는 포지션이 없는 상황에서 진행되었습니다. 위에서 언급드렸다시피 기획에 어느정도 적극적이였던 분들이 자연스럽게 프로젝트를 리드하게 되었습니다. 리드를 하셨던 분은 저를 포함한 3명이였습니다. 하지만 이 분들 역시 본 포지션은 백엔드 개발자였고 각자 맡아서 해야하는 부분의 개발이 있었기 때문에 기획에 누락이 어느정도 있었다고 생각합니다.

 

이번 프로젝트를 하면서 기획을 할 때에는 조금 더 디테일에 신경을 써야한다는 것을 느꼈고 그 해당 도메인에 대해서 조금 더 생각을 해야한다는 생각이 들었습니다.

2. Jira, Discord 연동

개인적으로 많이 아쉬움이 남았던 부분이였습니다. Jira와 Discord의 연동의 경우 현재를 기준으로 연동과 관련된 유용한 플러그인이 없어서(있긴한데 유료...) Automation을 사용하여 이벤트가 발생하면 Discord의 WebHook으로 요청을 전송하는 방식으로 알림을 받았었습니다.

 

이 부분은 Jira와 Discord를 연동할 때 아쉬운 부분이긴 합니다... Slack의 경우 Jira와 연동이 굉장히 잘 되는데 Automation을 사용한 알림의 개인적으로 좀 아쉬운 부분이 많았습니다. 우리는 Jira의 무료 요금제를 사용하였고 그에 따라 월 100번의 Automation만을 사용할 수 있었습니다. 그래서 100번을 초과하면 알림이 오지 않았습니다.

 

무엇보다 Discord의 WebHook으로 요청을 전송하는 방식을 사용해서 Jira에서 양식에 맞게 JSON이 만들어지지 않으면 Error가 발생했었습니다. Error 발생으로 인해 내부적으로 예외를 처리하지 않아서 알림이 오지않는 경우도 있었습니다.

3. CI 속도가 느림

테스트 코드를 다들 적극적으로 작성하였고 테스트 코드의 양이 많아질 수록 속도가 느려져서 나중에는 20분이 넘게 걸리는 경우가 생겼습니다. 게다가 CI 스크립트가 작동하는 경우 다른 PR이 생성되면 여러 쓰레드가 생성되어서 동시성 문제까지 발생하였습니다.

 

그래서 한 사람이 PR을 날리면 그대로 다른 사람들이 기다려야하는 참사가 발생하였습니다... 프로젝트 막바지에 이 문제를 발견하였으며 이 문제를 해결해야하는 사람인 제가 프로젝트 중간에 취업을 하게 되었고 회사 일과 병행하느라 절대적인 시간 자체가 부족하여 결국 이 문제를 해결하지는 못하였습니다...

 

이 부분에 대한 문제점은 다음과 같다고 생각했습니다.

1. Test 실행 자체가 느림.

2. 매번 Gradle Build를 진행한다.

 

따라서 일반적인 해결책은 다음과 같았습니다.

1. .gradle/gradle.yml 추가

2. --parallel 옵션 추가

3. Build를 Cache함

 

이정도로 생각하였습니다. 이 부분은 나중에 무조건 만날거라고 판단하여서 반드시 해결하고싶습니다...

4. CD 작업 속도가 늦어짐

CI 부분에서도 말씀드렸다시피 CI/CD를 도입하는 부분에서 시간이 생각보다 많이 소요가 되었습니다. 일단 저는 2월초부터 취업하여 회사에 다니기 시작하였습니다. 따라서 회사업무와 병행하면서 같이 사이드 프로젝트를 하게 되었습니다. 또 Docker 기반의 배포가 처음이였고 CI/CD의 경우 Jenkins만 사용했었으며 GitHub Actions를 사용하는 것은 처음이였습니다.

 

따라서 배포를 진행하기 위해서는 Docker, GitHub Actions에 대해서 새로 학습을 했어야 했습니다. 물론 온전히 이 프로젝트에만 투자할 수 있다면 이것은 전혀 문제가 되지 않지만 회사일에 적응하면서 진행하는 것이 쉬운 일은 아니였습니다. 그래서 제가 생각했던 배포에 필요한 시간보다 일주일이나 더 걸리게 되었습니다. 이는 프론트엔드와 협업하는데에 많은 차질이 있었고 결과적으로 프론트엔드의 결과물이 늦게 나오는 이유중 하나가 되었던거 같습니다.

5. 프론트엔드 배포가 예상했던 흐름대로 가지 못함.

원래 프론트엔드의 배포의 경우 NCP의 CloudFront인 Global CDN을 사용하려고 하였습니다.

 

Global CDN을 사용하려고 했던 이유는 백엔드와 프론트엔드를 완전하게 분리하기 위한 방법중 최상의 방법이라고 생각이 들었습니다.

 

제가 생각했던 배포를 진행하는 방식은 크게 3가지였습니다.

1. Spring안에 React 빌드파일을 넣는 것

2. 따로 NCP Server의 인스턴스를 만들어 Nginx에 React 빌드파일을 넣는 것

3. Global CDN을 사용하는 방법

 

이중 1번의 경우 백엔드와 프론트엔드를 분리할 수 없기에 제외하고 2번과 3번중 고민하였고 Global CDN을 사용하는 방식을 사용하게 되었습니다.

 

Global CDN을 사용하려고 했던 이유는 크게 두가지입니다.

 

첫번째로는 배포 방식이 간단해서입니다. React에서 빌드된 정적 파일을 S3에 넣기만 하면 Global CDN의 Origin이 변경될 수 있겠다는 생각을 하였습니다. 2번의 경우 따로 서버안에 기존 정적 파일을 제거하고 빌드된 정적 파일을 새로 넣어야 합니다. 이 부분에 해당하는 배포 스크립트를 작성해야하기에 번거롭다고 판단하였습니다.

 

두번째로는 캐싱이 된다는 점이였습니다. 이커머스의 서비스 특성상 사람들의 구매율을 높이기 위해서 특정 상품을 맨 위에 노출시키도록 합니다. 그러면 자연스럽게 해당 상품의 접속수가 많아질 것이라고 생각하였습니다. 서버의 트래픽을 어느정도 막기위해서 캐시가 필요하다고 생각하였고 그래서 괜찮은 서비스라고 생각하였습니다.

 

크게 생각했던 것은 위 두가지의 이유였고 직접 NCP Server의 인스턴스를 만들어서 그 안에 Nginx를 실행시키는 것보다 금액이 훨씬 저렴하다는 장점도 알아냈습니다.

 

Global CDN으로 배포를 진행한다면 예상되는 아키텍처는 다음과 같습니다.

 

Global CDN를 사용하기 위해서는 React에서 정적 파일을 빌드해서 배포해야 했습니다. 하지만 프론트엔드에서는 Next.js를 사용하여 SSR를 진행하고 있었고 이를 확인하지 못했었습니다. 그렇게해서 프론트엔드의 배포는 예상했던 Global CDN을 사용하지 않고 Vercel을 사용하게 되었습니다.

 

사실 이번 프로젝트에서는 SSR이 필요한 프로젝트가 아니였다고 생각하는데 이부분에 있어서 처음부터 서포트하지 못하였습니다. 또 프론트엔드와 백엔드가 서로 소통이 안되었다고 생각하였고 이건 저의 잘못이라고 판단하였습니다.

6. 코드를 많이 못침

프로젝트를 시작한 초창기에는 아무래도 제가 조금 더 기능 구현에 집중할 수 있는 환경이였습니다. 하지만 회사를 취업하면서부터 저의 절대적으로 투자할 수 있는 시간이 줄어들었고 배포에 관련된 부분이 이 프로젝트에 조금 더 기여할 수 있다고 판단하여서 그 순간부터 거의 코드 리뷰에 집중하고 CI/CD를 신경쓰며 상품 결제 프로세스, 상품 취소 프로세스, 상품 환불 프로세스 기획을 진행하게 되었습니다.

 

프로젝트에 조금 더 기여를 할 수 있다는 점에서 어쩔 수 없는 선택이라고 생각을 하긴 했습니다. 하지만 저의 개인적인 욕심으로는 백엔드 개발자로서 코드를 치면서 기능 구현에 기여하고싶다는 생각을 하기도 했습니다. 그래서 저는 기술적 성장을 많이 이뤄내지 못했다고 생각하였습니다. 하지만 한 영상을 보면서 성장이란 무엇일까에 대한 생각을 하게 되었습니다.

 

바로 제가 즐겨보는 개발바닥이라는 유튜버가 올린 영상을 보면서 어느정도 생각을 바로 잡게 되었습니다. 저에게 강렬한 메시지를 준 내용은 다음과 같습니다.

 

성장이라는 것이 결국 기술적으로 성장하는 것만이 아니다. 라는 말이 너무 와닿았습니다. 저 역시도 주니어의 시각에서 바라봤을 때는 사실 백엔드 개발자로서 많이 성장하지 못했다고 생각할 수도 있겠습니다.

 

하지만 나의 목표가 CTO이라고 한다면 배포에 대한 것도 알아두고 사람들과 커뮤니케이션하는 방법에 대해서도 알아두는 것이 나중을 위한 발판이 되지 않을까 생각을 하게 되었습니다.

 

흔히 생각하는 백엔드 개발자의 역량이란 이런 것이 있으니깐요!

 

제가 이번 프로젝트를 통해 얻어간 것은 구현 능력 그 이상이였습니다. 그렇기에 어쨋든 저는 이번 프로젝트를 통해서 성장했다고 봐도 될거같습니다!!

 

제가 참고한 영상은 다음과 같습니다.

 

https://youtu.be/U89-1xChtgI?si=iVsUOrPugOX68D8r

 

https://youtu.be/JfjH-uVkzAk?si=k1jhfEGzg7QS4mRQ

7. 고도화를 제대로 진행하지 못함 (동시성, 검색엔진)

프로젝트는 총 1차와 2차 이렇게 두가지로 나뉘게 되었습니다. 리더들의 세부적인 역할을 나눈 것은 2차가 시작되면서 나누었고 그 시기에 취업을 하게되면서 절대적인 시간도 많이 부족해졌습니다.

 

그렇게 되다보니 자연스럽게 원래 계획했었던 고도화를 진행하면서 해결하려고 했던 동시성에 관련된 문제라던지 키워드 검색에 따른 검색엔진 도입에 관련된 부분에 대해서 신경을 쓸 수 없게 되었습니다.

 

기술적으로 도전하지 못한 부분이 조금은 아쉬웠습니다. 이 부분에 대해서는 앞으로 프로젝트가 끝나도 조금 더 공부를 해야하는 영역인거 같습니다.

느낀 점

이렇게 개인적으로 잘한 점과 아쉬운 점에 대해서 글을 남겨보았습니다. 프로젝트를 진행하면서 느낀점을 말씀드리려고 합니다.

1. 꾸준하게 학습한다는 것은 정말 어려운 것

특히 온전히 학습에만 집중할 수 있는 환경이 아닌 회사 업무를 같이 병행해야 한다는 것은 참 쉬운 일이 아니였습니다. 절대적인 시간이 부족했었고 내 개인적으로 성장할 수 있는 시간이 줄어드는 것에 대한 조급함이 느껴지기도 했습니다.

 

하지만 조급하게 생각하는 것이 아닌 하루하루 꾸준하게 학습한다면 어느덧 성장한 나 자신을 발견하게 될 수 있겠다는 생각을 하기도 했습니다. 빨리 가려면 혼자가는게 맞지만 멀리 가려면 함께 가라는 말이 있듯이 이번 프로젝트를 통해 친해진 동료들과 함께 학습할 것입니다.

2. 온라인으로 소통하는 것은 참 어려운 일

커뮤니케이션은 정말 중요합니다. 이번 프로젝트에서는 내 의도를 정확히 전달하는 것에 대해서 깊게 고민하게 되었고 더 나아가 이 팀에서의 최선의 커뮤니케이션 방법은 무엇일까에 대해서 생각하게 되었습니다.

 

커뮤니케이션에 대한 고민을 하던 중 온라인으로 소통하는 일이 많아지면서 자연스럽게 비언어적 표현도 중요하다는 사실을 깨닫게 되었습니다. 오로지 목소리로만 커뮤니케이션하는게 어려웠습니다. 목소리만으로는 상대방이 내 의도를 정확하게 이해했는지와 상대가 어떤 상태인지에 대해서 정확하게 알 수 없었습니다. 나의 손짓과 표정같은 것이 생각보다 중요하다는 사실을 알았습니다.

 

나중에 온라인으로 커뮤니케이션 해야하는 일이 생긴다면 최대한 나의 의도를 잘 전달하기 위한 수단에 대해서 연구해야 할 거 같습니다.

3. 리더쉽

아무래도 프로젝트의 리더 그룹에 속했던 입장에서 리더쉽이라는 것에 대해서 고민을 할 수 밖에 없었습니다. 특히 이 부분은 이번 프로젝트에서 제일 많이 느꼈습니다. 좋은 리더쉽이라는 것에 대해서 다시 한번 생각하게 되었고 CTO가 되기 위한 능력중 하나이므로 앞으로도 이 부분에 대해서 계속 생각해야 할 거 같습니다.

 

좋은 리더쉽이란 무엇인가? 에 대해서는 아래 3가지로 정리해보았습니다.

 

1. 쓴 소리는 무조건 뒤에서 따로

리드를 하다보면 쓴 소리를 해야하는 순간이 있습니다. 이때 대놓고 쓴 소리를 하는 것보다는 뒤에서 쓴 소리를 해야한다는 것을 알았습니다. 그 내용이 다른 모두가 알아야 한다는 내용이라면 미리 이야기를 해놓고 사람들 앞에서 정확한 사실만 앞에서 말하던가 앞에서 말할 내용만 말하고 뒤에서 쓴 소리를 해야한다는 것을 느꼈습니다. 

 

2. 부정적인 피드백보다는 긍정적인 피드백을 해야한다.

저를 많이 성장시켰던 피드백은 확실히 부정적인 피드백보다는 긍정적인 피드백이 저를 더 성장시켰습니다. 이 부분을 본받아서 다른 팀원들에게도 긍정적인 피드백을 해주려고 많이 노력하였습니다.

 

3. 다른 사람들을 변화시키는 방법은 계몽이 아닌 전염시키는 것

다른 사람들을 변화시키고 싶다면 단순히 앞에서 좋다고만 말하는 것이 아닌 내가 직접 행동으로 보여줄 필요가 있다고 생각하였습니다. 실제로 테스트 코드를 도입시키는 것, Jira와 Confluence의 사용을 적극적으로 유도하고 서로 공유할 수 있는 문화를 만드는 것, 리뷰에 대한 막연한 두려움을 어느정도 없애주는 것 등등 이 모든 것들을 제가 먼저 시도하려고 하였습니다.

 

위 3가지에 대해서는 앞으로도 계속 지켜가며 내가 먼저 모범이 될 수 있는 CTO가 되도록 해야겠습니다.

마무리

이렇게 이번 프로젝트에 대한 글을 마치게 되었습니다. 길다면 길고 짧다면 짧은 3개월이라는 시간속에서 많은 것을 배울 수 있었습니다.

 

기술적인 부분에서만 성장한 것이 아닌 다른 부분에서의 성장도 맛볼 수 있는 시간이였던거 같습니다.

 

또 이번 프로젝트를 진행하면서 좋은 동료들을 만날 수 있어서 좋았습니다. 앞으로 제가 정한 목표를 이루기 위해서 나 혼자만 빨리 가는 것이 아닌 다른 동료들과도 함께 성장할 수 있는 사람이 되도록 하겠습니다.

 

길고 길었던 저의 글을 끝까지 읽어주셔서 감사합니다! 그럼 안녕!

반응형