회원가입
로그인
계좌목록
계좌생성
계좌이체
계좌상세
1. 회원가입 (/join)
: 회원가입 폼을 열고, 사용자가 가입 요청을 보냄

🔹 실행 흐름:
/join-form요청 → 회원가입 화면을 반환
- 사용자가 정보 입력 후 회원가입 요청 (Post/join)
- 중복된
username체크 후, DB에 사용자 저장
🔹 [파일]
UserController.java@GetMapping("/join-form")
public String joinForm() {
return "user/join-form"; // 회원가입 페이지
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userService.회원가입(joinDTO);
return "redirect:/login-form"; // 회원가입 후 로그인 페이지로 이동
}🔹 [파일]
UserService.java@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
User user = userRepository.findByUsername(joinDTO.getUsername());
if (user != null) throw new RuntimeException("동일한 username이 존재합니다");
userRepository.save(joinDTO.getUsername(), joinDTO.getPassword(), joinDTO.getFullname());
}
🔹 [파일]
UserRepository.javapublic 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();
}
2. 로그인 (/login)
: 사용자가 로그인하면 세션에 저장됨

🔹 실행 흐름:
/login-form요청 → 로그인 페이지 반환
- 사용자가 로그인 요청 (
POST /login) →sessionUser저장
🔹 [파일]
UserController.java@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO) {
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser); // 로그인 성공 시 세션 저장
return "redirect:/";
}🔹 [파일]
UserService.javapublic User 로그인(UserRequest.LoginDTO loginDTO) {
User user = userRepository.findByUsername(loginDTO.getUsername());
if (user == null) throw new RuntimeException("해당 username이 없습니다");
if (!(user.getPassword().equals(loginDTO.getPassword())))
throw new RuntimeException("비밀번호가 틀렸습니다");
return user; // 로그인 성공 시 유저 반환
}🔹 [파일]
UserRepository.javapublic 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;
}
}3. 계좌 생성 페이지 이동 (/account/save-form)
: 로그인된 사용자가 계좌를 만들기 위해
/account/save-form으로 이동
🔹 실행 흐름:
/account/save-form요청 → 세션 확인 후 계좌 생성 폼 반환
🔹 [파일]
AccountController.java@GetMapping("/account/save-form")
public String saveForm() {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) throw new RuntimeException("로그인 후 사용해주세요");
return "account/save-form";
}4. 계좌 생성 (/account/save)
: 사용자가 계좌 정보를 입력하고, [계좌 생성] 버튼 클릭
🔹 실행 흐름:
- 계좌 생성 요청 (
POST /account/save)
- 로그인 확인 → DB에 계좌 저장 → 계좌 목록 페이지로 이동
🔹 [파일]
AccountController.java@PostMapping("/account/save")
public String save(AccountRequest.SaveDTO saveDTO) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) throw new RuntimeException("로그인 후 사용해주세요");
accountService.계좌생성(saveDTO, sessionUser.getId());
return "redirect:/account"; // 계좌 생성 후 계좌 목록으로 이동
}🔹 [파일]
AccountService.java@Transactional
public void 계좌생성(AccountRequest.SaveDTO saveDTO, int userId) {
accountRepository.save(saveDTO.getNumber(), saveDTO.getPassword(), saveDTO.getBalance(), userId);
}🔹 [파일]
AccountRepository.javapublic void save(Integer number, String password, Integer balance, int userId) {
Query query = em.createNativeQuery(
"INSERT INTO account_tb(number, password, balance, user_id, created_at) VALUES (?, ?, ?, ?, now())"
);
query.setParameter(1, number);
query.setParameter(2, password);
query.setParameter(3, balance);
query.setParameter(4, userId);
query.executeUpdate();
}5. 계좌 목록 조회 (/account)
: 사용자가 본인의 계좌 목록을 확인

🔹 실행 흐름:
/account요청 → DB에서 로그인한 사용자의 계좌 목록 조회
- 조회된 데이터를 뷰에 전달하여 화면에 출력
🔹 [파일]
AccountController.java@GetMapping("/account")
public String list(HttpServletRequest request) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) throw new RuntimeException("로그인 후 사용해주세요");
List<Account> accountList = accountService.나의계좌목록(sessionUser.getId());
request.setAttribute("models", accountList);
return "account/list"; // 계좌 목록 페이지
}🔹 [파일]
AccountService.javapublic List<Account> 나의계좌목록(Integer userId) {
return accountRepository.findAllByUserId(userId);
}🔹 [파일]
AccountRepository.javapublic List<Account> findAllByUserId(Integer userId) {
Query query = em.createNativeQuery(
"SELECT * FROM account_tb WHERE user_id = ? ORDER BY created_at DESC", Account.class
);
query.setParameter(1, userId);
return query.getResultList();
}6. 계좌 이체

📍 실행 흐름:
1. 이체 페이지로 이동
: 로그인한 사용자만 접근 가능
📂
AccountController.java@GetMapping("/account/transfer-form")
public String transferForm() {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) throw new RuntimeException("로그인 후 사용해주세요");
return "account/transfer-form"; // Mustache 뷰 반환
}sessionUser가 없으면 "로그인 후 사용해주세요" 예외 발생
- 로그인된 사용자만 이체 페이지 (
transfer-form.mustache)에 접근 가능
2. 사용자가 이체 요청
: 사용자가 출금 계좌, 입금 계좌, 금액, 비밀번호 입력 후 요청
📂
AccountController.java@PostMapping("/account/transfer")
public String transfer(AccountRequest.TransferDTO transferDTO) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) throw new RuntimeException("로그인 후 사용해주세요");
accountService.계좌이체(transferDTO, sessionUser.getId());
return "redirect:/"; // 계좌이체 후 홈으로 이동
}transferDTO에서 출금 계좌번호, 입금 계좌번호, 금액, 비밀번호 가져오기
- 로그인한 사용자만 요청 가능
3. 출금 & 입금 계좌 검증
: 출금 계좌 & 입금 계좌가 존재 하는지 확인
📂
AccountService.java@Transactional
public void 계좌이체(AccountRequest.TransferDTO transferDTO, int userId) {
// 1️⃣ 출금 계좌 조회
Account withdrawAccount = accountRepository.findByNumber(transferDTO.getWithdrawNumber());
if (withdrawAccount == null) throw new RuntimeException("출금 계좌가 존재하지 않습니다");
// 2️⃣ 입금 계좌 조회
Account depositAccount = accountRepository.findByNumber(transferDTO.getDepositNumber());
if (depositAccount == null) throw new RuntimeException("입금 계좌가 존재하지 않습니다");
}withdrawAccount또는depositAccount가 DB에 없으면 예외 발생
📂
AccountRepository.javapublic Account findByNumber(Integer number) {
Query query = em.createNativeQuery("SELECT * FROM account_tb WHERE number = ?", Account.class);
query.setParameter(1, number);
try {
return (Account) query.getSingleResult();
} catch (Exception e) {
return null;
}
}- 계좌번호로 계좌 정보를 조회하는 쿼리
4. 출금 계좌 검증
: 비밀번호 & 소유자 확인
📂
AccountService.java// 3️⃣ 출금 계좌 잔액 검사
if (withdrawAccount.getBalance() < transferDTO.getAmount()) {
throw new RuntimeException("출금 계좌의 잔액이 부족합니다");
}
// 4️⃣ 출금 계좌 비밀번호 확인
if (!withdrawAccount.getPassword().equals(transferDTO.getWithdrawPassword())) {
throw new RuntimeException("출금 계좌 비밀번호가 틀렸습니다");
}
// 5️⃣ 출금 계좌 소유자 확인
if (!withdrawAccount.getUserId().equals(userId)) {
throw new RuntimeException("출금 계좌의 권한이 없습니다");
}- 잔액 부족하면 예외 발생
- 출금 계좌 비밀번호가 일치하지 않으면 예외 발생
- 출금 계좌 주인이 현재 로그인한 사용자와 다르면 예외 발생
5. 잔액 업데이트
: 출금 계좌 잔액 차감, 입금 계좌 잔액 추가
📂
AccountService.java// 6️⃣ 출금 계좌 잔액 업데이트
int withdrawBalance = withdrawAccount.getBalance() - transferDTO.getAmount();
accountRepository.updateByNumber(withdrawBalance, withdrawAccount.getPassword(), withdrawAccount.getNumber());
// 7️⃣ 입금 계좌 잔액 업데이트
int depositBalance = depositAccount.getBalance() + transferDTO.getAmount();
accountRepository.updateByNumber(depositBalance, depositAccount.getPassword(), depositAccount.getNumber());📂
AccountRepository.javapublic void updateByNumber(int balance, String password, int number) {
Query query = em.createNativeQuery("UPDATE account_tb SET balance = ?, password = ? WHERE number = ?");
query.setParameter(1, balance);
query.setParameter(2, password);
query.setParameter(3, number);
query.executeUpdate();
}
- 출금 계좌 잔액 감소 (
balance - amount)
- 입금 계좌 잔액 증가 (
balance + amount)
- 계좌 번호 기준으로
UPDATE실행
6. 거래 내역 저장
: 거래 내역을
history_tb 테이블에 저장
📂 AccountService.java// 8️⃣ 거래 내역 저장
historyRepository.save(transferDTO.getWithdrawNumber(), transferDTO.getDepositNumber(), transferDTO.getAmount(), withdrawBalance);📂
HistoryRepository.javapublic void save(int withdrawNumber, int depositNumber, int amount, int withdrawBalance) {
Query query = em.createNativeQuery(
"INSERT INTO history_tb(withdraw_number, deposit_number, amount, withdraw_balance, created_at) VALUES (?, ?, ?, ?, now())"
);
query.setParameter(1, withdrawNumber);
query.setParameter(2, depositNumber);
query.setParameter(3, amount);
query.setParameter(4, withdrawBalance);
query.executeUpdate();
}- 거래 내역을 history_tb 테이블에 저장
7. 계좌 상세 보기
: 사용자가 본인의 특정 계좌의 입출금 내역을 확인
[1] 복잡한 쿼리 작성 (한방 쿼리)
- 거래 내역과 계좌, 사용자 테이블 조인
- 특정 계좌 번호(
1111)의 입출금 내역을 한 번의 쿼리로 조회
select
dt.*,
substr(created_at, 1, 16) created_at,
withdraw_number w_number,
deposit_number d_number,
amount 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.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 = 1111
) dt on 1=1
where deposit_number = 1111 or withdraw_number = 1111;
🔹 주요 동작
history_tb(거래 내역)에서 입출금 계좌가 해당 계좌 번호인 모든 거래 조회
account_tb,user_tb조인 → 계좌 정보와 소유자 정보도 함께 가져옴
withdraw_number(출금 계좌)가 해당 계좌면"출금", 아니라면"입금"으로 구분
[2] 계좌 상세 DTO 객체 생성
AccountResponse.java: 쿼리 결과를 객체로 변환하기 위한 DTO
package com.metacoding.bankv1.account;
import lombok.AllArgsConstructor;
import lombok.Data;
public class AccountResponse {
@AllArgsConstructor
@Data
public static class DetailDTO {
private int accountNumber;
private int accountBalance;
private String accountOwner;
private String createdAt;
private int wNumber;
private int dNumber;
private int amount;
private int balance;
private String type;
}
}@AllArgsConstructor → 모든 필드를 포함한 생성자 자동 생성[3] Repository (DB 조회)
AccountRepository.javapublic 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;
}✔ 설명
- 한방 쿼리를 실행하여 거래 내역 조회
Object[]배열을 DTO 객체(DetailDTO)로 변환하여 리스트에 저장
findAllByNumber(int number): 특정 계좌의 모든 거래 내역 반환
[4] 복잡한 쿼리 테스트
AccountRepositoryTest.javapackage 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);
}
}
}✔ 설명
findAllByNumber_test(): 특정 계좌(1111)의 거래 내역이 정상 조회되는지 테스트
@DataJpaTest: JPA 관련 테스트 환경 구성
@Import(AccountRepository.class): 리포지토리 수동 등록
[5] 서비스 구현
AccountService.javapublic 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> detailDTOList = accountRepository.findAllByNumber(number);
return detailDTOList;
}✔ 설명
계좌상세보기()- 계좌 존재 여부 확인
- 계좌 주인인지 검증
- 거래 내역 조회 후 반환
[6] 컨트롤러 구현
AccountController.java@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);
return "account/detail"; // 계좌 상세 페이지
}
✔ 설명
@PathVariable("number"): URL에서{number}값을 추출
@RequestParam("type"): 입금/출금 필터링 (기본값"전체")
- 계좌 상세 정보를 조회하여 JSP로 전달
[7] 화면 출력
account/detail.mustache{{>layout/header}}
<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>
<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}}✔ 설명
- 거래 내역을 반복문(
{{#models}})을 이용해 출력
- 출금/입금 내역을 테이블로 정리
8. 계좌 상세보기 (동적쿼리)
: 사용자가 특정 계좌의 거래 내역을 유형(전체, 입금, 출금)별로 필터링하여 조회
1. 화면에서 거래 내역 필터링 버튼 추가
detail.mustache<div class="mt-3 mb-3">
<a href="/account/{{models.0.accountNumber}}?type=전체" class="btn btn-outline-primary">전체</a>
<a href="/account/{{models.0.accountNumber}}?type=입금" class="btn btn-outline-primary">입금</a>
<a href="/account/{{models.0.accountNumber}}?type=출금" class="btn btn-outline-primary">출금</a>
</div>✔ 설명
{{models.0.accountNumber}}: 계좌 번호를 URL 파라미터로 전달
- 사용자가 버튼을 클릭하면 해당 유형(전체, 입금, 출금)에 따라 계좌 거래 내역을 필터링
?type=입금,?type=출금등의 URL을 생성하여 필터링 기능 활성화
2. 서비스에서 필터링 타입(type) 전달
AccountService.javaList<AccountResponse.DetailDTO> detailDTOList = accountRepository.findAllByNumber(number, type);
✔ 설명
findAllByNumber(number, type)호출 시 조회할 거래 유형을 인자로 전달
type값에 따라전체,입금,출금쿼리를 선택적으로 실행
3. 동적 쿼리 작성
AccountRepository.javapublic List<AccountResponse.DetailDTO> findAllByNumber(int number, String type) {
String allSql = """
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 = ?;
""";
String withdrawSql = """
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,
withdraw_balance balance,
'출금' 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 withdraw_number = ?;
""";
String depositSql = """
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,
deposit_balance balance,
'입금' 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 = ?;
""";
Query query = null;
if (type.equals("입금")) {
query = em.createNativeQuery(depositSql);
query.setParameter(1, number);
query.setParameter(2, number);
} else if (type.equals("출금")) {
query = em.createNativeQuery(withdrawSql);
query.setParameter(1, number);
query.setParameter(2, number);
} else {
query = em.createNativeQuery(allSql);
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;
}✔ 설명
- 거래 유형(type)에 따라 서로 다른 SQL 실행
allSql: 전체 거래 내역 조회withdrawSql: 출금 내역만 조회depositSql: 입금 내역만 조회
Query query = em.createNativeQuery(...)를 통해 SQL 동적으로 실행
query.setParameter(n, value)를 활용해 바인딩
4. 서비스에서 필터링 적용
AccountService.javapublic 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> detailDTOList = accountRepository.findAllByNumber(number, type);
return detailDTOList;
}✔ 설명
findAllByNumber(number, type)를 호출하여 필터링된 내역 반환
5. 컨트롤러에서 필터링 값 처리
AccountController.java@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);
return "account/detail"; // 계좌 상세 페이지
}✔ 설명
@RequestParam(value = "type", required = false, defaultValue = "전체")- type 값을 URL에서 받아옴 (기본값
"전체") ?type=입금→ 입금 내역만 조회?type=출금→ 출금 내역만 조회
- 조회된 거래 내역을
models속성으로 Mustache 템플릿에 전달
Share article