Java 기반 스프링부트/SSM + Vue + 유니앱을 활용한 향정부 관리 시스템 상세 설계 및 구현

시스템 소개

본 시스템은 현대적인 웹 기술을 활용하여 향정부의 업무 효율성을 높이기 위한 종합 관리 솔루션입니다. 전통적인 관리 방식의 한계를 극복하고 디지털 전환을 실현하는 데 초점을 맞추어 설계되었습니다.

기술 스택

백엔드 프레임워크: 스프링부트

스프링부트는 톰캣, 제티, 언더토우와 같은 내장 서버를 포함하고 있어 추가 설치 없이 바로 사용할 수 있습니다. 이 프레임워크의 핵심 장점은 자동 구성 기능입니다. 프로젝트 의존성에 따라 애플리케이션을 자동으로 구성하여 개발자가 각 의존성을 수동으로 설정할 필요가 없게 합니다. 또한 스프링 데이터, 스프링 시큐리티, 스프링 클라우드와 같은 다양한 기능과 플러그인을 제공하여 개발 속도를 향상시키고 다른 기술과의 통합을 용이하게 합니다.

프론트엔드 프레임워크: Vue.js

Vue.js의 핵심 기술은 가상 DOM입니다. 가상 DOM은 메모리 내 데이터 구조로 Vue.js가 효율적인 DOM 조작을 구현할 수 있게 해줍니다. 반응형 데이터 바인딩, 가상 DOM, 컴포넌트 기반 아키텍처와 같은 현대적 기술을 채택하여 개발자에게 유연하고 효율적이며 유지보수가 용이한 개발 환경을 제공합니다. 데이터가 변경될 때 UI가 자동으로 업데이트되므로 개발자는 UI 수동 업데이트보다 데이터 처리에 더 집중할 수 있습니다.

데이터 지속성 프레임워크: 마이바티스 플러스

마이바티스 플러스는 마이바티스 프레임워크를 기반으로 한 개발을 단순화하기 위한 도구입니다. 이 오픈소스 자바 프레임워크는 MySQL, 오라클, SQL 서버, PostgreSQL 등 다양한 데이터베이스를 지원합니다. 풍부한 API와 애노테이션을 제공하여 간단한 설정으로 ORM 작업을 구현하고 SQL 작성량을 크게 줄여줍니다. 또한 코드 생성기를 통해 엔티티 클래스, 매퍼 인터페이스 및 XML 매핑 파일을 자동 생성하여 개발 프로세스를 크게 단순화합니다. 페이징 쿼리, 동적 쿼리, 낙관적 락, 성능 분석 등 실용적 기능을 지원하여 효율적인 데이터 작업을 가능하게 합니다.

시스템 테스트

테스트 목적

향정부 관리 시스템의 개발 주기에서 시스템 테스트는 필수적이고 인내심을 요구하는 과정입니다. 이 과정의 중요성은 시스템 품질과 안정성을 보장하는 최후의 관문이며, 전체 시스템 개발 과정의 마지막 검사 단계이기 때문입니다. 주요 목적은 사용자가 시스템을 사용할 때 발생할 수 있는 문제를 방지하고 사용자 경험을 향상시키는 것입니다. 다양한 시나리오를 통해 시스템의 결함을 발견하고 해결함으로써 시스템의 완성도를 높입니다.

기능 테스트

사용자 인증 테스트: 시스템 접근 시 아이디와 비밀번호로 사용자를 인증합니다. 데이터베이스에 저장된 정보와 일치하지 않는 입력값에 대해 오류 메시지를 표시합니다. 또한 사용자 역할에 따른 접근 권한을 검증합니다.

사용자 관리 테스트: 사용자 추가, 편집, 삭제, 검색 기능을 테스트합니다. 필수 항목이 누락된 경우 시스템이 비어있음을 검증하는지 확인합니다. 중복 사용자 이름을 입력할 경우 중복 오류 메시지가 표시되는지 확인합니다. 사용자 삭제 시 확인 절차가 정상적으로 작동하는지 테스트합니다.

테스트 결과

본 시스템은 주로 블랙박스 테스트 방식을 사용하여 사용자 시나리오를 기반으로 기능 모듈을 검증했습니다. 테스트를 통해 시스템의 기능과 성능이 초기 설계 요구사항을 충족함을 확인했습니다. 시스템은 복잡한 로직 처리보다는 사용자 친화적인 인터페이스에 중점을 두어 설계되었습니다.

핵심 코드 예시

// 인증이 필요 없는 로그인 엔드포인트
@SkipAuthValidation
@PostMapping("/login")
public ResponseEntity<ApiResponse> authenticateUser(String userId, String userPassword, String captchaCode, HttpServletRequest httpRequest) {
    UserAccountEntity userAccount = userAccountService.findUser(new EntityWrapper<UserAccountEntity>().eq("userId", userId));
    if(userAccount == null || !userAccount.getPassword().equals(userPassword)) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ApiResponse(false, "아이디 또는 비밀번호가 올바르지 않습니다"));
    }
    String authToken = tokenService.createToken(userAccount.getId(), userId, "user_accounts", userAccount.getUserRole());
    return ResponseEntity.ok(new ApiResponse(true).addData("authToken", authToken));
}

@Override
public String createToken(Long userId, String userName, String tableName, String userRole) {
    TokenEntity existingToken = this.selectOne(new EntityWrapper<TokenEntity>().eq("userId", userId).eq("userRole", userRole));
    String newToken = SecurityUtil.generateRandomString(32);
    Calendar tokenExpiration = Calendar.getInstance();   
    tokenExpiration.setTime(new Date());   
    tokenExpiration.add(Calendar.HOUR_OF_DAY, 1);
    
    if(existingToken != null) {
        existingToken.setToken(newToken);
        existingToken.setExpirationTime(tokenExpiration.getTime());
        this.updateById(existingToken);
    } else {
        this.insert(new TokenEntity(userId, userName, tableName, userRole, newToken, tokenExpiration.getTime()));
    }
    return newToken;
}

/**
 * 인증 토큰 검증 인터셉터
 */
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    public static final String AUTH_TOKEN_HEADER = "Authorization";

    @Autowired
    private TokenService tokenService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // CORS 요청 허용
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,request-source,Authorization, Origin,imgType, Content-Type, cache-control");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        
        // OPTIONS 요청에 대한 처리
        if(request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        SkipAuthValidation annotation;
        if(handler instanceof HandlerMethod) {
            annotation = ((HandlerMethod) handler).getMethodAnnotation(SkipAuthValidation.class);
        } else {
            return true;
        }

        // 헤더에서 토큰 추출
        String token = request.getHeader(AUTH_TOKEN_HEADER);
        
        // 인증이 필요 없는 메소드는 통과
        if(annotation != null) {
            return true;
        }
        
        TokenEntity tokenEntity = null;
        if(StringUtils.isNotBlank(token)) {
            tokenEntity = tokenService.findTokenByValue(token);
        }
        
        if(tokenEntity != null) {
            request.getSession().setAttribute("userId", tokenEntity.getUserId());
            request.getSession().setAttribute("userRole", tokenEntity.getUserRole());
            request.getSession().setAttribute("tableName", tokenEntity.getTableName());
            request.getSession().setAttribute("userName", tokenEntity.getUserName());
            return true;
        }
        
        // 인증 실패 처리
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            response.getWriter().print(JSONObject.toJSONString(new ApiResponse(false, 401, "로그인이 필요합니다")));
        } finally {
            response.getWriter().close();
        }
        return false;
    }
}

데이터베이스 구조

-- 토큰 테이블 구조
DROP TABLE IF EXISTS `auth_tokens`;
CREATE TABLE `auth_tokens` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '기본 키',
  `user_id` bigint(20) NOT NULL COMMENT '사용자 ID',
  `user_name` varchar(100) NOT NULL COMMENT '사용자 이름',
  `table_name` varchar(100) DEFAULT NULL COMMENT '테이블명',
  `user_role` varchar(100) DEFAULT NULL COMMENT '사용자 역할',
  `token_value` varchar(200) NOT NULL COMMENT '인증 토큰',
  `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
  `expiration_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '만료 시간',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='인증 토큰 테이블';

-- 샘플 데이터
INSERT INTO `auth_tokens` VALUES ('9', '23', 'cd01', 'user_accounts', '일사용자', 'al6svx5qkei1wljry5o1npswhdpqcpcg', '2023-02-23 21:46:45', '2023-03-15 14:01:36');
INSERT INTO `auth_tokens` VALUES ('10', '11', 'xh01', 'user_accounts', '일사용자', 'fahmrd9bkhqy04sq0fzrl4h9m86cu6kx', '2023-02-27 18:33:52', '2023-03-17 18:27:42');
INSERT INTO `auth_tokens` VALUES ('11', '17', 'ch01', 'user_accounts', '일사용자', 'u5km44scxvzuv5yumdah2lhva0gp4393', '2023-02-27 18:46:19', '2023-02-27 19:48:58');
INSERT INTO `auth_tokens` VALUES ('12', '1', 'admin', 'user_accounts', '관리자', 'h1pqzsb9bldh93m92j9m2sljy9bt1wdh', '2023-02-27 19:37:01', '2023-03-17 18:23:02');
INSERT INTO `auth_tokens` VALUES ('13', '21', 'xiaohao', 'user_accounts', '관리자', 'zdm7j8h1wnfe27pkxyiuzvxxy27ykl2a', '2023-02-27 19:38:07', '2023-03-17 18:25:20');
INSERT INTO `auth_tokens` VALUES ('14', '27', 'djy01', 'user_accounts', '일사용자', 'g3teq4335pe21nwuwj2sqkrpqoabqomm', '2023-03-15 12:56:17', '2023-03-15 14:00:16');
INSERT INTO `auth_tokens` VALUES ('15', '29', 'dajiyue', 'user_accounts', '관리자', '0vb1x9xn7riewlp5ddma5ro7lp4u8m9j', '2023-03-15 12:58:08', '2023-03-15 14:03:48');

소스 코드 획득

본 시스템의 전체 소스 코드, 데이터베이스 스크립트 및 배포 문서는 공식 저장소에서 다운로드할 수 있습니다. 기술 문의나 추가 정보가 필요한 경우 개발팀에 연락하시기 바랍니다.

태그: java SpringBoot Vue.js MyBatis UniApp

6월 27일 03:40에 게시됨