영속성이 부여되면 스프링부트를 재부팅 해도, 컴퓨터를 재부팅해도 데이터가 유지됩니다.
-> JPA를 사용하여 데이터베이스를 다룰 수 있다.
특정 GIT 브랜치에서 작업을 이어가는 방법 2가지
1.git clone 후 checkout
인텔리제이로 GIT CLONE 하는 방법
메뉴 => File => New => Project From Version Control 또는, 웰컴스크린 => Get from VCS
URL : https://github.com/jhs512/demo03-2024 입력
직접 GIT CLONE 하는 방법
GIT BASH 로 ideaProjects 폴더로 이동
git clone https://github.com/jhs512/demo03-2024
ideaProejcts/demo03-2024 폴더를 인텔리제이로 열기
해당 프로젝트의 터미널 창에서 git checkout -b chapter-03 origin/chapter-03
ORM(통역기능)을 사용하면 개발자가 SQL을 직접 작성하지 않고 관련작업을 자바코드만으로 수행할 수 있습니다. ORM이 개발자 대신 상황에 맞는 SQL을 생성하고 실행합니다. Spring Data JPA는 스프링부트 웹개발 분야에서 가장 유명하고 유용한 ORM 입니다. Spring Data JPA은 데이터소스를 통해서 본인(ORM)이 다뤄야 하는 실제 DB에 대한 정보를 얻습니다.
데이터소스는 설정파일에서 아래의 4가지 정보를 입력함으로써 세팅할 수 있습니다.
spring.datasource.url
spring.datasource.username
spring.datasource.password
spring.datasource.driver-class-name
Spring Data JPA 의 처리 구조 : Spring Data JPA => JPA => 하이버네이트 => JDBC Driver => MySQL Driver => MySQL


dependencies에 추가해준다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
}
@Entity public class Article 에서 @Entity 는 JPA가 article 테이블을 생성하도록 명령합니다. 이때 article 테이블의 칼럼들은 Article 클래스의 변수들을 기초로 생성됩니다. @Id 는 PRIMARY KEY 를 의미합니다. @GeneratedValue(strategy = IDENTITY) 는 AUTO_INCREMENT 를 의미합니다.
spring.jpa.hibernate.ddl-auto=update 라고 설정하면 DB 테이블의 자동으로 세팅됩니다.
실습과정
Article.java
package com.ll.demo03;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // null이 들어갈 수 있다.
private String title;
private String body;
private String title2;
private String title3;
private String title5;
}

article class에서 수정해도 남아있음.
drop table article; 통해 다시 만들어도 되고
alter table article drop title3; 를 통해 삭제해도됨
default_bach_fetch_size: 100 로 SQL 관련된 대표적인 문제인 N + 1 문제를 해결할 수 있습니다.
properties:
hibernate:
default_batch_fetch_size: 100
format_sql: true
highlight_sql: true
use_sql_comments: true
logging:
level:
com.ll.demo03: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
org.hibernate.orm.jdbc.extract: TRACE
org.springframework.transaction.interceptor: TRACE
properties와 logging 추가

터미널에서 확인할 수 있게 된다.
빈은 개발자가 직접 new 를 통해서 객체를 생성하지 않아도 되도록 스프링부트가 직접 관리하는 객체를 말합니다.
실습과정
NotProd.java
package com.ll.demo03.global.initData;
import com.ll.demo03.domain.article.article.entity.Article;
import com.ll.demo03.domain.article.article.repository.ArticleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
//!prod == dev or test
@Profile("!prod")
@Configuration
@RequiredArgsConstructor
public class NotProd {
private final ArticleRepository articleRepository;
@Bean
public ApplicationRunner initNotprod(){ //자동으로 실행되는 스크립트
return args -> {
System.out.println("NotProd.initNotprod1");
System.out.println("NotProd.initNotprod2");
System.out.println("NotProd.initNotprod3");
Article articleFrist = Article.builder()
.title("제목")
.body("내용")
.build();
Article articleSecond = Article.builder()
.title("제목")
.body("내용")
.build();
System.out.println("articleFrist.id = " + articleFrist.getId());
System.out.println("articleSecond.id = " + articleSecond.getId());
articleRepository.save(articleFrist);
articleRepository.save(articleSecond);
System.out.println("articleFrist.id = " + articleFrist.getId());
System.out.println("articleSecond.id = " + articleSecond.getId());
};
}
}
개발(dev) 또는 테스트(test) 환경에서만 실행되는 초기화 코드
초기 데이터 로딩, 확인용 로그, 설정 검증 등에 유용하게 쓰임.
이게 두번 실행되어서


9번과 10번에 들어간 것을 확인할 수 있다.

COUNT 함수를 통해서 특정 조건에 만족하는 ROW 의 개수를 구할 수 있습니다.

Builder 를 추가할거면 @AllArgsConstructor와 @NoArgsConstructor가 필요하다. 인자가 필요하기 때문이다.

DBeaver에서 삭제하는것에는 truncate article; delete from article;이 있는데 truncate가 더 좋다.
수정한 NotProd.java
package com.ll.demo03.global.initData;
import com.ll.demo03.domain.article.article.entity.Article;
import com.ll.demo03.domain.article.article.repository.ArticleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
//!prod == dev or test
@Profile("!prod")
@Configuration
@RequiredArgsConstructor
public class NotProd {
private final ArticleRepository articleRepository;
@Bean
public ApplicationRunner initNotprod(){
return args -> {
if ( articleRepository.count() > 0 ) return;
Article article1 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
Article article2 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
articleRepository.save(article1);
articleRepository.save(article2);
articleRepository.delete(article1);
};
}
}
MySQL 초기화(제너럴 로그 켠 모드로 시작)
# MySQL 없을 때 띄우는 방법
cd ~ # 운영환경에서는 `cd /`
# 기존 컨테이너와 볼륨 제거
docker ps -a | grep -q mysql-1 && docker rm -f mysql-1
rm -rf dockerProjects/mysql-1
# 설정파일 만들기
mkdir -p dockerProjects/mysql-1/volumes/etc/mysql/conf.d
# 원하는 설정을 적어주세요.
echo "[mysqld]
general_log = ON
general_log_file = /etc/mysql/conf.d/general.log" > dockerProjects/mysql-1/volumes/etc/mysql/conf.d/my.cnf
chmod 444 dockerProjects/mysql-1/volumes/etc/mysql/conf.d/my.cnf
docker run \
--name mysql-1 \
-p 3306:3306 \
-v /${PWD}/dockerProjects/mysql-1/volumes/var/lib/mysql:/var/lib/mysql \
-v /${PWD}/dockerProjects/mysql-1/volumes/etc/mysql/conf.d:/etc/mysql/conf.d \
-e TZ=Asia/Seoul \
-e MYSQL_ROOT_PASSWORD=lldj123414 \
-d \
mysql:8.4.1
제너럴 로그 초기화 및 확인
# 이동
cd ~ # 운영환경에서는 `cd /`
echo '' > dockerProjects/mysql-1/volumes/etc/mysql/conf.d/general.log
cat dockerProjects/mysql-1/volumes/etc/mysql/conf.d/general.log | less # 방향키 위/아래 로 이동, 종료는 q

물리적인 트랜잭션은 DBMS 에서의 트랜잭션을 말합니다. 논리적인 트랜잭션(스프링부트 내부에서의 트랜잭션)은 따로 있다. 물리적인 트랜잭션의 시작과 끝을 로그를 통해서 볼 수 있습니다. 일반적으로는 @Transactional 안의 @Transactional 은 무시됩니다. 바깥쪽 @Transactional 만 인정됩니다. 그래서 하나의 트랜잭션으로 묶였습니다.
수정한 NotProd.java
package com.ll.demo03.global.initData;
import com.ll.demo03.domain.article.article.entity.Article;
import com.ll.demo03.domain.article.article.repository.ArticleRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
//!prod == dev or test
@Profile("!prod")
@Configuration
@RequiredArgsConstructor
public class NotProd {
private final ArticleRepository articleRepository;
@Bean
public ApplicationRunner initNotprod(){
return new ApplicationRunner() {
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
if ( articleRepository.count() > 0 ) return;
Article article1 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
Article article2 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
articleRepository.save(article1);
articleRepository.save(article2);
article2.setTitle("제목!!");
articleRepository.delete(article1);
}
};
}
}

업데이트 된 것을 확인할 수 있다.
우리가 만든 엔티티 객체와 영속성 컨텍스트의 스냅샷이 다른 것을 더티라고 한다. 일일이 하나씩 비교해보는데 이를 더티체킹이라고 한다.
수정한 NotProd.java
package com.ll.demo03.global.initData;
import com.ll.demo03.domain.article.article.entity.Article;
import com.ll.demo03.domain.article.article.repository.ArticleRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
//!prod == dev or test
@Profile("!prod")
@Configuration
@RequiredArgsConstructor
public class NotProd {
@Lazy
@Autowired
private NotProd self;
private final ArticleRepository articleRepository;
@Bean
public ApplicationRunner initNotprod(){
return args -> {
self.work1();
self.work2();
};
}
@Transactional
public void work1() {
if ( articleRepository.count() > 0 ) return;
Article article1 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
Article article2 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
articleRepository.save(article1);
articleRepository.save(article2);
article2.setTitle("제목!!");
articleRepository.delete(article1);
}
@Transactional
public void work2() {
//List : 0 ~ N
//Optional : 0 ~ 1
Article article = articleRepository.findById(2L).get();
List<Article> articles = articleRepository.findAll();
}
}
this 를 통한 객체 내부 메서드에 의한 호출은 @Transactional 을 발동시키지 않습니다. findById, findAll 은 JpaRepository 인터페이스에서 기본적으로 제공합니다. Optional 은 리스트와 비슷하지만, 값이 최대 1개만 저장될 수 있습니다.
findById, findAll 과 다르게 다른 메서드는 여러분이 인터페이스에 정의해 줘야 사용할 수 있습니다.
Article.Repository.java
package com.ll.demo03.domain.article.article.repository;
import com.ll.demo03.domain.article.article.entity.Article;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ArticleRepository extends JpaRepository<Article, Long> {
// 실제로 쓰이지는 않음, JAP 학습용
List<Article> findByIdInOrderByTitleDescIdAsc(List<Long> ids);
// 실제로 쓰이지는 않음, JAP 학습용
List<Article> findByTitleContaining(String title);
// 실제로 쓰이지는 않음, JAP 학습용
List<Article> findByTitleAndBody(String title, String body);
}
수정한 NotProd.java
package com.ll.demo03.global.initData;
import com.ll.demo03.domain.article.article.entity.Article;
import com.ll.demo03.domain.article.article.repository.ArticleRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import java.util.List;
//!prod == dev or test
@Profile("!prod")
@Configuration
@RequiredArgsConstructor
public class NotProd {
@Lazy
@Autowired
private NotProd self;
private final ArticleRepository articleRepository;
@Bean
public ApplicationRunner initNotprod(){
return args -> {
self.work1();
self.work2();
};
}
@Transactional
public void work1() {
if ( articleRepository.count() > 0 ) return;
Article article1 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
Article article2 = Article.builder()
.title("제목 1")
.body("내용 2")
.build();
articleRepository.save(article1);
articleRepository.save(article2);
article2.setTitle("제목!!");
articleRepository.delete(article1);
}
@Transactional
public void work2() {
//List : 0 ~ N
//Optional : 0 ~ 1
Article article = articleRepository.findById(2L).get();
List<Article> articles = articleRepository.findAll();
articleRepository.findByIdInOrderByTitleDescIdAsc(List.of(1L, 2L));
articleRepository.findByTitleContaining("제목");
articleRepository.findByTitleAndBody("제목", "내용");
}
}
findByIdInOrderByTitleDescIdAsc 와 같이 메서드명을 짓는 규칙이 있고 지켜야 한다.
SQL 쿼리(Query)에서 자주 사용되는 조건문
SQL 1 : WHERE id IN (?, ?, ?) + ORDER BY id ASC
id 컬럼의 값이 특정 목록 안에 있는지 확인합니다. 예: id가 1, 2, 3 중 하나인 경우만 조회합니다. 또한, 결과를 id 오름차순(작은 값 → 큰 값)으로 정렬합니다.
SQL 2 : WHERE title LIKE ?
title 컬럼에서 특정 패턴과 일치하는 문자열을 찾습니다.
SQL 3 : WHERE title = ? AND body = ?
title 값이 특정 값과 정확히 일치하고, body 값도 특정 값과 정확히 일치해야 조회됩니다.