[Spring] Lombok과 JPA — 어노테이션 기반 생산성 향상
목차
Lombok이란


자바 코딩할 때 변수 선언마다 생성자, getter/setter를 일일이 작성해야 했는데, IntelliJ도 생성자 자동 생성을 지원하지 않는 경우가 있다. Lombok은 어노테이션 하나로 이 모든 것을 해결해준다.

@Data 어노테이션을 붙여주면 기본 생성자부터 getter/setter 메서드까지 한 번에 자동 생성된다.
아래처럼 생성자를 오버로딩하던 방식은 생산성이 낮다.
public SearchParam(){
}
public SearchParam(String account) {
this.account = account;
}
public SearchParam(String account, String email, int page) {
this.account = account;
this.email = email;
this.page = page;
}
Lombok 설치
File → Plugins에서 Lombok을 검색해 설치한다.


팁: 하단 Structure 탭을 클릭하면 현재 클래스의 구성 요소를 한눈에 볼 수 있다.
기존에 직접 작성한 코드는 모두 지우자.

플러그인만 설치하고 어노테이션을 쓰려 하면 오류가 발생한다. Gradle에 라이브러리를 추가하지 않았기 때문이다.

최신 버전 기준으로 아래와 같이 build.gradle에 추가한다.
plugins {
id 'org.springframework.boot' version '2.4.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
compile('org.projectlombok:lombok')
testCompile('org.projectlombok:lombok')
annotationProcessor('org.projectlombok:lombok')
testAnnotationProcessor('org.projectlombok:lombok')
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}

@Data를 추가하면 Lombok이 임포트되면서 getter/setter와 생성자가 자동으로 생성된다.
IntelliJ Settings → Build, Execution, Deployment → Compiler → Annotation Processors에서 Enable annotation processing을 체크해야 정상 실행된다.

JPA
**JPA(Java Persistence API)**는 ORM(Object Relational Mapping) 기반으로, RDB 데이터를 객체지향으로 손쉽게 다룰 수 있게 해주는 도구다.
| 개념 | 설명 |
|---|---|
| Object | 자바 객체 |
| Relation | 관계형 데이터베이스 |
| ORM | 둘 사이를 자동으로 매핑 |
- 쿼리보다 객체 중심으로 프로그래밍할 수 있다.
JpaRepository를 상속받으면 기본 CRUD가 자동 제공된다.
MySQL 스키마 및 테이블 생성

MySQL Workbench에서 스키마를 하나 만든다.

생성 시 utf-8, utf-8_bin으로 한글 인코딩을 설정한다. Apply 전에 Workbench가 자동으로 쿼리문을 생성해주므로 버튼만 누르면 된다.

테이블을 위와 같이 생성해 두자.

테이블이 정상 생성된 것을 확인할 수 있다.
Gradle 의존성 추가
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('mysql:mysql-connector-java')
compile('org.projectlombok:lombok')
build.gradle에 위 의존성을 추가한다.

이후 src/main/resources/application.properties에 DB 연결 설정을 추가한다. 이 파일은 추가한 라이브러리들의 설정을 관리하는 곳이다.
# properties
spring.datasource.url=jdbc:mysql://localhost:3306/study?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
# db response name
spring.datasource.username=root
# db response password
spring.datasource.password=root

JPA와 Tomcat 포트 8080이 제대로 올라왔다면 정상 실행된 것이다.
Entity
네이밍 컨벤션부터 정리하자.
| 케이스 | 사용처 | 예시 |
|---|---|---|
| Camel Case | Java 변수 | phoneNumber, createdAt |
| Snake Case | DB 컬럼, API 통신 | phone_number, created_at |
Entity: JPA에서 테이블을 자동으로 생성해주는 단위.
DB Table == JPA Entity

package com.example.study.model.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDate;
@Data
@AllArgsConstructor
@Entity //엔티티라는 거 정의
//@Table(name = "user") //table을 유저라는 테이블을 가진 곳에 매핑시킬거다 선언
//근데 클래스의 이름이 동일하면 굳이 table 어노테이션을 설정 안해줘도 된다.
public class User { //이 클래스 이름은 디비의 이름과 동일하게(여기선 카멜 케이스에 맞게 선언)
@Id//식별자에 대해선 Id를 붙이고
@GeneratedValue(strategy = GenerationType.IDENTITY) //어떤식으로 관리할지 전략 설정
private Long id;
// @Column(name = "account") 이거도 마찬가지로 이름이 동일하면 안 써줘도 된다.
private String account;
private String email;
private String phoneNumber;
private LocalDate createdAt;
private String createdBy;
private LocalDate updatedAt;
private String updatedBy;
public User() { //이 부분은 에러나서 넣어줬다. Allargs넣으니까 에러나는데 ㅇㅅㅇ..
}
}
//기본적으로 이 위까지가 mysql과 테이블 어떻게 할지 설정 완료
- 클래스 이름과 DB 테이블 이름이 동일하면
@Table어노테이션 생략 가능 - 컬럼 이름이 동일하면
@Column생략 가능

Repository
JpaRepository를 상속받으면 쿼리 작성 없이 기본 CRUD가 모두 제공된다.
| 메서드 | 동작 |
|---|---|
save() | CREATE / UPDATE |
findById() | READ (SELECT) |
delete() | DELETE |


main은 실제 서비스 코드, test는 실제 동작에 영향을 주지 않는 테스트 전용 폴더다.
package com.example.study.repository;
import com.example.study.StudyApplicationTests;
import com.example.study.model.entity.User;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDate;
public class UserRepositoryTest extends StudyApplicationTests {
@Autowired//DI로 Dependency Inㅓection으로 우선순위 주입을 뜻한다. 직접 객체를 만들지 않고(new) 스프링이 직접 관리하겠다는 뜻.
private UserRepository userRepository;
@Test
public void create(){
//String sql = insert into user(%s,%s,%d) value (account, email, age);
User user = new User();//싱글톤으로 User는 매번 다른 값이 들어갈 수 있어 매번 생성하고 사용해야
user.setAccount("TestUser01");
user.setEmail("TestUser01@gmail.com");
user.setPhoneNumber("010-1111-1111");
user.setCreatedAt(LocalDate.now());
user.setCreatedBy("admin");
//데이터 들어갈 타입 맞춰서 컬럼으로 생성
User newUser = userRepository.save(user);
System.out.println("newUser:"+newUser);
}
public void read(){
}
public void update(){
}
public void delete(){
}
}
테스트 실행 전 Settings에서 확인할 사항이 있다.

Run Tests using 항목을 IntelliJ IDEA로 변경해야 정상 실행된다.

입력한 값이 정상적으로 출력된다. Lombok의 @Data가 toString()을 자동으로 재정의해주므로 깔끔하게 확인할 수 있다.

Workbench에서 테이블을 조회하면

쿼리가 자동 생성되어 데이터가 저장된 것을 확인할 수 있다.
JPA가 실행하는 SQL을 콘솔에서 보려면 application.properties에 아래를 추가한다.
#이 부분을 추가해주자. jpa가 실행한 옵션에 대해 보겠다는 뜻.
spring.jpa.show-sql=true

실행하면 Hibernate가 어떤 SQL을 수행했는지 콘솔에 출력된다.
READ
@Test
public void read(){
Optional<User> user= userRepository.findById(2L);
//2L는 lonlong이고 옵셔널은 제너릭 타입으로 받게 됨.
user.ifPresent(selectUser->{ //있을때만 실행에 대한 결과를 받겠다.
//seleectUser가 있으면 그 값을 꺼내 달라는 뜻.
System.out.println("user:"+selectUser);
});
}
read 메서드에 @Test를 추가하고, Workbench에서 id=2인 행을 선택한 뒤 테스트를 실행한다.

정보가 정상적으로 조회된다.
UPDATE
@Test
public void update(){
Optional<User> user= userRepository.findById(2L); //2번 셀렉트
//2L는 lonlong이고 옵셔널은 제너릭 타입으로 받게 됨.
user.ifPresent(selectUser->{
selectUser.setAccount("PPPP");
selectUser.setUpdatedAt(LocalDate.now());
selectUser.setUpdatedBy("update Method");
//값은 이거만 바꿨지만 jpa에서는 셀렉트유저값에 들어있는 특정 아이디값을 검색하고 한번더 꺼낸 다음
//한번 더 업데이트 쳐줌.
userRepository.save(selectUser);
//쿼리문 통해 특정 유저 셀렉트 해주고 아이디를 한번 더 셀렉트 값 변경되서 값 찾고 그 값에 대해 업데이트 시킴.
});
}

실행 결과, 값이 정상적으로 변경되었다.
DELETE
@Test
public void delete(){
Optional<User> user= userRepository.findById(2L); //2번 셀렉트
//2L는 lonlong이고 옵셔널은 제너릭 타입으로 받게 됨.
user.ifPresent(selectUser->{
userRepository.delete(selectUser);
});
//이제 유저가 진짜 삭제 됐는지 확인해보자,(딜리트 된 유저 삭제 되었는지 확인)
Optional<User> deleteUser = userRepository.findById(2L);
if(deleteUser.isPresent()){
System.out.println("데이터 존재: "+ deleteUser.get());
}else{
System.out.println("데이터 삭제 데이터 없음. ");
}


id=2의 PPPP 데이터가 삭제된 것을 확인할 수 있다.
테스트 코드에서는 Assert를 활용하는 것이 좋다.
@Test
public void delete(){
Optional<User> user= userRepository.findById(1L); //2번 셀렉트
//2L는 lonlong이고 옵셔널은 제너릭 타입으로 받게 됨.
Assert.assertTrue(user.isPresent()); // 반드시 값이 있는 값 통과해서
user.ifPresent(selectUser->{
userRepository.delete(selectUser);
});
//이제 유저가 진짜 삭제 됐는지 확인해보자,(딜리트 된 유저 삭제 되었는지 확인)
Optional<User> deleteUser = userRepository.findById(1L);
Assert.assertFalse(deleteUser.isPresent()); //false 그값이 삭제해서 반드시 false가 된다

값이 정상 삭제되었으며 Workbench에서도 id=1 행이 없어진 것을 확인할 수 있다.
Assert.assertTrue(user.isPresent()): 삭제 전 값이 반드시 존재함을 검증Assert.assertFalse(deleteUser.isPresent()): 삭제 후 값이 반드시 없음을 검증
@Transactional: 마지막 데이터까지 삭제하려 하면 동작하지 않는다. (DB에 데이터가 아예 없으면 의미가 없으므로)
JPA 연관관계


Workbench에서 ERD를 직접 그려보자.


정보를 입력하고


관련 DB를 체크하면

ERD를 그릴 수 있는 창이 열리고 관련 DB가 생성된다.

이 관계도를 ERD로 매핑해보자.
- 실선: 양쪽 모두 관계를 가지는 경우
- 점선: 한쪽만 관계를 가지는 경우

상단 메뉴 Database → Forward Engineer를 클릭한다. Reverse Engineering으로 ERD를 만들었다면, 이번엔 반대로 ERD를 실제 데이터 테이블로 만드는 과정이다.



Workbench Forward Engineering이 안 될 경우 직접 테이블을 생성한다.

study 스키마에 3개의 테이블이 생성되면 성공이다.
정 안 되면 아래 SQL을 직접 실행한다.
-- MySQL Workbench Forward Engineering
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
-- -----------------------------------------------------
-- Schema study
-- -----------------------------------------------------
-- -----------------------------------------------------
-- Schema study
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `study` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ;
USE `study` ;
-- -----------------------------------------------------
-- Table `study`.`user`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `study`.`user` ;
CREATE TABLE IF NOT EXISTS `study`.`user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`account` VARCHAR(45) NOT NULL,
`email` VARCHAR(45) NULL DEFAULT NULL,
`phone_number` VARCHAR(45) NULL DEFAULT NULL,
`created_at` DATETIME NOT NULL,
`created_by` VARCHAR(45) NOT NULL,
`updated_at` DATETIME NULL DEFAULT NULL,
`updated_by` VARCHAR(45) NULL DEFAULT NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB
AUTO_INCREMENT = 7
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin;
-- -----------------------------------------------------
-- Table `study`.`item`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `study`.`item` ;
CREATE TABLE IF NOT EXISTS `study`.`item` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL,
`price` INT NOT NULL,
`content` VARCHAR(45) NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `study`.`order_detail`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `study`.`order_detail` ;
CREATE TABLE IF NOT EXISTS `study`.`order_detail` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`order_at` DATETIME NULL,
`user_id` BIGINT(20) NOT NULL,
`item_id` BIGINT(20) NOT NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
package com.example.study.repository;
import com.example.study.StudyApplicationTests;
import com.example.study.model.entity.OrderDetail;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
public class OrderDetailRepositoryTest extends StudyApplicationTests {
@Autowired
private OrderDetailRepository orderDetailRepository;
@Test
public void create(){
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderAt(LocalDateTime.now());
//어떤 사람? 4번 아이디를 가진 사람이
orderDetail.setItemId(7L);
//어떤 상품? 1번의 인덱스 아이디.
orderDetail.setUserId(1L);
}
}
id=1인 상품을 id=7인 사용자가 구매하는 주문 데이터를 생성하는 예시다.


JPA 연관관계 — FetchType
//1:N
//fetch 타입
//Lazy = 지연 로딩 , Eager = 즉시로딩.
@OneToMany(fetch = FetchType.LAZY, mappedBy = “item”)
private List
| FetchType | 설명 | 권장 사용처 |
|---|---|---|
| LAZY (지연 로딩) | 실제 사용 시점에 쿼리 실행 | 기본값으로 항상 우선 사용 |
| EAGER (즉시 로딩) | 조회 즉시 연관 데이터도 함께 로딩 | 1:1 관계처럼 단일 연관일 때만 |
여러 연관관계가 존재할 경우 반드시 LAZY를 사용해야 한다.