TIL/[TIL]

[TIL]Spring Data JPA로 메뉴 등록, 수정, 삭제 구현하기

namerong 2026. 6. 18. 11:49

1. 학습 주제

이번에는 Spring Data JPA를 이용해 조회 중심 구조에서 한 단계 더 나아가,
실제로 메뉴를 등록하고, 수정하고, 삭제하는 흐름까지 연결했다.

핵심은 다음과 같다.

  • JpaRepository의 기본 CRUD 메서드 활용
  • 요청 DTO와 응답 DTO를 분리한 API 설계
  • 연관 엔티티를 조회한 뒤 연결해서 저장하는 방식
  • @Transactional 안에서 변경 감지로 수정 처리하는 방식
  • 삭제 전 존재 여부 확인과 deleteById() 사용

즉 이번 범위는
Spring Data JPA를 이용한 실전 CRUD 서비스 흐름을 익히는 단계였다.


2. 전체 흐름

메뉴 관련 API 흐름은 다음처럼 구성되었다.

  1. Controller가 요청을 받는다
  2. Service가 비즈니스 로직을 수행한다
  3. Repository가 DB 접근을 담당한다
  4. 엔티티를 DTO로 변환해 응답한다

이 구조는 이전과 같지만,
이번에는 단순 조회뿐 아니라 생성, 수정, 삭제까지 포함되면서
JPA의 장점이 더 잘 드러났다.


3. 요청 DTO와 응답 DTO를 나누는 이유

메뉴 등록과 수정에는 MenuRequestDTO를 사용하고,
응답에는 MenuResponseDTO를 사용했다.

이렇게 나누는 이유는 다음과 같다.

요청 DTO

클라이언트가 보내는 입력 데이터 전용

예:

  • 메뉴 이름
  • 가격
  • 카테고리 코드
  • 판매 여부

응답 DTO

서버가 돌려주는 출력 데이터 전용

예:

  • 메뉴 코드
  • 메뉴 이름
  • 가격
  • 판매 여부
  • 카테고리 정보

즉 요청과 응답의 목적이 다르기 때문에 DTO도 분리하는 것이 자연스럽다.

이 구조의 장점은:

  • 입력값 검증과 응답 구조를 각각 설계하기 쉽고
  • 엔티티를 외부에 직접 노출하지 않으며
  • API 형태를 더 명확하게 제어할 수 있다는 점이다.

4. 메뉴 단건 조회

메뉴 상세 조회는 findById()를 이용해 처리했다.

흐름은 단순하다.

  1. 메뉴 코드로 조회 요청
  2. Repository에서 findById() 수행
  3. 없으면 예외 발생
  4. 있으면 DTO로 변환 후 응답

여기서 중요한 점은 findById()가 Optional을 반환한다는 것이다.
즉 조회 결과가 없을 수도 있다는 사실을 코드 수준에서 표현한다.

이전보다 더 명시적으로 “없을 수 있는 결과”를 다룬다는 점이 Spring Data JPA의 특징 중 하나다.


5. 메뉴 목록 조회와 페이징

메뉴 목록 조회는 findAll(pageable)로 처리했다.

여기서 Pageable은:

  • 몇 개를 가져올지
  • 어떤 기준으로 정렬할지
  • 어느 페이지를 볼지

같은 정보를 담고 있다.

즉 컨트롤러는 Pageable을 받고,
서비스는 Page<Menu>를 조회한 뒤
이를 Page<MenuResponseDTO>로 변환해서 응답한다.

이 구조의 장점은:

  • 대량 데이터를 잘라서 가져올 수 있고
  • 응답에 페이징 정보가 함께 포함되며
  • 화면에서 페이지 UI를 만들기 쉽다는 점이다.

6. 가격 기준 검색과 쿼리 메서드

가격으로 검색하는 기능은 쿼리 메서드로 구현되었다.

예를 들어:

  • 특정 가격보다 큰 메뉴 찾기
  • 가격 내림차순 정렬
  • 페이징과 함께 검색

같은 작업을 메서드 이름만으로 표현할 수 있다.

이게 가능한 이유는 Spring Data JPA가 메서드 이름을 규칙으로 해석하기 때문이다.

즉 개발자는 JPQL을 직접 적지 않고도
단순 조건 검색을 꽤 쉽게 만들 수 있다.

이 방식은:

  • 간단한 조회
  • 정렬
  • 페이징 조건

에서 특히 강력하다.


7. 카테고리 목록 조회

카테고리 목록 조회도 함께 구성되었다.

서비스에서는 카테고리 엔티티 목록을 가져와 CategoryDTO로 변환해서 반환한다.

이 흐름이 중요한 이유는,
메뉴 등록이나 수정에서 카테고리 선택이 필요하기 때문이다.

즉 메뉴 CRUD를 만들려면
메뉴 자체뿐 아니라 연관된 카테고리 정보도 함께 다룰 수 있어야 한다.


8. 메뉴 등록

메뉴 등록 흐름은 이번 범위의 핵심 중 하나였다.

흐름을 순서대로 보면:

  1. 요청 DTO로 입력을 받는다
  2. 요청에 들어 있는 카테고리 코드를 기준으로 카테고리 엔티티를 조회한다
  3. 카테고리가 없으면 예외를 발생시킨다
  4. 메뉴 엔티티를 생성한다
  5. save()로 저장한다
  6. 저장된 엔티티를 응답 DTO로 변환한다

이때 중요한 포인트는 메뉴 엔티티를 만들 때
카테고리 코드 숫자만 넣는 게 아니라
실제 카테고리 엔티티를 찾아서 연결한다는 점이다.

즉 JPA에서는 외래키 값을 직접 다루기보다,
연관된 엔티티 객체를 연결하는 방식으로 생각해야 한다.


9. save()는 내부적으로 무엇을 할까

등록 시 Repository의 save()를 사용했다.

Spring Data JPA에서 save()는 내부적으로 JPA의 영속성 흐름과 연결된다.

새 엔티티라면 보통:

  • EntityManager.persist() 흐름으로 처리되고
  • 영속성 컨텍스트에 등록된 뒤
  • 트랜잭션 commit 시 INSERT가 반영된다

즉 save()는 단순 helper 메서드가 아니라
JPA 저장 메커니즘을 감싼 Spring Data JPA의 대표 메서드라고 보면 된다.


10. 메뉴 수정

메뉴 수정에서는 등록과 다른 점이 있다.

등록은 새 엔티티를 만들지만,
수정은 기존 엔티티를 먼저 찾아야 한다.

흐름은 다음과 같다.

  1. 수정 대상 메뉴를 조회한다
  2. 없으면 예외를 발생시킨다
  3. 변경할 카테고리도 다시 조회한다
  4. 엔티티의 수정 메서드를 호출해 상태를 바꾼다
  5. 트랜잭션 commit 시 변경 내용이 UPDATE 된다

여기서 핵심은
수정할 때 save()를 다시 부르지 않았다는 점이다.

이게 JPA의 변경 감지와 연결된다.


11. 변경 감지란 무엇인가

메뉴 수정에서 엔티티를 조회한 뒤:

  • 이름 바꾸고
  • 가격 바꾸고
  • 카테고리 바꾸고
  • 판매 여부 바꿨다

그런데 별도의 UPDATE SQL을 직접 쓰지 않는다.

이게 가능한 이유는 해당 엔티티가 영속 상태이기 때문이다.

영속 상태 엔티티는 JPA가 관리하므로,
필드 값이 바뀌면 그 변경을 추적한다.

그리고 트랜잭션이 끝날 때
JPA가 필요한 UPDATE SQL을 만들어 반영한다.

즉 수정은:

  • 엔티티를 다시 저장하는 것보다
  • 영속 상태 엔티티의 값을 바꾸는 것

으로 이해하는 것이 더 정확하다.


12. 왜 엔티티 안에 수정 메서드를 두는가

메뉴 엔티티에는 수정 메서드가 있었다.

이 방식은 의미가 크다.

예전에는 setter를 여기저기서 직접 호출하기 쉬웠지만,
엔티티 안에 수정 메서드를 두면:

  • 상태 변경 책임이 엔티티 안에 모이고
  • 어떤 값들이 같이 수정되는지 분명해지고
  • 도메인 객체가 스스로 상태를 관리하는 구조가 된다

즉 엔티티를 단순 데이터 상자가 아니라
상태와 동작을 가진 객체로 다루는 방향이라고 볼 수 있다.


13. 메뉴 삭제

삭제 흐름도 중요하다.

  1. 먼저 해당 메뉴가 존재하는지 확인한다
  2. 존재하지 않으면 예외를 발생시킨다
  3. 존재하면 deleteById()를 호출한다
  4. 응답은 204 No Content로 처리한다

삭제 전에 존재 여부를 확인하는 이유는
없는 데이터를 지우려고 했을 때도 명확한 의미를 주기 위해서다.

즉 삭제 역시 단순히 “지워라”가 아니라
대상이 존재하는지 확인하고 적절한 예외 흐름을 가진다는 점이 중요하다.


14. @Transactional이 왜 필요한가

등록, 수정, 삭제 메서드에는 @Transactional이 붙어 있었다.

이 어노테이션이 필요한 이유는 다음과 같다.

등록

새 엔티티를 영속성 컨텍스트에 등록하고
commit 시 실제 INSERT를 반영해야 한다.

수정

조회한 영속 상태 엔티티의 변경을
commit 시 UPDATE로 반영해야 한다.

삭제

삭제 요청을 하나의 작업 단위로 처리하고
문제 발생 시 rollback 가능해야 한다.

즉 쓰기 작업은 모두 트랜잭션 경계가 필요하다.

특히 수정에서는 @Transactional이 더 중요하다.
왜냐하면 변경 감지는 트랜잭션 commit 시점과 강하게 연결되기 때문이다.


15. 카테고리 엔티티를 왜 따로 조회하나

등록과 수정에서 공통으로 한 일이 있다.

  • 요청 DTO에는 카테고리 코드만 들어온다
  • 실제 저장할 때는 카테고리 엔티티를 조회해서 연결한다

즉 단순히 categoryCode 정수만 엔티티에 넣는 게 아니라,
JPA 연관관계답게 실제 Category 엔티티 객체를 연결하는 방식을 사용했다.

이게 중요한 이유는 JPA가 객체 간 관계를 다루는 기술이기 때문이다.

즉 메뉴는 카테고리 코드를 “가지고 있는” 게 아니라,
카테고리 엔티티를 “참조하고 있다”는 쪽이 더 정확하다.


16. CategoryRepository의 커스텀 조회 방식

카테고리 Repository에서는 기본 findAll() 외에
직접 쿼리를 지정할 수 있는 구조도 보였다.

즉 Spring Data JPA에서는:

  • 기본 CRUD 메서드
  • 쿼리 메서드
  • JPQL 기반 @Query
  • Native Query 기반 조회

처럼 여러 방식으로 Repository를 확장할 수 있다.

이번 범위에서는 그중 “커스텀 조회도 가능하다”는 감각까지 함께 볼 수 있었다.


17. Controller 응답 구조

컨트롤러는 CRUD에 맞춰 응답을 나누고 있었다.

상세 조회

  • 200 OK
  • DTO 반환

목록 조회

  • 200 OK
  • Page<DTO> 반환

등록

  • 201 Created
  • 생성된 DTO 반환

수정

  • 200 OK
  • 수정된 DTO 반환

삭제

  • 204 No Content
  • 본문 없음

즉 단순히 성공/실패만 보는 것이 아니라
HTTP 의미에 맞는 상태 코드를 사용하는 API 구조를 익히는 데도 의미가 있었다.


18. 이번 범위를 한 흐름으로 보면

이번 학습은 Spring Data JPA로 “읽기” 중심 구조를 “쓰기”까지 확장한 단계라고 볼 수 있다.

즉 이전 흐름이:

  • 조회
  • 목록
  • 검색
  • 페이징

이었다면,

이번에는 여기에:

  • 등록
  • 수정
  • 삭제
  • 연관 엔티티 연결
  • 변경 감지
  • 트랜잭션

이 붙었다.

즉 Spring Data JPA가 단순 조회 라이브러리가 아니라
실제 서비스 CRUD 전반을 구성할 수 있는 도구라는 점이 더 선명해졌다.


19. 학습 흐름 정리

19.1 조회

  • findById()로 단건 조회
  • findAll(pageable)로 목록 조회
  • 쿼리 메서드로 가격 조건 검색
  • Pageable, Page로 페이징 처리

19.2 등록

  • 요청 DTO 수신
  • 카테고리 엔티티 조회
  • 메뉴 엔티티 생성
  • save() 호출
  • 응답 DTO 반환

19.3 수정

  • 수정 대상 메뉴 조회
  • 새 카테고리 조회
  • 엔티티 수정 메서드 호출
  • 변경 감지로 UPDATE 반영

19.4 삭제

  • 존재 여부 확인
  • deleteById() 실행
  • 204 No Content 응답

20. 핵심 정리

  1. Spring Data JPA는 JpaRepository를 통해 CRUD 코드를 크게 줄여준다.
  2. 단건 조회는 findById(), 목록 조회는 findAll(pageable)로 쉽게 처리할 수 있다.
  3. 요청 DTO와 응답 DTO를 분리하면 입력 구조와 출력 구조를 명확하게 설계할 수 있다.
  4. 메뉴 등록 시에는 카테고리 코드를 직접 넣는 것이 아니라 실제 카테고리 엔티티를 조회해 연결한다.
  5. save()는 내부적으로 JPA의 영속성 흐름과 연결되어 새 엔티티 저장을 처리한다.
  6. 수정은 새로 저장하는 것이 아니라 영속 상태 엔티티의 값을 바꾸고, 변경 감지로 반영된다.
  7. 엔티티 안에 수정 메서드를 두면 상태 변경 책임을 객체 안으로 모을 수 있다.
  8. 삭제 전 존재 여부를 확인하면 잘못된 삭제 요청에 더 명확하게 대응할 수 있다.
  9. 등록, 수정, 삭제 같은 쓰기 작업에는 @Transactional이 필요하다.
  10. Spring Data JPA는 기본 CRUD뿐 아니라 쿼리 메서드, 커스텀 쿼리, 페이징까지 자연스럽게 확장할 수 있다.