Spring boot

[Spring boot] Bank App 만들어 보기 - 15. 회원 가입(트랜잭션, 예외 처리, H2 테이블 생성)

ekkkang 2025. 3. 5. 15:26

 

💡1. dto 설계 하기 및 개념 확인(사전 기반 지식)
     2. UserController, UserService 설계 및 유효성 검사, 예외 처리
     3. h2 스키마 및 초기 데이터 셋팅
     4. 회원 가입 화면 구현

 

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 lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

// SignUpFormDTO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignUpDTO {
	
	private String username;
	private String password; 
	private String fullname; 

//  ToDo 추후 진행 예정 	
//	private MultipartFile customFile; // name 속성과 일치 시켜야 함
//	private String originFileName; 
//	private String uploadFileName; 
//	private String eMail;
}

 

 

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
@RequestMapping("/user") // 대문 처리 
public class UserController {
	
	// 1단계 
	@Autowired // DI 처리 
	private UserService userService;
	// 2단계 
	//private final UserService userService;
	
	// 2단계
	// private final UserService userService
	// final 처리를 하는 이유는 (성능향상)
	// @Autowired 함께 표시 가능, 표시 하는 이유는 (가독성 향상)
	public UserController(UserService userService) {
		this.userService = userService;
	}
	
	/**
	 * 회원 가입 페이지 요청 
	 * 주소 설계 http://localhost:8080/user/sign-up
	 * @return signUp.jsp 파일 리턴
	 */
	@GetMapping("/sign-up")
	public String signUpPage() {
	   //   prefix: /WEB-INF/view/
	   //   suffix: .jsp
		return "user/signUp";
	}
	
	
	// 회원 가입 요청 처리 
	// 주소 설계 http://localhost:8800/user/sign-up
	// Get, Post -> sign-up 같은 도메인이라도 구분이 가능하다. 
	// REST API 를 사용하는 이유에 대해한번 더 살펴 보세요  
	@PostMapping("/sign-up")
	public String signProc(SignUpDTO dto) {
		
		// 1. 인증검사 x 
		// 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:/user/sign-up"; 
	}
	
	
}

 

 

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 final UserRepository userRepository;

	// 생성자 의존 주입 DI
	public UserService(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	// 회원 가입 처리 
	// 예외 처리 
	// DB 에서 연결이나 쿼리 실행, 제약 사항 위한 같은
	// 예외는 RuntimeException 으로 예외를 잡을 수 없습니다. 
	@Transactional // 트랜 잭션 처리 습관
	public void createUser(SignUpDTO dto) {
		// Http 응답으로 클라이언트에게 전달할 오류 메시지는 최소한으로 유지하고,
		// 보안 및 사용자 경험 측면에서 민감한 정보를 노출하지 않도록 합니다.
		int result = 0; 
		try {
			result = userRepository.insert(dto.toUser());
			// 여기서 예외 처리를 하면 상위 catch 블록에서 예외를 잡는다. 
		} catch (DataAccessException e) {
			// DataAccessException는 Spring의 데이터 액세스 예외 클래스로,
			// 데이터베이스 연결이나 쿼리 실행과 관련된 문제를 처리합니다.
			throw new DataDeliveryException("잘못된 처리 입니다",HttpStatus.INTERNAL_SERVER_ERROR);
		} catch (Exception e) {
			// 그 외 예외 처리 - 페이지 이동 처리 
			throw new RedirectException("알 수 없는 오류" , HttpStatus.SERVICE_UNAVAILABLE);
		}
		// 예외 클래스가 발생이 안되지만 프로세스 입장에서 예외 상황으로 바라 봄 
		if (result != 1) {
			// 삽입된 행의 수가 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

 

비밀번호 설정을 빈 문자열로 했기 때문에 Connect 눌러서 바로 접근 가능 합니다. 접속하면 우리가 사용할 스키마 및 초기 데이터가 전혀 없는 상태 입니다. 이번 예제에서 설정 할 수 있도록 연습해 봅시다.

 

💡 h2 스키마 및 초기 데이터 작업 순서
1. yml 파일에 서버가 매번 실행 될 때 마다 테이블을 생성하고 초기 데이터를 insert 할 수 있도록 설정
2. resourcese/db 패키지 생성 및 table.sql 파일 생성 data.sql 파일 생성
3. sql 쿼리문 입력 (복사 붙여넣기)
4. H2 인 메모리 접근 및 데이터 확인

 

1. yml … 설정 (인메모리H2설정)

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  # JDBC 드라이버 클래스를 설정합니다.
    username: sa  # 데이터베이스 연결을 위한 사용자 이름을 설정합니다.
    password: ''  # 데이터베이스 연결을 위한 비밀번호를 설정합니다. 여기서는 비밀번호를 빈 문자열로 설정했습니다.
  sql:  # <---- 코드를 추가 
    init:
      schema-locations:
      - classpath:db/table.sql  
      data-locations:
      - classpath:db/data.sql

 

(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

 

 

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

 

 

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

 

table.sql 파일에 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(20) not null, 
    balance bigint not null comment '계좌잔액',
    user_id int, 
    created_at timestamp not null default now()
);

-- 계좌 사용 내역 테이블 - 입금, 출금, 이체 
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 파일에 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());

select * from user_tb;

-- 기본 계좌 등록 
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());        

select * from account_tb;

-- 1번계좌 1000원 
-- 2번계좌 1000원 
-- 3번계좌 0원 

-- 이체 내역을 기록 
-- (1번 계좌에서 2번계좌로 100원 이체한다) 
insert into history_tb(amount, w_balance, 
			d_balance, w_account_id, d_account_id, created_at)
values(100, 900, 1100, 1, 2, now());

-- ATM 출금만! 1번 계좌에서 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());

-- 입금 내역만 ! (1번계좌에 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());

 

 

4. H2 인 메모리 접근 및 데이터 확인 - 로컬 서버 재 실행

 

 

 

4. 회원 가입 화면 구현

이번 프로젝트에서는 프론트엔드 부분을 가능한 한 간략하게 설계합니다. 대신 로직 작성에 집중합니다. 브라우저에서 서버측으로 데이터를 전송하는 폼 화면을 구현해 보겠습니다. 또한 MIME 타입이라는 개념은 중요하기 때문에 실습 코드 이후에 개념을 따로 조사해 보도록 합시다.

 

완성 화면 샘플

 

 

 

부트스트랩 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 태그에서 코드를 복사해서 가져 옵니다.

 

 

2.Bootstrap 4 Form 코드 복사

 

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(메인 영역)   -->
<div class="col-sm-8">
	<h2>회원 가입</h2>
	<h5>Bank App 오신 걸 환영합니다.</h5>
	<form action="/user/sign-up" method="post">
		<div class="form-group">
			<label for="username">Username:</label>
			<input type="text" class="form-control" placeholder="Enter username" id="username" name="username" value="cos">
		</div>
		<div class="form-group">
			<label for="pwd">Password: </label>
			<input type="password" class="form-control" placeholder="Enter password" id="pwd" name="password" value="1234">
		</div>
		<div class="form-group">
			<label for="fullname">Fullname: </label>
			<input type="text" class="form-control" placeholder="Enter fullname" id="fullname" name="fullname" value="spring">
		</div>
		<div class="text-right">
			<button type="submit" class="btn btn-primary">회원가입</button>
		</div>
	</form>
</div>
</div>
</div>

<!-- end of content.jsp(메인 영역)   -->

<!-- footer.jsp -->
<%@ include file="/WEB-INF/view/layout/footer.jsp"%>
  • 도전 과제로 - 스프링 부트 기본 파싱전략이 어떻게 되는지 조사해보세요
  • 반드시 name 속성을 넣어 주어야 합니다.
  • 코드 테스트 및 개발 단계에 기본값을 넣어 둘 수 있도록 습관을 가져 보세요

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

 

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

 

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