Spring Boot와 JPA를 이용한 REST API 개발
사용 기술 : Spring Tool Suite4(STS 4), Spring Boot, Spring Web, Spring Data JPA, Spring Boot DevTools, Oracle DB, Oracle Driver, Lombok, Thymeleaf
- REST API(Representational State Transfer API) : 클라이언트와 서버 간 통신을 HTTP 프로토콜을 기반으로 설계하는 방식
- Resource(자원) 중심 설계 : URI를 통해 자원 식별
- HTTP 메서드
- GET : 데이터 조회
- POST : 데이터 생성
- PUT : 데이터 수정
- DELETE : 데이터 삭제
- JSON, XML 응답
- JPA(Java Presistence API) : Java에서 ORM(Object-Relational Mapping)을 수행하기 위한 표준 인터페이스
프로젝트 생성
Oracle SQL Develope에서 프로젝트 생성
application.properties 설정
spring.application.name=NewProject
# application type setting (웹 애플리케이션 타입 : servlet)
spring.main.web-application-type=servlet
# server-port
server.port=7070
# data-source setting
# JDBC 드라이버 클래스명 설정
spring.datasource.dbcp2.driver-class-name=oracle.jdbc.OracleDriver
# 데이터베이스 URL 설정 (localhost의 XE 인스턴스 사용)
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:XE
# 데이터베이스 사용자명 설정
spring.datasource.username=test
# 데이터베이스 비밀번호 설정
spring.datasource.password=test
# JPA setting
# Hibernate의 DDL 전략을 'update'로 설정하여, 애플리케이션 시작 시 스키마를 자동으로 갱신
spring.jpa.hibernate.ddl-auto=update
# Hibernate가 DDL을 생성하도록 설정
spring.jpa.generate-ddl=true
# 실행되는 SQL 쿼리 콘솔 출력 여부 설정 (개발 중 확인용)
spring.jpa.show-sql=true
# 사용되는 데이터베이스의 방언(Dialect) 설정 (Oracle DB에 맞는 Hibernate 방언 사용)
spring.jpa.database-platform=org.hibernate.dialect.OracleDialect
# 출력되는 SQL을 사람이 읽기 쉽게 포맷
spring.jpa.properties.hibernate.format-sql=true
Member 엔티티 클래스 생성
package com.demo.domain;
import java.util.Date;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Entity
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
public class Member {
@Id
private String id; // 회원ID
private String password; // 회원 비밀번호
private String name; // 회원 이름
private String email; // 회원 이메일
@Temporal(TemporalType.TIMESTAMP)
private Date invitedDate; // 가입날짜
}
- 어노테이션
- Entity : 클래스가 JPA 엔티티임을 선언(데이터베이스 테이블과 매핑되는 객체)
- Getter, Setter : getter, setter 메서드 자동 생성
- Builder : 빌더 패턴을 사용해서 객체를 생성할 수 있게 함
- ToString : toString() 메서드를 자동으로 생성
- NoArgsConstructor : 기본 생성자 자동 생성
- AllArgsConstructor : 모든 필드를 매개변수로 갖는 생성자 자동 생성
- DynamicInsert : 엔티티를 insert할 때 null인 필드는 제외하고 insert 쿼리 생성
- DynamicUpdate : 엔티티를 update할 때 변경된 필드만 update 쿼리에 포함
- Id : 엔티티의 기본 키(primary key) 지정
- Temporal(TemporalType.TIMESTAMP)
- Date 또는 Calendar 필드에 대해 날짜 또는 시간 정보를 어떻게 저장할지 지정하는 어노테이션
- TemporalType.TIMESTAMP : 날짜와 시간을 함께 저장하며, TIMESTAMP 형식으로 데이터를 저장
Repository 생성
package com.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.demo.domain.Member;
public interface MemberRepository extends JpaRepository<Member, String> {
Member findByNameAndEmail(String name, String email);
Member findByIdAndNameAndEmail(String id, String name, String email);
}
- JpaRepository : Spring Data JPA에서 자동으로 구현되는 기본 CRUD 메서드를 제공
- save(S Entity) : 엔티티 저장 또는 수정
- findBy... : 메서드 이름을 분석해 SQL 쿼리를 자동으로 생성
- findById : SELECT * FROM member WHERE id = ?와 같은 쿼리가 자동으로 실행
- findByIdAndPassword : SELECT * FROM member WHERE id = ? AND password = ?와 같은 쿼리가 자동으로 실행
- findAll() : 모든 엔티티를 리스트 형식으로 조회
- delete(S entity) : 주어진 엔티티를 삭제
Service 생성
package com.demo.service;
import java.util.Date;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.demo.domain.Member;
import com.demo.repository.MemberRepository;
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepo;
// 회원 가입
public void insertMember(Member member) {
member.setInvitedDate(new Date()); // 현재 날짜를 invitedDate에 설정
memberRepo.save(member);
}
// ID로 회원 조회
public Optional<Member> getMember(String id) {
return memberRepo.findById(id);
}
// 로그인 처리
public int login(Member member) {
int result = -1;
// 회원 조회
Member loginMember = memberRepo.findById(member.getId()).orElse(null);
if (loginMember != null) {
//
if (loginMember.getPassword().equals(member.getPassword())) {
result = 1; // ID와 Password 일치
} else {
result = 0; // Password 불일치
}
} else {
result = -1; // ID 없음
}
return result;
}
// 이름과 이메일로 ID 찾기
public Member findId(String name, String email) {
return memberRepo.findByNameAndEmail(name, email);
}
// ID, 이름, 이메일로 비밀번호 찾기
public Member findPassword(String id, String name, String email) {
return memberRepo.findByIdAndNameAndEmail(id, name, email);
}
// 회원 정보 수정
public Member updateMember(Member member) {
Member existMember = memberRepo.findById(member.getId()).orElse(null);
if (existMember != null) {
// 입력값이 null이 아닐 때만 기존 데이터 업데이트
if (member.getName() != null) {
existMember.setName(member.getName());
}
if (member.getPassword() != null) {
existMember.setPassword(member.getPassword());
}
if (member.getEmail() != null) {
existMember.setEmail(member.getEmail());
}
return memberRepo.save(existMember);
}
return null; // 회원 정보가 없을 경우 null 반환
}
// 회원 삭제
public void deleteMember(Member member) {
Optional<Member> existMember = memberRepo.findById(member.getId());
// 회원이 존재할 경우 삭제
if (existMember.isPresent()) {
memberRepo.delete(member);
}
}
}
- 작성한 메서드
- insertMember : save()를 사용해서 ID가 없으면 INSERT, 있으면 UPDATE를 수행
- getMember : findById()는 Optional로 감싸서 반환 (회원이 없을 수도 있어서)
- login : ID로 회원을 조회하고, 입력한 비밀번호와 일치하는지 확인하는 로직
- findId : findByNameAndEmail()은 JPA의 메서드 쿼리 기능을 사용 (자동으로 쿼리 생성됨)
- findPassword : findByIdAndNameAndEmail()도 마찬가지로 JPA의 메서드 쿼리 기능을 사용 (자동으로 쿼리 생성됨)
- updateMember : 수정 내용만 저장하고 save()를 사용해서 UPDATE
- deleteMember : delete() 호출 전에 findById()로 회원이 존재하는지 확인한 뒤 삭제
Controller 구현
package com.demo.controller;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import com.demo.domain.Member;
import com.demo.service.MemberService;
import jakarta.servlet.http.HttpSession;
@Controller
@RequestMapping("/members")
@SessionAttributes("loginUser")
public class MemberController {
@Autowired
private MemberService memberService;
// 회원 가입
@GetMapping("/register")
public String registerView() {
return "members/register"; // register.html로 이동
}
@PostMapping("/register")
public String registerMember(Member member) {
memberService.insertMember(member);
return "redirect:/members/login"; // 회원가입 후 로그인 페이지로 리다이렉트
}
// 로그인
@GetMapping("/login")
public String loginView() {
return "members/login"; // login.html로 이동
}
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(HttpSession session, Member member) {
int result = memberService.login(member);
Map<String, Object> response = new HashMap<>();
if (result == 1) {
Optional<Member> user = memberService.getMember(member.getId());
if (user.isPresent()) {
session.setAttribute("loginUser", user.get()); // 실제 user 객체를 세션에 저장
response.put("success", true);
response.put("message", "로그인 성공");
}
} else if (result == 0) {
response.put("success", false);
response.put("message", "비밀번호가 일치하지 않습니다.");
} else {
response.put("success", false);
response.put("message", "아이디가 존재하지 않습니다.");
}
return ResponseEntity.ok(response);
}
// 로그아웃
@GetMapping("/logout")
public String logout(SessionStatus status, HttpSession session) {
session.invalidate(); // 세션 무효화
status.setComplete(); // 세션 완료 상태로 설정
return "redirect:/main"; // 로그아웃 후 초기 화면으로 이동
}
// 회원 수정
@GetMapping("/update")
public String updateView(@SessionAttribute("loginUser") Member loginUser, Model model) {
// 로그인한 회원 정보 전달
model.addAttribute("member", loginUser);
return "members/update"; // update.html로 이동
}
@PutMapping("/update")
public String updateMember(@RequestBody Member member, HttpSession session) {
Member updateMember = memberService.updateMember(member);
if (updateMember != null) {
session.setAttribute("loginUser", updateMember); // 수정된 정보로 세션 갱신
return "redirect:/main"; // 성공 시 대시보드로 이동
}
return "redirect:/members/update"; // 실패 시 다시 수정 페이지로 이동
}
// 회원 삭제
@DeleteMapping("/delete")
public String deleteMember(@SessionAttribute("loginUser") Member loginUser) {
memberService.deleteMember(loginUser); // 로그인된 회원 정보로 삭제
return "redirect:/members/login"; // 삭제 후 로그인 페이지로 이동
}
}
HTML / js
main.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<link rel="stylesheet" th:href="@{/css/main.css}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script th:src="@{/js/main.js}"></script>
</head>
<body>
<!-- 로그인 상태 확인 -->
<div th:if="${session.loginUser == null}">
<!-- 비로그인 상태 -->
<button id="join_btn">회원가입</button>
<button id="login_btn">로그인</button>
</div>
<div th:if="${session.loginUser != null}">
<!-- 로그인 상태 -->
<button id="update_btn">정보수정</button>
<button id="logout_btn">로그아웃</button>
</div>
</body>
</html>
main.js
$(document).ready(function() {
// 회원가입 버튼
$("#join_btn").click(function() {
window.location.href = "/members/register"; // 회원가입 페이지 리다이렉트
});
// 로그인 버튼
$("#login_btn").click(function() {
window.location.href = "/members/login"; // 로그인 페이지 리다이렉트
});
// 정보수정 버튼
$("#update_btn").click(function() {
window.location.href = "/members/update"; // 정보수정 페이지 리다이렉트
});
// 로그아웃 버튼
$("#logout_btn").click(function() {
// AJAX 요청
$.ajax({
type : "GET", // GET 요청으로 로그아웃 처리
type: 'GET', // GET 방식으로 요청
success : function(response) {
window.location.href = "/main"; // 로그아웃 후 메인 페이지로 리다이렉트
},
error : function() {
swal.fire({
icon : 'error',
title : '로그아웃 실패',
text : '로그아웃에 실패했습니다. 다시 시도해주세요.'
});
}
});
});
});
register.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<link rel="stylesheet" th:href="@{/css/member.css}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script th:src="@{/js/member.js}"></script>
</head>
<body>
<!-- 회원가입 폼 -->
<form id="registerForm" method="post">
<span>ID는 영문자와 숫자로만 구성되어야 하고, 3자 이상 15자 이하여야 합니다.</span>
<input type="text" name="id" id="id" placeholder="ID">
<span>이메일 형식은 "example@domain.com" 형태여야 합니다.</span>
<input type="email" name="email" id="email" placeholder="Email">
<span>비밀번호는 최소 8자 이상이며, 대문자, 소문자, 숫자, 특수문자를 모두 포함해야 합니다.</span>
<input type="password" name="password" id="password" placeholder="Password">
<input type="password" name="pwdcheck" id="pwdcheck" placeholder="PasswordCheck">
<input type="text" name="name" id="name" placeholder="Name">
<button type="button" id="join_btn">회원가입</button>
</form>
</body>
</html>
update.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<link rel="stylesheet" th:href="@{/css/member.css}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script th:src="@{/js/member.js}"></script>
</head>
<body>
<!-- 정보수정 폼 -->
<form id="updateForm" method="post">
<input type="text" name="id" id="id" th:value="${member.id}" placeholder="ID" readonly>
<input type="email" name="email" id="email" th:value="${member.email}" placeholder="Email" required>
<span>비밀번호는 최소 8자 이상, 대문자, 소문자, 숫자, 특수문자를 모두 포함해야 합니다.</span>
<input type="password" name="password" id="password" placeholder="New Password" required>
<input type="password" name="pwdcheck" id="pwdcheck" placeholder="Check-New Password">
<input type="text" name="name" id="name" th:value="${member.name}" readonly>
<button type="button" id="update_btn">정보수정</button>
</form>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.css">
<link rel="stylesheet" th:href="@{/css/member.css}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.4.10/dist/sweetalert2.min.js"></script>
<script th:src="@{/js/member.js}"></script>
</head>
<body>
<!-- 로그인 폼 -->
<form id="loginForm" method="post">
<input type="text" name="id" id="id" placeholder="아이디" required>
<input type="password" name="password" id="password" placeholder="비밀번호" required>
<button type="button" id="login_btn">로그인</button>
<button type="button" onclick="location.href='/members/register'">회원가입</button>
</form>
</body>
</html>
member.js
$(document).ready(function() {
// 회원가입
$("#join_btn").click(function() {
// 필드 값
var id = $("#id").val();
var email = $("#email").val();
var password = $("#password").val();
var pwdcheck = $("#pwdcheck").val();
var name = $("#name").val();
// ID 유효성 겁사
var idRegex = /^[a-zA-Z0-9]{3,15}$/;
if (!idRegex.test(id)) {
swal.fire({
icon: 'error',
title: '아이디 형식 오류',
text: '아이디는 영문자와 숫자로만 구성되어야 하며, 3자 이상 15자 이하여야 합니다.'
});
return;
}
// 이메일 유효성 검사
var emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) {
swal.fire({
icon: 'error',
title: '이메일 형식 오류',
text: '유효한 이메일 주소를 입력하세요.'
});
return;
}
// 비밀번호 유효성 검사 (8자 이상, 대문자, 소문자, 숫자, 특수문자 포함)
var passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$/;
if (!passwordRegex.test(password)) {
swal.fire({
icon: 'error',
title: '비밀번호 형식 오류',
text: '비밀번호는 최소 8자 이상이어야 하며, 대문자, 소문자, 숫자, 특수문자를 포함해야 합니다.'
});
return;
}
// 비밀번호 확인
if (password !== pwdcheck) {
swal.fire({
icon: 'error',
title: '비밀번호 확인 오류',
text: '비밀번호와 비밀번호 확인이 일치하지 않습니다.'
});
return;
}
// 이름 입력 여부 검사
if (name.trim() === "") {
swal.fire({
icon: 'error',
title: '이름 입력 오류',
text: '이름을 입력하세요.'
});
return;
}
// 모든 검사 통과 시 폼 데이터
var formData = {
id: id,
email: email,
password: password,
name: name
};
// AJAX를 통해 회원가입 데이터 전송
$.ajax({
url: '/members/register',
type: 'POST',
data: formData,
success: function(response) {
swal.fire({
icon: 'success',
title: '회원가입 성공!',
text: '회원가입이 완료되었습니다.',
confirmButtonText: '로그인 화면으로 이동'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '/members/login';
}
});
},
error: function() {
swal.fire({
icon: 'error',
title: '회원가입 실패',
text: '회원가입에 실패했습니다. 다시 시도해주세요.'
});
}
});
});
// 정보수정
$("#update_btn").click(function() {
// 폼 데이터 수집
var formData = {
id: $("#id").val(),
email: $("#email").val(),
password: $("#password").val(),
pwdcheck: $("#pwdcheck").val(),
name: $("#name").val()
};
// 비밀번호 확인
if (formData.password !== formData.pwdcheck) {
swal.fire({
icon: 'error',
title: '비밀번호 불일치',
text: '비밀번호를 다시 확인해주세요'
});
return;
}
// 정보수정 요청
$.ajax({
url: "/members/update",
type: 'PUT',
contentType: 'application/json', // 전송 형식 설정
data: JSON.stringify(formData), // 데이터를 JSON 형식으로 변환
success: function(response) {
swal.fire({
icon: 'success',
title: '정보수정 성공',
text: '회원 정보가 업데이트 되었습니다.'
}).then(() => {
window.location.href = "/main";
});
},
error: function(error) {
swal.fire({
icon: 'error',
title: '정보수정 실패',
text: '회원 정보 수정 중 오류 발생. 다시 시도해주세요'
});
}
});
});
// 로그인
$("#login_btn").click(function() {
var id = $("#id").val();
var password = $("#password").val();
// 유효성 검사 (아이디와 비밀번호가 입력되었는지)
if (id.trim() === "") {
swal.fire({
icon: 'error',
title: '아이디 입력 오류',
text: '아이디를 입력하세요.'
});
return;
}
if (password.trim() === "") {
swal.fire({
icon: 'error',
title: '비밀번호 입력 오류',
text: '비밀번호를 입력하세요.'
});
return;
}
var formData = {
id: id,
password: password
};
$.ajax({
url: '/members/login',
type: 'POST',
data: formData,
success: function(response) {
// 로그인 성공 시
if (response.success) {
swal.fire({
icon: 'success',
title: '로그인 성공',
text: response.message
}).then(() => {
window.location.href = "/main"; // 로그인 후 대시보드로 이동
});
} else {
// 로그인 실패 시
swal.fire({
icon: 'error',
title: '로그인 실패',
text: response.message
});
}
},
error: function() {
swal.fire({
icon: 'error',
title: '로그인 실패',
text: '서버 오류가 발생했습니다. 다시 시도해주세요.'
});
}
});
});
});
프로젝트 실행
Hibernate는 Spring Boot 프로젝트 실행 시 자동으로 데이터베이스 테이블 생성하는 기능을 제공함.
>> 자동 테이블 생성(Auto DDL)
초반에 application.properties에서 spring.jpa.hibernate.ddl-auto=update로 설정 >> 테이블을 엔티티 클래스에 맞게 업데이트하고, 테이블이 변경되면 자동으로 수정
메인 페이지
>> 각 버튼 클릭 시
회원가입 해보기
로그인 하기
정보수정 하기
이메일 비밀번호 수정하기
데이터베이스의 값들도 수정
로그아웃은 바로 메인페이지로 이동.
'SpringBoot 프로젝트' 카테고리의 다른 글
[Spring Boot] JPA로 REST API 개발하기 (3) (0) | 2025.04.05 |
---|---|
[Spring Boot] JPA로 REST API 개발하기 (2) (0) | 2025.04.03 |
Spring Boot - 국립 도서관 Open API를 활용한 도서 검색 구현 (0) | 2024.12.10 |
Spring Boot - 국립 도서관 Open API를 활용한 사서 추천 도서 목록 구현 (0) | 2024.12.10 |
Spring Boot - 관리자 페이지 (5) (0) | 2024.12.07 |