[스프링부트] 11. Bank 만들기 v1. Prototype

문정준's avatar
Mar 24, 2025
[스프링부트] 11. Bank 만들기 v1. Prototype

1. IntelliJ 사전 준비

  • spring devtools의 auto-restart 기능 활성화
 
  • Advanced Settings → Compiler → ‘Allow auto-make to start…’ 체크
notion image
 
  • ‘auto’ 검색 → Build, Execution, Deployment → Build Tools → Compiler
    • Build project automatically 체크
notion image
 

2. New Project → Git Clone

  • Menu → File → New → Project from Version Control 클릭
notion image
 
  • 저장할 위치 지정 : pro_lec
notion image
 
  • \폴더명을 추가로 적어 새 폴더 안에 프로젝트 생성
notion image
 
  • 예제 링크
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

notion image
 

2️⃣ 회원 가입

notion image
 

3️⃣ 로그인

notion image
 

4️⃣ 계좌 생성

notion image
 

5️⃣ 계좌 목록

notion image
 

6️⃣ 계좌 이체

notion image
 

4. 화면 연결 (임시)

 
  • 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
notion image
account_tb
notion image
history_tb
notion image
 

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
notion image
Exception
  • Username이 12글자를 넘어갈 때 (유효성 검사)
notion image
 
  • Username이 중복될 경우
notion image
 

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로 로그인 성공 & 화면 정상 출력
notion image
Exception
  • 아이디가 틀림 (필터링)
notion image
  • 비밀번호가 틀림
notion image
 

2️⃣-1️⃣ 로그아웃

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

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의 계좌)
notion image
Exception
  • Username이 12글자를 넘어갈 때 (유효성 검사)
notion image
 
  • Username이 중복될 경우
notion image
 

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
notion image
  • 계좌목록 페이지에도 표시
notion image
Exception
  • Username이 12글자를 넘어갈 때 (유효성 검사)
notion image
 
  • Username이 중복될 경우
notion image
 

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원 증가
notion image
  • 이체를 시도한 1111의 계좌 잔액이 줄은 것을 확인 가능
    • 계좌 상세내역에서 이체 내역 확인 가능
notion image
  • 이체를 받은 3333의 계좌 잔액이 늘은 것을 확인 가능
notion image
  • 이체 내역 생성 → 상세보기에서 확인 가능
notion image
 
Exception
  • 현재 로그인한 유저는 ssar
  • 출금 계좌가 존재하지 않을 경우
notion image
notion image
 
  • 입금 계좌가 존재하지 않을 경우
notion image
notion image
 
  • 이체 금액이 잔액보다 클 경우
notion image
notion image
 
  • 계좌 비밀번호가 일치하지 않을 시
notion image
notion image
 
  • 현재 로그인한 유저와 출금 계좌의 정보가 일치하지 않을 시 (인증)
    • 현재 로그인한 유저의 계좌만 선택할 수 있도록 프론트 수정 필요
notion image
notion image
 

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)
    • 서버는 쿠키를 가져와 데이터를 사용
 
notion image
 
  • 세션은 Hashmap의 구조로 동작
    • Hashmap의 key를 세션 ID라고 하며, 이는 자동 생성되어 클라이언트에게 쿠키로 저장됨
    • 세션에 접근(저장) 할 때에만 생성됨
  • 쿠키는 서버 측에서 데이터를 저장시키고 싶을 때 Response Header에 담음
    • Set-Cookie라는 key와 쿠키에 담을 값을 value로 Attribute 지정 후 응답
 

세션 vs 쿠키

  • 세션은 서버에 저장되는 데이터
    • 세션 키도 쿠키를 사용하여 저장
    • 보안상 브라우저에 저장되는 쿠키보다는 안전
    • 서버의 메모리를 차지함 : 성능 저하
  • 쿠키는 서버가 브라우저에 저장시키는 데이터
    • 쿠키는 사용자의 정보를 브라우저에 저장 : 서버의 자원을 사용하지 않음
    • 요청 속도 (데이터 로딩 속도)가 빠름
    • 데이터 변질, 스니핑 등 보안에 취약
    • 쿠키는 만료 시간 (라이프사이클)이 존재
      • 세션 만료 : 세션을 찾을 수 있는 ID인 쿠키가 삭제됨 (라이프사이클, 브라우저 종료 등)
 
notion image
 

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
notion image
notion image
 

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;
 
notion image
 

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;
notion image
 

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;
notion image
 

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); } } }
 
  • 출력 정상 확인
notion image
 

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개만 필요 → 변경해야 함
notion image
 

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; }
 

결과

  • 입금
notion image
 
  • 출금
notion image
 
  • 전체
notion image
 
Share article

sxias