본문 바로가기

Backend/Spring

[SpringBoot] 블로그 프로젝트 #2 JPA 설정 및 Entity 생성

저번 포스팅에

 

https://chanho0912.tistory.com/23

 

[SpringBoot] 블로그 프로젝트 #1 JSP 설정

본 포스팅은 https://chanho0912.tistory.com/19?category=866707 [SpringBoot] 블로그 프로젝트 #0 Github 연동하기 본 포스팅은 https://chanho0912.tistory.com/18 [SpringBoot] SpringBoot와 Mysql 연동 기본..

chanho0912.tistory.com

 

JSP 설정을 마쳤습니다.

 

이번에는 JPA 기본 설정 및 Entity class를 한번 작성해볼게요!

 

우선 이번 포스팅에 필요한 JPA dependency를 추가해주겠습니다.

 

build.gradle

buildscript{
    ext {
        springBootVersion = '2.4.4'
    }

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}


apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'


group = 'com.StudyProject'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    ...
    
    /* for JPA */
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	
    ...
}

test {
    useJUnitPlatform()
}

 

JPA 관련 의존성인 spring-boot-starter-data-jpa를 추가해줍니다.

 

그다음 application.properties에서 JPA관련 설정값을 추가해줍니다.

 

 

application.properties

spring.profiles.include=real-db
spring.session.store-type=jdbc

# Server
server.port=8000
server.servlet.context-path=/BlogProject
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true

# View with JSP
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# Mysql
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Seoul
spring.datasource.username=chanho
spring.datasource.password=cksgh912

# JPA
spring.jpa.open-in-view=true
spring.jpa.hibernate.ddl-auto=create
# ddl query 자동 생성

spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# 변수명 그대로 필드 생성

spring.jpa.hibernate.use-new-id-generator-mappings=false
# JPA의 기본 넘버링 전략을 따라가지 않음

spring.jpa.show_sql=true
# spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57InnoDBDialect
spring.jpa.properties.hibernate.format_sql=true

# Jackson
spring.jackson.serialization.fail-on-empty-beans=false

# Logging
logging.level.org.springframework.web=DEBUG




 

저는 보통 공부하는 단계이기 때문에 sql문을 확인하기 위해서 show_sql = true로 설정을 해줍니다!

 

naming stratege로 PhysicalNamingStrategyStandardImpl을 설정해주면, 처음 table이 생성될 때 별도의 설정값이 없으면 class의 속성명 그대로 table이 생성되게 됩니다.

 

그리고 primary Key값으로 auto-increment 전략을 사용할 것이기 때문에 JPA 기본 넘버링 전략을 사용하지 않습니다.

 

자 그러면 gradle build를 실행해주시고, 이상 없이 의존성 설정이 끝났다면 Entity를 설계해 보겠습니다.

 

간단하게

 

Accout(유저 계정)

Board(게시글)

Comment(답글)

 

세가지로 이번 프로젝트를 진행해보겠습니다.

 

우선 다음과 같이 각각 domain에 맞는 package명으로 package를 생성해줍니다.

 

Entity가 생성되고 삽입, 수정될 때마다 자동으로 시간을 갱신해주기 위해서 BaseTimeEntity를 생성해줍니다.

package com.BlogWithSpringBoot;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

 

@MappedSuperclass : 이 class가 상속받을 때 필드를 column으로 인식합니다.

@EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킵니다.

@CreatedDate : 엔티티가 생성되어 저장될 때 시간이 자동으로 저장됩니다.

@LastModifiedDate : 엔티티가 마지막으로 수정된 시간이 자동으로 저장됩니다.

 

package com.BlogWithSpringBoot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class BlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }
}

 

그리고 SpringBootApplication class 위에 @EnableJpaAuditing 어노테이션을 추가하여 앱이 실행되는 동안 Jpa Auditing 기능을 활성화시킵니다. 

 

이제 BaseTimeEntity를 상속받은 class는 CreatedDate와 LastModifiedDate를 자동으로 Auditing 받습니다.

 

package com.BlogWithSpringBoot.User;

import com.BlogWithSpringBoot.BaseTimeEntity;
import com.BlogWithSpringBoot.RoleType;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.lang.Nullable;

import javax.persistence.*;


@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
@Entity
public class User extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 30)
    private String username;

    @Column(nullable = false, length = 100)
    private String password;

    @Column(nullable = false, length = 50)
    private String email;

    // DB는 RoleType이라는 형이 없음
    @Enumerated(EnumType.STRING)
    private RoleType role;

}

 

자 이제 User Class를 작성해 볼게요!

 

PrimaryKey인 id값은 Mysql의 기본 넘버링 전략인 auto_increment로 설정합니다. 해당 방법은 strategy=GenerationType.IDENTITY를 설정함으로써 이 테이블에 삽입될 때마다 자동으로 id값이 설정됩니다.

 

username은 로그인할때 필요한 email에 해당합니다.

password는 비밀번호입니다.

email은 유저가 사용하는 email입니다.

그리고 EnumType으로 role을 추가해 주었습니다.

package com.BlogWithSpringBoot;

public enum RoleType {
    ROLE_USER, ROLE_ADMIN
}

간단하게 ROLE_USER와 ROLE_ADMIN을 사용해주겠습니다.

 

Database에서는 저희가 custom 하게 정의한 RoleType의 적절한 반환형에 대한 규약이 없기 때문에 String으로 넣어 달라고 @Enumerated(EnumType.STRING)을 붙여줍니다.

 

 

Board class는 다음과 같이 사용하겠습니다.

package com.BlogWithSpringBoot.Board;

import com.BlogWithSpringBoot.BaseTimeEntity;
import com.BlogWithSpringBoot.Comment.Comment;
import com.BlogWithSpringBoot.User.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;

import javax.persistence.*;
import java.util.List;

@Getter @Builder
@NoArgsConstructor @AllArgsConstructor
@Entity
public class Board extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;


    @ManyToOne
    @JoinColumn(name="userId")
    private User user;
    // Database에는 object를 저장할 수 없음
    // Java는 가능
    // JPA에서 userId로 매핑해줌
    // Board(Many) to User(one)

    @Column(nullable = false, length = 100)
    private String title;

    // Large Data
    @Lob
    private String content;

    @OneToMany(mappedBy = "board", fetch = FetchType.EAGER)
    private List<Comment> commentList;

    @ColumnDefault("0")
    private int count;
}

 

한명의 User는 여러 개의 게시글을 작성할 수 있습니다.

 

따라서 Board(Many) to User(one) 관계이므로 ManyToOne 어노테이션을 사용해줍니다. 

 

기본적으로 원래 테이블에는 객체정보가 모두 들어갈 수 없고, userId를 참조키로 참조하게 되어있습니다. 하지만 JPA는 객체지향적 관점으로 코드를 작성할 수 있게 해 주기 때문에 private User user와 같이 class 자체를 class내에서 관리할 수 있게 해 줍니다. 물론 실제 database에는 userId라는 이름으로 user table이 참조될 것입니다.

 

content는 내용인데, 많을 수 있으므로 @Lob 어노테이션을 활용하여 큰 데이터라고 명시해줍니다.

 

그다음 CommentList인데, 이 부분이 조금 중요합니다.

 

저희는 기본적으로 Blog에서 게시글 목록에서 한 게시글을 선택하면 게시글 상세보기 페이지로 이동하게 설계합니다.

 

즉 이 Board를 기준으로 Mapping 해서 필요한 정보들을 가져와야 합니다.

 

Board를 선택했다고 생각해 보면, Board에는 작성한 User, 그리고 이 Board에 달린 Comment들이 같이 필요하게 됩니다.

 

당연히 하나의 Board에는 여러 Comment가 유효하기 때문에 OneToMany 전략이 필요합니다.

 

하지만 JoinColumn을 작성하지 않습니다. 이유는

 

boardId, title, content... , CommentId, count 이런 식으로 JoinTable이 만들어지게 되면

 

CommentId 필드에 여러 개의 데이터가 필요하게 됩니다.

 

당연히 데이터베이스의 한 필드에는 하나의 값만 들어가야 하기 때문에 해당 방법으로 구현하시면 안 됩니다.

 

우선 여기까지 이해하시고, Comment class를 살펴보겠습니다.

 

package com.BlogWithSpringBoot.Comment;

import com.BlogWithSpringBoot.BaseTimeEntity;
import com.BlogWithSpringBoot.Board.Board;
import com.BlogWithSpringBoot.User.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter @Builder
@NoArgsConstructor @AllArgsConstructor
@Entity
public class Comment extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String content;


    @ManyToOne
    @JoinColumn(name = "boardId")
    private Board board;

    @ManyToOne
    @JoinColumn(name = "userId")
    private User user;
}

 

여기서 보실 점은 Board가 Comment입장에서 ManyToOne 매핑이 되어있는 것을 볼 수 있습니다.

 

그러면 이 Comment Table에 board라는 필드를 사용하여

 

위의 Board에서 mappedBy("board")라고 붙여주게 되면 Comment Table에서 boardId를 기준으로 연관 Comment들을 가져오게 됩니다.

 

즉 FK는 Board가 아니라 Comment에서 가지고 있게 되는 거죠. 이렇게 되면 BoardId를 기준으로 Comment 데이터들을 가져온 뒤 Board class에 JPA가 넣어주게 됩니다.

 

FetchType.EAGER는 처음 board를 가져올 때 위의 방법으로 mapping 된 데이터를 가져오게 되고, FetchType.LAZY는 필요한 시점에서 가져오게 됩니다.

 

우선 EAGER전략을 사용하고, 추후에 LAZY전략으로 수정이 필요하면 LAZY전략으로 변경하도록 하겠습니다.

 

여기까지 Entity 설계를 마치겠습니다.

 

마지막으로 Application을 실행해보면 아까 application.properties에서 show_sql=true로 설정을 하여 Entity가 생성되는 쿼리를 확인하실 수 있습니다.

 

 

 

다음 포스팅에서는 Controller를 간단하게 설계 및 리팩터링을 해보겠습니다.

 

*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.