Spring boot

Bank App 만들기 - 중간 리팩토링

ryeonng 2024. 8. 13. 17:26

 

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

 

 

리팩토링의 목적

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

 

리팩토링의 원칙

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

 

리팩토링의 예

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

 

Define.java 클래스 만들기 (상수로 만들자)
package com.tenco.bank.utils;

public class Define {
	//  상수
	public static final String PRINCIPAL = "principal";
	
	// 이미지 관련
	public static final String UPLOAD_FILE_DIRECTORY = "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을 사용하여 정의하는 것이 메모리를 효율적으로 사용하는 방법이다.

 

 

UserController 수정
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.SignInDTO;
import com.tenco.bank.dto.SignUpDTO;
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 // IoC의 대상 (싱글톤 패턴으로 관리)
@RequestMapping("/user") // 대문 처리
public class UserController {

	private UserService userService;
	private final HttpSession session; // final : 초기화 필요 > HttpSession session
	
	@Autowired // 노란색 경고는 사용할 필요 없음 - 가독성 위해서 선언해도 된다.
	public UserController(UserService service, HttpSession session) {
		this.userService = service;
		this.session = session;
	}
	
	/**
	 * 회원 가입 페이지 요청
	 * 주소설계 - http://localhost:8080/user/sign-up
	 * @return signUp.jsp
	 */
	@GetMapping("/sign-up")
	public String signUpPage() {
		// prefix : WEB-INF/view/
		// suffix : .jsp
		return "user/signUp"; // user폴더의 signUp.jsp 파일을 불러온다.
	}

	/**
	 * 회원가입 로직 처리 요청 (기능 요청)
	 * 주소설계 - http://localhost:8080/user/sign-up
	 * @param dto
	 * @return
	 */
	@PostMapping("/sign-up")
	public String signUpProc(SignUpDTO dto) { // dto와 매핑
		System.out.println("test : " + dto.toString());
		// * controller에서 일반적인 코드 작업
		// 1. 인증 검사 (여기서는 인증검사 불필요 - 회원가입에는 로그인 유무가 필요없다.)
		
		// 2. 유효성 검사
		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);
		
		// TODO - 추후 수정
		return "redirect:/user/sign-in";
	}
	
	/**
	 * 로그인 화면 요청
	 * 주소설계 - http://localhost:8080/user/sign-in
	 * @return
	 */
	@GetMapping("/sign-in")
	public String signInPage() {
		// 인증검사,유효성검사 <- 로그인 화면은 필요 없다.
		return "user/signIn"; 
	}
	
	/**
	 * 로그인 요청 처리
	 * 주소설계 - http://localhost:8080/user/sign-in
	 * @return
	 */
	@PostMapping("/sign-in")
	public String signProc(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.readUser(dto);
		// 세션 메모리에 등록 처리
		session.setAttribute(Define.PRINCIPAL, principal); // User Object 값이 담김
		// 새로운 페이지로 이동 처리
		// TODO - 계좌 목록 페이지 이동 처리 예정
		return "redirect:/account/list";
	}
	
	// 코드흐름 : 유저컨트롤러 포스트로 보냄 > 유저 서비스로 > 레파지토리 > xml > 세션 등록 > 리다이렉트처리
	
	/**
	 * 로그아웃 처리
	 * @return
	 */
	@GetMapping("/logout")
	public String logout() {
		session.invalidate(); // 호출 시 로그아웃 처리 됨
		return "redirect:/user/sign-in";
	}
	
}

 

AccountController
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.SaveDTO;
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 // IoC대상 (싱글톤으로 관리)
@RequestMapping("/account") // 대문열기 
public class AccountController {

	// 계좌 생성 화면 요청 - DI 처리
	private final HttpSession session;
	private final AccountService accountService; // 멤버변수 선언 시 final 사용하면 성능적으로 더 낫다.
	
	// @Autowired
	public AccountController(HttpSession session, AccountService accountService) {
		this.session = session;
		this.accountService = accountService;
	}
	
	/**
	 * 계좌 생성 페이지 요청
	 * 주소설계 - http://localhost:8080/account/save
	 * @return save.jsp
	 */
	@GetMapping("/save")
	public String savePage() {
		
		// 1. 인증 검사 필요(account 전체가 필요하다.)
		User principal = (User)session.getAttribute(Define.PRINCIPAL);
		if(principal == null) { // 로그인 하지 않았다면
			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
		} 
		return "account/save";
	}

	/**
	 * 계좌 생성 기능 요청
	 * 주소설계 - http://localhost:8080/account/save
	 * @return : 추후 계좌 목록 페이지로 이동 처리
	 */
	@PostMapping("/save")
	public String saveProc(SaveDTO dto) {
		// 1. form 데이터 추출 (파싱 전략)
		// 2. 인증 검사
		User principal = (User)session.getAttribute(Define.PRINCIPAL);
		if(principal == null) { // 로그인 하지 않았다면
			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
		}
		
		// 3. 유효성 검사
		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.ENTER_YOUR_BALANCE, HttpStatus.BAD_REQUEST);
		}
		
		// 4. 서비스 호출
		accountService.createAccount(dto, principal.getId());
		
		return "redirect:/index";
	}
	
	/**
	 * 계좌 목록 페이지 요청
	 * 주소설계 - http://localhost:8080/account/list , ..../
	 * @return
	 */
	@GetMapping({"/list", "/"}) // url 매핑을 두 개 설정 가능하다.
	public String listPage(Model model) { // TODO - 검색 기능 + 페이징 처리 추가 가능
		
		// 1. 인증검사
		User principal = (User)session.getAttribute(Define.PRINCIPAL);
		if(principal == null) {
			throw new UnAuthorizedException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.UNAUTHORIZED);
		}
		
		// 2. 유효성 검사 - 추출할 데이터 없으므로 아직 필요 x
		
		// 3. 서비스 호출
		List<Account> accountList = accountService.readAccountListByUserId(principal.getId());
		if(accountList.isEmpty()) { // 리스트는 있으나, 값이 비어있는 경우
			model.addAttribute("accountList", null); // 값은 null
		} else {
			model.addAttribute("accountList", accountList);
		}
		// JSP에 데이터를 넣어주는 기술 - set/getAttribute
		return "account/list";
	}
	
}

 

GlobalControllerAdvice 코드 수정 (unAuthorizedException 메서드 수정)
	@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(); 
	}