[LG U+ 유레카 3기] Spring Data JPA CRUD + Lombok + 패턴 실습 정리

2025. 11. 25. 17:59Java/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;

    // 나머지 메서드들...
}
  • @RequiredArgsConstructorStudentRepository를 파라미터로 받는 생성자를 자동 생성
  • 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 있으면 update
  • deleteById(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 있음 → 기존 엔티티로 간주 → 내부적으로 selectupdate 처리

강사님 주석 내용처럼,
“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)을 체감했다.