SpringBoot 프로젝트

[Spring Boot] JPA로 REST API 개발하기 (1)

orin602 2025. 3. 28. 17:55

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로 설정 >> 테이블을 엔티티 클래스에 맞게 업데이트하고, 테이블이 변경되면 자동으로 수정

 

메인 페이지

>> 각 버튼 클릭 시

 

회원가입 해보기

로그인 하기

 

 

정보수정 하기

이메일 비밀번호 수정하기

데이터베이스의 값들도 수정

 

로그아웃은 바로 메인페이지로 이동.