2025. 11. 25. 17:59ㆍJava/JPA
Spring Data JPA CRUD + Lombok + 패턴 + 레이어드 아키텍처 한 번에 정리
1. 오늘 실습 상황 정리
이번 실습에서는 Spring Boot + Spring Data JPA + Lombok 조합으로 Student 테이블 CRUD + 페이징을 구현하고,
추가로 레이어드 아키텍처 개념, Lombok의 역할, Spring Data JPA 구조, Builder 패턴, Singleton 패턴까지 연결해서 정리했다.
구조는 다음처럼 구성했다.
- entity : Student 엔티티
- repository : StudentRepository (JpaRepository 상속)
- service : StudentServiceCrud / StudentServiceCrudImpl
- controller : StudentControllerCrud (REST API)
- pattern : builder / methodchain / singleton 개념 예제
- lombok : Lombok 어노테이션 맛보기

2. 레이어드 아키텍처 vs Spring MVC (개념 정리)
2-1. 레이어드 아키텍처란?
레이어드 아키텍처(Layered Architecture)는 애플리케이션을 역할별 층(layer)으로 나눠서 설계하는 방식이다.
대표적인 구조:
[Controller] ← 프레젠테이션 계층 (웹 요청/응답)
↓
[Service] ← 비즈니스 로직 계층
↓
[Repository] ← 데이터 접근 계층 (DB 조회/저장)
↓
[DB] ← 실제 데이터베이스 (MySQL 등)
- 각 계층은 “아래 계층의 기능만 사용”하도록 설계 → 결합도 감소, 변경 용이
- 테스트, 유지보수, 역할 분리가 쉬워진다.
2-2. Spring MVC는 무엇인가?
Spring MVC는 웹 요청(HTTP 요청/응답)을 처리하는 프레임워크다.
레이어드 아키텍처 중 프레젠테이션 계층(Controller + View)을 담당하는 기술이라고 보면 된다.
@Controller,@RestController로 요청 처리@GetMapping,@PostMapping등으로 URL + HTTP Method 매핑
2-3. 관계 정리
- 레이어드 아키텍처 = 애플리케이션 전체 구조 설계 방식
- Spring MVC = 그 구조 중 “웹 계층(Controller)”을 구현하는 웹 프레임워크
한 줄로 요약하면,
레이어드 아키텍처는 건물 설계도, Spring MVC는 1층 로비를 구현하는 기술 정도로 이해하면 된다.
3. DB & Student 엔티티 매핑
3-1. Student 테이블 DDL (요약)
create table student (
id int not null auto_increment,
email varchar(200) default null,
phone varchar(13) default '010-0000-0000',
name varchar(255) default null,
primary key (id)
);
실습에서는 미리 1~100까지 홍길동1~홍길동100 데이터를 insert 해 두고, 곧바로 조회/페이징 테스트가 가능하도록 구성했다.
3-2. Student 엔티티 코드
package com.mycom.myapp.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "student")
@Getter
@Setter
@ToString
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT
private Integer id;
@Column(length = 200)
private String email;
@Column(length = 13, columnDefinition = "varchar(13) default '010-0000-0000'")
private String phone;
@Column(length = 255)
private String name;
}
@Entity: JPA가 관리하는 엔티티 클래스@Table(name = "student"): 실제 매핑될 테이블 이름@Id+@GeneratedValue(IDENTITY): PK + AUTO_INCREMENT 전략 사용- Lombok(
@Getter,@Setter,@ToString)로 보일러플레이트 코드 제거
4. Lombok이 실제로 하는 일과 정체
4-1. Lombok 이란?
Lombok은 컴파일 시점에 어노테이션을 보고 자바 코드를 자동 생성해주는 라이브러리이다.
- 자바 표준이 아니라 외부 라이브러리
- IDE 플러그인 + Gradle/Maven 의존성 필요
- 소스에는 코드가 안 보여도,
.class파일에는 실제 메서드가 생성되어 들어간다.
4-2. 실습에서 사용한 Lombok 어노테이션
@Getter/@Setter: getter / setter 자동 생성@ToString:toString()자동 생성@EqualsAndHashCode: equals, hashCode 자동 생성 (이번엔 직접 쓰진 않았지만 대표적인 기능)@AllArgsConstructor,@NoArgsConstructor: 모든 필드/기본 생성자 자동 생성@Builder: 빌더 패턴 코드 자동 생성@RequiredArgsConstructor: final 필드만 받는 생성자 자동 생성 → 생성자 주입(DI)에 많이 사용
4-3. 왜 Spring과 궁합이 좋나?
예를 들어 Service에서 이렇게 작성하면:
@Service
@RequiredArgsConstructor
public class StudentServiceCrudImpl implements StudentServiceCrud {
private final StudentRepository studentRepository;
// 나머지 메서드들...
}
@RequiredArgsConstructor가StudentRepository를 파라미터로 받는 생성자를 자동 생성- Spring 이 이 생성자를 사용해서 생성자 주입(DI)을 수행
- 우리는 굳이
public StudentServiceCrudImpl(StudentRepository studentRepository) {...}를 안 적어도 된다.

STS4 에 자동으로 설정되는것이아닌 직접 Lombok.jar을 열어
sts의 exe 파일을 지정해줬다.
5. Spring Data JPA & JpaRepository
5-1. JPA / Hibernate / Spring Data JPA 관계
- JPA : 자바 ORM에 대한 표준 스펙(인터페이스 규격)
- Hibernate : JPA 구현체 중 하나 (우리가 실제로 사용하는 라이브러리)
- Spring Data JPA : JPA/Hibernate 위에 얹혀 Repository 인터페이스만 정의하면 CRUD/페이징 등을 자동 구현해 주는 스프링 프레임워크
5-2. StudentRepository 코드
package com.mycom.myapp.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.mycom.myapp.entity.Student;
// Spring Data Jpa 의 시작은 제공되는 interface 를 상속받는 것.
// 이를 통해서 Student 에 대한 기본적인 CRUD 는 자동화 처리
// 이 interface 를 구현하는 클래스를 생성 X <= Spring Data Jpa 가 자동으로 생성
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
여기서 중요한 부분:
JpaRepository<Student, Integer>상속 → 이 순간부터 Student에 대한 CRUD, 페이징, 정렬 기능이 자동 제공- 우리가 구현체 클래스를 만들지 않아도, Spring Data JPA가 프록시 객체를 만들어서 빈으로 등록
5-3. 기본 제공 메서드
findAll(): 전체 목록 조회findById(id): PK로 한 건 조회 →Optional<Student>save(entity): id 없으면 insert, id 있으면 updatedeleteById(id): PK로 삭제count(): 전체 건수findAll(Pageable pageable): 페이징 & 정렬된 목록
6. Service 계층 – StudentServiceCrud & Impl
6-1. Service 인터페이스
package com.mycom.myapp.service;
import java.util.List;
import java.util.Optional;
import com.mycom.myapp.entity.Student;
// 학습 예제 코드의 간단함을 위해 Student <-> StudentDto 생략
public interface StudentServiceCrud {
// 목록, 상세
List<Student> listStudent();
Optional<Student> detailStudent(int id);
// 등록, 수정, 삭제
Student insertStudent(Student student);
Optional<Student> updateStudent(Student student);
void deleteStudent(int id);
// 전체 건수, 페이징
long countStudent();
List<Student> listStudent(int pageNumber, int pageSize);
}
- Controller는 이 인터페이스만 바라보고 호출
- 구현체를 다른 걸로 교체해도 Controller 코드는 그대로 유지 가능 → 느슨한 결합
6-2. Service 구현 – StudentServiceCrudImpl
package com.mycom.myapp.service;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.mycom.myapp.entity.Student;
import com.mycom.myapp.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class StudentServiceCrudImpl implements StudentServiceCrud {
// 생성자 주입
private final StudentRepository studentRepository;
@Override
public List<Student> listStudent() {
return studentRepository.findAll(); // 전체 목록
}
@Override
public Optional<Student> detailStudent(int id) {
return studentRepository.findById(id);
}
// save()
// 전달되는 엔티티 객체에 id 가 있으면 select - update
// 전달되는 엔티티 객체에 id 가 없으면 insert
@Override
public Student insertStudent(Student student) {
return studentRepository.save(student);
}
@Override
public Optional<Student> updateStudent(Student student) {
// 무조건 save 호출
return Optional.of(studentRepository.save(student));
// 체크하고 save 호출 (예시)
// Optional<Student> existingStudent = studentRepository.findById(student.getId());
// if (existingStudent.isPresent()) {
// return Optional.of(studentRepository.save(student));
// }
// return Optional.empty();
}
@Override
public void deleteStudent(int id) {
studentRepository.deleteById(id);
}
@Override
public long countStudent() {
return studentRepository.count();
}
// 마지막 페이지에 대한 요청을 제외하고, 페이지 요청을 하면 항상 count() 를 통해서 Page 객체를 구성한다.
// 단순 목록 외 나머지 항목들 계산을 위해 count() 수행
@Override
public List<Student> listStudent(int pageNumber, int pageSize) {
Pageable pageable = PageRequest.of(pageNumber, pageSize);
Page<Student> page = studentRepository.findAll(pageable);
return page.toList();
}
}
6-3. save() 내부 동작 이해
- id 없음 → 신규 엔티티로 판단 →
insert into student ... - id 있음 → 기존 엔티티로 간주 → 내부적으로
select후update처리
강사님 주석 내용처럼,
“id 가 없는 update 처리. findById() 와 if() 코드를 따르지 않고, 항상 save 하도록 하면 insert 가 수행된다.”
→ 이게 바로 save()의 동작 원리.
7. Controller 계층 – StudentControllerCrud
package com.mycom.myapp.controller;
import java.util.List;
import java.util.Optional;
import org.springframework.web.bind.annotation.*;
import com.mycom.myapp.entity.Student;
import com.mycom.myapp.service.StudentServiceCrud;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/students")
@RequiredArgsConstructor
public class StudentControllerCrud {
private final StudentServiceCrud studentServiceCrud;
// 1. 전체 목록
@GetMapping("/list")
public List<Student> listStudent() {
return studentServiceCrud.listStudent();
}
// 2. 상세 조회
@GetMapping("/detail/{id}")
public Optional<Student> detailStudent(@PathVariable("id") int id) {
return studentServiceCrud.detailStudent(id);
}
// 3. 등록 (JSON Body)
@PostMapping("/insert")
public Student insertStudent(@RequestBody Student student) {
return studentServiceCrud.insertStudent(student);
}
// 4. 수정 (JSON Body)
@PutMapping("/update")
public Optional<Student> updateStudent(@RequestBody Student student) {
return studentServiceCrud.updateStudent(student);
}
// 5. 삭제
@DeleteMapping("/delete/{id}")
public void deleteStudent(@PathVariable("id") int id) {
studentServiceCrud.deleteStudent(id);
}
// 6. 전체 건수
@GetMapping("/count")
public long countStudent() {
return studentServiceCrud.countStudent();
}
// 7. 페이징 목록
@GetMapping("/page")
public List<Student> listStudent(
@RequestParam("pageNumber") Integer pageNumber,
@RequestParam("pageSize") Integer pageSize) {
return studentServiceCrud.listStudent(pageNumber, pageSize);
}
}
각 API 요약 (Postman 기준)
- 전체 목록 :
GET /students/list - 상세 :
GET /students/detail/{id}(예:/students/detail/1) - 등록 :
POST /students/insert+ Body(JSON) - 수정 :
PUT /students/update+ Body(JSON) - 삭제 :
DELETE /students/delete/{id} - 전체 건수 :
GET /students/count - 페이징 :
GET /students/page?pageNumber=0&pageSize=10

8. Postman 테스트 & -parameters 문제 트러블슈팅
8-1. -parameters 에러 상황
삭제 API 호출 시 다음과 같은 에러가 떴다.
IllegalArgumentException: Name for argument of type [int] not specified,
and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.
원인:
- Java 컴파일 시, 메서드 파라미터 이름(
id) 정보가 class에 포함되지 않음 - Spring이
@PathVariable int id에서id란 이름을 reflection으로 읽지 못해 에러
8-2. 해결 방법 (Gradle + STS4)
build.gradle에 다음 추가:
tasks.withType(JavaCompile) {
options.compilerArgs += ["-parameters"]
}
추가 후:
- Gradle Refresh
- Project Clean
- Spring Boot 재실행
8-3. @PathVariable 명시적으로 이름 지정
다음처럼 이름을 명시해 주면 더 안전하다.
@DeleteMapping("/delete/{id}")
public void deleteStudent(@PathVariable("id") int id) {
studentServiceCrud.deleteStudent(id);
}
8-4. Postman DELETE 요청 주의
- Method:
DELETE - URL:
http://localhost:8080/students/delete/100 - Body: none (DELETE는 보통 Body 필요 없음)
9. 패턴 패키지 – Builder / Method Chaining / Singleton
9-1. Builder 패턴 예제 – Board
package com.mycom.myapp.pattern.builder;
// builder pattern (Inner Class 버전)
public class Board {
private final String title;
private final String content;
private final String category;
Board(Builder builder){
this.title = builder.title;
this.content = builder.content;
this.category = builder.category;
}
public static class Builder{
private String title;
private String content;
private String category;
public Builder title(String title) {
this.title = title;
return this;
}
public Builder content(String content) {
this.content = content;
return this;
}
public Builder category(String category) {
this.category = category;
return this;
}
public Board build() {
return new Board(this);
}
}
@Override
public String toString() {
return "Board [title=" + title + ", content=" + content + ", category=" + category + "]";
}
}
public class Test {
public static void main(String[] args) {
Board board = new Board.Builder()
.title("게시글 제목")
.content("게시글 내용")
.category("분류 A")
.build();
System.out.println(board);
}
}
9-2. Builder 패턴 vs 메서드 체이닝
- 메서드 체이닝(Method Chaining):
obj.a().b().c()처럼 메서드 호출을 이어서 쓰는 “스타일” - Builder 패턴: 이 체이닝 스타일을 활용해서 복잡한 객체 생성 문제를 푸는 디자인 패턴
이번 Board 예제에서:
new Board.Builder()→ 빌더 객체 생성.title(),.content(),.category()→ 빌더에 값 세팅 (체이닝).build()→ 최종Board객체 생성
핵심은 “필드가 많은 객체를, 생성자 대신 가독성 좋게 만들기”라는 점이다.
9-3. Singleton 패턴 – Logger 예제
public class Logger {
private static final Logger instance = new Logger();
private Logger() {}
public static Logger getInstance() {
return instance;
}
public void log(String msg) {
System.out.println(msg);
}
}
private static final Logger instance = new Logger();
→ 애플리케이션 전체에서 단 하나만 존재하는 객체를 미리 생성private Logger() {}→ 외부에서new Logger()금지 (생성자 숨김)getInstance()→ 이 메서드를 통해서만 Logger에 접근
즉, 질문했던 것처럼private static final Logger instance = new Logger(); 이 줄이 “싱글톤 인스턴스 선언 + 생성”하는 부분이 맞다.
다만, 진짜 싱글톤 패턴은 보통 “private 생성자 + static 단일 필드 + static getter” 이 세트를 함께 본다.
Spring에서는 @Service, @Repository, @Controller 빈이 기본적으로 싱글톤 스코프라, 이런 패턴을 직접 구현하는 일이 많이 줄어든다.
10. 오늘 Q&A 개념 정리 요약
10-1. 레이어드 아키텍처 vs Spring MVC
- 레이어드 아키텍처 = 애플리케이션 전체를 계층(Controller / Service / Repository / DB)으로 나눈 설계 방식
- Spring MVC = 그 중 “웹 계층(Controller)”을 구현하는 웹 프레임워크
10-2. Lombok 이란?
- 컴파일 시점에 어노테이션을 보고 코드(getter/setter/생성자/builder 등)를 생성해주는 라이브러리
- 보일러플레이트 코드 제거 → 코드 양 줄이고 가독성 증가
10-3. Spring Data JPA & JpaRepository
JpaRepository<Student, Integer>상속 = “이 엔티티에 대한 CRUD/페이징/정렬을 Spring Data JPA가 자동 구현해줘” 라는 의미- 우리는 Repository 인터페이스만 선언하고, 구현체는 만들지 않는다.
10-4. Builder 패턴 핵심
- 메서드 체이닝을 이용하는 “스타일”이면서, 동시에 “객체 생성 문제”를 해결하는 디자인 패턴
- 필드가 많거나, 선택적인 값이 많은 경우에 유용
10-5. Singleton 패턴 핵심
- 애플리케이션 전체에서 인스턴스가 딱 하나만 존재해야 할 때 사용하는 패턴
private static final 인스턴스 + private 생성자 + public static getter세트로 이해
11. 교훈 / 핵심 요약
- 레이어드 아키텍처를 이해하면, 프로젝트 구조가 단숨에 눈에 들어온다.
- Lombok은 코드 양을 줄여줄 뿐 아니라, 생성자 주입(
@RequiredArgsConstructor)과 같이 스프링과 궁합이 매우 좋다. - Spring Data JPA는 JPA 위에 얹힌 추상화 계층으로, Repository 인터페이스만 정의하면 CRUD/페이징을 거의 공짜로 얻는다.
- Builder 패턴은 메서드 체이닝을 활용해 복잡한 객체 생성을 가독성 좋게 만드는 패턴이다.
- Singleton 패턴은 “단일 인스턴스”를 보장하는 패턴이고, 스프링 빈 기본 스코프와 개념적으로 연결된다.
- Postman으로 API를 직접 호출하면서, Controller → Service → Repository → DB 흐름을 눈으로 확인해 보며 JPA 동작 방식(save, find, delete, paging)을 체감했다.
'Java > JPA' 카테고리의 다른 글
| [LG U+ 유레카 3기] Spring Data JPA find() 메서드 실습 (0) | 2025.11.26 |
|---|---|
| [LG U+ 유레카 3기] JPA N+1 · Fetch Join · JPQL Join (0) | 2025.11.20 |
| [LG U+ 유레카 3기] 순수 JPA 복합키 실습(@IdClass & @EmbeddedId) (1) | 2025.11.17 |
| [LG U+ 유레카 3기]순수JPA 실습 — JPQL (Java Persistence Query Language) (0) | 2025.10.20 |
| [LG U+ 유레카 3기]순수 JPA 실습 엔티티 생명주기 (persist / find / merge / detach / remove) (0) | 2025.10.20 |