시스템 개요
본 글에서는 Spring Boot, Vue.js, UniApp 기술 스택을 활용하여 임대 관리 시스템을 설계하고 구현하는 방법을 상세히 설명합니다. 이 시스템은 웹과 모바일 환경을 모두 지원하며, 관리자와 일반 사용자(임차인, 임대인)를 위한 다양한 기능을 제공합니다. 시스템의 주요 목표는 부동산 등록, 검색, 계약 관리, 결제 내역 조회 등을 효율적으로 처리하는 것입니다.
핵심 기술 스택
백엔드: Spring Boot
Spring Boot는 내장 서버(Tomcat, Jetty 등)와 자동 설정(auto-configuration) 기능을 제공하여 개발 속도를 크게 향상시킵니다. 개발자는 복잡한 XML 설정 없이 애플리케이션을 빠르게 시작할 수 있습니다. 또한 Spring Data JPA, Spring Security와 같은 다양한 스타터(starter) 패키지를 쉽게 통합할 수 있어 보안, 데이터 접근, RESTful API 구축이 간편해집니다.
프론트엔드: Vue.js
Vue.js는 반응형 데이터 바인딩과 가상 DOM(Virtual DOM)을 사용하여 UI 업데이트를 효율적으로 처리합니다. 컴포넌트 기반 아키텍처를 통해 코드 재사용성과 유지보수성을 높였습니다. Vue Router를 이용한 SPA(Single Page Application) 라우팅과 Vuex를 통한 상태 관리는 대규모 애플리케이션에서도 일관된 데이터 흐름을 보장합니다.
데이터 접근: MyBatis-Plus
MyBatis-Plus는 MyBatis의 확장판으로, CRUD 작업을 위한 기본 메서드와 조건 빌더(ConditionBuilder)를 제공합니다. 복잡한 SQL을 직접 작성할 필요 없이 어노테이션 또는 메서드 호출만으로 데이터 조작이 가능합니다. 분산 페이징, 동적 쿼리, 낙관적 락(optimistic lock) 등 실용적인 기능을 내장하고 있어 개발 생산성을 높입니다.
시스템 테스트 전략
시스템의 안정성과 기능 완전성을 보장하기 위해 블랙박스 테스트(Black-box Testing)를 주로 사용합니다. 테스트 시나리오는 실제 사용자 행동을 모방하여 작성되며, 각 기능 모듈의 입력값과 예상 출력값을 비교합니다. 아래는 로그인 기능에 대한 테스트 케이스 예시입니다.
| 입력 데이터 | 예상 결과 | 실제 결과 | 분석 |
|---|---|---|---|
| 사용자명: admin, 비밀번호: 123456, 인증코드: 올바름 | 시스템 로그인 성공 | 로그인 성공, 대시보드로 이동 | 예상과 일치 |
| 사용자명: admin, 비밀번호: 111111, 인증코드: 올바름 | 비밀번호 오류 메시지 출력 | "비밀번호가 일치하지 않습니다" 메시지 표시 | 예상과 일치 |
| 사용자명: 공백, 비밀번호: 123456, 인증코드: 올바름 | 사용자명 필수 입력 경고 | "사용자명을 입력해주세요" 메시지 표시 | 예상과 일치 |
사용자 관리 기능 테스트에서는 추가, 수정, 삭제, 검색 작업을 검증합니다. 예를 들어, 기존에 등록된 사용자명으로 새 계정을 생성하려 하면 중복 경고가 발생해야 합니다. 삭제 시에는 확인 대화상자가 나타나고, 사용자가 확인한 후에만 레코드가 제거됩니다.
핵심 코드 예제
로그인 API 구현
@PostMapping("/authenticate")
public ResponseEntity<Map<String, Object>> authenticate(@RequestParam String username,
@RequestParam String password,
@RequestParam String captcha,
HttpServletRequest request) {
UserEntity user = userService.findByUsername(username);
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
return ResponseEntity.badRequest().body(Map.of("message", "아이디 또는 비밀번호가 잘못되었습니다."));
}
String accessToken = jwtTokenProvider.generateToken(user.getId(), user.getUsername(), "users", user.getRole());
return ResponseEntity.ok(Map.of("token", accessToken));
}
JWT 토큰 생성 로직
@Override
public String generateToken(Long userId, String username, String tableName, String role) {
TokenEntity existingToken = tokenRepository.findByUserIdAndRole(userId, role);
String newToken = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.HOUR_OF_DAY, 1); // 1시간 후 만료
if (existingToken != null) {
existingToken.setToken(newToken);
existingToken.setExpirationDate(calendar.getTime());
tokenRepository.save(existingToken);
} else {
tokenRepository.save(new TokenEntity(userId, username, tableName, role, newToken, calendar.getTime()));
}
return newToken;
}
권한 검증 인터셉터
@Component
public class AuthInterceptor implements HandlerInterceptor {
private static final String AUTH_HEADER = "Authorization";
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// CORS 헤더 설정
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.OK.value());
return false;
}
// @IgnoreAuth 어노테이션이 적용된 메서드는 인증 생략
if (handler instanceof HandlerMethod) {
IgnoreAuth ignoreAuth = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);
if (ignoreAuth != null) {
return true;
}
}
String token = request.getHeader(AUTH_HEADER);
if (token == null || token.isEmpty()) {
sendUnauthorizedResponse(response, "인증 토큰이 필요합니다.");
return false;
}
TokenEntity tokenEntity = tokenService.findByToken(token);
if (tokenEntity == null || tokenEntity.getExpirationDate().before(new Date())) {
sendUnauthorizedResponse(response, "토큰이 유효하지 않거나 만료되었습니다.");
return false;
}
// 사용자 정보를 세션에 저장
request.getSession().setAttribute("userId", tokenEntity.getUserId());
request.getSession().setAttribute("userRole", tokenEntity.getRole());
return true;
}
private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"message\":\"" + message + "\"}");
}
}
데이터베이스 설계 예시
다음은 인증 토큰 관리를 위한 auth_tokens 테이블입니다.
CREATE TABLE auth_tokens (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '고유 식별자',
user_id BIGINT NOT NULL COMMENT '사용자 ID',
username 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 '토큰 값',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간',
expires_at TIMESTAMP NOT NULL COMMENT '만료 시간',
INDEX idx_user_role (user_id, user_role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='인증 토큰 테이블';
위 테이블 구조는 사용자별 역할 기반의 토큰 관리를 지원하며, 만료 시간을 통해 보안을 강화합니다.
프로젝트 설정 및 빌드
Spring Boot 프로젝트는 Maven 또는 Gradle을 통해 빌드할 수 있습니다. 주요 의존성은 다음과 같습니다.
- spring-boot-starter-web (REST API)
- mybatis-plus-boot-starter (데이터 접근)
- spring-boot-starter-security (인증/인가)
- jjwt (JWT 토큰 처리)
- mysql-connector-java (데이터베이스 연결)
프론트엔드는 Vue CLI를 사용하여 프로젝트를 생성하고, Axios를 통해 백엔드 API와 통신합니다. 모바일 앱은 UniApp을 사용하여 단일 코드베이스로 iOS와 Android를 지원합니다.