알고리즘 공부/C++

[AIoT] 무인 사물함 프로젝트 MVC 모델 작성 Serivce, ServiceImplement

마달랭 2025. 2. 13. 19:20
반응형

개요

서비스는 인터페이스로 구현되며 비즈니스 로직의 추상화된 계약을 정의한다.

수행해야 할 비즈니스 메서드의 시그니처(이름, 파라미터, 반환 타입)를 선언한다.

구체적인 구현 로직은 포함하지 않고, 메서드의 동작 방식만을 정의합니다.

 

서비스임플은 서비스 인터페이스의 실제 구현을 담당한다.

구체적인 비즈니스 로직을 작성하고, Repository와 상호작용하여 데이터 처리 및 비즈니스 규칙을 적용한다.

 

 

Service와 ServiceImpl을 분리하는 이유

1. 느슨한 결합(Loose Coupling)

  • 인터페이스를 통해 구현을 분리함으로써 시스템의 유연성을 높인다.
  • 구현 클래스를 쉽게 교체하거나 변경할 수 있다.

2. 다형성(Polymorphism) 지원

  • 여러 구현체를 만들어 다양한 비즈니스 로직을 적용할 수 있다.
  • 예: 테스트용 목(Mock) 구현, 다른 비즈니스 로직 구현 등

3. 의존성 주입(Dependency Injection) 용이

  • 스프링과 같은 프레임워크에서 인터페이스를 통해 쉽게 의존성을 주입할 수 있다.

4. 테스트 용이성

  • 모킹(Mocking)이 쉬워져 단위 테스트 작성이 간편해진다.
  • 구현 로직과 인터페이스를 분리하여 각각 독립적으로 테스트 가능하다.

5. 관심사의 분리(Separation of Concerns)

  • 비즈니스 로직의 계약(인터페이스)과 실제 구현을 명확히 분리한다.
  • 코드의 가독성과 유지보수성을 향상시킨다.

 

하기에 작성된 서비스와 서비스임플을 설명한다.

 

 

AdminService.java

package com.a207.smartlocker.service;

import com.a207.smartlocker.model.dto.*;
import com.a207.smartlocker.model.entity.LockerUsageLog;

import java.time.LocalDateTime;
import java.util.List;

public interface AdminService {
    AdminLoginResponse login(AdminLoginRequest request);
    List<RobotResponse> getAllRobots();
    List<UserUsageResponse> getUserUsageStatistics();
    List<LockerUsageLogResponse> getUsageLogByDateRange(LocalDateTime startDate, LocalDateTime endDate);
    SessionResponse createSessionId();
}

 

 

AdminServiceImpl.java

package com.a207.smartlocker.serviceImpl;

import com.a207.smartlocker.model.dto.*;
import com.a207.smartlocker.model.entity.Certification;
import com.a207.smartlocker.model.entity.LockerUsageLog;
import com.a207.smartlocker.model.entity.Robot;
import com.a207.smartlocker.repository.CertificationRepository;
import com.a207.smartlocker.repository.LockerUsageLogRepository;
import com.a207.smartlocker.repository.RobotRepository;
import com.a207.smartlocker.repository.UserRepository;
import com.a207.smartlocker.service.AdminService;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class AdminServiceImpl implements AdminService {
    private final CertificationRepository certificationRepository;
    private final RobotRepository robotRepository;
    private final LockerUsageLogRepository lockerUsageLogRepository;

    @Override
    public AdminLoginResponse login(AdminLoginRequest request) {
        Optional<Certification> certificationOpt = certificationRepository
                .findByAdminIdAndAdminPassword(request.getAdminId(), request.getAdminPassword());

        if (certificationOpt.isPresent()) {
            return new AdminLoginResponse(true, "로그인 성공");
        }
        return new AdminLoginResponse(false, "아이디 또는 비밀번호가 일치하지 않습니다");
    }

    @Override
    public SessionResponse createSessionId() {
        String sessionId = UUID.randomUUID().toString();
        return new SessionResponse(sessionId);
    }

    @Override
    public List<RobotResponse> getAllRobots() {
        return robotRepository.findAllByOrderByRobotIdAsc().stream()
                .map(RobotResponse::from)
                .collect(Collectors.toList());
    }

    @Override
    public List<UserUsageResponse> getUserUsageStatistics() {
        return lockerUsageLogRepository.findUserUsageStatistics();
    }

    @Override
    public List<LockerUsageLogResponse> getUsageLogByDateRange(LocalDateTime startDate, LocalDateTime endDate) {
        return lockerUsageLogRepository.findByStoreTimeBetween(startDate, endDate).stream()
                .map(LockerUsageLogResponse::from)
                .collect(Collectors.toList());
    }
}

 

관리자가 호출할 API의 비즈니스 로직을 실행할 서비스이다.

AdminService에서 비즈니스 로직을 추상적으로 구현한다.

AdminServiceImpl에서 AdminService를 임플리먼트 하고, 추상 메서드를 오버라이드 하여 구체적인 구현 내용을 작성한다.

기능 구현에 있어 DB와 통신이 필요한 경우 작성해 놓은 Repository를 변수로 사용하여 통신해 준다.

 

 

LockerService.java

package com.a207.smartlocker.service;


import com.a207.smartlocker.model.dto.*;

import com.a207.smartlocker.model.entity.Locker;

import java.util.List;


public interface LockerService {
    RetrieveResponse retrieveItem(RetrieveRequest request) throws Exception;
    StorageResponse storeItem(StorageRequest request);
    List<Locker> getLockersByLocation(String locationName);
    List<TaskQueueResponse> getRetrieveTasks();
}

 

 

LockerServiceImpl.java

package com.a207.smartlocker.serviceImpl;


import com.a207.smartlocker.exception.custom.LockerAlreadyInUseException;
import com.a207.smartlocker.exception.custom.TaskAlreadyInQueueException;
import com.a207.smartlocker.model.dto.*;
import com.a207.smartlocker.model.entity.*;
import com.a207.smartlocker.repository.*;
import com.a207.smartlocker.exception.custom.NotFoundException;
import com.a207.smartlocker.service.LockerService;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

@Service
@Transactional
@RequiredArgsConstructor
public class LockerServiceImpl implements LockerService {
    private final UserRepository userRepository;
    private final AccessTokenRepository accessTokenRepository;
    private final LockerRepository lockerRepository;
    private final LockerStatusRepository lockerStatusRepository;
    private final RobotRepository robotRepository;
    private final LockerUsageLogRepository lockerUsageLogRepository;
    private final RobotControlServiceImpl robotControlService;
    private final LockerQueueRepository lockerQueueRepository;

    @Override
    public StorageResponse storeItem(StorageRequest request) {
        // 1. 라커 사용 가능 여부 체크
        Locker locker = lockerRepository.findByLockerId(request.getLockerId())
                .orElseThrow(() -> new NotFoundException("존재하지 않는 사물함 번호입니다."));

        if (locker.getLockerStatus().getLockerStatusId() != 1L) {
            throw new LockerAlreadyInUseException("현재 사용이 불가능한 사물함입니다.");
        }

        // 2. 사용자 확인/생성
        User user = userRepository.findByPhoneNumber(request.getPhoneNumber())
                .orElseGet(() -> userRepository.save(User.builder()
                        .phoneNumber(request.getPhoneNumber())
                        .build()));

        // 3. 6자리 토큰 생성
        int tokenValue = generateRandomToken();
        AccessToken accessToken = accessTokenRepository.save(AccessToken.builder()
                .tokenValue((long) tokenValue)
                .build());

        // 4. 락커 상태 업데이트
        LockerStatus status = lockerStatusRepository.findById(2L)
                .orElseThrow(() -> new NotFoundException("LockerStatus not found"));

        locker.updateLockerStatus(status, accessToken);
        lockerRepository.save(locker);

        // 5. 락커 큐에 추가
        lockerQueueRepository.save(LockerQueue.builder()
                .lockerId(locker)
                .requestType("Store")
                .build());

        // 6. 사용 로그 생성
        LockerUsageLog usageLog = LockerUsageLog.builder()
                .locker(locker)
                .user(user)
                .storeTime(LocalDateTime.now())
                .build();
        lockerUsageLogRepository.save(usageLog);

        // 7. 결과 리턴
        return StorageResponse.builder()
                .lockerId(locker.getLockerId())
                .tokenValue(accessToken.getTokenValue())
                .message(request.getLockerId() + "번 보관함의 보관 요청 완료")
                .build();
    }

    @Override
    public RetrieveResponse retrieveItem(RetrieveRequest request) throws Exception {
        // 1. 락커 조회
        Locker locker = lockerRepository.findById(request.getLockerId())
                .orElseThrow(() -> new Exception("해당 락커를 찾을 수 없음: " + request.getLockerId()));

        if (locker.getLockerStatus().getLockerStatusId() != 2L) {
            throw new LockerAlreadyInUseException("사용중이 아닌 사물함입니다.");
        }

        // 2. 토큰 확인
        Long tokenId = locker.getTokenId();
        if (tokenId == null) {
            throw new Exception("사용중인 라커가 아님: " + request.getLockerId());
        }

        AccessToken accessToken = accessTokenRepository.findById(tokenId)
                .orElseThrow(() -> new Exception("토큰을 찾을 수 없음: " + tokenId));

        if (accessToken.getTokenValue() != request.getTokenValue()) {
            throw new Exception("토큰 불일치: " + request.getLockerId());
        }

        // 3. 락커 큐에 추가
        if (lockerQueueRepository.findByLockerIdAndRequestType(locker, "Retrieve").isPresent()) {
            throw new TaskAlreadyInQueueException("이미 수령 요청이 완료된 작업입니다.");
        }

        lockerQueueRepository.save(LockerQueue.builder()
                .lockerId(locker)
                .requestType("Retrieve")
                .build());

        // 4. 결과 리턴
        return RetrieveResponse.builder()
                .lockerId(request.getLockerId())
                .message(request.getLockerId() + "번 보관함의 수령 요청 완료")
                .build();
    }

    @Override
    public List<Locker> getLockersByLocation(String locationName) {
        return lockerRepository.findLockersByLocationName(locationName);
    }

    @Override
    public List<TaskQueueResponse> getRetrieveTasks() {
        return lockerQueueRepository.findFirst20Tasks().stream()
                .map(TaskQueueResponse::from)
                .collect(Collectors.toList());
    }

    private int generateRandomToken() {
        return 100000 + new Random().nextInt(900000);
    }
}


사물함 이용에 대한 API의 비즈니스 로직을 구현한 서비스이다.

물품의 보관, 수령, 작업 정보 조회, 토큰 번호를 위한 난수 생성등의 로직이 작성되어 있다.

 

 

RobotTaskService.java

package com.a207.smartlocker.service;

import com.a207.smartlocker.model.dto.RobotTaskResponse;

public interface RobotTaskService {
    RobotTaskResponse processNextTask() throws Exception;
}



RobotTaskServiceImpl.java

package com.a207.smartlocker.serviceImpl;

import com.a207.smartlocker.exception.custom.NoAvailableRobotException;
import com.a207.smartlocker.exception.custom.NotFoundException;
import com.a207.smartlocker.model.dto.RobotTaskResponse;
import com.a207.smartlocker.model.entity.*;
import com.a207.smartlocker.repository.*;
import com.a207.smartlocker.service.RobotTaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
@Transactional
@RequiredArgsConstructor
public class RobotTaskServiceImpl implements RobotTaskService {
    private final LockerQueueRepository lockerQueueRepository;
    private final RobotRepository robotRepository;
    private final LockerRepository lockerRepository;
    private final LockerStatusRepository lockerStatusRepository;
    private final LockerUsageLogRepository lockerUsageLogRepository;
    private final RobotControlServiceImpl robotControlService;
    private final UserRepository userRepository;

    @Override
    public RobotTaskResponse processNextTask() throws Exception {
        // 1. 가장 오래된 대기 중인 큐 작업 조회
        LockerQueue lockerQueue = lockerQueueRepository.findFirstByOrderByQueueIdAsc()
                .orElseThrow(() -> new NotFoundException("대기 중인 작업이 없습니다."));

        // 2. 사용 가능한 로봇 선택 및 상태 변경
        Robot robot = robotRepository.findAndUpdateRobotStatus(1L, 2L)
                .orElseThrow(() -> new NoAvailableRobotException("사용 가능한 로봇이 없습니다."));

        // 3. 락커 조회
        Locker locker = lockerQueue.getLockerId();

        // 4. 로봇 작업 수행
        boolean robotResult = robotControlService.controlRobot(
                robot.getRobotId(),
                locker.getLockerId(),
                lockerQueue.getRequestType().toLowerCase()
        );

        if (!robotResult) {
            throw new RuntimeException("로봇 작동 중 오류가 발생하였습니다.");
        }

        // 5. 락커 상태 업데이트 (수령 시에만)
        updateLockerStatus(locker, lockerQueue.getRequestType());

        // 6. 사용 로그 업데이트
        updateLockerUsageLog(locker, robot, lockerQueue.getRequestType());

        // 7. 로봇 상태를 대기 중으로 변경
        robotRepository.updateRobotStatus(robot.getRobotId(), 1L);

        // 8. 큐 작업 제거
        lockerQueueRepository.delete(lockerQueue);

        // 9. 응답 생성
        return RobotTaskResponse.builder()
                .success(true)
                .message("로봇 작업 완료")
                .lockerId(lockerQueue.getLockerId().getLockerId())
                .requestType(lockerQueue.getRequestType())
                .build();
    }

    private void updateLockerStatus(Locker locker, String requestType) {
        LockerStatus status;
        if ("Retrieve".equalsIgnoreCase(requestType)) {
            status = lockerStatusRepository.findById(1L)
                    .orElseThrow(() -> new NotFoundException("락커 상태가 확인되지 않습니다."));
            locker.updateLockerStatus(status, null);
        }
        lockerRepository.save(locker);
    }

    private void updateLockerUsageLog(Locker locker, Robot robot, String requestType) {
        if ("Store".equalsIgnoreCase(requestType)) { // 보관 작업 시
            // LockerId가 일치하면서 StoreRobotId가 NULL인 레코드 찾기
            LockerUsageLog usageLog = lockerUsageLogRepository
                    .findFirstByLocker_LockerIdAndStoreRobotIdIsNull(locker.getLockerId())
                    .orElseThrow(() -> new NotFoundException("사용 중인 로그를 찾을 수 없음"));

            usageLog.setStoreRobotId(robot);
            lockerUsageLogRepository.save(usageLog);
        } else if ("Retrieve".equalsIgnoreCase(requestType)) { // 수령 작업 시
            // LockerId가 일치하면서 RetrieveRobotId가 NULL인 레코드 찾기
            LockerUsageLog usageLog = lockerUsageLogRepository
                    .findFirstByLocker_LockerIdAndRetrieveRobotIdIsNull(locker.getLockerId())
                    .orElseThrow(() -> new NotFoundException("사용 중인 로그를 찾을 수 없음"));

            usageLog.setRetrieveRobotId(robot);
            usageLog.setRetrieveTime(LocalDateTime.now());
            lockerUsageLogRepository.save(usageLog);
        }
    }
}

 

대기중인 작업을 로봇에게 할당하고, 작업을 처리하도록 요청하는 API의 비즈니스 로직을 구현한 서비스

가장 오래 대기중인 작업을 조회하고, 작업 요청 및 성공 시 DB를 업데이트하는 로직이 구현되어 있다.



RobotControlService.java

package com.a207.smartlocker.service;

public interface RobotControlService {
    boolean controlRobot(Long robotId, Long lockerId, String action);
}

 

 

RobotControlServiceImpl.java

package com.a207.smartlocker.serviceImpl;

import com.a207.smartlocker.exception.custom.RobotControlException;
import com.a207.smartlocker.model.dto.RobotControlRequest;
import com.a207.smartlocker.service.RobotControlService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.client.HttpStatusCodeException;

@Service
public class RobotControlServiceImpl implements RobotControlService {
    private final RestTemplate restTemplate = new RestTemplate();

    @Value("${robot.server.url}")
    private String ROBOT_SERVER_URL;

    public boolean controlRobot(Long robotId, Long lockerId, String action) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();

            RobotControlRequest request = RobotControlRequest.builder()
                    .robot_Id(robotId)
                    .locker_Id(lockerId)
                    .action(action)
                    .build();

            // 객체를 JSON 문자열로 직렬화
            String jsonRequest = objectMapper.writeValueAsString(request);
            System.out.println("JSON Request: " + jsonRequest);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            HttpEntity<String> requestEntity = new HttpEntity<>(jsonRequest, headers);

            ResponseEntity<String> response = restTemplate.postForEntity(
                    ROBOT_SERVER_URL,
                    requestEntity,
                    String.class
            );

            return "done".equals(response.getBody());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RobotControlException("로봇 작동 중 예외가 발생하였습니다 : " + e.getMessage());
        }
    }
}


젯슨 나노 보드로 작업 요청을 하기 위한 서비스

젯슨 나노 보드의 내부 서버로 사용할 로봇의Id, 사물함 번호, 수행 작업을 Json형태로 파싱하여 전달해 준다.

작업이 정상적으로 수행 되었을 경우 다음 로직을 진행하고, 아니라면 예외를 던진다.

 

728x90
반응형