본문 바로가기

Spring boot/Bank App 만들기(deployment)

Bank App 만들기 ( deployment ) - 회원 가입( 트랜잭션, 예외 처리, H2 테이블 생성 )

1. 사전 기반 지식

DTO 패키지와 Model 패키지를 분리하는 것이 바람직하다.

💡 DTO(Data Transfer Object)와 모델 클래스를 분리하여 패키지를 만드는 것이 좋다. 그 이유는 다음과 같다

1. 코드의 가독성 및 유지 보수성
DTO와 모델 클래스를 별도의 패키지로 구분함으로써 코드의 구조가 명확해지고, 관련 클래스를 찾기 쉬워진다. 이를 통해 유지 보수성이 향상된다.

2. 객체의 역할 구분: 모델 클래스는 데이터베이스의 테이블 구조를 표현하는 반면, DTO는 클라이언트와 서버 간의 데이터 전송을 담당한다. 이 두 객체의 역할이 다르기 때문에, 별도의 패키지로 구분하는 것이 좋다.

3. 유연한 변경: 애플리케이션의 요구 사항이 변경되면 DTO와 모델 클래스의 변경이 독립적으로 이루어질 수 있다. 이렇게 구조를 분리해 놓으면, 한쪽의 변경이 다른 쪽에 영향을 미치는 것을 최소화할 수 있다.

따라서, 코드의 가독성, 유지 보수성 및 유연성을 높이기 위해 DTO 패키지와 모델 패키지를 따로 구성하는 것이 좋다.

 

 

우리는 스프링 부트 Web MVC 프레임워크로 개발중에 있다.

💡 자주 듣게 되는 Layer 라는 의미

Web Layer : Rest API를 제공하며, Client 중심의 로직 적용 Business Layer : 내부 정책에 따른 logic를 개발하며, 주로 핵심업무 부분을 개발 Data Layer : 데이터 베이스 및 외부와의 연동 처리

서비스는 일반적으로 비즈니스 로직을 수행하는 클래스이다.

비즈니스 로직(Business Logic)이란, 애플리케이션에서 수행되는 비즈니스 규칙이나 프로세스 등을 의미한다. 즉, 애플리케이션의 핵심적인 업무 처리를 담당하는 로직을 말한다.

예를 들어, 온라인 쇼핑몰에서 주문을 처리하는 기능은 주문 상태를 변경하거나, 재고 수량을 업데이트하는 등의 처리 과정을 수행한다. 이러한 과정들이 바로 비즈니스 로직에 해당된다. 또한, 비즈니스 로직은 애플리케이션에서 수행되는 모든 로직 중에서 가장 중요한 부분으로, 애플리케이션의 성격과 특성에 따라 다양한 형태로 구현될 수 있다.

Spring Boot에서 비즈니스 로직은 주로 서비스(Service) 레이어에서 구현된다. 이를 통해 비즈니스 로직을 분리하여 컨트롤러(Controller)나 뷰(View)와 같은 다른 레이어와 분리하여 개발하고, 애플리케이션의 유지 보수성과 확장성을 높일 수 있고 트랜잭션 관리도 가능 한다.

 

 

트랜잭션에 이해

💡 트랜잭션(Transaction)은 데이터베이스(Database)에서 수행되는 작업의 단위를 의미한다. 즉, 데이터베이스에서 데이터를 읽거나 쓰는 작업을 수행할 때, 한 번에 실행되어야 하는 일련의 작업을 의미한다.

트랜잭션은 ACID라는 성질을 갖는다. ACID는 원자성(Atomicity), 일관성(Consistency), 고립성(Isolation), 지속성(Durability)의 약어이다. 이 중에서 원자성은 트랜잭션이 성공하거나 실패할 때, 모든 작업이 반영되거나, 아무 것도 반영되지 않아야 한다는 것을 의미한다. 즉, 트랜잭션에서 한 번에 처리되어야 하는 작업들은 모두 완전히 수행되거나, 아예 수행되지 않아야 한다.

예를 들어, 은행에서 계좌 이체를 처리하는 작업을 수행할 때, 계좌에서 출금하는 작업과 입금하는 작업은 원자성을 가져야 한다. 즉, 출금 작업이 완전히 수행되지 않았을 경우, 입금 작업도 반영되지 않아야 한다.

Spring Boot에서는 트랜잭션을 처리하기 위해 @Transactional 어노테이션을 제공한다. 이 어노테이션을 사용하면, 트랜잭션 범위 내에서 실행되는 모든 작업이 원자성을 갖도록 보장할 수 있다. 또한, 트랜잭션의 고립성, 일관성, 지속성 등의 ACID 성질을 보장하기 위해 다양한 설정 옵션을 제공한다.

일관성(Consistency): 트랜잭션이 실행을 성공적으로 완료하면 데이터베이스는 일관된 상태를 유지해야 한다. 즉, 트랜잭션이 적용된 후에도 데이터베이스는 일관된 규칙에 따라 유효한 상태여야 한다.

고립성(Isolation): 동시에 실행되는 여러 트랜잭션이 서로 간섭하지 않도록 보장해야 합니다. 즉, 하나의 트랜잭션이 실행되는 동안 다른 트랜잭션의 작업에 영향을 받지 않아야 합니다.

지속성(Durability): 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 반영되어야 한다. 즉, 시스템이 고장나더라도 트랜잭션의 결과는 영구적으로 보존되어야 한다.

 

 

 

 

2. UserController, UserService 설계 및 유효성 검사, 예외 처리

SignUpDTO
package com.tenco.bank.dto;

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

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class SignUpDTO {
	
	private String username; 
	private String password; 
	private String fullname;
	
	// 2단계 로직 - User Object 반환 
	public User toUser() {
		return User.builder()
				.username(this.username)
				.password(this.password)
				.fullname(this.fullname)
				.build();
	} 
	
	// todo - 추후 사진 업로드 기능 추가 예정 	
}

 

 

 

 

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.SignUpDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.service.UserService;

@Controller // IoC에 대상(싱글톤 패턴으로 관리됨) 
@RequestMapping("/user") // 대문 처리 
public class UserController {
	
	@Autowired // DI 처리 
	private UserService userService;

	/**
	 * 회원 가입 페이지 요청 
	 * 주소 설계 : http://localhost:8080/user/sign-up
	 * @return signUp.jsp 
	 */
	@GetMapping("/sign-up")
	public String signUpPage() {
		// prefix: /WEB-INF/view/
		// return: user/signUp
		// suffix: .jsp 
		return "user/signUp";
	}
	
	/**
	 * 회원 가입 로직 처리 요청
	 * 주소 설계 : http://localhost:8080/user/sign-up
	 * @param dto
	 * @return
	 */
	@PostMapping("/sign-up")
	public String signUpProc(SignUpDTO dto) {
		
		// controller 에서 일반적이 코드 작업 
		// 1. 인증검사 (여기서는 인증검사 불 필요) 
		// 2. 유효성 검사 
		if(dto.getUsername() == null || dto.getUsername().isEmpty()) {
			throw new DataDeliveryException("username을 입력 하세요", HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException("password을 입력 하세요", HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getFullname() == null || dto.getFullname().isEmpty()) {
			throw new DataDeliveryException("fullname을 입력 하세요", HttpStatus.BAD_REQUEST);
		}
		// 서비스 객체로 전달 
		userService.createUser(dto);
		// TODO - 추후 수정 
		return "redirect:/index";
	}
}

 

 

 

UserService
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.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.repository.interfaces.UserRepository;

@Service // IoC 대상( 싱글톤으로 관리) 
public class UserService {
	
	@Autowired
	private UserRepository userRepository;
	
//  @Autowired 어노테이션으로 대체 가능 하다.
//  생성자 의존 주입 - DI 	
//	public UserService(UserRepository userRepository) {
//		this.userRepository = userRepository;
//	}
	
	/**
	 * 회원 등록 서비스 기능
	 * 트랜잭션 처리  
	 * @param dto
	 */
	@Transactional // 트랜잭션 처리는 반드시 습관화 
	public void createUser(SignUpDTO dto) {
		int result = 0; 
		try {
			result = userRepository.insert(dto.toUser());
		} catch (DataAccessException e) {
			throw new DataDeliveryException("잘못된 처리입니다", HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			throw new RedirectException("알 수 없는 오류", HttpStatus.SERVICE_UNAVAILABLE);
		}
		if(result != 1) {
			throw new DataDeliveryException("회원가입 실패", HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}
}

 

 

 

user.xml 파일 반드시 수정
	<insert id="insert">
		insert into user_tb(username, password, fullname, created_at) 
		values( #{username}, #{password}, #{fullname}, now())
	</insert>

 

 

 

테스트 값 입력 및 검증하기 (탤런트 API, 포스트 맨 활용)

 

 

 

 

3. h2 스키마 및 초기 데이터 셋팅 

💠 h2 로컬 DB 사용 중(개발시) 확인 사항

build.gradle 파일에 의존성 설정 확인 - runtimeOnly 'com.h2database:h2’

yml db 설정 확인

url: jdbc:h2:mem:bankdb;MODE=MySQL  # 데이터베이스 연결을 위한 URL을 설정합니다.
driver-class-name: org.h2.Driver  # JDBC 드라이버 클래스를 설정합니다.
username: sa  # 데이터베이스 연결을 위한 사용자 이름을 설정합니다.
password: ''  # 데이터베이스 연결을 위한 비밀번호를 설정합니다. 여기서는 비밀번호를 빈 문자열로 설정했습니다.

h2 DB 접근 방법

로컬 서버 실행

브라우저에서 주소 입력 http://localhost:8080/h2-console

 

 

h2 스키마 및 초기 데이터 작업 순서

1. yml 파일에 서버가 매번 실행 될 때 마다 테이블을 생성하고 초기 데이터를 insert 할 수 있도록 설정

2. resourcese/db 패키지 생성 및 table.sql 파일 생성 data.sql 파일 생성

3. sql 쿼리문 입력 (복사 붙여넣기)

4. H2 인 메모리 접근 및 데이터 확인

 

 

 

 

table.sql
create table user_tb(
	id int auto_increment primary key, 
    username varchar(50) not null unique, 
	password varchar(100) not null, 
	fullname varchar(50) not null, 
    created_at timestamp not null default now()
); 

create table account_tb(
	id int auto_increment primary key, 
    number varchar(30) not null unique, 
    password varchar(30) not null, 
    balance bigint not null comment '계좌잔액', 
    created_at timestamp not null default now(), 
	user_id int 
);

create table history_tb(
	id int auto_increment primary key comment '거래내역 ID',
	amount bigint not null comment '거래금액',
    w_account_id int comment '출금 계좌 ID',
    d_account_id int comment '입금 계좌 ID',
    w_balance bigint comment '출금 요청 후 계좌 잔액',
    d_balance bigint comment '입금 요청 후 계좌 잔액', 
    created_at timestamp not null default now() 
);

 

 

 

 

data.sql
insert into user_tb(username, password, fullname, created_at)
values('길동', '1234', '고', now());

insert into user_tb(username, password, fullname, created_at)
values('둘리', '1234', '애기공룡', now());

insert into user_tb(username, password, fullname, created_at)
values('마이', '1234', '콜', now());


insert into account_tb
		(number, password, balance, user_id, created_at)
values('1111', '1234', 1300, 1, now());        

insert into account_tb
		(number, password, balance, user_id, created_at)
values('2222', '1234', 1100, 2, now());        

insert into account_tb
		(number, password, balance, user_id, created_at)
values('3333', '1234', 0, 3, now());


insert into history_tb(amount, w_balance, d_balance, w_account_id, d_account_id, created_at)
			values(100, 900, 1100, 1, 2, now());

-- 2. ATM 기기에서 출금 
-- 1111 계좌에서 100원만 출금하는 히스토리를 만들어 보세요 
insert into history_tb(amount, w_balance, d_balance, w_account_id, d_account_id, created_at)
			values(100, 800, null, 1, null, now());

-- 3. ATM 기기에서 입금 
-- 1111 계좌로 500원만 입금하는 히스토리를 만들어 보세요 
insert into history_tb(amount, w_balance, d_balance, w_account_id, d_account_id, created_at)
			values(500, null, 1300, null, 1, now());

 

 

 

 

(mysql설정 - 의존성 충돌시 로컬 DB로 사용하자)
spring:
  mvc:
    view:
      prefix: /WEB-INF/view/
      suffix: .jsp
  servlet:
    multipart:
      max-file-size: 20MB #파일 용량 설정 최대 20MB   
      max-request-size: 20MB
  datasource:
    url: jdbc:mysql://localhost:3306/mybank?serverTimeZone=Asia/Seoul
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: asd1234

 

 

 

 

yml 파일 확인
server:
  port: 8080 #서버가 사용할 포트 번호 설정 
  servlet:
    encoding:
      charset: utf-8 #서블릿의 응답과 요청 인코딩을 UTF-8 로 설정 
      force: true # 요청과 응답에 대해 이 인코딩을 강제로 사용하도록 설정합니다. 
      
spring:
  mvc:
    view: 
      prefix: /WEB-INF/view/ #JSP파일이 위치한 디렉토리 접두사를 설정합니다.
      suffix: .jsp #뷰 이름에 자동으로 추가될 파일 확장자를 설정합니다.
  datasource:
    url: jdbc:h2:mem:bankdb;MODE=MySQL  #데이터 베이스 연결을 위한 URL을 설정 합니다.
    driver-class-name: org.h2.Driver #드라이버 클래스를 설정 합니다.
    username: sa #사용자 ID를 지정
    password: '' #DB 비밀번호 여기서는 빈 문자열로 설정
  sql:
    init:
      schema-locations:
      - classpath:db/table.sql
      data-locations:
      - classpath:db/data.sql 
  
  h2:
    console:
      enabled: true #H2 데이터 베이스 콘솔을 활성화 합니다.   
  
  output:
    ansi:
      enabled: always #콘솔 출력에 ANSI 색상 코드를 사용할 수 있도록 설정 
      
#mybatis 설정
mybatis:
  mapper-locations:
    - classpath:mapper/**/*.xml  #MyBatis 매퍼 파일 위치를 설정합니다. **은 모든 디렉토리, *.xml 은 모든 XML 파일을 의미합니다.
  configuration:
    map-underscore-to-camel-case: true #데이터베이스의 언더스코어 네이밍(column_name)을 카멜 케이스(columnName)로 자동 매핑합니다.
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #SQL 로깅 구현체를 설정합니다.        

logging:
  level:
    org.apache.ibatis: DEBUG #MyBatis 로깅 레벨을 DEBUG로 설정하여 실행되는 SQL 쿼리와 내부 로깅 정보를 콘솔에 출력합니다.

 

 

 

 

@Autowired 오류 때문에 코드 수정

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.SignUpDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.service.UserService;

@Controller // IoC에 대상(싱글톤 패턴으로 관리됨) 
@RequestMapping("/user") // 대문 처리 
public class UserController {
	
	 
	private UserService userService;
	
	// DI 처리 
	@Autowired // 노란색 경고는 사용할 필요 없음 - 가독성 위해서 선언해도 됨
	public UserController(UserService service) {
		this.userService = service;
	}
	
	/**
	 * 회원 가입 페이지 요청 
	 * 주소 설계 : http://localhost:8080/user/sign-up
	 * @return signUp.jsp 
	 */
	@GetMapping("/sign-up")
	public String signUpPage() {
		// prefix: /WEB-INF/view/
		// return: user/signUp
		// suffix: .jsp 
		return "user/signUp";
	}
	
	/**
	 * 회원 가입 로직 처리 요청
	 * 주소 설계 : http://localhost:8080/user/sign-up
	 * @param dto
	 * @return
	 */
	@PostMapping("/sign-up")
	public String signUpProc(SignUpDTO dto) {
		
		// controller 에서 일반적이 코드 작업 
		// 1. 인증검사 (여기서는 인증검사 불 필요) 
		// 2. 유효성 검사 
		if(dto.getUsername() == null || dto.getUsername().isEmpty()) {
			throw new DataDeliveryException("username을 입력 하세요", HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
			throw new DataDeliveryException("password을 입력 하세요", HttpStatus.BAD_REQUEST);
		}
		
		if(dto.getFullname() == null || dto.getFullname().isEmpty()) {
			throw new DataDeliveryException("fullname을 입력 하세요", HttpStatus.BAD_REQUEST);
		}
		
		// 서비스 객체로 전달 
		userService.createUser(dto);
		
		// TODO - 추후 수정 
		return "redirect:/index";
	}
}

 

 

 

 

UserService 코드 수정
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.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.repository.interfaces.UserRepository;

@Service // IoC 대상( 싱글톤으로 관리) 
public class UserService {
	
	
	private UserRepository userRepository;
	
//  @Autowired 어노테이션으로 대체 가능 하다.
//  생성자 의존 주입 - DI 	
	@Autowired
	public UserService(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
	
	/**
	 * 회원 등록 서비스 기능
	 * 트랜잭션 처리  
	 * @param dto
	 */
	@Transactional // 트랜잭션 처리는 반드시 습관화 
	public void createUser(SignUpDTO dto) {
		int result = 0; 
		try {
			result = userRepository.insert(dto.toUser());
		} catch (DataAccessException e) {
			throw new DataDeliveryException("잘못된 처리입니다", HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			throw new RedirectException("알 수 없는 오류", HttpStatus.SERVICE_UNAVAILABLE);
		}
		if(result != 1) {
			throw new DataDeliveryException("회원가입 실패", HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}	
}

 

 

 

 

 

 

4. 회원 가입 화면 구현

완성 화면 샘플 - 부트스트랩 4 Form 태그 활용

https://www.w3schools.com/bootstrap4/bootstrap_forms.asp

 

W3Schools.com

W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.

www.w3schools.com

 

 


 

signUp.jsp 파일을 생성

signUp.jsp 작업 순서

1. mainPage.jsp 파일에서 코드를 복사후에 signUp.jsp 파일로 붙여넣기

2. Bootstrap 4 Form 태그에서 코드를 복사해서 가져오기

 

 

 

user/signUp.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!-- header.jsp  -->
<%@ include file="/WEB-INF/view/layout/header.jsp"%>

<!-- start of content.jsp(xxx.jsp)   -->
<div class="col-sm-8">
	<h2>회원 가입</h2>
	<h5>Bank App에 오신걸 환영합니다</h5>
	
	<form action="/">
		<div class="form-group">
			<label for="username">username:</label>
			<input type="text" class="form-control" placeholder="Enter username" id="username" name="username">
		</div>
		<div class="form-group">
			<label for="pwd">Password:</label>
			<input type="password" class="form-control" placeholder="Enter password" id="pwd" name="password">
		</div>
		<div class="form-group">
			<label for="fullname">fullname:</label>
			<input type="text" class="form-control" placeholder="Enter fullname" id="fullname" name="fullname">
		</div>
		<button type="submit" class="btn btn-primary">회원가입</button>
	</form>

</div>
<!-- end of col-sm-8  -->
</div>
</div>
<!-- end of content.jsp(xxx.jsp)   -->

<!-- footer.jsp  -->
<%@ include file="/WEB-INF/view/layout/footer.jsp"%>

 

 

 

 

 

회원 가입 결과 확인 및 오류 테스트

username 컬럼에는 DB 제약 사항인 유니크 키가 걸려 있다.

 

 

 

 

동일한 username 으로 가입 재 요청시 오류 확인

 

728x90