Spring boot

[Spring boot] Bank App 만들어 보기 - 20. 중간 리팩토링

ekkkang 2025. 3. 5. 15:54

 

💡 리팩토링이란(Refactoring)
소프트웨어의 외부 동작을 변경하지 않으면서 내부 구조를 체계적으로 개선하는 과정을 말합니다. 이 과정은 코드의 가독성을 높이고, 유지보수를 용이하게 하며, 오류 발견 및 수정을 용이하게 하는 것을 목표로 합니다. 리팩토링은 소프트웨어 개발의 중요한 부분으로, 코드의 품질을 지속적으로 향상시키기 위해 필요합니다.

 

리팩토링의 목적

  • 가독성 향상: 코드를 더 이해하기 쉽게 만들어 다른 개발자가 코드를 빠르게 이해하고 수정할 수 있도록 합니다.
  • 유지보수성 개선: 코드의 구조를 개선하여 나중에 버그를 수정하거나 새로운 기능을 추가할 때 필요한 노력을 줄입니다.
  • 성능 최적화: 비효율적인 코드를 개선하여 애플리케이션의 실행 성능을 향상시킬 수 있습니다.
  • 재사용성 증가: 코드의 모듈성을 높여 다른 프로젝트나 다른 부분에서 코드를 재사용하기 쉽게 만듭니다.
  • 버그 발견: 코드를 정리하고 개선하는 과정에서 숨어 있던 버그를 발견하고 수정할 기회를 얻습니다.

리팩토링의 원칙

  1. 외부 동작 유지: 리팩토링은 코드의 외부 동작을 변경해서는 안 됩니다. 사용자 입장에서는 리팩토링 전후에 애플리케이션이 동일하게 동작해야 합니다.
  2. 작은 단계로 진행: 대규모 변경보다는 작은 변경을 반복적으로 적용하여 점진적으로 코드를 개선합니다.
  3. 테스팅: 리팩토링 과정에서는 지속적으로 테스트를 수행하여 리팩토링이 외부 동작에 영향을 미치지 않도록 합니다.
  4. 지속적인 개선: 리팩토링은 한 번에 끝나는 과정이 아니라, 지속적으로 코드를 개선해 나가는 과정입니다.

리팩토링의 예

  • 변수 이름 변경: 더 의미 있는 변수 이름을 사용하여 코드의 의도를 명확하게 합니다.
  • 함수 분리: 크고 복잡한 함수를 더 작고 관리하기 쉬운 여러 함수로 분리합니다.
  • 중복 코드 제거: 반복되는 코드를 찾아내어 함수로 추출하거나 다른 구조로 재구성합니다.
  • 디자인 패턴 적용: 코드 구조를 개선하기 위해 적절한 디자인 패턴을 적용합니다.
  • 조건문 간소화: 복잡한 조건문을 더 단순하거나 명확한 로직으로 재작성합니다.

 

💡 작업 순서
1. utils/Define.java 파일 생성
2. Controller 부분 코드 수정
3. Service 부분 코드 수정
4. Repository 부분 코드 수정
5. GlobalControllerAdvice 부분 코드 수정

 

 

1. utils/Define.java 파일 생성

 

Define.java - 복사 붙여넣기 하셔도 됩니다 (추후 추가될 내용이 더 있을 수 있습니다)

package com.tenco.bank.utils;


public class Define {
	//  상수
	public static final String PRINCIPAL = "principal";
	
	// 이미지 관련
	public static final String UPLOAD_FILE_DERECTORY = "C:\\work_spring\\upload/";
	public static final int MAX_FILE_SIZE = 1024 * 1024 * 20; // 20MB

	//  Account
	public static final String EXIST_ACCOUNT = "이미 계좌가 존재합니다.";
	public static final String NOT_EXIST_ACCOUNT = "존재하는 계좌가 없습니다.";
	public static final String FAIL_TO_CREATE_ACCOUNT = "계좌 생성이 실패하였습니다.";
	public static final String FAIL_ACCOUNT_PASSWROD = "계좌 비밀번호가 틀렸습니다.";
	public static final String LACK_Of_BALANCE = "출금 잔액이 부족 합니다.";
	public static final String NOT_ACCOUNT_OWNER = "계좌 소유자가 아닙니다.";
	

	//  User
	public static final String ENTER_YOUR_LOGIN = "로그인 먼저 해주세요.";
	public static final String ENTER_YOUR_USERNAME = "username을 입력해 주세요.";
	public static final String ENTER_YOUR_FULLNAME = "fullname을 입력해 주세요.";
	public static final String ENTER_YOUR_ACCOUNT_NUMBER = "계좌번호를 입력해 주세요.";
	public static final String ENTER_YOUR_PASSWORD = "패스워드를 입력해 주세요.";
	public static final String ENTER_YOUR_BALANCE = "금액을 입력해 주세요.";
	public static final String D_BALANCE_VALUE ="입금 금액이 0원 이하 일 수 없습니다.";
	public static final String W_BALANCE_VALUE ="출금 금액이 0원 이하 일 수 없습니다.";
	
	// etc 
	public static final String FAIL_TO_CREATE_USER = "회원가입 실패.";
	public static final String NOT_AN_AUTHENTICATED_USER = "인증된 사용자가 아닙니다.";
	public static final String INVALID_INPUT = "잘못된 입력입니다.";
	public static final String UNKNOWN = "알 수 없는 동작입니다";
	public static final String FAILED_PROCESSING = "정상 처리 되지 않았습니다.";
}

static 변수는 클래스 당 하나만 생성되며, 모든 인스턴스가 이 변수를 공유합니다. 따라서 같은 값을 반복적으로 사용할 때 메모리 사용을 최소화할 수 있습니다. 상수 값은 애플리케이션 전반에 걸쳐 변경되지 않고 반복적으로 사용되므로 final static을 사용하여 정의하는 것이 메모리를 효율적으로 사용하는 방법입니다.

 

💡Tip
웹 프로그래밍에서 전역 변수나 클래스의 공개(public) 인스턴스 변수를 사용하여 다른 사용자가 변경 가능하게 하는 것은 여러 가지 이유로 지양해야 합니다

보안 취약성, 데이터 무결성, 유지보수의 어려움 등 여러가지 이유가 있습니다 그중에 웹 애플리케이션은 대개
멀티스레드 환경에서 실행됩니다. 여러 스레드가 동일한 변수에 동시에 접근하고 변경할 수 있는 경우, 경쟁 조건(race condition)이 발생할 수 있습니다. 이는 데이터의 일관성과 애플리케이션의 안정성을 해칠 수 있습니다. 따라서, 공유 데이터에 대한 접근을 적절히 제어하고 동기화하는 것이 중요합니다.

 

 

 2. Controller 부분 코드 수정

controller/MainController.java 파일에 코드를 수정 합니다.

package com.tenco.bank.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller 
public class MainController {
	
	@GetMapping({ "/main-page", "/index", "/" })
	public String mainPage() {
		// mainPage.jsp 파일은 도전 과제 입니다. 
		// 여러분들이 원하는 디자인으로 프로젝트  
		// 종료 후에 직접 만들어 보세요 
		return "mainPage";
	}
	
}

 

 

controller/UserController.java 파일에 코드를 수정 합니다.

Define클래스 활용하여 하드코딩된 부분 수정 및 회원 가입 후 로그인 페이지 이동 처리 부분을 수정 합니다. 불필요 한 주석도 제거 해주세요

package com.tenco.bank.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tenco.bank.dto.SignUpDTO;
import com.tenco.bank.dto.SigninDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.service.UserService;
import com.tenco.bank.utils.Define;

import jakarta.servlet.http.HttpSession;

@Controller
@RequestMapping("/user") 
public class UserController {

	@Autowired
	private final UserService userService;
	@Autowired
	private final HttpSession session;

	public UserController(UserService userService, HttpSession session) {
		this.userService = userService;
		this.session = session;
	}

	/**
	 * 회원 가입 페이지 요청
	 * @return signUp.jsp 파일 리턴
	 */
	@GetMapping("/sign-up")
	public String signUpPage() {
		return "user/signUp";
	}

	/**
	 * 회원 가입 요청 처리 
	 * @param SignUpDTO
	 * @return 로그인 페이지 이동
	 */
	@PostMapping("/sign-up")
	public String signProc(SignUpDTO dto) {
		
		if (dto.getUsername() == null || dto.getUsername().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_USERNAME, HttpStatus.BAD_REQUEST);
		}

		if (dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}

		if (dto.getFullname() == null || dto.getFullname().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_FULLNAME, HttpStatus.BAD_REQUEST);
		}
		userService.createUser(dto);
	
		return "redirect:/user/sign-in";
	}

	/**
	 * 로그인 화면 요청 
	 */
	@GetMapping("/sign-in")
	public String signInPage() {
		return "user/signin";
	}


	/**
	 * 로그인 처리
	 * 
	 * @param signInFormDto
	 * @return 메인 페이지 이동  
	 * 
	 */
	@PostMapping("/sign-in")
	public String signInProc(SigninDTO dto) {

		if (dto.getUsername() == null || dto.getUsername().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_USERNAME, HttpStatus.BAD_REQUEST);
		}
		if (dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}
		User principal = userService.signIn(dto);
		session.setAttribute(Define.PRINCIPAL, principal);
		return "redirect:/main-page";
	}

	/**
	 * 로그 아웃 
	 * @return 로그인 페이지 이동 
	 */
	@GetMapping("/logout")
	public String logout() {
		session.invalidate();
		return "redirect:/user/sign-in";
	}

}

 

 

controller/AccountController.java 파일에 코드를 수정 합니다.

package com.tenco.bank.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tenco.bank.dto.AccountSaveDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.UnAuthorizedException;
import com.tenco.bank.repository.model.Account;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.service.AccountService;
import com.tenco.bank.utils.Define;

import jakarta.servlet.http.HttpSession;

@Controller 
@RequestMapping("/account")
public class AccountController {

	@Autowired
	private final HttpSession session;
	@Autowired
	private final AccountService accountService;

	public AccountController(HttpSession session, AccountService accountService) {
		this.session = session;
		this.accountService = accountService;
	}

	/**
	 * 계좌 목록 페이지
	 * 
	 * @param model - accountList
	 * @return list.jsp
	 */
	@GetMapping({ "/list", "/" })
	public String listPage(Model model) {
		
		User principal = (User) session.getAttribute(Define.PRINCIPAL);
		if (principal == null) {
			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
		}

		List<Account> accountList = accountService.readAccountListByUserId(principal.getId());

		if (accountList.isEmpty()) {
			model.addAttribute("accountList", null);
		} else {
			model.addAttribute("accountList", accountList);
		}

		return "account/list";
	}

	/**
	 * 계좌 생성 화면 요청 
	 * @return account/save.jsp 
	 */
	@GetMapping("/save")
	public String savePage() {
		User principal = (User) session.getAttribute(Define.PRINCIPAL);
		if (principal == null) {
			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
		}
		return "account/save";
	}
	
	/**
	 * 계좌 생성 처리 
	 * @param AccountSaveDTO
	 * @return 계좌 목록 화면 이동 
	 */
	@PostMapping("/save")
	public String saveProc(AccountSaveDTO dto) {
		
		User principal = (User) session.getAttribute(Define.PRINCIPAL);
		if (principal == null) {
			throw new UnAuthorizedException(Define.ENTER_YOUR_LOGIN, HttpStatus.UNAUTHORIZED);
		}
		if (dto.getNumber() == null || dto.getNumber().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_ACCOUNT_NUMBER, HttpStatus.BAD_REQUEST);
		}
		if (dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException(Define.ENTER_YOUR_PASSWORD, HttpStatus.BAD_REQUEST);
		}
		if (dto.getBalance() == null || dto.getBalance() <= 0) {
			throw new DataDeliveryException(Define.INVALID_INPUT, HttpStatus.BAD_REQUEST);
		}
		accountService.createAccount(dto, principal.getId());

		return "redirect:/account/list";
	}

}

 

 

3. Service 부분 코드 수정

service/UserService.java

package com.tenco.bank.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.tenco.bank.dto.SignUpDTO;
import com.tenco.bank.dto.SigninDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.repository.interfaces.UserRepository;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.utils.Define;

@Service  
public class UserService {

	@Autowired
	private final UserRepository userRepository;

	public UserService(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
	
	/**
	 * 회원 생성 서비스  
	 * @param SignUpDTO
	 */
	public void createUser(SignUpDTO dto) {
		int result = 0; 
		try {
			result = userRepository.insert(dto.toUser());
		} catch (DataAccessException e) {
			throw new DataDeliveryException(Define.INVALID_INPUT,HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			throw new RedirectException(Define.UNKNOWN, HttpStatus.SERVICE_UNAVAILABLE);
		}
		
		if (result != 1) {
			throw new DataDeliveryException(Define.FAIL_TO_CREATE_USER, HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}
	
	/**
	 * 로그인 서비스 
	 * @param SigninDTO
	 * @return userEntity or null 
	 */
	public User signIn(SigninDTO dto) {
		User userEntity = null; 
		try {
			userEntity = userRepository.findByUsernameAndPassword(dto.getUsername(), dto.getPassword());
		} catch (DataAccessException e) {
			throw new DataDeliveryException(Define.INVALID_INPUT,HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			throw new RedirectException(Define.UNKNOWN , HttpStatus.SERVICE_UNAVAILABLE);
		}
		// todo - 추후 수정 예정 
		// username, password 분리 예정 
		if(userEntity == null) {
			throw new DataDeliveryException("아이디 혹은 비번이 틀렸습니다",
					HttpStatus.BAD_REQUEST);
		}
		return userEntity;
	}

}

 

 

service/AccountService.java

package com.tenco.bank.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.tenco.bank.dto.AccountSaveDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.repository.interfaces.AccountRepository;
import com.tenco.bank.repository.model.Account;
import com.tenco.bank.utils.Define;

@Service
public class AccountService {

	@Autowired
	private final AccountRepository accountRepository;

	public AccountService(AccountRepository accountRepository) {
		this.accountRepository = accountRepository;
	}

	/**
	 * 계좌 생성 기능
	 * 
	 * @param dto
	 * @param pricipalId 
	 */
	@Transactional
	public void createAccount(AccountSaveDTO dto, Integer pricipalId) {
		try {
			accountRepository.insert(dto.toAccount(pricipalId));
		} catch (DataAccessException e) {
			throw new DataDeliveryException(Define.INVALID_INPUT, HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			throw new RedirectException(Define.UNKNOWN, HttpStatus.SERVICE_UNAVAILABLE);
		}
	}
	
	/**
	 * 사용자별 계좌 번호 조회 서비스 
	 * @param principalId
	 * @return List<Account> or Null 
	 */
	public List<Account> readAccountListByUserId(Integer principalId) {
		List<Account> accountListEntity = null;
		try {
			accountListEntity = accountRepository.findAllByUserId(principalId);
		} catch (DataAccessException e) {
			throw new DataDeliveryException(Define.INVALID_INPUT, HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			throw new RedirectException(Define.UNKNOWN, HttpStatus.SERVICE_UNAVAILABLE);
		}
		return accountListEntity;
	}

}

 

 

4. Repository 부분 코드 수정

repository/interfaces/UserReposity.java

package com.tenco.bank.repository.interfaces;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.tenco.bank.repository.model.User;

  
@Mapper  
public interface UserRepository {
	public int insert(User user);
	public int updateById(User user);
	public int deleteById(Integer id);
	public User findById(Integer id);
	public List<User> findAll();
	public User findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);
	
}

 

 

repository/interfaces/UserReposity.java

package com.tenco.bank.repository.interfaces;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.tenco.bank.repository.model.Account;

@Mapper  
public interface AccountRepository {
	
	public int insert(Account account);
	public int updateById(Account account);
	public int deleteById(Integer id); 
	public List<Account> findAllByUserId(@Param("userId") Integer principalId);
	public Account findByNumber(Integer id);
}

 

 

repository/interfaces/HistoryRepository.java

package com.tenco.bank.repository.interfaces;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.tenco.bank.repository.model.History;

@Mapper
public interface HistoryRepository {
	
	public int insert(History history);
	public int updateById(History history);
	public int deleteById(Integer id);
	public History findById(Integer id);
	public List<History> findAll();
}

 

 

 

5. GlobalControllerAdvice 코드 수정 (unAuthorizedException 메서드 수정)

package com.tenco.bank.handler;

 ... 생략 (GlobalControllerAdvice.java) 

	@ResponseBody
	@ExceptionHandler(UnAuthorizedException.class)
	public String unAuthorizedException(UnAuthorizedException e) {
		StringBuffer sb = new StringBuffer();

		sb.append("<script>");
		sb.append("alert('" + e.getMessage() + "');");
		// 코드 수정 
		sb.append("location.href='/user/sign-in';");
		sb.append("</script>");
		return sb.toString();
	}

}