SpringBoot, MyBatis-Plus, UniApp 기반 무인 바둑/카드방 관리 시스템 아키텍처 및 구현

무인 바둑/카드방 관리 시스템 개요 및 기술 과제

최근 무인 운영 방식의 바둑/카드방은 자동화된 예약, 원격 출입 통제, 정산 시스템을 통해 운영 효율성을 높이고 사용자 편의를 제공하는 새로운 형태의 오프라인 엔터테인먼트로 자리 잡고 있습니다. 이러한 시스템을 안정적이고 효율적으로 구축하기 위해서는 몇 가지 기술적 도전 과제를 해결해야 합니다.

  • 기기 및 서비스 간 실시간 연동: 도어락, 카메라와 같은 기기와 중앙 시스템 간의 실시간 통신 및 연동이 필수적입니다. 이는 출입 관리 및 요금 정산의 정확성을 보장하는 핵심 요소입니다.
  • 고동시성 예약 및 결제 처리: 피크 시간대에는 수많은 예약 및 결제 요청이 동시에 발생할 수 있습니다. 시스템은 이러한 고동시성 트랜잭션을 안정적으로 처리할 수 있는 능력을 갖춰야 합니다.
  • 예외 처리 및 데이터 일관성: 결제 성공 후 출입이 불가능하거나 그 반대의 상황이 발생하지 않도록 결제와 출입 프로세스 간의 데이터 일관성을 철저히 유지해야 합니다.

본 시스템은 다음과 같은 기술 스택을 기반으로 설계 및 구현되었습니다:

  • 백엔드 서비스: SpringBoot + MyBatis-Plus + MySQL
  • 사용자 클라이언트: UniApp (Vue 문법 기반)
  • 관리자/유지보수 클라이언트: UniApp (Vue 문법 기반)
  • 웹 관리자 페이지: Vue + ElementUI
  • 캐시: Redis (기기 상태 및 예약 정보 조회 성능 향상)
  • 메시지 큐: RabbitMQ (예약, 결제, 출입 요청 등의 비동기 처리로 시스템 안정성 확보)

시스템 아키텍처 및 기술 스택 선정

시스템 아키텍처 설계

무인 바둑/카드방 시스템은 분산 계층형 아키텍처를 채택하여 다음과 같은 핵심 모듈로 구성됩니다.

  1. 사용자 클라이언트 (UniApp)
    • 기능: 사용자가 앱을 통해 공간 예약, 출입 제어, 결제, 정산을 수행합니다.
    • 기술 스택: UniApp (Vue 문법)을 사용하여 iOS 및 Android 크로스 플랫폼 호환성을 제공합니다.
  2. 관리자/유지보수 클라이언트 (UniApp)
    • 기능: 관리자나 유지보수 담당자가 기기 점검, 장애 처리, 공간 상태 모니터링을 수행합니다.
    • 기술 스택: UniApp 개발로 다양한 플랫폼을 지원합니다.
  3. 백엔드 서비스 (SpringBoot + MyBatis-Plus + MySQL)
    • 기능: 사용자 예약, 공간 상태 관리, 출입 기록, 결제 처리, 기기 연동 등의 비즈니스 로직을 담당합니다.
    • 기술 스택:
      • SpringBoot: RESTful API를 제공합니다.
      • MyBatis-Plus: 데이터베이스 작업을 간소화합니다.
      • Redis: 공간 상태 및 예약 정보를 캐시하여 데이터베이스 부하를 줄입니다.
      • RabbitMQ: 결제 및 출입 명령을 비동기적으로 처리하여 데이터 일관성 및 시스템 안정성을 보장합니다.
  4. 웹 관리자 페이지 (Vue + ElementUI)
    • 기능: 공간, 예약, 사용자, 결제 정보 관리 및 데이터 통계 및 시각화 기능을 제공합니다.
    • 기술 스택: Vue 프레임워크와 ElementUI를 사용하여 관리자 인터페이스를 구축합니다.

아키텍처 흐름 설명:

  1. 클라이언트 상호작용: 사용자 및 관리자 클라이언트는 UniApp을 통해 백엔드 서비스 API를 호출합니다.
  2. 백엔드 서비스: SpringBoot는 예약, 결제, 출입 요청을 처리하는 API를 제공하며, Redis는 공간 상태를 캐시하여 데이터베이스 조회 부담을 줄입니다.
  3. 비동기 처리: RabbitMQ는 비동기 예약 처리, 결제 및 출입 명령의 신뢰성 있는 전달을 보장합니다.
  4. 관리 시스템: Vue + ElementUI 기반의 웹 관리자 페이지는 예약 및 기기 관리를 수행합니다.

기술 스택 선정

모듈 기술 스택 주요 기능
사용자 클라이언트 UniApp (Vue 문법) 크로스 플랫폼 앱을 통한 공간 예약 및 출입 제어
관리자/유지보수 클라이언트 UniApp (Vue 문법) 기기 유지보수 및 공간 관리
웹 관리자 페이지 Vue + ElementUI 예약, 기기 관리 및 데이터 통계
백엔드 서비스 SpringBoot + MyBatis-Plus 예약, 결제, 출입 비즈니스 로직 처리
데이터 저장소 MySQL 회원, 예약, 공간 데이터 저장
캐시 Redis 공간 상태 및 예약 정보 캐싱
메시지 큐 RabbitMQ 비동기 예약 및 출입 처리
로그 및 모니터링 ELK (Elasticsearch + Logstash + Kibana) 시스템 로그 모니터링 및 검색

핵심 기능 구현: 사용자 예약 및 자동 출입

데이터베이스 설계

MySQL에 다음과 같은 핵심 테이블을 생성합니다:

-- 회원 정보 테이블
CREATE TABLE `member_info` (
  `member_id` INT PRIMARY KEY AUTO_INCREMENT,
  `member_name` VARCHAR(100) NOT NULL,
  `contact_number` VARCHAR(20) UNIQUE NOT NULL,
  `registration_date` DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 공간 정보 테이블
CREATE TABLE `play_space` (
  `space_id` INT PRIMARY KEY AUTO_INCREMENT,
  `space_name` VARCHAR(100) NOT NULL,
  `current_status` ENUM('AVAILABLE', 'OCCUPIED') DEFAULT 'AVAILABLE',
  `hourly_rate` DECIMAL(12,2) NOT NULL,
  `last_occupied_time` DATETIME
);

-- 예약 정보 테이블
CREATE TABLE `space_reservation` (
  `reservation_id` INT PRIMARY KEY AUTO_INCREMENT,
  `fk_member_id` INT NOT NULL,
  `fk_space_id` INT NOT NULL,
  `reservation_status` ENUM('REQUESTED', 'ACTIVE', 'FINISHED', 'CANCELLED') DEFAULT 'REQUESTED',
  `total_amount` DECIMAL(12,2) NOT NULL,
  `reserved_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (`fk_member_id`) REFERENCES `member_info`(`member_id`),
  FOREIGN KEY (`fk_space_id`) REFERENCES `play_space`(`space_id`)
);

백엔드 서비스 구현

엔티티 클래스 및 MyBatis-Plus 리포지토리

PlaySpace.java

import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("play_space")
public class PlaySpace {
    @TableId // 자동 증가 ID로 가정
    private Integer spaceId;
    private String spaceName;
    private String currentStatus; // DB의 ENUM과 매핑
    private BigDecimal hourlyRate;
    private LocalDateTime lastOccupiedTime;
}

PlaySpaceRepository.java

import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

@Mapper
public interface PlaySpaceRepository extends BaseMapper<PlaySpace> {
    // 필요한 경우 추가적인 쿼리 메소드를 정의할 수 있습니다.
}

SpaceReservation.java

import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("space_reservation")
public class SpaceReservation {
    @TableId
    private Integer reservationId;
    private Integer fkMemberId;
    private Integer fkSpaceId;
    private String reservationStatus;
    private BigDecimal totalAmount;
    private LocalDateTime reservedAt;
    private LocalDateTime updatedAt;
}

SpaceReservationRepository.java

import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

@Mapper
public interface SpaceReservationRepository extends BaseMapper<SpaceReservation> {
}

서비스 계층

SpaceBookingService.java

import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Optional;

@Service
public class SpaceBookingService {

    private final SpaceReservationRepository reservationRepository;
    private final PlaySpaceRepository playSpaceRepository;

    @Autowired
    public SpaceBookingService(SpaceReservationRepository reservationRepository, PlaySpaceRepository playSpaceRepository) {
        this.reservationRepository = reservationRepository;
        this.playSpaceRepository = playSpaceRepository;
    }

    @Transactional
    public String bookSpace(Integer memberId, Integer spaceId, BigDecimal initialAmount) {
        Optional<PlaySpace> optionalSpace = playSpaceRepository.selectById(spaceId);
        if (optionalSpace.isEmpty()) {
            throw new IllegalArgumentException("요청된 공간을 찾을 수 없습니다.");
        }

        PlaySpace space = optionalSpace.get();
        if (!"AVAILABLE".equals(space.getCurrentStatus())) {
            throw new IllegalStateException("해당 공간은 현재 예약할 수 없습니다.");
        }

        SpaceReservation newReservation = new SpaceReservation();
        newReservation.setFkMemberId(memberId);
        newReservation.setFkSpaceId(spaceId);
        newReservation.setReservationStatus("REQUESTED"); // 초기 상태
        newReservation.setTotalAmount(initialAmount);
        newReservation.setReservedAt(LocalDateTime.now());

        reservationRepository.insert(newReservation);

        space.setCurrentStatus("OCCUPIED"); // 예약 요청 후 즉시 점유 상태로 변경
        space.setLastOccupiedTime(LocalDateTime.now());
        playSpaceRepository.updateById(space);

        return "공간 예약 요청이 성공적으로 접수되었습니다.";
    }
}

컨트롤러 계층

SpaceReservationController.java

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;

@RestController
@RequestMapping("/api/space/reservations") // 변경된 API 경로
public class SpaceReservationController {

    private final SpaceBookingService spaceBookingService;

    @Autowired
    public SpaceReservationController(SpaceBookingService spaceBookingService) {
        this.spaceBookingService = spaceBookingService;
    }

    @PostMapping("/book") // 변경된 엔드포인트
    public ResponseEntity<String> createSpaceReservation(
            @RequestParam("memberId") Integer memberId,
            @RequestParam("spaceId") Integer spaceId,
            @RequestParam("amount") BigDecimal amount) { // 예약 금액 추가
        try {
            String result = spaceBookingService.bookSpace(memberId, spaceId, amount);
            return new ResponseEntity<>(result, HttpStatus.CREATED);
        } catch (IllegalArgumentException | IllegalStateException e) {
            return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
        } catch (Exception e) {
            return new ResponseEntity<>("예약 처리 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

사용자 클라이언트 (UniApp)

// 페이지 로드 시 예약 요청 예시
onLoad() {
  const memberIdentifier = 5; // 예시 회원 ID
  const targetSpaceId = 205;   // 예시 공간 ID
  const reservationAmount = 50.00; // 예시 금액 (실제 결제 로직은 더 복잡함)

  uni.request({
    url: 'http://localhost:8080/api/space/reservations/book', // 변경된 API 경로
    method: 'POST',
    data: { 
      memberId: memberIdentifier, 
      spaceId: targetSpaceId,
      amount: reservationAmount
    },
    header: {
      'Content-Type': 'application/x-www-form-urlencoded' 
    },
    success: (response) => {
      if (response.statusCode === 201) { // 201 Created
        console.log('공간 예약 요청 성공:', response.data);
        // 성공 시 추가 로직 (예: 결제 페이지로 이동, 예약 목록 업데이트 등)
      } else {
        console.error('공간 예약 실패:', response.data);
        // 실패 시 에러 메시지 표시
      }
    },
    fail: (error) => {
      console.error('네트워크 또는 서버 오류:', error);
      // 네트워크 오류 처리
    }
  });
}

성능 최적화 전략

무인 시스템의 안정적인 운영을 위해 다음과 같은 성능 최적화 전략을 적용할 수 있습니다.

최적화 방안 장점 고려사항
Redis를 활용한 공간 상태 캐싱 데이터베이스 조회 부하를 크게 감소시킵니다. 캐시와 데이터베이스 간의 데이터 일관성 유지 전략이 필요합니다.
RabbitMQ를 통한 비동기 출입 처리 백엔드 서비스의 응답 속도를 향상시키고, 동시성 처리 능력을 높입니다. 메시지 큐 시스템의 장애 발생 시 데이터 손실 가능성을 대비해야 합니다.

태그: SpringBoot MyBatis-Plus UniApp Redis RabbitMQ

7월 23일 16:39에 게시됨