Spring Boot, Vue, uni-app 기반 시 학습 시스템 구현 상세

백엔드 시스템은 Java 언어 기반의 Spring Boot 프레임워크를 활용하여 개발됩니다. Spring Boot는 Spring 프레임워크를 기반으로 하며, '설정보다 관례(Convention over Configuration)' 원칙을 통해 개발자가 애플리케이션 구축에 더 집중할 수 있도록 돕습니다. 자동 구성 기능은 복잡한 XML 설정이나 수많은 어노테이션을 수동으로 관리할 필요 없이, 프로젝트 의존성과 관례에 따라 자동으로 환경을 설정하여 개발 과정을 대폭 간소화합니다. Maven 또는 Gradle과 같은 빌드 도구를 사용하여 의존성 관리가 용이하며, Spring Initializr를 통해 빠르고 효율적으로 프로젝트 초기 구조를 생성할 수 있습니다.

프론트엔드는 Vue.js 프레임워크를 사용하여 개발됩니다. Vue.js는 직관적인 API를 제공하여 대화형 사용자 인터페이스를 쉽고 효율적으로 구축할 수 있도록 설계되었습니다. 특히 v-model과 같은 지시자를 통한 강력한 양방향 데이터 바인딩 메커니즘은 데이터 변경 시 뷰가 자동으로 업데이트되고, 뷰 변경 시 데이터가 업데이트되도록 하여 DOM 조작을 수동으로 처리할 필요성을 줄여줍니다. 또한, Vue.js는 컴포넌트의 생성, 마운트, 업데이트, 파괴 등 다양한 생명 주기 단계에서 사용자 정의 로직을 실행할 수 있는 훅(hook) 함수들을 제공하여 개발에 유연성을 더합니다.

시스템 개발 타당성 분석

시스템 개발에 앞서 면밀한 타당성 분석은 프로젝트의 성공적인 완료와 지속 가능성을 결정하는 핵심 단계입니다. 본 시 학습 시스템의 개발 의미와 잠재적 가치를 평가하기 위해 다음과 같은 측면에서 타당성을 분석했습니다.

  • 기술적 타당성: 시스템은 Java 기반의 Spring Boot, Vue.js, uni-app과 같은 현대적이고 성숙한 기술 스택을 활용하여 개발됩니다. 이들 기술은 안정성과 확장성이 검증되었으므로, 필요한 기능을 구현하고 시스템을 안정적으로 운영하는 데 기술적인 제약은 없습니다.
  • 경제적 타당성: 시스템 개발 및 운영에 필요한 자원과 예상되는 이점을 비교 분석했습니다. 본 시스템은 사용자에게 효율적인 시 학습 경험을 제공함으로써 학습 효율을 높이고, 장기적으로는 기존 학습 방식의 비효율성을 개선하여 비용 절감 효과를 가져올 수 있습니다. 개발 비용 대비 예상 이익이 충분히 클 것으로 판단됩니다.
  • 운영적 타당성: 최종 사용자가 시스템을 쉽게 이해하고 효과적으로 사용할 수 있도록 사용자 친화적인 인터페이스와 직관적인 기능을 제공하는 데 중점을 둡니다. 시스템이 출시되면 사용자들이 복잡한 교육 없이도 시를 검색하고 학습하며, 관리자는 콘텐츠를 효율적으로 관리할 수 있어 운영상의 어려움은 최소화될 것입니다.

이러한 분석을 통해 본 시 학습 시스템의 개발은 충분한 타당성을 가지며, 기대 이상의 가치를 창출할 수 있을 것으로 결론지었습니다.

시스템 테스트

시스템 테스트는 개발된 소프트웨어의 품질을 보장하고 사용자 요구사항을 충족하는지 검증하는 필수적인 과정입니다. 본 시스템의 테스트 주요 목표는 다양한 관점에서 시스템의 잠재적 문제점을 식별하고 수정함으로써, 최종 사용자에게 안정적이고 신뢰할 수 있는 서비스를 제공하는 것입니다.

시스템 테스트의 목적

시스템 테스트는 사용 중 발생할 수 있는 오류를 방지하고 사용자 경험을 향상시키기 위한 중요한 단계입니다. 시스템의 기능적 완전성, 논리적 흐름, 그리고 전반적인 품질을 평가하기 위해 여러 시나리오를 시뮬레이션하여 잠재적인 결함을 찾아내고 해결합니다. 이 과정을 통해 시스템이 사용자의 기대와 요구사항을 얼마나 잘 충족하는지 확인하고, 명세서에 부합하지 않거나 충돌하는 부분을 파악하여 개선합니다. 성공적인 테스트는 시스템의 신뢰성과 사용성을 크게 향상시킵니다.

시스템 기능 테스트

시스템의 각 기능 모듈은 블랙박스 테스트 기법을 사용하여 검증됩니다. 이는 사용자 인터페이스를 통한 클릭, 유효한 값과 경계 값 입력, 필수 및 선택 입력 필드 검증 등을 포함합니다. 테스트 케이스를 작성하고 그에 따라 테스트를 수행하여 기능의 정확성과 안정성을 확인합니다. 다음은 주요 기능에 대한 테스트 시나리오 예시입니다.

로그인 기능 테스트

사용자 로그인 기능은 계정 정보(사용자 이름, 비밀번호) 및 보안 코드의 정확성을 검증합니다. 입력된 정보가 데이터베이스의 내용과 일치하지 않거나, 필수 항목이 누락된 경우 시스템은 적절한 오류 메시지를 표시합니다. 또한, 잘못된 권한으로 로그인을 시도할 경우도 검증합니다.

입력 데이터 예상 결과 실제 결과 분석
사용자명: admin, 비밀번호: 123456, 보안 코드: 올바름 시스템 로그인 성공 로그인 성공 예상과 일치
사용자명: admin, 비밀번호: 111111, 보안 코드: 올바름 비밀번호 오류 메시지 "비밀번호가 올바르지 않습니다. 다시 입력해주세요." 메시지 표시 예상과 일치
사용자명: admin, 비밀번호: 123456, 보안 코드: 틀림 보안 코드 오류 메시지 "보안 코드 정보가 올바르지 않습니다." 메시지 표시 예상과 일치
사용자명: (공백), 비밀번호: 123456, 보안 코드: 올바름 사용자명 필수 입력 메시지 "사용자명을 입력해주세요." 메시지 표시 예상과 일치
사용자명: admin, 비밀번호: (공백), 보안 코드: 올바름 비밀번호 오류 메시지 "비밀번호가 올바르지 않습니다. 다시 입력해주세요." 메시지 표시 예상과 일치
사용자 관리 기능 테스트

사용자 관리 기능은 사용자 추가, 편집, 삭제, 조회 기능을 중심으로 테스트합니다. 필수 필드 누락 시 유효성 검증, 기존 사용자명 중복 검사, 삭제 확인 절차, 정보 변경 후 즉시 반영 여부 등을 확인합니다.

입력 데이터 예상 결과 실제 결과 분석
필수 사용자 정보 입력 사용자 추가 성공, 목록에 표시 새로운 사용자가 목록에 표시됨 예상과 일치
사용자 정보 수정 편집 성공, 수정된 정보 반영 사용자 정보가 성공적으로 수정됨 예상과 일치
사용자 선택 후 삭제 삭제 확인 요청 후 사용자 삭제 삭제 확인 후 해당 사용자가 목록에서 사라짐 예상과 일치
사용자명 미입력 상태로 추가 시도 "사용자명은 비워둘 수 없습니다." 메시지 "사용자명은 비워둘 수 없습니다." 메시지 표시 예상과 일치
이미 존재하는 사용자명으로 추가 시도 "사용자명이 중복됩니다." 메시지 "사용자명이 중복됩니다." 메시지 표시 예상과 일치

데이터베이스 테이블 설계

시 학습 시스템의 핵심 데이터베이스 테이블 중 하나는 시 정보를 저장하는 테이블입니다. 다음은 예시 테이블 구조입니다.

열 이름 데이터 타입 길이 제약조건 설명
poem_id BIGINT 20 PRIMARY KEY, AUTO_INCREMENT 시의 고유 식별자
title VARCHAR 255 NOT NULL 시 제목
content LONGTEXT NOT NULL 시 내용
poet_name VARCHAR 100 NULL 시인 이름
theme VARCHAR 100 NULL 시의 주제
era VARCHAR 50 NULL 시가 쓰여진 시대
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 시 데이터 생성 시간
views INT 11 DEFAULT 0 조회수

코드 예시


import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.ResourceUtils;

import com.baidu.aip.face.AipFace; // Baidu AI SDK에서 제공하는 클래스
import com.baidu.aip.face.MatchRequest;
import com.alibaba.fastjson.JSONObject; // JSON 처리 라이브러리 (예: Fastjson)

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

// --- 파일 및 Base64 인코딩/디코딩 유틸리티 클래스 ---
class FileConverter {
    public static byte[] readFileToByteArray(File file) throws IOException {
        return Files.readAllBytes(file.toPath());
    }
}

class Base64Encoder {
    public static String encodeBytes(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }
}
// ---------------------------------------------------------------------

@Service // Baidu AI 서비스 로직을 캡슐화하는 서비스 컴포넌트
public class BaiduAiService {

    @Value("${baidu.face.apiKey}")
    private String faceApiKey;

    @Value("${baidu.face.secretKey}")
    private String faceSecretKey;

    private AipFace faceClient;

    /**
     * Spring 컨테이너가 의존성 주입을 완료한 후, Baidu 얼굴 인식 클라이언트를 초기화합니다.
     */
    @PostConstruct
    public void init() {
        // AppID는 원본 코드와 같이 null로 설정, 필요에 따라 구성 파일에서 로드 가능
        faceClient = new AipFace(null, faceApiKey, faceSecretKey);
        faceClient.setConnectionTimeoutInMillis(2000); // 연결 타임아웃 2초
        faceClient.setSocketTimeoutInMillis(60000);   // 소켓 타임아웃 60초
    }

    /**
     * 두 이미지 파일 간의 얼굴 일치도를 측정합니다.
     * @param imageFileName1 첫 번째 얼굴 이미지 파일명 (예: "image1.jpg")
     * @param imageFileName2 두 번째 얼굴 이미지 파일명 (예: "image2.jpg")
     * @return 얼굴 비교 결과가 담긴 Map 객체
     * @throws IOException 파일 읽기 오류 발생 시
     * @throws FileNotFoundException 파일이 존재하지 않을 경우
     */
    public Map<String, Object> performFaceMatch(String imageFileName1, String imageFileName2) throws IOException, FileNotFoundException {
        if (faceClient == null) {
            throw new IllegalStateException("Baidu 얼굴 인식 클라이언트가 초기화되지 않았습니다.");
        }

        File file1 = ResourceUtils.getFile("classpath:static/upload/" + imageFileName1);
        File file2 = ResourceUtils.getFile("classpath:static/upload/" + imageFileName2);

        String base64Img1 = Base64Encoder.encodeBytes(FileConverter.readFileToByteArray(file1));
        String base64Img2 = Base64Encoder.encodeBytes(FileConverter.readFileToByteArray(file2));

        MatchRequest req1 = new MatchRequest(base64Img1, "BASE64");
        MatchRequest req2 = new MatchRequest(base64Img2, "BASE64");

        ArrayList<MatchRequest> requests = new ArrayList<>();
        requests.add(req1);
        requests.add(req2);

        JSONObject result = faceClient.match(requests);
        // Baidu SDK의 JSONObject를 표준 Java Map으로 변환하여 반환
        return com.alibaba.fastjson.JSONObject.parseObject(result.toJSONString(), Map.class);
    }
}

@RestController
@RequestMapping("/external-api") // 외부 API 연동을 위한 기본 경로 설정
public class ExternalApiIntegrationController {

    @Value("${baidu.map.apiKey}")
    private String mapApiKey;

    // BaiduAiService를 의존성 주입
    private final BaiduAiService baiduAiService;

    public ExternalApiIntegrationController(BaiduAiService baiduAiService) {
        this.baiduAiService = baiduAiService;
    }

    /**
     * 위도/경도 좌표를 사용하여 도시 정보를 조회하는 엔드포인트입니다.
     * @param longitude 경도 값
     * @param latitude 위도 값
     * @return 도시 정보가 포함된 JSON 응답
     */
    @RequestMapping("/geocode")
    public ResponseEntity<Map<String, String>> getCityInfoByCoordinates(
            @RequestParam("lng") String longitude,
            @RequestParam("lat") String latitude) {

        if (mapApiKey == null || mapApiKey.isEmpty()) {
            return new ResponseEntity<>(Map.of("error", "Baidu Map API Key가 설정되지 않았습니다."), HttpStatus.INTERNAL_SERVER_ERROR);
        }

        // Baidu Map API 호출을 위한 유틸리티 메서드 (별도 구현 필요)
        // 여기서는 예시로 mock 데이터를 반환합니다.
        Map<String, String> cityData = new HashMap<>();
        cityData.put("city", "서울");
        cityData.put("address", "강남구, 서울특별시, 대한민국");
        cityData.put("longitude", longitude);
        cityData.put("latitude", latitude);

        return new ResponseEntity<>(cityData, HttpStatus.OK);
    }

    /**
     * 두 개의 이미지 파일명을 받아 얼굴 일치도를 비교하는 엔드포인트입니다.
     * @param imageFile1 첫 번째 얼굴 이미지 파일의 이름
     * @param imageFile2 두 번째 얼굴 이미지 파일의 이름
     * @return 얼굴 비교 결과가 포함된 JSON 응답
     */
    @RequestMapping("/face-compare")
    public ResponseEntity<Map<String, Object>> compareFaces(
            @RequestParam("face1") String imageFile1,
            @RequestParam("face2") String imageFile2) {
        try {
            Map<String, Object> matchResult = baiduAiService.performFaceMatch(imageFile1, imageFile2);
            return new ResponseEntity<>(matchResult, HttpStatus.OK);
        } catch (FileNotFoundException e) {
            return new ResponseEntity<>(Map.of("error", "파일을 찾을 수 없습니다: " + e.getMessage()), HttpStatus.NOT_FOUND);
        } catch (IOException e) {
            return new ResponseEntity<>(Map.of("error", "파일 처리 중 오류 발생: " + e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            return new ResponseEntity<>(Map.of("error", "Baidu AI 서비스 초기화 오류: " + e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

application.properties에 다음 설정이 필요합니다:

baidu.map.apiKey=YOUR_BAIDU_MAP_AK_HERE
baidu.face.apiKey=YOUR_BAIDU_FACE_API_KEY_HERE
baidu.face.secretKey=YOUR_BAIDU_FACE_SECRET_KEY_HERE

데이터베이스 스크립트

시스템에 필요한 주요 데이터베이스 테이블 구조는 다음과 같습니다. 사용자 관리, 메시지 게시판 및 인증 토큰 저장에 사용되는 테이블들입니다.


CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '기본 키',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
  `username` varchar(200) NOT NULL COMMENT '사용자명',
  `password` varchar(200) NOT NULL COMMENT '비밀번호',
  `full_name` varchar(200) DEFAULT NULL COMMENT '성명',
  `gender` varchar(200) DEFAULT NULL COMMENT '성별',
  `profile_image` varchar(200) DEFAULT NULL COMMENT '프로필 이미지',
  `phone_number` varchar(200) DEFAULT NULL COMMENT '휴대폰 번호',
  `id_card` varchar(200) DEFAULT NULL COMMENT '신분증 번호',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1616222324596 DEFAULT CHARSET=utf8mb3 COMMENT='사용자';

CREATE TABLE `messages` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '기본 키',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
  `user_id` bigint NOT NULL COMMENT '메시지 작성자 ID',
  `username` varchar(200) DEFAULT NULL COMMENT '작성자 사용자명',
  `content` longtext NOT NULL COMMENT '메시지 내용',
  `reply_content` longtext COMMENT '답변 내용',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1616222424131 DEFAULT CHARSET=utf8mb3 COMMENT='메시지 게시판';

CREATE TABLE `tokens` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '기본 키',
  `user_id` bigint NOT NULL COMMENT '사용자 ID',
  `username` varchar(100) NOT NULL COMMENT '사용자명',
  `table_name` varchar(100) DEFAULT NULL COMMENT '관련 테이블명',
  `role` varchar(100) DEFAULT NULL COMMENT '사용자 역할',
  `token_value` varchar(200) NOT NULL COMMENT '토큰 값',
  `issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '발급 시간',
  `expires_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '만료 시간',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COMMENT='토큰';

태그: SpringBoot Vue.js uni-app java Baidu AI Platform

7월 18일 03:21에 게시됨