Spring Boot 기반 도서 전자상거래 시스템 구현

개요

이 문서는 Spring Boot 프레임워크를 활용하여 구축된 도서 전자상거래 웹사이트의 설계 및 구현 과정을 다룹니다. 현대 사회에서 온라인 쇼핑몰은 필수적인 서비스로 자리 잡았으며, 본 시스템은 사용자에게 편리한 도서 구매 경험을 제공하고 관리자에게는 효율적인 시스템 운영 기능을 제공하는 것을 목표로 합니다.

백엔드 개발에는 Java 언어와 Spring Boot 프레임워크를 사용하였으며, 데이터 영속성 계층은 MyBatis-Plus를 통해 구현되었습니다. 데이터베이스로는 MySQL 5.7 이상 버전을 활용하여 안정적인 데이터 저장 및 관리를 보장합니다. 시스템은 웹 기반으로 동작하며, Tomcat 서버 환경에서 배포될 수 있도록 설계되었습니다.

주요 기능

관리자 기능

  • 회원 관리: 시스템에 등록된 사용자 정보를 추가, 수정, 삭제, 조회할 수 있습니다.
  • 도서 분류 관리: 도서 카테고리를 생성, 편집, 삭제, 검색하여 도서 정보를 체계적으로 관리합니다.
  • 도서 정보 관리: 도서의 상세 정보(제목, 저자, 가격, 설명 등)를 등록, 수정, 삭제, 조회할 수 있습니다.
  • 주문 관리: 사용자들의 결제 완료된 주문을 확인하고, 주문 상태를 변경하며 배송 처리할 수 있습니다.
  • 시스템 관리: (예: 공지사항 관리, 시스템 설정 등 전반적인 시스템 운영 관련 기능)

사용자 기능

  • 도서 탐색: 다양한 도서 정보를 조회하고, 검색 기능을 통해 원하는 도서를 찾을 수 있습니다.
  • 장바구니 및 구매: 관심 있는 도서를 장바구니에 담거나 즉시 구매를 진행할 수 있습니다.
  • 리뷰 및 즐겨찾기: 구매한 도서에 대한 리뷰를 작성하고, 마음에 드는 도서를 즐겨찾기에 추가하여 관리할 수 있습니다.
  • 장바구니 관리: 장바구니에 담긴 도서의 수량을 변경하거나 필요 없는 도서를 삭제할 수 있습니다.
  • 결제 및 배송지 설정: 장바구니에서 주문을 확정한 후, 배송지 정보를 수정하고 다양한 결제 수단을 통해 결제를 진행합니다.
  • 즐겨찾기 목록: 즐겨찾기에 추가된 도서 목록을 한눈에 확인하고 관리할 수 있습니다.

구현 코드 예시

다음은 시스템의 핵심 구성 요소인 컨트롤러, 서비스, 데이터 접근 계층 및 인증 인터셉터에 대한 코드 예시입니다. 여기서는 사용자 계정 관리와 관련된 부분을 중심으로 살펴보겠습니다. (원본 코드의 변수명, 클래스명, 유틸리티 클래스명, MyBatis-Plus 버전 등이 변경되었습니다.)

참고: 아래 코드 예시의 AppUser 엔티티는 id, loginId, passwordHash, role 등의 필드를 포함하는 사용자 데이터 객체이며, NoAuthRequired는 인증이 필요 없는 API에 붙이는 커스텀 어노테이션입니다.

컨트롤러 계층 (UserAuthController)

사용자 인증 및 계정 관련 요청을 처리하는 REST 컨트롤러입니다.


package com.bookstore.controller;

import java.util.Arrays;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.bookstore.annotation.NoAuthRequired; // 인증 건너뛰기 어노테이션
import com.bookstore.entity.AppUser; // 사용자 엔티티
import com.bookstore.service.UserAuthService; // 사용자 인증 및 관리 서비스
import com.bookstore.service.TokenGenService; // 토큰 생성 서비스
import com.bookstore.util.ResponseUtil; // 응답 유틸리티
import com.bookstore.util.PaginationUtil; // 페이지네이션 유틸리티
import com.bookstore.util.QueryWrapperUtil; // 쿼리 래퍼 유틸리티

/**
 * 사용자 인증 및 관리 API 컨트롤러
 */
@RequestMapping("/api/users")
@RestController
public class UserAuthController {
    
    @Autowired
    private UserAuthService userAuthService;
    
    @Autowired
    private TokenGenService tokenGenService;

    /**
     * 사용자 로그인 처리
     * @param loginId 사용자 로그인 아이디
     * @param userPass 사용자 비밀번호
     * @return 성공 시 토큰 포함 응답, 실패 시 에러 응답
     */
    @NoAuthRequired
    @PostMapping("/login")
    public ResponseUtil userLogin(String loginId, String userPass) {
        AppUser userAccount = userAuthService.getUserByLoginId(loginId);
        if (userAccount == null || !userAccount.getPasswordHash().equals(userPass)) {
            return ResponseUtil.error("아이디 또는 비밀번호가 올바르지 않습니다.");
        }
        String userToken = tokenGenService.generateAuthToken(userAccount.getId(), loginId, "users", userAccount.getRole());
        return ResponseUtil.ok().put("token", userToken);
    }
    
    /**
     * 새로운 사용자 등록 (회원가입)
     * @param newUser 등록할 사용자 정보
     * @return 성공/실패 응답
     */
    @NoAuthRequired
    @PostMapping("/register")
    public ResponseUtil userRegister(@RequestBody AppUser newUser) {
        if (userAuthService.getUserByLoginId(newUser.getLoginId()) != null) {
            return ResponseUtil.error("이미 존재하는 사용자 아이디입니다.");
        }
        userAuthService.createUser(newUser);
        return ResponseUtil.ok("회원가입이 완료되었습니다.");
    }

    /**
     * 사용자 로그아웃 처리
     * @param request HTTP 요청 객체
     * @return 성공 응답
     */
    @GetMapping("/logout")
    public ResponseUtil userLogout(HttpServletRequest request) {
        request.getSession().invalidate();
        return ResponseUtil.ok("로그아웃 되었습니다.");
    }
    
    /**
     * 사용자 비밀번호 초기화
     * @param loginId 비밀번호를 초기화할 사용자 아이디
     * @return 성공/실패 응답
     */
    @NoAuthRequired
    @RequestMapping("/reset-password")
    public ResponseUtil resetUserPassword(String loginId) {
        AppUser targetUser = userAuthService.getUserByLoginId(loginId);
        if (targetUser == null) {
            return ResponseUtil.error("존재하지 않는 사용자 아이디입니다.");
        }
        targetUser.setPasswordHash("123456"); // 기본 비밀번호로 초기화
        userAuthService.updateUser(targetUser);
        return ResponseUtil.ok("비밀번호가 '123456'으로 초기화되었습니다.");
    }
    
    /**
     * 사용자 목록 페이지네이션 조회 (관리자용)
     * @param queryParams 페이지 및 검색 조건
     * @param userFilter 필터링할 사용자 엔티티
     * @return 페이지네이션된 사용자 목록
     */
    @RequestMapping("/list-paged")
    public ResponseUtil listUsersPaged(@RequestParam Map queryParams, AppUser userFilter) {
        PaginationUtil pagedList = userAuthService.retrieveUserPage(queryParams, 
            QueryWrapperUtil.applySorting(QueryWrapperUtil.applyConditions(userFilter), queryParams));
        return ResponseUtil.ok().put("data", pagedList);
    }

    /**
     * 특정 사용자 상세 정보 조회
     * @param userId 조회할 사용자 ID
     * @return 사용자 상세 정보
     */
    @RequestMapping("/details/{id}")
    public ResponseUtil getUserDetails(@PathVariable("id") Long userId) {
        AppUser foundUser = userAuthService.getUserById(userId);
        return ResponseUtil.ok().put("data", foundUser);
    }
    
    /**
     * 현재 로그인된 사용자의 세션 정보 조회
     * @param request HTTP 요청 객체
     * @return 현재 사용자 정보
     */
    @RequestMapping("/current-session")
    public ResponseUtil getCurrentSessionUser(HttpServletRequest request) {
        Long sessionId = (Long) request.getSession().getAttribute("currentUserId");
        AppUser sessionUser = userAuthService.getUserById(sessionId);
        return ResponseUtil.ok().put("data", sessionUser);
    }

    /**
     * 새로운 사용자 정보 저장 (관리자용)
     * @param userToAdd 저장할 사용자 엔티티
     * @return 성공/실패 응답
     */
    @PostMapping("/save")
    public ResponseUtil saveNewUser(@RequestBody AppUser userToAdd) {
        if (userAuthService.getUserByLoginId(userToAdd.getLoginId()) != null) {
            return ResponseUtil.error("해당 로그인 아이디는 이미 사용 중입니다.");
        }
        userAuthService.createUser(userToAdd);
        return ResponseUtil.ok("새 사용자 정보가 저장되었습니다.");
    }

    /**
     * 기존 사용자 정보 업데이트
     * @param userToUpdate 업데이트할 사용자 엔티티
     * @return 성공 응답
     */
    @RequestMapping("/update")
    public ResponseUtil updateExistingUser(@RequestBody AppUser userToUpdate) {
        userAuthService.updateUser(userToUpdate);
        return ResponseUtil.ok("사용자 정보가 업데이트되었습니다.");
    }

    /**
     * 사용자 삭제 (다중 삭제 가능)
     * @param userIds 삭제할 사용자 ID 배열
     * @return 성공 응답
     */
    @RequestMapping("/delete")
    public ResponseUtil deleteUsers(@RequestBody Long[] userIds) {
        userAuthService.deleteUsersByIds(Arrays.asList(userIds));
        return ResponseUtil.ok("선택된 사용자들이 삭제되었습니다.");
    }
}

데이터 접근 계층 (UserAccountMapper)

MyBatis-Plus의 BaseMapper를 확장하여 사용자 엔티티에 대한 기본적인 CRUD 및 사용자 정의 쿼리를 제공하는 인터페이스입니다.


package com.bookstore.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.bookstore.entity.AppUser;

/**
 * 사용자 계정 데이터 매퍼 인터페이스
 */
@Mapper
public interface UserAccountMapper extends BaseMapper<AppUser> {
    
    /**
     * 특정 쿼리 조건에 맞는 사용자 목록을 조회합니다.
     * @param queryCond 쿼리 조건 래퍼
     * @return 사용자 엔티티 목록
     */
    List<AppUser> selectUserView(@Param("queryCond") Wrapper<AppUser> queryCond);

    /**
     * 페이지네이션을 적용하여 사용자 목록을 조회합니다.
     * @param page 페이지 정보 객체
     * @param queryCond 쿼리 조건 래퍼
     * @return 페이지네이션된 사용자 엔티티 목록
     */
    List<AppUser> selectUserViewPage(Page<AppUser> page, @Param("queryCond") Wrapper<AppUser> queryCond);
    
}

서비스 계층 인터페이스 (UserAuthService)

사용자 인증 및 계정 관리에 필요한 비즈니스 로직을 정의하는 인터페이스입니다.


package com.bookstore.service;

import java.util.List;
import java.util.Map;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bookstore.entity.AppUser;
import com.bookstore.util.PaginationUtil;

/**
 * 사용자 인증 및 비즈니스 로직 서비스 인터페이스
 */
public interface UserAuthService extends IService<AppUser> {
    
    /**
     * 모든 사용자 데이터를 페이지네이션하여 조회합니다.
     * @param params 쿼리 파라미터 (페이지, 정렬 등)
     * @return 페이지네이션 유틸리티 객체
     */
    PaginationUtil retrieveUserPage(Map params);
    
    /**
     * 특정 조건에 맞는 사용자 목록을 조회합니다.
     * @param queryWrapper 쿼리 조건 래퍼
     * @return 사용자 엔티티 목록
     */
    List<AppUser> retrieveUserList(Wrapper<AppUser> queryWrapper);
    
    /**
     * 특정 조건에 맞는 사용자 데이터를 페이지네이션하여 조회합니다.
     * @param params 쿼리 파라미터
     * @param queryWrapper 쿼리 조건 래퍼
     * @return 페이지네이션 유틸리티 객체
     */
    PaginationUtil retrieveUserPage(Map params, Wrapper<AppUser> queryWrapper);

    // 사용자 정의 비즈니스 메서드
    AppUser getUserByLoginId(String loginId);
    AppUser getUserById(Long id);
    void createUser(AppUser user);
    void updateUser(AppUser user);
    void deleteUsersByIds(List<Long> ids);
}

서비스 계층 구현 (UserAuthServiceImpl)

UserAuthService 인터페이스의 구현 클래스로, 실제 비즈니스 로직을 수행합니다.


package com.bookstore.service.impl;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.bookstore.mapper.UserAccountMapper;
import com.bookstore.entity.AppUser;
import com.bookstore.service.UserAuthService;
import com.bookstore.util.PaginationUtil;
import com.bookstore.util.QueryParamParser; // 쿼리 파라미터 파서 유틸리티

/**
 * 사용자 인증 및 관리 서비스 구현체
 */
@Service("userAuthService")
public class UserAuthServiceImpl extends ServiceImpl implements UserAuthService {

    @Override
    public PaginationUtil retrieveUserPage(Map params) {
        Page<AppUser> appUserPage = this.page(
            new QueryParamParser<AppUser>(params).getPage(),
            new QueryWrapper<AppUser>()
        );
        return new PaginationUtil(appUserPage);
    }

    @Override
    public List<AppUser> retrieveUserList(com.baomidou.mybatisplus.core.conditions.Wrapper<AppUser> queryWrapper) {
        return baseMapper.selectList(queryWrapper);
    }

    @Override
    public PaginationUtil retrieveUserPage(Map params,
            com.baomidou.mybatisplus.core.conditions.Wrapper<AppUser> queryWrapper) {
        Page<AppUser> appUserPage = new QueryParamParser<AppUser>(params).getPage();
        appUserPage.setRecords(baseMapper.selectUserViewPage(appUserPage, queryWrapper));
        return new PaginationUtil(appUserPage);
    }

    @Override
    public AppUser getUserByLoginId(String loginId) {
        return baseMapper.selectOne(new QueryWrapper<AppUser>().eq("login_id", loginId));
    }

    @Override
    public AppUser getUserById(Long id) {
        return baseMapper.selectById(id);
    }

    @Override
    public void createUser(AppUser user) {
        baseMapper.insert(user);
    }

    @Override
    public void updateUser(AppUser user) {
        baseMapper.updateById(user);
    }

    @Override
    public void deleteUsersByIds(List<Long> ids) {
        baseMapper.deleteBatchIds(ids);
    }
}

권한 검증 인터셉터 (AuthenticationInterceptor)

모든 API 요청에 대해 사용자 토큰의 유효성을 검증하고, 필요한 경우 권한 부여를 처리하는 스프링 인터셉터입니다.


package com.bookstore.interceptor;

import java.io.PrintWriter;
import com.fasterxml.jackson.databind.ObjectMapper; // JSON 처리 라이브러리
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import com.bookstore.annotation.NoAuthRequired; // 인증 건너뛰기 어노테이션
import com.bookstore.entity.AuthTokenEntity; // 인증 토큰 엔티티 (가정)
import com.bookstore.service.TokenGenService; // 토큰 생성 및 검증 서비스
import com.bookstore.util.ResponseUtil; // 응답 유틸리티

/**
 * 인증(Token) 검증 인터셉터
 */
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    public static final String AUTH_HEADER_KEY = "X-Auth-Token"; // 인증 토큰 헤더 키

    @Autowired
    private TokenGenService tokenGenService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // CORS(Cross-Origin Resource Sharing) 설정
        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, X-Auth-Token, Origin, Content-Type, Accept");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));

        NoAuthRequired noAuthAnnotation;
        if (handler instanceof HandlerMethod) {
            noAuthAnnotation = ((HandlerMethod) handler).getMethodAnnotation(NoAuthRequired.class);
        } else {
            return true; // HandlerMethod가 아닌 경우(예: 리소스 핸들러) 검증 없이 통과
        }

        // @NoAuthRequired 어노테이션이 붙은 메서드는 인증 검증을 건너뜜
        if (noAuthAnnotation != null) {
            return true;
        }
        
        // HTTP 헤더에서 토큰 추출
        String requestToken = request.getHeader(AUTH_HEADER_KEY);
        
        AuthTokenEntity currentToken = null;
        if (StringUtils.isNotBlank(requestToken)) {
            currentToken = tokenGenService.validateAuthToken(requestToken); // 토큰 유효성 검증
        }
        
        if (currentToken != null) {
            // 토큰이 유효하면 세션에 사용자 정보 저장
            request.getSession().setAttribute("currentUserId", currentToken.getAssociatedUserId());
            request.getSession().setAttribute("userRole", currentToken.getRoleIdentifier());
            request.getSession().setAttribute("userTable", currentToken.getTableName());
            request.getSession().setAttribute("loginUsername", currentToken.getLoginUsername());
            return true;
        }
        
        // 토큰이 없거나 유효하지 않은 경우, 401 Unauthorized 응답
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            ObjectMapper mapper = new ObjectMapper();
            writer.print(mapper.writeValueAsString(ResponseUtil.error(401, "로그인이 필요합니다.")));
        }
        return false;
    }
}

태그: Spring Boot 전자상거래 java MyBatis-Plus MySQL

9월 16일 09:39에 게시됨