본문 바로가기
Spring Boot

스프링 부트(Spring Boot) JPA 게시판 - CRUD 처리하기 [Thymeleaf, MariaDB, IntelliJ, Gradle]

by 도뎡 2023. 10. 26.
반응형
  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • 본 게시판 프로젝트에서 데이터는 MariaDB로 관리하며, MariaDB가 설치되어 있지 않으시다면, 선행 작업으로 MariaDB 설치하기를 꼭! 진행해 주세요.

이전 글에서는 JPA 게시판 프로젝트 진행을 위해, 스프링 부트 프로젝트를 생성해 보았습니다. 이번에는 스프링 부트와 MariaDB를 연동하고, 게시판에 사용될 회원 테이블을 생성한 후 JPA를 가볍게 경험하는 시간을 가져볼 건데요.

DB가 익숙하지 않으시거나, 데이터 소스(Data Source)에 대한 개념이 하나도 잡혀있지 않으신 분들은 아래 글을 가볍게 읽어보시기를 권장드립니다.

 

스프링 부트(Spring Boot) - 게시판 MariaDB(HikariCP) 연동하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다. 본 포스팅은 DBeaver를 기준으로 작성된 글이며, 만약 MariaDB가 설치되어 있지 않으시다면, 선행

congsong.tistory.com

 

1. DB 스키마 생성하기

가장 먼저, DBMS 관리 툴(DBeaver, HeidiSQL 등)에서 커넥션을 생성한 후 아래 명령어를 실행해 'board' 스키마를 생성 및 확인해 주세요.

-- 스키마 생성
create schema board collate utf8mb4_general_ci;

-- 스키마 조회
show databases;

-- 사용할 스키마 변경 (매번 스키마를 변경하는 건 번거로운 일이니, 커넥션의 기본 스키마를 board로 변경하시는 걸 권장드립니다.)
use board;

 

2. 회원 테이블 생성하기

이번에는 아래 명령어를 실행해서 board 스키마에 회원 테이블을 생성해 주세요.

CREATE TABLE `tb_member` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '회원 번호 (PK)',
  `login_id` varchar(20) NOT NULL COMMENT '로그인 ID',
  `password` varchar(68) NOT NULL COMMENT '비밀번호',
  `name` varchar(20) NOT NULL COMMENT '이름',
  `gender` varchar(1) NOT NULL COMMENT '성별',
  `birthday` date NOT NULL comment '생년월일',
  `delete_yn` tinyint(1) NOT NULL DEFAULT 0 COMMENT '삭제 여부',
  `created_date` datetime NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
  `modified_date` datetime DEFAULT NULL COMMENT '최종 수정일시',
  PRIMARY KEY (`id`),
  UNIQUE KEY uix_member_login_id (`login_id`)
) COMMENT '회원';

 

생성된 회원 테이블의 구조는 다음과 같습니다. (이미지를 클릭하시면 확대해 보실 수 있습니다.)

생성된 회원 테이블 구조

 

3. yml 설정 파일 세팅하기

이전에 진행한 게시판 프로젝트는 모든 설정을 properties에 선언했었는데요. 이번 프로젝트에서는 가독성 측면에서도 유리하고, 동일한 코드의 중복을 피할 수 있는 yml을 이용해보려 합니다.

Spring Boot 프로젝트 생성 시 기본으로 resources 디렉터리에 생성되는 application.properties의 이름을 application.yml로 변경하고, 아래 소스 코드를 붙여 넣어 주시면 되는데요.

데이터 소스의 username과 password는 반드시 여러분의 환경에 알맞게 설정해 주셔야 합니다.

# 데이터 소스
spring:
  datasource:
    hikari:
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
      jdbc-url: jdbc:log4jdbc:mariadb://localhost:3306/board?serverTimezone=Asia/Seoul&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true
      username: username
      password: password

# 정적 파일 변경 실시간 반영
  devtools:
    restart:
      enabled: 'false'
    livereload:
      enabled: 'true'

# 타임리프 캐싱 OFF
  thymeleaf:
    cache: 'false'

# JPA 설정
  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    generate-ddl: 'false'
    show-sql: 'true'
    open-in-view: 'false'
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        format_sql: 'true'
        use_sql_comments: 'true'
        default_batch_fetch_size: '100'

 

4. Java 설정 파일(Configuration Class)로 데이터 소스(Data Source) 처리하기

데이터 소스는 이전에 진행했던 MyBatis 기반의 게시판 프로젝트와 마찬가지로 Java 설정 파일에 선언합니다. 다만, 이번 프로젝트에서는 MyBatis가 전혀 사용되지 않으므로 SqlSessionFactory와 SqlSessionTemplate 빈(Bean)이 필요하지 않습니다.

 

4-1) DatabaseConfig 생성하기

패키지에 해당되는 경로에 아래 클래스를 추가한 후 소스 코드를 작성해 주세요.

package com.study.common.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DatabaseConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    @Bean
    public DataSource dataSource(HikariConfig hikariConfig) {
        HikariDataSource dataSource = new HikariDataSource(hikariConfig);
        return dataSource;
    }

}

 

4-2) DatabaseConfig 구성요소 알아보기

구성 요소 설명
@Configuration 이 어노테이션이 선언된 클래스를 Java 기반의 설정 파일로 지정합니다. 설정 파일은 Spring IoC 컨테이너에 의해 관리되며, 빈(Bean) 구성에 사용됩니다.
@Bean 이 어노테이션이 선언된 메서드를 Spring IoC 컨테이너에 의해 관리되는 빈(Bean)으로 등록합니다.
@ConfigurationProperties applicant.yml에서 "spring.datasource.hikari" 접두사를 가진 설정 정보를 읽어오기 위한 어노테이션으로, 읽어온 정보를 hikariConfig( ) 메서드에 매핑(바인딩)합니다.
hikariConfig( ) @ConfigurationProperties를 이용하여 읽어온 정보를 통해 HikariConfig 객체를 생성합니다.
dataSource( ) HikariConfig 객체를 통해 HikariDataSource를 생성합니다. 이 데이터 소스는 HikariCP를 사용하여 데이터베이스 연결을 관리하는 데 사용됩니다. HikariCP는 커넥션 풀(Connection Pool) 라이브러리의 한 종류입니다.

 

5. 보기 좋은 SQL 쿼리 로그 출력을 위한 Logback 설정하기

어떤 프로젝트던 개발 단계에서의 SQL 쿼리 로그 출력은 필수 중의 필수입니다. 여기서는 자세한 설명은 생략하고 설정 방법만 설명드릴 건데요. Logback을 자세히 알아보고 싶으신 분들은 여기를 참고해 주세요.

 

5-1) logback-spring.xml 생성하기

src/main/resources에 logback-spring.xml을 추가한 후 아래 소스 코드를 작성해 주세요.

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>%d %5p [%c] %m%n</Pattern>
		</encoder>
	</appender>

	<appender name="console-infolog" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>%d %5p %m%n</Pattern>
		</encoder>
	</appender>

	<logger name="com.study" level="DEBUG" appender-ref="console" />
	<logger name="jdbc.sqlonly" level="INFO" appender-ref="console-infolog" />
	<logger name="jdbc.resultsettable" level="INFO" appender-ref="console-infolog" />

	<root level="off">
		<appender-ref ref="console" />
	</root>
</configuration>

 

5-2) Log4JDBC 라이브러리 추가하기

build.gradle의 dependencies에 Log4JDBC 라이브러리를 추가한 후 그레이들을 리로드 해주시면 됩니다. 이때 implementation은 꼭 implementation끼리 뭉쳐있어야 합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'    /* Log4JDBC */
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

5-3) log4jdbc.log4j2.properties 생성하기

마지막으로 src/main/resources에 log4jdbc.log4j2.properties를 추가한 후 아래 소스 코드를 작성해 주시면 됩니다.

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

 

6. 회원 엔티티(Entity) 클래스 처리하기

JPA에서 엔티티는 데이터베이스 테이블과 매핑되는 자바 객체를 의미합니다. 엔티티는 테이블에 존재하는 모든 칼럼에 해당되는 멤버 변수를 포함하며, 엔티티와 레파지토리(Repository)라는 인터페이스를 통해 데이터(레코드)를 CRUD 처리할 수 있습니다.

 

6-1) Member 엔티티 생성하기

패키지에 해당되는 경로에 Member 엔티티를 추가해 주시면 되는데요. 해당 클래스가 상속하는 BaseTimeEntity와 성별(gender)의 타입인 Gender는 무시하고 진행해 주시면 됩니다.

package com.study.domain.member;

import com.study.domain.common.BaseTimeEntity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.LocalDate;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "tb_member")
public class Member extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;             // 회원 번호 (PK)

    @Column(name = "login_id")
    private String loginId;      // 로그인 ID

    @Column(name = "password")
    private String password;     // 비밀번호

    @Column(name = "name")
    private String name;         // 이름

    @Column(name = "gender")
    private Gender gender;       // 성별

    @Column(name = "birthday")
    private LocalDate birthday;  // 생년월일

    @Column(name = "delete_yn")
    private Boolean deleteYn;    // 삭제 여부

    @Builder
    public Member(String loginId, String password, String name, Gender gender, LocalDate birthday, Boolean deleteYn) {
        this.loginId = loginId;
        this.password = password;
        this.name = name;
        this.gender = gender;
        this.birthday = birthday;
        this.deleteYn = deleteYn;
    }

}

 

6-2) Member 엔티티 구성요소 알아보기

구성 요소 설명
@Getter 롬복(Lombok) 라이브러리에서 제공해주는 어노테이션으로, 클래스의 모든 멤버 변수에 대한 get 메서드(getter)를 생성해 줍니다.
@NoArgsConstructor 롬복에서 제공해주는 어노테이션으로, 매개변수를 갖지 않는 기본 생성자를 생성해주며, 일반적으로 access = AccessLevel.PROTECTED 옵션을 선언해 무분별한 객체 생성을 방지합니다.
@Entity @Entity가 선언된 클래스를 데이터베이스 테이블과 매핑합니다. JPA는 @Entity가 선언된 클래스를 통해 데이터베이스와 상호 작용합니다.
@Table 데이터베이스상의 실제 테이블명을 명시하여 엔티티와 데이터베이스 테이블을 매핑합니다.
@Id 멤버 변수 id를 데이터베이스 테이블의 기본 키(primary key)로 지정합니다.
@GeneratedValue 기본 키(primary key) 생성 전략을 지정합니다. MariaDB 또는 MySQL과 같이 PK 자동 증가(auto-increment)를 제공해주는 데이터베이스는 strategy = GenerationType.IDENTITY 옵션을 통해 데이터(레코드)가 새로 생성되는 시점에 id 값을 자등으로 증가시킬 수 있습니다.
@Column 엔티티 클래스의 멤버 변수와 데이터베이스 테이블의 컬럼을 매핑합니다. 변수명과 컬럼명이 완전히 일치한 경우에는 생략해도 되지만, name 속성을 통해 정확한 컬럼명을 명시해주는 게 좋습니다.
@Builder 롬복에서 제공해주는 어노테이션으로, 클래스에 빌더 패턴을 자동으로 생성해 줍니다. 빌더 패턴은 생성자 또는 자바 빈 패턴 보다 객체 생성을 가독성 있고 편리하게 해주는 디자인 패턴 중 하나로, 테스트 과정에서 자세히 알아보겠습니다.

 

6-3) Gender Enum 생성하기

다음으로 성별을 관리할 Enum 클래스입니다. 회원 테이블의 gender에는 남자인 경우 'M', 여자인 경우 'F'가 저장됩니다.

package com.study.domain.member;

public enum Gender {

    M, F

}

 

6-4) BaseTimeEntity 생성하기

마지막으로 Member 엔티티가 상속하는 BaseTimeEntity입니다. 이 클래스는 데이터베이스 테이블의 생성일시(created_date)와 최종 수정일시(modified_date)를 자동으로 관리하는 데 사용됩니다.

package com.study.domain.common;

import lombok.Getter;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
public abstract class BaseTimeEntity {

    @Column(name = "created_date")
    private LocalDateTime createdDate;   // 생성일시

    @Column(name = "modified_date")
    private LocalDateTime modifiedDate;  // 최종 수정일시

    @PrePersist
    public void prePersist() {
        this.createdDate = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        this.modifiedDate = LocalDateTime.now();
    }

}

 

6-5) BaseTimeEntity 핵심 어노테이션 알아보기

어노테이션 설명
@MappedSuperclass BaseTimeEntity가 JPA 엔티티의 공통 매핑 정보를 포함하는 클래스임을 의미합니다. 쉽게 말해 BaseTimeEntity에 선언된 필드들은 어떠한 엔티티에서든 상속하여 사용할 수 있으며, 이 어노테이션을 통해 코드 중복을 방지하고 매핑 정보를 재사용할 수 있습니다.
@PrePersist JPA 엔티티가 저장(Insert)되기 전에 실행할 메서드를 지정합니다. 여기서는 엔티티가 데이터베이스에 최초로 생성되는 시점에 prePersist( ) 메서드가 호출되며, 이를 통해 엔티티의 생성일시를(created_date)를 자동으로 관리합니다.
@PreUpdate JPA 엔티티가 수정(Update)되기 전에 실행할 메서드를 지정합니다. 여기서는 엔티티가 데이터베이스에 업데이트될 때 preUpdate( ) 메서드가 호출되며, 이를 통해 엔티티의 최종 수정일시(modified_date)를 자동으로 관리합니다.

 

7. 회원 레파지토리(Repository) 인터페이스 처리하기

JPA에서 레파지토리는 Spring Data JPA에서 제공해 주는 인터페이스 중 하나로, 데이터베이스와의 상호작용을 하는 데 도움을 주는 인터페이스입니다.

우리는 JpaRepository 인터페이스를 상속해서 데이터를 처리하게 되는데요. JpaRepository는 데이터베이스 테이블에 대한 CRUD 작업을 쉽게 수행할 수 있는 다양한 메서드를 제공해 줍니다.

 

7-1) MemberRepository 인터페이스 생성하기

Member 엔티티와 동일한 경로에 MemberRepository를 추가한 후 아래 소스 코드를 작성해 주세요.

package com.study.domain.member;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

7-2) JpaRepository 알아보기

JpaRepository는 제네릭 타입 매개변수인 T와 ID를 사용합니다. T는 레파지토리가 조작할 JPA 엔티티 타입을 의미하고, ID는 엔티티의 PK(일반적으로 Long 또는 Integer) 타입을 의미합니다.

데이터베이스에서 회원 테이블의 PK는 bigint이므로 자바에서는 Long 타입으로 선언했고, 이에 따라 JpaRepository의 ID로 전달하는 값은  Member 엔티티의 ID인 Long 타입이 됩니다.

아래 코드는 JpaRepository의 전체 소스 코드입니다. 자세한 건 점차 기능을 붙여나가는 과정에서 하나씩 알아보도록 하겠습니다.

/*
 * Copyright 2008-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.jpa.repository;

import java.util.List;

import javax.persistence.EntityManager;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

/**
 * JPA specific extension of {@link org.springframework.data.repository.Repository}.
 *
 * @author Oliver Gierke
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Sander Krabbenborg
 * @author Jesse Wouters
 * @author Greg Turnquist
 */
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll()
	 */
	@Override
	List<T> findAll();

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
	 */
	@Override
	List<T> findAll(Sort sort);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
	 */
	@Override
	List<T> findAllById(Iterable<ID> ids);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
	 */
	@Override
	<S extends T> List<S> saveAll(Iterable<S> entities);

	/**
	 * Flushes all pending changes to the database.
	 */
	void flush();

	/**
	 * Saves an entity and flushes changes instantly.
	 *
	 * @param entity entity to be saved. Must not be {@literal null}.
	 * @return the saved entity
	 */
	<S extends T> S saveAndFlush(S entity);

	/**
	 * Saves all entities and flushes changes instantly.
	 *
	 * @param entities entities to be saved. Must not be {@literal null}.
	 * @return the saved entities
	 * @since 2.5
	 */
	<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

	/**
	 * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
	 * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
	 * method.
	 *
	 * @param entities entities to be deleted. Must not be {@literal null}.
	 * @deprecated Use {@link #deleteAllInBatch(Iterable)} instead.
	 */
	@Deprecated
	default void deleteInBatch(Iterable<T> entities) {
		deleteAllInBatch(entities);
	}

	/**
	 * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
	 * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
	 * method.
	 *
	 * @param entities entities to be deleted. Must not be {@literal null}.
	 * @since 2.5
	 */
	void deleteAllInBatch(Iterable<T> entities);

	/**
	 * Deletes the entities identified by the given ids using a single query. This kind of operation leaves JPAs first
	 * level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this method.
	 *
	 * @param ids the ids of the entities to be deleted. Must not be {@literal null}.
	 * @since 2.5
	 */
	void deleteAllByIdInBatch(Iterable<ID> ids);

	/**
	 * Deletes all entities in a batch call.
	 */
	void deleteAllInBatch();

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
	 */
	@Deprecated
	T getOne(ID id);

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
	 * @since 2.5
	 */
	@Deprecated
	T getById(ID id);

	/**
	 * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
	 * implemented this is very likely to always return an instance and throw an
	 * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
	 * immediately.
	 *
	 * @param id must not be {@literal null}.
	 * @return a reference to the entity with the given identifier.
	 * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
	 * @since 2.7
	 */
	T getReferenceById(ID id);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
	 */
	@Override
	<S extends T> List<S> findAll(Example<S> example);

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
	 */
	@Override
	<S extends T> List<S> findAll(Example<S> example, Sort sort);
}

 

8. JUnit 단위 테스트로 회원 데이터 CRUD 처리해 보기

마지막으로 JUnit과 MmberRepository를 이용해서 회원 데이터를 관리하는 방법을 알아보도록 하겠습니다.

 

8-1) MemberRepositoryTest 클래스 생성하기

test 디렉터리에 MemberRepositoryTest를 추가한 후 아래 소스 코드를 작성해 주세요.

package com.study;

import com.study.domain.member.Gender;
import com.study.domain.member.Member;
import com.study.domain.member.MemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityNotFoundException;
import java.time.LocalDate;

@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    // 회원 정보 생성
    @Test
    void saveMember() {
        Member saveParams = Member.builder()
                .loginId("dyl6266")
                .password("1234")
                .name("도뎡")
                .gender(Gender.M)
                .birthday(LocalDate.of(1994, 9, 12))
                .deleteYn(false)
                .build();

        Member member = memberRepository.save(saveParams);
        Assertions.assertEquals(member.getLoginId(), "dyl6266");
    }

    // 전체 회원 조회
    @Test
    void findAllMember() {
        memberRepository.findAll();
    }

    // 회원 상세정보 조회
    @Test
    void findMemberById() {
        Member member = memberRepository.findById(1L).orElseThrow(() -> new EntityNotFoundException());
        Assertions.assertEquals(member.getLoginId(), "dyl6266");
    }

    // 회원 정보 삭제
    @Test
    void deleteMemberById() {
        memberRepository.deleteById(1L);
    }

}

 

8-2) saveMember( )

가장 먼저 회원 정보를 생성하는 saveMember 메서드입니다. Member 클래스의 생성자 빌더 패턴을 이용하여 Member 타입의 객체인 saveParams를 생성합니다. 생성된 객체의 구조는 아래와 같습니다.

saveParams 구조

 

다음으로 32번 라인의 memberRepository.save 메서드가 실행되면 IDE 콘솔에는 아래와 같은 로그가 출력됩니다. values에 매핑된 쿼리 파라미터는 IDE에서 스크롤을 우측으로 이동하시면 네모 박스 처리한 라인(줄)에서 확인하실 수 있습니다.

memberRepository.save 메서드 실행 결과

 

실제로 실행된 INSERT 쿼리는 아래와 같습니다.

/* insert com.study.domain.member.Member */ insert into tb_member (created_date, modified_date, birthday, delete_yn, gender, login_id, name, password) values ('10/26/2023 16:11:59.323', NULL, '09/12/1994 00:00:00.000', 0, 0, 'dyl6266', '도뎡', '1234')

 

8-3) findAllMember( )

두 번째는 findAllMember 메서드입니다. 어떠한 조건도, 정렬도 없이 회원 테이블의 모든 데이터를 조회합니다. 40번 라인의 memberRepository.findAll 메서드가 실행되면 아래와 같은 로그가 출력됩니다.

memberRepository.findAll 메서드 실행 결과

 

실제로 실행된 전체 회원 SELECT 쿼리는 아래와 같습니다.

/* select generatedAlias0 from Member as generatedAlias0 */ select member0_.id as id1_0_, member0_.created_date as created_2_0_, member0_.modified_date as modified3_0_, member0_.birthday as birthday4_0_, member0_.delete_yn as delete_y5_0_, member0_.gender as gender6_0_, member0_.login_id as login_id7_0_, member0_.name as name8_0_, member0_.password as password9_0_ from tb_member member0_

 

8-4) findMemberById( )

세 번째는 findMemberById 메서드입니다. 회원 테이블의 PK인 id 값을 이용하여 회원의 상세정보를 조회합니다. JpaRepository의 findById 메서드는 Java 8 버전부터 지원되는 Optional을 리턴하며, id에 해당되는 데이터가 없는 경우 예외를 throw 합니다.

47번 라인의 memberRepository.findById 메서드가 실행되면 아래와 같은 로그가 출력됩니다.

memberRepository.findById 메서드 실행 결과

 

실제로 실행된 1번 회원 SELECT 쿼리는 아래와 같습니다.

select member0_.id as id1_0_0_, member0_.created_date as created_2_0_0_, member0_.modified_date as modified3_0_0_, member0_.birthday as birthday4_0_0_, member0_.delete_yn as delete_y5_0_0_, member0_.gender as gender6_0_0_, member0_.login_id as login_id7_0_0_, member0_.name as name8_0_0_, member0_.password as password9_0_0_ from tb_member member0_ where member0_.id=1

 

8-5) deleteMemberById( )

마지막으로 deleteMemberById 메서드입니다. findMemberById 메서드와 마찬가지로 id 값을 이용하여 회원 데이터를 삭제 처리합니다. 여기서 삭제는 물리적인 삭제를 의미합니다.

54번 라인의 memberRepository.deleteById 메서드가 실행되면 아래와 같은 로그가 출력되는데요. DELETE는 데이터를 삭제 처리하기 전에 SELECT 쿼리를 실행해서 데이터의 존재 여부를 검사한다는 특징이 있습니다.

 

실제로 실행된 1번 회원 DELETE 쿼리는 아래와 같습니다.

/* delete com.study.domain.member.Member */ delete from tb_member where id=1

 

마치며

최근에 업무에 크게 치이기도 했고, 쉴 시간이 많지 않았다 보니 "잠시만 나태함을 즐겨보자"라는 생각이 뇌를 지배했던 것 같습니다.. (글 안 쓴 지 거진 두 달이 되어가네요..)

그래도 많은 분들께서 블로그 방문해 주시고, 질문도 달아 주시고, 좋은 말씀도 많이 해주신 덕분에 블태기 극복하게 되었습니다. 모두에게 진심으로 감사드립니다.

이번 글에 이어 다음 글에서는 MVC 패턴에서 M(Model)과 C(Controller)에 해당되는 서비스(Service)와 컨트롤러(Controller)를 처리하는 시간을 가져볼 건데요.

이번 JPA 게시판 프로젝트의 핵심은 페이지와 데이터를 처리하는 컨트롤러를 완전히 분리한다는 점, 그리고 게시글 데이터의 CRUD 관련 API를 모두 처리한 후에 화면을 처리한다는 점입니다. 이 두 가지를 꼭 숙지하고 진행해 주셨으면 합니다.

연말이기도 하고 앞으로도 계속 정신없을 테지만, 최선을 다해 글을 써나가 보도록 하겠습니다. 기다려 주신 모든 분들께 진심으로 감사의 말씀을 드립니다. 오늘도 좋은 하루 보내세요 :)

jpa-board.zip
0.20MB

반응형

댓글