개요
이 문서는 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;
}
}