13주차. Spring Data JPA로 CRUD 구현하기

박서영·2025년 11월 27일
post-thumbnail

Spring Data JPA의 주요 개념

JPA (Java Persistence API)

  • 자바 객체와 관계형 데이터베이스를 매핑하는 표준 명세(ORM)
  • SQL을 직접 작성하지 않아도 DB 작업이 가능해짐

Spring Data JPA

  • JPA를 더 쉽게 사용하도록 스프링이 추상화한 기술

JpaRepository

  • CRUD 기능을 제공하는 인터페이스

@Entity

  • DB와 매핑되는 객체임을 알려주는 어노테이션

실습

실습환경 설정

  • 이전 스프링부트 프로젝트에서 의존성에 Spring Data JPA, MySQL Driver 추가

application.properties에 데이터베이스 연결정보 추가

spring.application.name=CRUD API
spring.datasource.hikari.jdbc-url=jdbc:mariadb://127.0.0.1:3306/practice
spring.datasource.hikari.username=(DB 사용자이름)
spring.datasource.hikari.password=(DB 비밀번호)
spring.datasource.hikari.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.show-sql=true

MariaDB에 테이블 만들기

실습 내용 복기 겸 DB를 새로 하나 만들었다. practice라는 스키마 안에 user 테이블을 만들고, 필드는 Id, Name, Age 세 가지로 하였다.

Id는 자동으로 증가되도록 설정해주었다.

@Entity: DB 테이블 매핑 클래스

package kr.ac.ewha.java2.dao;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private int age;
    public User() {}
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //Getter & Setter
    public int getId() {return id;}
    public void setId(int id) {this.id = id;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
    public void setAge(int age) {this.age = age;}
}

정말 DB와 1:1로 매핑되게 필드를 만들었다. 그리고 DB와 매핑되는 객체임을 표시하기 위해서 @Entity 어노테이션을 위에 작성해주었다.

+)
@Id: DB의 primary key 컬럼과 매핑되는 어노테이션으로 모든 엔티티 클래스에 하나는 존재해야함

GeneratedValue(strategy = GenerationType.IDENTITY): 기본키의 값을 자동으로 생성하는 방법을 지정. AUTO_INCREMENT, 즉 데이터베이스에서 알아서 새로운 데이터를 저장할 때마다 자동으로 값을 증가시켜서 생성해주는 것을 의미.

UserRepository 만들기

UserRepositoryJpaRepository를 상속하고 있다.

package kr.ac.ewha.java2.repository;

import kr.ac.ewha.java2.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}

+) JpaRepository

  • Spring Data JPA에서 제공하는 핵심 인터페이스 -> 상속하게되면 데이터베이스 CRUD(Create, Read, Update, Delete) 작업을 위한 수많은 메소드를 자동으로 사용할 수 있게됨

  • 작동 원리 = "자동 메소드 제공(인터페이스 상속)"
    => 상위의 다른 인터페이스들(예 PagingSortingRepository, CrudRepository)의 모든 기능을 상속받음

  • CrudRepository: CRUD를 위한 메소드- save(), findById(), findAll(), delete()를 정의함

  • 개발자는 Repository 인터페이스를 선언하고 JpaRepository<T, ID>를 상속받기만 하면, 모든 메소드를 별도 구현 없이 사용할 수 있음


+) JpaRepository<T, ID>에서의 <T, ID>

  • 제너릭 타입 파라미터로 Spring Data JPA가 어떤 엔티티(@Entity)와 그 기본키(primary key) 타입을 다루어야하는지 알려주는 역할
  • 위의 User 관련 CRUD에서는 엔티티 = User, 기본키 = ID, 즉 int 타입이므로 JpaRepository<User, Integer>로 적어주는 것

UserService

UserService.java 에서 CRUD를 위한 서비스를 우선 구현한다.

package kr.ac.ewha.java2.service;

import kr.ac.ewha.java2.dao.User;
import kr.ac.ewha.java2.repository.UserRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService (UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    //CREATE
    public User create (User user) {
        return userRepository.save(user);
    }

    //READ
    public List<User> read() {
        return userRepository.findAll();
    }
    public User readById(int id) {
        return userRepository.findById(id).orElse(null);
    }

    //UPDATE
    public User update(int id, User updatedUser) {
        User user = readById(id); //기존 사용자 가져오기
        if (user == null) return null;

        user.setName(updatedUser.getName());
        user.setAge(updatedUser.getAge());
        user.setStudentID(updatedUser.getStudentID());

        return userRepository.save(user);
    }

    //DELETE: ID로 찾아서 사용자 삭제 -> 성공 여부 boolean으로 반환
    public Boolean delete (int id) {
        if (!userRepository.existsById(id)) return false;
        userRepository.deleteById(id);
        return true;
    }

}

이전에 JDBC를 썼을 때와 마찬가지로 DB의 사용자 정보를 생성, 업데이트, 조회, 삭제할 수 있도록 한다. 이전에는 SQL 쿼리문을 직접 작성하였다면, 지금은 앞에서 만든 UserRepository가 상속한 JpaRepository의 메소드들을 사용하면 스프링이 알아서 연관 쿼리문 처리를 해주는 듯 하다.

SELECT, DELETE 같은 쿼리는 없이, findAll(), findById() 와 같은 메소드를 사용해 조회나 삭제가 가능하다.


UserController

package kr.ac.ewha.java2.controller;

import kr.ac.ewha.java2.dao.User;
import kr.ac.ewha.java2.service.UserService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    //CREATE
    @PostMapping("/submitForm")
    @ResponseBody
    public String create(@RequestParam("name") String name, @RequestParam("age") int age,
                         @RequestParam("studentID") String studentID) {

        User newUser = new User(name, age, studentID);
        userService.create(newUser);
        return "<h3>등록 완료</h3>" +
                "<button onclick=\"location.href='/user-crud.html'\">돌아가기</button>";
    }

    //READ: id로 사용자 조회
    @GetMapping //GET 요청. /user?id=...을 처리
    @ResponseBody
    public String readById(@RequestParam("id") int id) {
        User user = userService.readById(id);

        if (user == null) return "<h3>사용자가 존재하지 않습니다.</h3>";

        return "<h3>사용자 정보</h3>" +
                "<p>ID: " + user.getId() + "</p>" +
                "<p>이름: " + user.getName() + "</p>" +
                "<p>나이: " + user.getAge() + "</p>" +
                "<button onclick=\"location.href='/user-crud.html'\">돌아가기</button>";
    }

    @GetMapping("/list")
    @ResponseBody
    public String list() {
        List<User> users = userService.read();

        StringBuilder html = new StringBuilder("<h3>전체 사용자 목록</h3><table border='1'><tr><th>ID</th><th>이름</th><th>나이</th></tr>");
        for (User u : users) {
            html.append("<tr><td>").append(u.getId()).append("</td><td>")
                    .append(u.getName()).append("</td><td>")
                    .append(u.getAge()).append("</td></tr>");
        }
        html.append("</table><br><button onclick=\"location.href='/user-crud.html'\">돌아가기</button>");
        return html.toString();
    }

    @PostMapping("updateForm")
    @ResponseBody
    public String update(@RequestParam("id") int id, @RequestParam("name") String name,
                         @RequestParam("age") int age, @RequestParam("studentID") String studentID) {
        User result = userService.update(id, new User(name, age, studentID));

        if (result == null) {
            return "<h3>수정 실패: 사용자를 찾을 수 없습니다.</h3><a href='/user-crud.html'>돌아가기</a>"; // 실패 시 메시지
        }
        return "<h3>사용자 정보 수정 완료</h3>" +
                "<button onclick=\"location.href='/user-crud.html'\">돌아가기</button>";
    }

    //DELETE
    @PostMapping("/deleteForm")
    @ResponseBody
    public String delete(@RequestParam("id") int id) {
        boolean success = userService.delete(id);

        if (!success) return  "<h3>사용자를 찾을 수 없습니다.</h3>" + "<button a href='/user-crud.html> 돌아가기</button>";
        return "<h3>사용자 정보 삭제 완료</h3>" +
                "<button onclick=\"location.href='/user-crud.html'\">돌아가기</button>";
    }

}

실습하다가 파라미터 받는걸 테스트해보려고..? DB 테이블에 속성 하나를 추가해봤다. 학번까지 받아보려고 한다. UserServiceupdate() 메소드에서 학번까지 업데이트하는걸로 수정해주고, User의 필드로 학번을 뒀다.


실습 결과 화면

1. 사용자 등록 (CREATE)

2. 사용자 조회 (READ)

  • 전체 사용자 조회

  • ID값으로 조회

3. 사용자 업데이트 (UPDATE)


4. 사용자 삭제 (DELETE)


소감
조금 급하게 실습을 정리하면서 복기해서 학번을 생성이나 업데이트할 때 받아서 사용자를 생성, 업데이트하고 DB에도 반영을 하긴하는데, 웹에서 조회할 때 받은 학번을 출력하는 코드를 빼먹은걸 실습 결과 스크린샷하면서 깨달았다...ㅎㅎ

JDBC 사용할 때는 쿼리문 적는게 정말 헷갈렸는데 이렇게 쉽게 쓸 수 있다는게 사실 놀랍다. 사실 이전에 웹 실습했을 때 이걸 먼저 쓰고 수업 때 JDBC를 쓰면서 쿼리문을 적어야해서 당황했다.

이미 있는 라이브러리나 메소드를 그냥 가져다쓰는 것이 사실 익숙하지는 않아서 여전히 헷갈리는 것 같다...+ HTML이나 프론트엔드 코드도 어렴풋이만 알아서 좀 아쉬운..? 결과물인 것 같다. 후에 부족한 부분을 채워나가면 좋을 것 같다는 생각이 들었다.

profile
이불 밖은 위험해.

0개의 댓글