5주차 발표 - g-market/b-shop-backend GitHub Wiki
진행률 : 97.2%
- 상품 조회(년도, 카테고리)
- 주문에 대한 동시성제어
-
Synchronized
- 서버가 2대 이상일 경우, 동시성 제어 불가능
-
Pessimistic Lock(비관적 락)
- 실제로 데이터에 lock을 걸어 정합성을 맞추는 방식
- deadlock 발생 가능성, db에 직접 lock 걸기 때문에 성능저하가 유발될 수 있다.
-
Optimistic Lock(낙관적 락)
- version을 통해 정합성을 맞춘다.
- rollback 이 필요한 경우 직접 로직 작성해야함.
-
Redisson
- Pub-sub 기반으로 Lock 구현 제공
- 스핀락 방식을 사용하지 않고, timeout 기능을 통한 deadlock 방지, 분산환경에서 적합.
- 주문 시 재고 감소
- 주문 취소 시 재고 증가
- 관리자가 재고를 update
-
주문 생성 로직
@Transactional
@Transactional
public OrderCreateResponseDto createOrder(final Long memberId, final
OrderCreateRequestDto orderCreateRequestDto) {
OrderMapper.INSTANCE.ordersCreateDtoToEntity(memberId, orderCreateRequestDto);
...
itemOption.decreaseStockQuantity(count);
...
order.createOrder(orderItemList);
orderRepository.save(order);
}
=> Facade 패턴
을 이용하여 상위 계층 구성
@Service
public class OrderLockFacade {
...
public OrderCreateResponseDto purchase(final Long memberId, final OrderCreateRequestDto orderCreateRequestDto) {
Order order = orderService.createOrder(memberId, orderCreateRequestDto);
//lock
List<OrderItem> orderItemList = lockItemOptionList(orderCreateRequestDto.orderItemDtoList(), order);
//lock finish
OrderCreateResponseDto responseDto = orderService.saveOrder(order, orderItemList);
return responseDto;
}
}
=> Redisson
을 사용한 lock 구현
public List<OrderItem> lockItemOption(List<OrderItemDto> list, Order order) {
for (int i = 0; i < list.size(); i++) {
RLock lock = redissonClient.getLock(String.format("%d", list.get(i).itemOptionId()));
lockList.add(lock);
}
...
try {
boolean available = false;
for (RLock rlock : lockList) {
available = rlock.tryLock(5, 5, TimeUnit.SECONDS);
}
if (!available) {
System.out.println("redisson getLock timeout");
throw new IllegalArgumentException();
}else {
returnList = orderService.lockItemOptionList(list, order);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lockList.forEach(rLock -> rLock.unlock());
}
return returnList;
}
=> 개선해야할 점
timeout 시간
rlock.tryLock(5, 5, TimeUnit.SECONDS); // watiTime, leaseTime
- Lock을 걸 때
최소한의 데이터로
- DB IO 최소화
1. 기능 구현
-
사용자마다 현재 담아놓은 상품 목록을 조회 및 24시간이 지나면 목록이 초기화
-
마지막으로 담아놓은 상품을 기준으로 변경사항 없이 24시간이 지나면 목록이 초기화 되는 요구사항에는 영속성을 보장하는 데이터 베이스 보다는 레디스 TTL을 적용한 자료구조가 적합할 것으로 판단
-
요구사항 24시간 기준으로 목록이 초기화 되는 것을 30초로 설정하고 테스트
2. 고민 사항
- Key 조회가 안되다니 말이돼??
-
in절 컬럼 2개를 통해 한번에 조회
- 현재 장바구니에는 itemId, itemOptionId, orderCount에 정보가 담겨 있습니다
- 사용자에게 제공되는 정보는 item 테이블의 정보와, itemOption 테이블의 정보등 다양한 정보들이 있습니다.
- 이에 따라 데이터베이스에서 이러한 정보를 가져온 후, 애플리케이션에서 orderCount를 조립하여 사용자에게 제공하였습니다.
- 이렇게 함으로써 레디스에서 조회된 itemId, itemOptionId에서 있지만
혹시 모른 이빨빠진..? itemId, itemOptionId
에 대해서 얘기치 못한 오류가 발생함도 줄일 수 있습니다.
1. 기능 구현
- Dependency:
implementation 'io.sentry:sentry-spring-boot-starter-jakarta:6.14.0'
- Application.yaml:
sentry.dsn = ${SENTRY-DSN}
2. 고민 사항
-
에러에 대한 Monitoring이 안된다...?
-
Application.yaml에
sentry.dsn = ${SENTRY-DSN}
만 기입하면 연결이 되고 에러 모니터링을 보내면 확인할 수 있을거라 오만했습니다.. -
-
현재 사용하는 DNS 및 IP에서는 SSL 인증서가 없어 외부 서버인 sentry로, 오류에 대한 통신을 할 수 없었습니다.
-
어떻게 에러 찾았냐!...
왜 안보내지는지에 대해 고민을 엄청하였는데 WIFI 바꾸니까 한번에 성공....? 및 블로깅을 통해 찾게 되었습니다..
-
해결 방법은
Jun
님의 도움으로 해결할 수 있었습니다.- 첫 번째로
http://gabia.cc/
에서 인증서를 다운 받아, JVM에 설치하여 해결하였습니다.
- 첫 번째로
-
-
이제 에러 모니터링이 되겠지..?...이게 끝이 아니였다..
- 현재 글로벌 ExceptionHandling 처리는 ControllerAdvice인
exception#GlobalExceptionHandler
가 역할을 수행하고 있습니다. - Sentry는 기본적으로 처리되지 않은 예외만 Sentry로 전송됩니다.
- 이는 ExceptionControllerAdvie의 ExceptionHandling의 순번이 더 먼저이기에 Sentry로 보내지지 않는 것입니다.
- Application.yaml에
sentry.exception-resolver-order: -2147483647
를 설정하여 현재 모든 Exception, RuntimeException을 보내는 것으로 설정하였습니다.
- 현재 글로벌 ExceptionHandling 처리는 ControllerAdvice인
*3. 추가 사항
- sentry에서 제공하는 메시지 기능과 더불어
Alert
기능을 팀원들과 얘기해서 이를 어떻게 구체화 - 모든 Exception에서 모니터링을 하고 있는데 exceptionHandler에서 캐치하지 못한 에러만 잡을지(sentry 기본 제공)에 대한 결정
-
이미지를 업로드하고 상품이미지 테이블을 CRUD 할 수 있는 API 구현
CODE
-
Controller
@Login(admin = true) @PostMapping("/images") public ResponseEntity<List<ImageResponse>> uploadImage( @RequestParam("fileList") MultipartFile[] fileList) { return ResponseEntity.ok().body(imageService.uploadImage(fileList)); }
-
Service
private final MinioClient minioClient; private final int MAX_IMAGE_UPLOAD_COUNT = 10; // TO_BE_CHANGED @Value("${minio.bucket}") private String bucketName; @Value("${minio.endpoint}") private String endpoint; public List<ImageResponse> uploadImage(final MultipartFile[] fileList) { if (fileList.length > MAX_IMAGE_UPLOAD_COUNT) { throw new ConflictException(MAX_FILE_UPLOAD_REQUEST_EXCEPTION, MAX_IMAGE_UPLOAD_COUNT); } List<ImageResponse> imageResponseList = new ArrayList<>(); try { for (MultipartFile file : fileList) { final String fileName = UUID.randomUUID().toString(); final PutObjectArgs putObjectArgs = PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build(); minioClient.putObject(putObjectArgs); imageResponseList.add(ImageResponse.builder() .fileName(file.getOriginalFilename()) .url(endpoint + "/" + bucketName + "/" + fileName).build() ); } } catch (Exception e) { throw new InternalServerException(MINIO_UPLOAD_EXCEPTION); } return imageResponseList; }```
-
-
POST /images
- 상품의 변경이력을 DB 테이블에서 관리할 수 있는
envers
라이브러리를 사용 -
<TABLE_NAME>_aud
형태로 이력을 관리하는 테이블이 자동 생성되어 변경내역을 확인 가능
-
상품의 상태를
RESERVED
->PUBLIC
으로 변경하기 위한scheduler
를 구현. -
여러개의 인스턴스가 동일한 스케쥴 된 작업을 중복적으로 수행하지 않게하기 위해
shedlock
라이브러리를 사용해 작업을 동기화. -
1분마다 상품 오픈예약 정보가 들어있는
Reserve
테이블에서 현재시간 보다 이전인 예약 내역을 조회하고 상태변경CODE
@Scheduled(cron = "0 * * * * *") // 1분 마다 실행 @SchedulerLock( name = "scheduledItemStatusUpdateTask", lockAtLeastFor = "5s", lockAtMostFor = "10s") @Transactional public void updateReservationStatus() { final List<Reservation> reservationList = itemReserveRepository.findAllByItemOpenAtBefore(LocalDateTime.now()); for (Reservation reservation : reservationList) { if (reservation.getItem().getItemStatus() == ItemStatus.RESERVED) { reservation.getItem().setItemStatus(ItemStatus.PUBLIC); } itemReserveRepository.delete(reservation); } }
-
b-shop 인프라를 VM 내부에 구성
- mysql:8.0.31
- redis:6.2.7(보안설정 적용)
- minio:latest
docker-compose.yml
version: "3.8" services: b-shop-redis: container_name: b-shop-redis image: redis:6.2.7 environment: - TZ=Asia/Seoul ports: - ${REDIS_PORT}:6379 networks: - b-shop-network volumes: - ./redis/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf b-shop-database: container_name: b-shop-database image: mysql:8.0.31 environment: - MYSQL_DATABASE=${DB_NAME} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD} - TZ=Asia/Seoul volumes: - ./database/config:/etc/mysql/conf.d ports: - ${DB_PORT}:3306 networks: - b-shop-network b-shop-minio: container_name: b-shop-minio image: quay.io/minio/minio volumes: - minio_storage:/data ports: - ${MINIO_PORT}:9000 - ${MINIO_WEB_PORT}:9001 environment: - MINIO_ROOT_USER=${MINIO_USER} - MINIO_ROOT_PASSWORD=${MINIO_PASSWORD} command: server --console-address ":9001" /data create_bucket: container_name: create-bucket image: minio/mc depends_on: - b-shop-minio entrypoint: > /bin/sh -c " mc config host add minio http://b-shop-minio:${MINIO_WEB_PORT} ${MINIO_USER} ${MINIO_PASSWORD}; mc mb minio/${MINIO_BUCKET}; mc anonymous set public minio/${MINIO_BUCKET}; " volumes: minio_storage: {} networks: b-shop-network: external: true
- 배포를 위한
docker-compose
,.env
작성 - 이미지
gitlab container registry
push 단계까지 진행 -
gitlab container registry
variables: IMAGE_NAME: mentoring-gitlab.gabia.com:5050/mentee/mentee_2023.01/team/g-market/gabia_b_shop_backend package-and-push: rules: - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"' stage: package image: docker:latest services: - name: docker:dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker pull $IMAGE_NAME:latest || true - docker build --cache-from $IMAGE_NAME:latest --tag $IMAGE_NAME:$CI_COMMIT_SHA --tag $IMAGE_NAME:latest . - docker push $IMAGE_NAME:$CI_COMMIT_SHA - docker push $IMAGE_NAME:latest after_script: - docker logout
인프라 구성과 배포를 위해 compose 파일을 분할 한 이후 mysql, redis에 접근을 해야하는데 다른 compose 파일 간 컨테이너 이름으로 연결이 안됨
해결 과정l
- 1차 시도 :
host network
를 백엔드 컨테이너에 연결 ->localhost
로 연결은 되지만 포트가 바인딩 안됨 - 2차 시도 :
bridge network
를 생성하여 컨테이너를 연결 -> 실패 - 3차 시도 :
external_links
를 사용 -> 실패 - 4차 시도 : 이미지 빌드에 사용되는 jar가 최신화되지 않음... -> 프로젝트를 다시 빌드하고 이미지 생성 -> 성공...
- 현재는 이미지를 업로드 하는 기능만 구현되어 있는데
itemImage
레코드가 제거될 경우minio
버킷에 올라간 이미지도 함께 제거되지는 않음 - 상품과 연관된 테이블은 모두
soft delete
하도록 설계되어있기 때문 - 버킷에 올라간 이미지를 어떻게 관리해야할지 추가적인 논의 필요
- 상품에 연결된 테이블(카테고리, 상품옵션, 상품이미지)에서 변경이 일어날 경우에도 이력이 관리되도록 설정이 되어있음
- 어떤 테이블의 어떤 컬럼까지 이력관리를 해야하는지 의사결정을 해야하는 상황 -> (ex) 상품 재고 같은 경우 변경이 빈번히 일어나기 때문에 이력관리가 필요한지 등..
- 변경 이력을 조회하는 API 가 필요할지(?)
- 기존 코드 리팩토링
- CD 파이프라인
-
dockerfile
작성 - gitlab
container registry
등록 - nginx
-
blue green
배포전략을 적용해 무중단 배포환경을 구축
-
-
- 동시성 제어
-
Redis
를 통한thread락
기능을 적용해 재고에 대한 동시성 제어 로직을 구현하고자 함
-
- 프론트엔드 구현
- 백엔드 기능 구현이 끝나면 시작
-
상품 조회
(카테고리별, 년별) 기능 추가적으로 구현 - 상품관련 CRUD 기능
테스트코드
추가 구현