1. IntelliJ 사전 준비
- spring devtools의 auto-restart 기능 활성화
- Advanced Settings → Compiler → ‘Allow auto-make to start…’ 체크

- ‘auto’ 검색 → Build, Execution, Deployment → Build Tools → Compiler
- Build project automatically 체크

2. New Project → Git Clone
- Menu → File → New → Project from Version Control 클릭

- 저장할 위치 지정 : pro_lec

- \폴더명을 추가로 적어 새 폴더 안에 프로젝트 생성

- 예제 링크
git clone 'repository_clone.git'
git remote rm origin
git remote -v
git remote add origin 'myrepository.git'
git push origin master
3. 화면 구성
- Bootstrap 5 사용
- 기본으로 제공되는 CSS Library 및 배치툴 사용
1️⃣ home

2️⃣ 회원 가입

3️⃣ 로그인

4️⃣ 계좌 생성

5️⃣ 계좌 목록

6️⃣ 계좌 이체

4. 화면 연결 (임시)
- application.properties 복사 / 붙여넣기
- HelloController : 연결 확인 (임시)
package com.metacoding.bankv1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("/t1")
public String t1() {
return "home";
}
@GetMapping("/t2")
public String t2() {
return "account/detail";
}
@GetMapping("/t3")
public String t3() {
return "account/list";
}
@GetMapping("/t4")
public String t4() {
return "account/save-form";
}
@GetMapping("/t5")
public String t5() {
return "account/transfer-form";
}
@GetMapping("/t6")
public String t6() {
return "user/join-form";
}
@GetMapping("/t7")
public String t7() {
return "user/login-form";
}
}
5. 테이블 설계
- 테이블 정보는 README.md에 함께 기록
- README.md
# Bank Table Setting
```sql
create table user_tb
(
created_at timestamp(6),
username varchar(12) not null unique,
fullname varchar(255) not null,
id varchar(255) generated by default as identity,
password varchar(255) not null,
primary key (id)
)
```
```sql
create table account_tb
(
balance integer,
number integer not null,
user_id integer,
created_at timestamp(6),
password varchar(255),
primary key (number)
)
```
```sql
create table history_tb
(
amount integer,
withdraw_balance integer,
deposit_number integer,
id integer generated by default as identity,
withdraw_number integer,
created_at varbinary(255),
primary key (id)
)
```
- Table Annotations & Importments
- 각 테이블 JPA 클래스에 추가 (테이블명은 맞추기)
JPA class (Table)
User
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true, nullable = false, length = 12)
private String username;
@Column(nullable = false, length = 12)
private String password;
@Column(nullable = false)
private String fullname;
private Timestamp createdAt; // 생성날짜 (insert 된 시간)
}
Account
public class Account {
@Id
private Integer number; // 계좌번호 PK
private String password;
private Integer balance; // 잔액
private Integer userId; // FK
private Timestamp createdAt;
}
History
public class History {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Integer withdrawNumber; // 1111 (FK)
private Integer depositNumber; // 2222 (FK)
private Integer amount;
private Integer withdrawBalance;
private Timestamp createdAt;
}
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.sql.Timestamp;
@NoArgsConstructor
@Getter
@Table(name = "table_name")
@Entity
- data.sql : dummy data
insert into user_tb(username, password, fullname, created_at) values('ssar', '1234', '쌀', now());
insert into user_tb(username, password, fullname, created_at) values('cos', '1234', '코스', now());
insert into account_tb(number, password, balance, user_id, created_at) values(1111, '1234', 900, 1, now());
insert into account_tb(number, password, balance, user_id, created_at) values(2222, '1234', 1100, 1, now());
insert into account_tb(number, password, balance, user_id, created_at) values(3333, '1234', 1000, 2, now());
insert into history_tb(withdraw_number, deposit_number, amount, withdraw_balance, created_at)
values(1111, 2222, 100, 900, now());
insert into history_tb(withdraw_number, deposit_number, amount, withdraw_balance, created_at)
values(1111, 3333, 100, 800, now());
insert into history_tb(withdraw_number, deposit_number, amount, withdraw_balance, created_at)
values(3333, 1111, 100, 1000, now());
결과
user_tb

account_tb

history_tb

6. 기능 설계
1️⃣ 회원가입
화면
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1 ~ 5) ex)mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<h1>회원가입 페이지</h1>
<form action="/join" method="POST">
<div class="mb-3 mt-3">
<label>Username:</label>
<input type="text" class="form-control" placeholder="Enter username.." name="username">
</div>
<div class="mb-3">
<label>Password:</label>
<input type="password" class="form-control" placeholder="Enter password.." name="password">
</div>
<div class="mb-3">
<label>Full name:</label>
<input type="text" class="form-control" placeholder="Enter fullname.." name="fullname">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</div>
{{>layout/footer}}
Codes
UserController
@GetMapping("/join-form")
public String joinForm() {
return "user/join-form";
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
// 유효성 검사
if(joinDTO.getUsername().length() > 12) throw new RuntimeException("Username too long");
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
UserService
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
// 1. 동일 Username 있는지 검사
User user = userRepository.findByUsername(joinDTO.getUsername());
// 2. 있으면, exception
if (user != null) {
throw new RuntimeException("동일한 Username이 존재합니다.");
}
// 3. 없으면 추가
else userRepository.save(joinDTO.getUsername(),joinDTO.getPassword(),joinDTO.getFullname());
}
UserRepository
public User findByUsername(String username) {
Query query = em.createNativeQuery("select * from user_tb where username = ?", User.class);
query.setParameter(1, username);
try{
return (User)query.getSingleResult();
} catch (Exception e) {
return null;
}
}
// 회원가입
public void save(String username, String password, String fullname) {
Query query = em.createNativeQuery("insert into user_tb(username, password, fullname, created_at) values(?, ?, ?, now())");
query.setParameter(1, username);
query.setParameter(2, password);
query.setParameter(3, fullname);
query.executeUpdate();
}
UserRequest
@Data
public static class JoinDTO {
private String username;
private String password;
private String fullname;
}
결과
정상 작동
- Data successfully inserted & Redirect to login page

Exception
- Username이 12글자를 넘어갈 때 (유효성 검사)

- Username이 중복될 경우

2️⃣ 로그인
화면
- 세션을 활용 (sessionUser 값) : 세션 ID로 받은 값에 따라 다른 기능 제공
- = Validated (인증)
로그인 페이지
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1 ~ 5) ex)mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<h1>로그인 페이지</h1>
<form action="/login" method="POST">
<div class="mb-3 mt-3">
<label>Username:</label>
<input type="text" class="form-control" placeholder="Enter username.." name="username">
</div>
<div class="mb-3">
<label>Password:</label>
<input type="password" class="form-control" placeholder="Enter password.." name="password">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
</div>
{{>layout/footer}}
메인화면 페이지
- 로그인 시에만 회원 정보를 출력 : mustache의 조건문 활용 ({{#값}})
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1 ~ 5) ex)mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<h1>메타코딩 은행에 오신 것을 환영합니다.</h1>
{{#sessionUser}}
<h3>{{sessionUser.fullname}}님</h3>
{{/sessionUser}}
</div>
</div>
{{>layout/footer}}
Header
- {{#값}} : 값이 boolean 또는 Object일 경우, 조건문으로 작동
- boolean : true or false
- Object : 값이 null이면 false, not null이면 true
<!DOCTYPE html>
<html lang="en">
<head>
<title>Bank</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">Home</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapsibleNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsibleNavbar">
<ul class="navbar-nav">
{{#sessionUser}}
<li class="nav-item">
<a class="nav-link" href="#">계좌목록</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">계좌생성</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">이체하기</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">로그아웃</a>
</li>
{{/sessionUser}}
{{^sessionUser}}
<li class="nav-item">
<a class="nav-link" href="/join-form">회원가입</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login-form">로그인</a>
</li>
{{/sessionUser}}
</ul>
</div>
</div>
</nav>
<!--네브바종료-->
Codes
UserController
@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
@PostMapping("/login") // select를 사용하지만 PostMapping : 개인정보의 노출로부터 보호
public String login(UserRequest.LoginDTO loginDTO) {
User sessionuser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionuser); // Stateful
return "redirect:/";
}
AccountController
@GetMapping("/")
public String home() {
session.getAttribute("sessionUser");
return "home";
}
UserService
public User 로그인(UserRequest.LoginDTO loginDTO) {
// 1. 동일 Username 있는지 검사
User user = userRepository.findByUsername(loginDTO.getUsername());
// 2. username, password가 다 있고 일치 시 로그인
// username이 다르면 null 반환 (user.getPassword() = null)
if(user == null) throw new RuntimeException("해당하는 아이디가 없습니다.");
// password가 틀리면 오류 출력
if (!(user.getPassword().equals(loginDTO.getPassword()))) {
throw new RuntimeException("아이디 또는 비밀번호가 틀립니다.");
}
// 3. 인증
return user;
}
UserRepository
- 코드 재사용
public User findByUsername(String username) {
Query query = em.createNativeQuery("select * from user_tb where username = ?", User.class);
query.setParameter(1, username);
try{
return (User)query.getSingleResult();
} catch (Exception e) {
return null;
}
}
UserRequest
@Data
public static class LoginDTO {
private String username;
private String password;
private String fullname;
}
결과
정상 작동
- ssar로 로그인 성공 & 화면 정상 출력

Exception
- 아이디가 틀림 (필터링)

- 비밀번호가 틀림

2️⃣-1️⃣ 로그아웃
Codes
UserController
- session.invalidate() : 세션 내의 value를 전부 삭제
- 재인증 요청이 들어와도 value를 찾지 못하므로 새로 인증이 필요 : 로그아웃
@GetMapping("/logout")
public String logout() {
session.invalidate();
return "redirect:/";
}
결과
정상 작동
- 메인 화면 이동

3️⃣ 계좌 목록
화면
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1 ~ 5) ex)mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<h1>회원가입 페이지</h1>
<form action="/join" method="POST">
<div class="mb-3 mt-3">
<label>Username:</label>
<input type="text" class="form-control" placeholder="Enter username.." name="username">
</div>
<div class="mb-3">
<label>Password:</label>
<input type="password" class="form-control" placeholder="Enter password.." name="password">
</div>
<div class="mb-3">
<label>Full name:</label>
<input type="text" class="form-control" placeholder="Enter fullname.." name="fullname">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</div>
{{>layout/footer}}
Codes
AccountController
@GetMapping("/account/list")
public String list(HttpServletRequest request) {
User validatedUser = (User) session.getAttribute("sessionUser");
if (validatedUser == null) throw new RuntimeException("로그인 후 이용해주세요.");
List<Account> accountList = accountService.계좌목록(validatedUser);
request.setAttribute("models", accountList);
return "account/list";
}
AccountService
public List<Account> 계좌목록(User validatedUser) {
return (List<Account>) accountRepository.findAllAccounts(validatedUser.getId());
}
AccountRepository
public List<Account> findAllAccounts(Integer id) {
Query q = em.createNativeQuery("select * from account_tb where user_id = ?", Account.class);
q.setParameter(1, id);
List<Account> list = q.getResultList();
return list;
}
결과
정상 작동
- 자신의 계좌 내역이 정상적으로 출력 (ssar의 계좌)

Exception
- Username이 12글자를 넘어갈 때 (유효성 검사)

- Username이 중복될 경우

4️⃣ 계좌 생성
화면
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1 ~ 5) ex)mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<h1>계좌생성 페이지</h1>
<form action="/account/save" method="POST">
<div class="mb-3 mt-3">
<label>Account Number:</label>
<input type="text" class="form-control" placeholder="Enter number.." name="number" value="4444">
</div>
<div class="mb-3">
<label>Account Password:</label>
<input type="password" class="form-control" placeholder="Enter password.." name="password" value="1234">
</div>
<div class="mb-3">
<label>Balance:</label>
<input type="text" class="form-control" placeholder="Enter balance.." name="balance" value="1000">
</div>
<button type="submit" class="btn btn-primary">계좌생성</button>
</form>
</div>
</div>
{{>layout/footer}}
Codes
AccountController
@GetMapping("/account/save-form")
public String saveForm() {
User validatedUser = (User) session.getAttribute("sessionUser");
if (validatedUser == null) throw new RuntimeException("로그인 후 이용해주세요.");
return "account/save-form";
}
@PostMapping("/account/save")
public String save(AccountRequest.SaveDTO saveDTO) {
User validatedUser = (User) session.getAttribute("sessionUser");
if (validatedUser == null) throw new RuntimeException("로그인 후 이용해주세요.");
accountService.계좌생성(saveDTO, validatedUser);
return "redirect:/";
}
AccountService
@Transactional
public void 계좌생성(AccountRequest.SaveDTO saveDTO, int userId) {
// // 1. 계좌번호 확인
// Account account = accountRepository.findByNumber(saveDTO.getNumber());
// // 2. 있으면 exception (계좌번호는 PK : Unique)
// if (account != null) throw new RuntimeException("이미 계좌가 존재합니다.");
// // 3. 없으면 생성
accountRepository.save(saveDTO.getNumber(),saveDTO.getPassword(),saveDTO.getBalance(), userId);
}
AccountRepository
// 필터링
public Account findByNumber(Integer number) {
Query q = em.createNativeQuery("select * from account where number = ?", Account.class);
q.setParameter(1, number);
try{
return (Account) q.getSingleResult();
} catch (Exception e) {
return null;
}
}
public void save(Integer number, String password, Integer balance, Integer userId) {
Query q = em.createNativeQuery("insert into account_tb(number, password, balance, user_id, created_at) values(?, ?, ?, ?, now())", Account.class);
q.setParameter(1, number);
q.setParameter(2, password);
q.setParameter(3, balance);
q.setParameter(4, userId);
q.executeUpdate();
}
AccountRequest
@Data
public static class SaveDTO{
private Integer number;
private String password;
private Integer balance;
}
결과
정상 작동
- Data successfully inserted

- 계좌목록 페이지에도 표시

Exception
- Username이 12글자를 넘어갈 때 (유효성 검사)

- Username이 중복될 경우

5️⃣ 계좌 이체
화면
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1 ~ 5) ex)mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<h1>계좌이체 페이지</h1>
<form action="/account/transfer" method="POST">
<div class="mb-3 mt-3">
<label>Amount:</label>
<input type="text" class="form-control" placeholder="Enter number.." name="amount" value="100">
</div>
<div class="mb-3">
<label>Withdraw Number:</label>
<input type="text" class="form-control" placeholder="Enter withdraw number.." name="withdrawNumber">
</div>
<div class="mb-3">
<label>Deposit Number:</label>
<input type="text" class="form-control" placeholder="Enter deposit number.." name="depositNumber">
</div>
<div class="mb-3">
<label>Withdraw Password:</label>
<input type="password" class="form-control" placeholder="Enter withdraw password.." name="withdrawPassword">
</div>
<button type="submit" class="btn btn-primary">이체하기</button>
</form>
</div>
</div>
{{>layout/footer}}
Codes
AccountController
@GetMapping("/account/transfer-form")
public String transferForm() {
User validatedUser = (User) session.getAttribute("sessionUser");
if (validatedUser == null) throw new RuntimeException("로그인 후 이용해주세요.");
return "account/transfer-form";
}
@PostMapping("/account/transfer")
public String transfer(AccountRequest.TransferDTO transferDTO) {
User validatedUser = (User) session.getAttribute("sessionUser");
if (validatedUser == null) throw new RuntimeException("로그인 후 이용해주세요.");
accountService.계좌이체(transferDTO, validatedUser);
return "redirect:/account/list"; // TODO
}
AccountService
@Transactional
public void 계좌이체(AccountRequest.TransferDTO transferDTO, User validatedUser) {
// 1. 출금 계좌 조회
Account withdrawAccount = accountRepository.findByNumber(transferDTO.getWithdrawNumber());
// 2. 출금 계좌 없으면 RuntimeException
if (withdrawAccount == null) throw new RuntimeException("출금 계좌가 존재하지 않습니다.");
// 3. 입금 계좌 조회
Account depositAccount = accountRepository.findByNumber(transferDTO.getDepositNumber());
// 4. 입금 계좌 없으면 RuntimeException
if (depositAccount == null) throw new RuntimeException("입금 계좌가 존재하지 않습니다.");
// 5. 입금, 출금 계좌의 잔액 조회
int withdrawBalance = withdrawAccount.getBalance();
int depositBalance = depositAccount.getBalance();
// 6. amount와 출금 계좌의 잔액 비교해서 amount가 더 크면 RuntimeException
if(transferDTO.getAmount() > withdrawBalance) throw new RuntimeException("출금 계좌의 잔액이 부족합니다.");
// 7. 출금 비밀번호 확인해서 동일한지 체크
if(!(transferDTO.getWithdrawPassword().equals(withdrawAccount.getPassword()))) throw new RuntimeException("비밀번호가 다릅니다.");
// 8. 출금 계좌와 로그인한 유저가 동일한지 인증 체크
if(!(withdrawAccount.getUserId().equals(validatedUser.getId()))) throw new RuntimeException("회원정보가 일치하지 않습니다.");
// 9. Account Update
accountRepository.updateByNumber(withdrawAccount.getNumber(), withdrawAccount.getPassword(),withdrawBalance - transferDTO.getAmount());
accountRepository.updateByNumber(depositAccount.getNumber(), depositAccount.getPassword(),depositBalance + transferDTO.getAmount());
// 10. History Save
historyRepository.saveHistory(transferDTO.getWithdrawNumber(), transferDTO.getDepositNumber(), transferDTO.getAmount(), withdrawBalance);
}
AccountRepository
- password가 필요없어도 넣는 이유 : 코드 재사용의 용이
public void updateByNumber(Integer number, String password, int balance) {
Query q = em.createNativeQuery("update account_tb set balance = ?, password = ? where number = ?");
q.setParameter(1, balance);
q.setParameter(2, password);
q.setParameter(3, number);
q.executeUpdate();
}
HistoryRepository
public void saveHistory(Integer withdrawNumber, Integer depositNumber, Integer amount, Integer withdrawBalance) {
Query q = em.createNativeQuery("insert into history_tb(amount, deposit_number, withdraw_number, withdraw_balance, created_at) values(?, ?, ?, ?, now())");
q.setParameter(1, amount);
q.setParameter(2, depositNumber);
q.setParameter(3, withdrawNumber);
q.setParameter(4, withdrawBalance);
q.executeUpdate();
}
결과
정상 작동
- 1111 → 3333으로 100원 이체
- 1111의 잔액이 100원 줄어들고, 3333의 잔액이 100원 증가

- 이체를 시도한 1111의 계좌 잔액이 줄은 것을 확인 가능
- 계좌 상세내역에서 이체 내역 확인 가능

- 이체를 받은 3333의 계좌 잔액이 늘은 것을 확인 가능

- 이체 내역 생성 → 상세보기에서 확인 가능

Exception
- 현재 로그인한 유저는 ssar
- 출금 계좌가 존재하지 않을 경우


- 입금 계좌가 존재하지 않을 경우


- 이체 금액이 잔액보다 클 경우


- 계좌 비밀번호가 일치하지 않을 시


- 현재 로그인한 유저와 출금 계좌의 정보가 일치하지 않을 시 (인증)
- 현재 로그인한 유저의 계좌만 선택할 수 있도록 프론트 수정 필요


6️⃣ 계좌 내역 (상세 보기)
화면
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1~5) ex) mt-5-->
<div class="container mt-2">
<table class="table table-hover">
<thead>
<tr>
<th>계좌번호</th>
<th>잔액</th>
</tr>
</thead>
<tbody>
{{#models}}
<tr>
<td>{{number}}</td>
<td>{{balance}}원</td>
</tr>
{{/models}}
</tbody>
</table>
</div>
{{>layout/footer}}
Codes
AccountController
@GetMapping("/account/list")
public String list(HttpServletRequest request) {
User validatedUser = (User) session.getAttribute("sessionUser");
if (validatedUser == null) throw new RuntimeException("로그인 후 이용해주세요.");
List<Account> accountList = accountService.계좌목록(validatedUser);
request.setAttribute("models", accountList);
return "account/list";
}
AccountService
public List<Account> 계좌목록(User validatedUser) {
return (List<Account>) accountRepository.findAllAccounts(validatedUser.getId());
}
AccontRepository
public List<Account> findAllAccounts(Integer id) {
Query q = em.createNativeQuery("select * from account_tb where user_id = ?", Account.class);
q.setParameter(1, id);
List<Account> list = q.getResultList();
return list;
}
결과
정상 작동
7. 인증 (세션)
- 세션 (Session) : 서버가 클라이언트의 상태(State)를 저장하기 위해 생성하는 저장소
- 쿠키 (Cookie) : 서버가 클라이언트에게 정보를 저장시키기 위해 사용하는 데이터
- 세션은 서버에 저장되고, 쿠키는 클라이언트(브라우저)에 저장됨
- 브라우저는 요청 시 저장된 쿠키를 담아서 요청 (Request Body)
- 서버는 쿠키를 가져와 데이터를 사용

- 세션은 Hashmap의 구조로 동작
- Hashmap의 key를 세션 ID라고 하며, 이는 자동 생성되어 클라이언트에게 쿠키로 저장됨
- 세션에 접근(저장) 할 때에만 생성됨
- 쿠키는 서버 측에서 데이터를 저장시키고 싶을 때 Response Header에 담음
- Set-Cookie라는 key와 쿠키에 담을 값을 value로 Attribute 지정 후 응답
세션 vs 쿠키
- 세션은 서버에 저장되는 데이터
- 세션 키도 쿠키를 사용하여 저장
- 보안상 브라우저에 저장되는 쿠키보다는 안전
- 서버의 메모리를 차지함 : 성능 저하
- 쿠키는 서버가 브라우저에 저장시키는 데이터
- 쿠키는 사용자의 정보를 브라우저에 저장 : 서버의 자원을 사용하지 않음
- 요청 속도 (데이터 로딩 속도)가 빠름
- 데이터 변질, 스니핑 등 보안에 취약
- 쿠키는 만료 시간 (라이프사이클)이 존재
- 세션 만료 : 세션을 찾을 수 있는 ID인 쿠키가 삭제됨 (라이프사이클, 브라우저 종료 등)

8. 상세 내역 쿼리 (한 번에)
1. User Info Query
select
at.balance account_balance,
at.number account_number,
ut.fullname account_owner
from account_tb at
inner join user_tb ut
on at.user_id = ut.id
where at.number=1111


2. Withdraw/Deposit List Query
select substr(created_at, 1, 16) created_at,
withdraw_number w_number,
deposit_number d_number,
amount,
case when withdraw_number = 1111 then withdraw_balance
else deposit_balance
end balance,
case when withdraw_number = 1111 then '출금'
else '입금'
end type
from history_tb
where deposit_number = 1111 or withdraw_number = 1111;

3. Dummy Join
select substr(created_at, 1, 16) created_at,
withdraw_number w_number,
deposit_number d_number,
amount,
case when withdraw_number = 1111 then withdraw_balance
else deposit_balance
end balance,
case when withdraw_number = 1111 then '출금'
else '입금'
end type,
dt.*
from history_tb ht
inner join (select 1, 2, 3 from dual) dt
on 1=1
where deposit_number = 1111 or withdraw_number = 1111;

4. One-shot Query (Not Recommended)
select
dt.account_balance,
dt.account_number,
dt.account_owner,
substr(created_at, 1, 16) created_at,
withdraw_number w_number,
deposit_number d_number,
amount,
case when withdraw_number = 1111 then withdraw_balance
else deposit_balance
end balance,
case when withdraw_number = 1111 then '출금'
else '입금'
end type
from history_tb ht
inner join (select
at.balance account_balance,
at.number account_number,
ut.fullname account_owner
from account_tb at
inner join user_tb ut
on at.user_id = ut.id
where at.number=1111
) dt
on 1=1
where deposit_number = 1111 or withdraw_number = 1111;

5. Link
public List<AccountResponse.DetailDTO> findAllByNumber(int number) {
String sql = """
select
dt.account_number,
dt.account_balance,
dt.account_owner,
substr(created_at, 1, 16) created_at,
withdraw_number w_number,
deposit_number d_number,
amount amount,
case when withdraw_number = ? then withdraw_balance
else deposit_balance
end balance,
case when withdraw_number = ? then '출금'
else '입금'
end type
from history_tb ht
inner join (select at.number account_number, at.balance account_balance, ut.fullname account_owner
from account_tb at
inner join user_tb ut on at.user_id = ut.id
where at.number = ?) dt on 1=1
where deposit_number = ? or withdraw_number = ?;
""";
Query query = em.createNativeQuery(sql);
query.setParameter(1, number);
query.setParameter(2, number);
query.setParameter(3, number);
query.setParameter(4, number);
query.setParameter(5, number);
List<Object[]> obsList = query.getResultList();
List<AccountResponse.DetailDTO> detailList = new ArrayList<>();
for (Object[] obs : obsList) {
AccountResponse.DetailDTO detail =
new AccountResponse.DetailDTO(
(int) obs[0],
(int) obs[1],
(String) obs[2],
(String) obs[3],
(int) obs[4],
(int) obs[5],
(int) obs[6],
(int) obs[7],
(String) obs[8]
);
detailList.add(detail);
}
return detailList;
}
- DetailDTO
@AllArgsConstructor
@Data
public static class DetailDTO {
private int accountNumber;
private int accountBalance;
private String accountOwner;
private String createdAt; // substr로 잘랐으므로 String 타입
private int wNumber;
private int dNumber;
private int amount;
private int balance;
private String type;
}
- DetailDTO : Refactored
- 실제 화면에 출력할 수 있는 형태로 작성된 DTO
6. Unit Test
package com.metacoding.bankv1.account;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import java.util.List;
@Import(AccountRepository.class)
@DataJpaTest
public class AccountRepositoryTest {
@Autowired
private AccountRepository accountRepository;
@Test
public void findAllByNumber_test() {
int number = 1111;
List<AccountResponse.DetailDTO> detailList = accountRepository.findAllByNumber(number);
for (AccountResponse.DetailDTO detail : detailList) {
System.out.println(detail);
}
}
}
- 출력 정상 확인

7. 구현
- AccountService
public List<AccountResponse.DetailDTO> 계좌상세보기(int number, String type, Integer sessionUserId) {
// 1. 계좌 존재 확인
Account account = accountRepository.findByNumber(number);
if (account == null) throw new RuntimeException("계좌가 존재하지 않습니다");
// 2. 계좌 주인 확인
account.계좌주인검사(sessionUserId);
// 3. 조회해서 주면 됨
List<AccountResponse.DetailDTO> detailList = accountRepository.findAllByNumber(number);
return detailList;
}
- AccountController
@GetMapping("/account/{number}")
public String detail(@PathVariable("number") int number,
@RequestParam(value = "type", required = false, defaultValue = "전체") String type, HttpServletRequest request) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) throw new RuntimeException("로그인 후 사용해주세요");
List<AccountResponse.DetailDTO> detailList = accountService.계좌상세보기(number, type, sessionUser.getId());
request.setAttribute("models", detailList);
// System.out.println("number = " + number);
// System.out.println("type = " + type);
return "account/detail";
}
8. detail form
{{>layout/header}}
<!--마진 : mt, mr, ml, mb (1~5) ex) mt-5-->
<div class="container mt-2">
<div class="mt-4 p-5 bg-light text-dark rounded-4">
<p>{{models.0.accountOwner}}님 계좌</p>
<p>계좌번호 : {{models.0.accountNumber}}</p>
<p>계좌잔액 : {{models.0.accountBalance}}원</p>
</div>
<div class="mt-3 mb-3">
<button type="button" class="btn btn-outline-primary">전체</button>
<button type="button" class="btn btn-outline-primary">입금</button>
<button type="button" class="btn btn-outline-primary">출금</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>날짜</th>
<th>출금계좌</th>
<th>입금계좌</th>
<th>금액</th>
<th>계좌잔액</th>
<th>출금/입금</th>
</tr>
</thead>
<tbody>
{{#models}}
<tr>
<td>{{createdAt}}</td>
<td>{{wNumber}}</td>
<td>{{dNumber}}</td>
<td>{{amount}}</td>
<td>{{balance}}원</td>
<td>{{type}}</td>
</tr>
{{/models}}
</tbody>
</table>
</div>
{{>layout/footer}}
결과
- 화면의 구성요소에 DTO가 완벽하게 들어맞지 않음 : Not DTO
- 위의 정보는 1개만 필요 → 변경해야 함

9. 상세보기 동적 쿼리
- 전체, 입금, 출금 유형에 따라 내역을 다르게 보는 화면 작성
<div class="mt-3 mb-3">
<a href="/account/{{models.0.accountNumber}}?type=전체" class="btn btn-outline-primary">전체</button>
<a href="/account/{{models.0.accountNumber}}?type=입금" class="btn btn-outline-primary">입금</button>
<a href="/account/{{models.0.accountNumber}}?type=출금" class="btn btn-outline-primary">출금</button>
</div>
- AccountService
public List<AccountResponse.DetailDTO> 계좌상세보기(int number, String type, Integer sessionUserId) {
// 1. 계좌 존재 확인
Account account = accountRepository.findByNumber(number);
if (account == null) throw new RuntimeException("계좌가 존재하지 않습니다");
// 2. 계좌 주인 확인
account.계좌주인검사(sessionUserId);
// 3. 조회해서 주면 됨
List<AccountResponse.DetailDTO> detailList = accountRepository.findAllByNumber(number, type);
return detailList;
}
- AccountRepository
- Refactored : sql도 String이므로 경우에 따라 붙여서 동적 작동 (String 이어붙히기)
public List<AccountResponse.DetailDTO> findAllByNumber(int number, String type) {
String sql = """
select
dt.account_number,
dt.account_balance,
dt.account_owner,
substr(created_at, 1, 16) created_at,
withdraw_number w_number,
deposit_number d_number,
amount amount,
case when withdraw_number = ? then withdraw_balance
else deposit_balance
end balance,
case when withdraw_number = ? then '출금'
else '입금'
end type
from history_tb ht
inner join (select at.number account_number, at.balance account_balance, ut.fullname account_owner
from account_tb at
inner join user_tb ut on at.user_id = ut.id
where at.number = ?) dt on 1=1
""";
String sql2 = "where deposit_number = ? or withdraw_number = ?;";
String sql3 = "where deposit_number = ?;";
String sql4 = "where withdraw_number = ?;";
if(type.equals("입금")) sql += sql3;
else if (type.equals("출금")) sql += sql4;
else sql += sql2;
Query query = em.createNativeQuery(sql);
query.setParameter(1, number);
query.setParameter(2, number);
query.setParameter(3, number);
query.setParameter(4, number);
if(type.equals("전체")) query.setParameter(5, number);
List<Object[]> obsList = query.getResultList();
List<AccountResponse.DetailDTO> detailList = new ArrayList<>();
for (Object[] obs : obsList) {
AccountResponse.DetailDTO detail =
new AccountResponse.DetailDTO(
(int) obs[0],
(int) obs[1],
(String) obs[2],
(String) obs[3],
(int) obs[4],
(int) obs[5],
(int) obs[6],
(int) obs[7],
(String) obs[8]
);
detailList.add(detail);
}
return detailList;
}
결과
- 입금

- 출금

- 전체

Share article