7주차/ Spring Data JPA로 영속성 부여

전진수·2025년 5월 11일

1. 이번 챕터의 학습목표 소개

영속성이 부여되면 스프링부트를 재부팅 해도, 컴퓨터를 재부팅해도 데이터가 유지됩니다.
-> JPA를 사용하여 데이터베이스를 다룰 수 있다.

2. 개발환경세팅

특정 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

  1. GITHUB 에서 바로 해당 브랜치의 소스코드를 다운로드
    브라우저로 https://github.com/jhs512/demo03-2024/tree/chapter-03 접속
    <> Code ▾ 버튼 클릭 Download ZIP 버튼 클릭하여 다운로드
    ideaProjects 폴더에서 압축풀기
    ideaProejcts/demo03-2024 폴더를 인텔리제이로 열기

3. JPA 의존성 추가

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'
}

4. Article 클래스로 article 테이블 생성

@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; 를 통해 삭제해도됨

5. JPA가 실행하는 SQL을 로깅

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 추가

터미널에서 확인할 수 있게 된다.

6. 게시물 INSERT

빈은 개발자가 직접 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번에 들어간 것을 확인할 수 있다.

7. 게시물 COUNT, DELETE

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);

        };
    }
}

8. 게시물 UPDATE

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);
            }
        };
    }
}


업데이트 된 것을 확인할 수 있다.

우리가 만든 엔티티 객체와 영속성 컨텍스트의 스냅샷이 다른 것을 더티라고 한다. 일일이 하나씩 비교해보는데 이를 더티체킹이라고 한다.

9. 게시물 SELECT

수정한 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개만 저장될 수 있습니다.

10. 더 다양한 게시물 SELECT

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 값도 특정 값과 정확히 일치해야 조회됩니다.

0개의 댓글