1. 학습 주제
이번에는 Spring Data JPA를 이용해 조회 중심 구조에서 한 단계 더 나아가,
실제로 메뉴를 등록하고, 수정하고, 삭제하는 흐름까지 연결했다.
핵심은 다음과 같다.
- JpaRepository의 기본 CRUD 메서드 활용
- 요청 DTO와 응답 DTO를 분리한 API 설계
- 연관 엔티티를 조회한 뒤 연결해서 저장하는 방식
- @Transactional 안에서 변경 감지로 수정 처리하는 방식
- 삭제 전 존재 여부 확인과 deleteById() 사용
즉 이번 범위는
Spring Data JPA를 이용한 실전 CRUD 서비스 흐름을 익히는 단계였다.
2. 전체 흐름
메뉴 관련 API 흐름은 다음처럼 구성되었다.
- Controller가 요청을 받는다
- Service가 비즈니스 로직을 수행한다
- Repository가 DB 접근을 담당한다
- 엔티티를 DTO로 변환해 응답한다
이 구조는 이전과 같지만,
이번에는 단순 조회뿐 아니라 생성, 수정, 삭제까지 포함되면서
JPA의 장점이 더 잘 드러났다.
3. 요청 DTO와 응답 DTO를 나누는 이유
메뉴 등록과 수정에는 MenuRequestDTO를 사용하고,
응답에는 MenuResponseDTO를 사용했다.
이렇게 나누는 이유는 다음과 같다.
요청 DTO
클라이언트가 보내는 입력 데이터 전용
예:
- 메뉴 이름
- 가격
- 카테고리 코드
- 판매 여부
응답 DTO
서버가 돌려주는 출력 데이터 전용
예:
- 메뉴 코드
- 메뉴 이름
- 가격
- 판매 여부
- 카테고리 정보
즉 요청과 응답의 목적이 다르기 때문에 DTO도 분리하는 것이 자연스럽다.
이 구조의 장점은:
- 입력값 검증과 응답 구조를 각각 설계하기 쉽고
- 엔티티를 외부에 직접 노출하지 않으며
- API 형태를 더 명확하게 제어할 수 있다는 점이다.
4. 메뉴 단건 조회
메뉴 상세 조회는 findById()를 이용해 처리했다.
흐름은 단순하다.
- 메뉴 코드로 조회 요청
- Repository에서 findById() 수행
- 없으면 예외 발생
- 있으면 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. 메뉴 등록
메뉴 등록 흐름은 이번 범위의 핵심 중 하나였다.
흐름을 순서대로 보면:
- 요청 DTO로 입력을 받는다
- 요청에 들어 있는 카테고리 코드를 기준으로 카테고리 엔티티를 조회한다
- 카테고리가 없으면 예외를 발생시킨다
- 메뉴 엔티티를 생성한다
- save()로 저장한다
- 저장된 엔티티를 응답 DTO로 변환한다
이때 중요한 포인트는 메뉴 엔티티를 만들 때
카테고리 코드 숫자만 넣는 게 아니라
실제 카테고리 엔티티를 찾아서 연결한다는 점이다.
즉 JPA에서는 외래키 값을 직접 다루기보다,
연관된 엔티티 객체를 연결하는 방식으로 생각해야 한다.
9. save()는 내부적으로 무엇을 할까
등록 시 Repository의 save()를 사용했다.
Spring Data JPA에서 save()는 내부적으로 JPA의 영속성 흐름과 연결된다.
새 엔티티라면 보통:
- EntityManager.persist() 흐름으로 처리되고
- 영속성 컨텍스트에 등록된 뒤
- 트랜잭션 commit 시 INSERT가 반영된다
즉 save()는 단순 helper 메서드가 아니라
JPA 저장 메커니즘을 감싼 Spring Data JPA의 대표 메서드라고 보면 된다.
10. 메뉴 수정
메뉴 수정에서는 등록과 다른 점이 있다.
등록은 새 엔티티를 만들지만,
수정은 기존 엔티티를 먼저 찾아야 한다.
흐름은 다음과 같다.
- 수정 대상 메뉴를 조회한다
- 없으면 예외를 발생시킨다
- 변경할 카테고리도 다시 조회한다
- 엔티티의 수정 메서드를 호출해 상태를 바꾼다
- 트랜잭션 commit 시 변경 내용이 UPDATE 된다
여기서 핵심은
수정할 때 save()를 다시 부르지 않았다는 점이다.
이게 JPA의 변경 감지와 연결된다.
11. 변경 감지란 무엇인가
메뉴 수정에서 엔티티를 조회한 뒤:
- 이름 바꾸고
- 가격 바꾸고
- 카테고리 바꾸고
- 판매 여부 바꿨다
그런데 별도의 UPDATE SQL을 직접 쓰지 않는다.
이게 가능한 이유는 해당 엔티티가 영속 상태이기 때문이다.
영속 상태 엔티티는 JPA가 관리하므로,
필드 값이 바뀌면 그 변경을 추적한다.
그리고 트랜잭션이 끝날 때
JPA가 필요한 UPDATE SQL을 만들어 반영한다.
즉 수정은:
- 엔티티를 다시 저장하는 것보다
- 영속 상태 엔티티의 값을 바꾸는 것
으로 이해하는 것이 더 정확하다.
12. 왜 엔티티 안에 수정 메서드를 두는가
메뉴 엔티티에는 수정 메서드가 있었다.
이 방식은 의미가 크다.
예전에는 setter를 여기저기서 직접 호출하기 쉬웠지만,
엔티티 안에 수정 메서드를 두면:
- 상태 변경 책임이 엔티티 안에 모이고
- 어떤 값들이 같이 수정되는지 분명해지고
- 도메인 객체가 스스로 상태를 관리하는 구조가 된다
즉 엔티티를 단순 데이터 상자가 아니라
상태와 동작을 가진 객체로 다루는 방향이라고 볼 수 있다.
13. 메뉴 삭제
삭제 흐름도 중요하다.
- 먼저 해당 메뉴가 존재하는지 확인한다
- 존재하지 않으면 예외를 발생시킨다
- 존재하면 deleteById()를 호출한다
- 응답은 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. 핵심 정리
- Spring Data JPA는 JpaRepository를 통해 CRUD 코드를 크게 줄여준다.
- 단건 조회는 findById(), 목록 조회는 findAll(pageable)로 쉽게 처리할 수 있다.
- 요청 DTO와 응답 DTO를 분리하면 입력 구조와 출력 구조를 명확하게 설계할 수 있다.
- 메뉴 등록 시에는 카테고리 코드를 직접 넣는 것이 아니라 실제 카테고리 엔티티를 조회해 연결한다.
- save()는 내부적으로 JPA의 영속성 흐름과 연결되어 새 엔티티 저장을 처리한다.
- 수정은 새로 저장하는 것이 아니라 영속 상태 엔티티의 값을 바꾸고, 변경 감지로 반영된다.
- 엔티티 안에 수정 메서드를 두면 상태 변경 책임을 객체 안으로 모을 수 있다.
- 삭제 전 존재 여부를 확인하면 잘못된 삭제 요청에 더 명확하게 대응할 수 있다.
- 등록, 수정, 삭제 같은 쓰기 작업에는 @Transactional이 필요하다.
- Spring Data JPA는 기본 CRUD뿐 아니라 쿼리 메서드, 커스텀 쿼리, 페이징까지 자연스럽게 확장할 수 있다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]Spring Security와 JWT 기반 인증 흐름 (0) | 2026.06.23 |
|---|---|
| TIL - Spring Security 기초, 세션과 토큰, Filter와 Interceptor, SSR과 CSR (0) | 2026.06.22 |
| [TIL]Spring Data JPA로 메뉴 조회 API 구성하기 (0) | 2026.06.17 |
| [TIL]JPQL JOIN, Native Query, 그리고 Spring Data JPA 시작 (0) | 2026.06.16 |
| [TIL]JPQL의 Projection, Paging, Group Function 정리 (0) | 2026.06.15 |