본문 바로가기
💻 뚝딱뚝딱/북북클럽

[개발일지#001] 엔터티 설계 및 개발

by 뚜루리 2025. 1. 6.
728x90
320x100

[사용기술]

Java, Spring Boot, Spring JPA, MySQL

 

[만들려는 것]

책을 위한 SNS.

 

[오늘 하려는 것]

회원(Member) 엔터티 설계 및 개발
타임라인(Timeline) 엔터티 설계 및 개발
책(Book) 엔터티 설계 및 개발
좋아요(Likes) 엔터티 설계 및 개발
팔로우(Follow) 엔터티 설계 및 개발

 


 

 

-- 회원(Member) 테이블 설계

CREATE TABLE member (
    member_seq INT AUTO_INCREMENT PRIMARY KEY,  -- 고유 식별자 (PK)
    member_id VARCHAR(180) UNIQUE NOT NULL,     -- 회원 ID (변경 가능)
    password VARCHAR(300) NOT NULL,             -- 비밀번호
    nickname VARCHAR(50) UNIQUE NOT NULL,       -- 닉네임
    profile_image VARCHAR(300),                 -- 프로필 사진 경로
    info VARCHAR(600),                          -- 회원 소개글
    role VARCHAR(20) NOT NULL,                  -- 역할 (예: user, admin)
    created_date DATETIME DEFAULT NOW(),        -- 가입일시
    updated_date DATETIME DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP -- 수정일시
    state VARCHAR(20) NOT NULL          		-- 상태 (예: active, inactive)
);

회원의 테이블 설계는 대략 이렇게 진행하였다.

  • PK로 사용할 고유식별자를 memeber_id가 아닌 별도의 고유식별자를 사용.
  • 프로필 사진 경로를 담을 컬럼 생성
  • 생성일시, 수정일시 컬럼을 생성

 


 

회원(Member) 엔터티 개발

데이터베이스 설계를 바탕으로 생성성된 회원 엔터티는 아래와 같다. 

package seulgi.bookbookclub.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer memberSeq;

    @Column(length = 180, nullable = false, unique = true)
    private String memberId;

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

    @Column(nullable = false, unique = true, length = 50)
    private String nickname;

    @Column(length = 300)
    private String profileImage;

    @Column(length = 600)
    private String info;

    @Enumerated(EnumType.STRING)
    @Column(length = 20, nullable = false)
    private Role role = Role.USER;

    @Enumerated(EnumType.STRING)
    @Column(length = 20, nullable = false)
    private State state = State.ACTIVE;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime created_date;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime last_modified_date;

    public Member(String memberId, String password, String nickname) {
        this.memberId = memberId;
        this.password = password;
        this.nickname = nickname;
    }

}

Java단에서는 카멜 표기법을 유지하고 데이터베이스는 언더스코어 표현법을 유지하였다.

  • @GeneratedValue 전략을 Identity 로 설정
  • length, nullable, unique등의 옵션을 추가하여 명시적표기 (Java단만 보고도 데이터베이스의 스키마를 확인 가능하게끔)
  • 회원의 권한과 상태 필드는 ENUM 타입을 사용하였음.
  • @CreatedDate, @LastModifiedDate 어노테이션을 사용하여 JPA 자체적으로 별도의 코드를 작성하지 않아도 작성되도록 사용.

 

 

ENUM 권한

package seulgi.bookbookclub.domain;

public enum Role {
    USER,ADMIN
}

ENUM 상태

package seulgi.bookbookclub.domain;

public enum State {
    ACTIVE, INACTIVE
}

 

※ ENUM을 사용하는 이유

  • 그냥 String 타입으로 할 때는 값에 제약이 없어 오타나 잘못된 값을 저장하거나 동일한 의미의 값이 중복될 가능성이 있는데 ENUM을 사용하면 컴파일 시점에 잘못된 값 사용을 방지 할 수 있음.
  • ENUM은 의미를 명확하게 표현할 수 있어서 코드 가독성, 유지보수성 높아짐
  • 새로운 권한이 추가되어도 관리하기 편함

 


 @CreatedDate, @LastModifiedDate 적용법

하지만 이 어노테이션을 사용하려면 별도의 적용법이 필요하다.

1. 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

※ Spring Data JPA와 함께 사용되기 때문에 추가해줘야 하지만 Spring Boot를 사용 중이라면 별도의 추가 의존성이 필요하지 않음.

 

2. Auditing 기능 활성화

Spring Data JPA에서 Auditing을 활성화하려면 @EnableJpaAuditing 어노테이션을 사용해야 함.

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing // Auditing 활성화
public class JpaConfig {
}

 

3. 엔터티 클래스에 적용

import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@EntityListeners(AuditingEntityListener.class) // Auditing 활성화
public class Member {

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

    private String name;

    @CreatedDate // 생성일시 자동 관리
    @Column(nullable = false, updatable = false) // 수정 불가능
    private LocalDateTime createdDate;

    @LastModifiedDate // 수정일시 자동 관리
    @Column(nullable = false)
    private LocalDateTime updatedDate;

    // Getter와 필요시 생성자 추가
}
  1. @EntityListeners(AuditingEntityListener.class) 사용하여 Auditing 을 활성화 함
  2. 엔터티 클래스에서 @CreatedDate와 @LastModifiedDate를 사용

 

4. application.yml 또는 application.properties에서 시간대를 명시적으로 설정

spring.jpa.properties.hibernate.jdbc.time_zone= UTC

※ 글로벌 서비스나 다중 서버 환경에서 데이터 정확성과 일관성을 보장

 


-- 타임라인(Timeline) 테이블 설계

CREATE TABLE timeline (
    timeline_seq INT AUTO_INCREMENT PRIMARY KEY,  -- 타임라인 고유 ID (PK)
    member_seq INT NOT NULL,                      -- 작성 회원의 고유 식별자 (FK)
    contents VARCHAR(600) NOT NULL,               -- 게시글 내용
    likes INT DEFAULT 0,                          -- 좋아요 수
    book_seq INT NOT NULL,                        -- BOOK 테이블 참조 (FK)
    created_date DATETIME DEFAULT NOW(),          -- 작성 일시
    updated_date DATETIME DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP -- 수정일시
    -- FOREIGN KEY (member_seq) REFERENCES member(member_seq) ON DELETE CASCADE,
    -- FOREIGN KEY (book_seq) REFERENCES book(book_seq) ON DELETE CASCADE
);

타임라인의 테이블은 이렇게 설계 하였다.

  • 타임라인 엔터티의 고유 식별자 컬럼을 별도로 생성.
  • 회원 엔터티의 고유 식별자와 책 엔터티의 고유 식별자를 외래키로 사용
  • 좋아요 수를 담는 컬럼을 생성하고 디폴트를 0으로 함
  • 생성일시, 수정일시 컬럼을 생성

※ 외래키 같은 스키마는 일단 배제. 연관관계를 설정하면서 천천히 적용해볼 예정.

 

타임라인(Timeline) 엔터티 개발

package seulgi.bookbookclub.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

import static jakarta.persistence.FetchType.*;

@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
public class Timeline {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer timelineSeq;

    @ManyToOne(fetch = LAZY, optional = false)
    @JoinColumn(name = "member_seq", nullable = false)
    private Member member;

    @Column(nullable = false, length = 600)
    private String contents;

    @Column(nullable = false)
    private int likes = 0;

    @ManyToOne(fetch = LAZY, optional = false)
    @JoinColumn(name = "book_seq", nullable = false)
    private Book book; // BOOK 테이블 참조 (FK)

    @Enumerated(EnumType.STRING)
    @Column(length = 20, nullable = false)
    private State state = State.ACTIVE;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedDate;

    public Timeline(Member member, String contents, Book book) {
        this.member = member;
        this.contents = contents;
        this.book = book;
    }

}
  • 타임라인과 회원, 타임라인과 책의 다대일 연관관계 설정 (패치조인 설정으로 N + 1 문제 개선)

 

 

nullable = false을 옵션으로 설정했음에도 optional = false을 추가로 설정해준 이유

둘 다 비슷한 역할을 하는 것 같지만 동작에는 미묘한 차이가 있다.

  • nullable = false : 데이터베이스 레벨에서 컬럼이 NOT NULL 제약조건을 갖도록 설정하며 DDL 생성시 반영됨.
  • optional = false : JPA 레벨에서 동작하며 외래키 관계가 Null 일 수 없음을 명시하고 DDL 생성시에는 영향을 미치지 않음.

즉, optional = false 사용시, 런타임 시점에 검증이 가능한데 예를 들어 null인 상태로 엔터티를 저장하려고 하면 JPA가 이를 감지해서 예외를 던져주고, 또 명시적으로 표현함으로 개발자가 개발할 때 관계 필드가 반드시 채워저야 하는 상태인 것을 명확히 할 수 있기 때문에 optional = false 옵션을 추가로 작성해주었음.

 


 

-- 책(Book) 테이블 설계

CREATE TABLE book (
    book_seq INT AUTO_INCREMENT PRIMARY KEY,      -- 책 고유 ID (PK)
    isbn VARCHAR(13) UNIQUE NOT NULL,             -- ISBN 코드
    title VARCHAR(300) NOT NULL,                  -- 책 제목
    author VARCHAR(200) NOT NULL,                 -- 책 저자
    publisher VARCHAR(200) NOT NULL,              -- 출판사
    image_url VARCHAR(300),                       -- 책 이미지 URL
    created_date DATETIME DEFAULT NOW(),           -- 책 정보 등록 일시
    updated_date DATETIME DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP -- 수정일시
);

책 테이블은 이렇게 설계 하였다.

  • 책 엔터티의 고유 식별자를 생성 (ISBN이라는 거의 불변이며 고유한 값이 있긴 하지만 외부에 노출될 가능성이 크기 때문에 고유 식별자를 별도로 만들어줌)
  • 생성일시, 수정일시 컬럼을 생성

※ 외래키 같은 스키마는 일단 배제. 연관관계를 설정하면서 천천히 적용해볼 예정.

 

책(Book) 엔터티 개발

package seulgi.bookbookclub.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@EntityListeners(AuditingEntityListener.class) // Auditing 활성화
@NoArgsConstructor
public class Book {

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

    @Column(nullable = false, unique = true, length = 13)
    private String isbn;

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

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

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

    @Column(length = 300)
    private String imageUrl;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedDate;

    public Book(String isbn, String title, String author, String publisher) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.publisher = publisher;
    }
}

 

 

 


 

-- 팔로우 (Follow) 테이블 설계

CREATE TABLE follow (
    follow_seq INT AUTO_INCREMENT PRIMARY KEY,  -- 팔로우 고유 ID (PK)
    follower_seq INT NOT NULL,                 -- 팔로우 하는 회원의 고유 식별자 (FK)
    following_seq INT NOT NULL,                -- 팔로잉 당하는 회원의 고유 식별자 (FK)
    created_date DATETIME DEFAULT NOW(),        -- 생성일시
    updated_date DATETIME DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP
);

 

 

팔로우 (Follow) 엔터티 구현

package seulgi.bookbookclub.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

import static jakarta.persistence.FetchType.*;

@Entity
@Getter
@EntityListeners(AuditingEntityListener.class) // Auditing 활성화
@NoArgsConstructor
public class Follow {

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

    @ManyToOne(fetch = LAZY, optional = false)
    @JoinColumn(name = "follower_seq", nullable = false)
    private Member follower; // 팔로우 하는 회원 (FK)

    @ManyToOne(fetch = LAZY, optional = false)
    @JoinColumn(name = "following_seq", nullable = false)
    private Member following; // 팔로잉 당하는 회원 (FK)

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedDate;

    public Follow(Member follower, Member following) {
        this.follower = follower;
        this.following = following;
    }

}

 

 

 


-- 좋아요 (Like) 테이블 설계

CREATE TABLE like_ (
like_seq INT AUTO_INCREMENT PRIMARY KEY,    -- 좋아요 고유 ID (PK)
member_seq INT NOT NULL,                    -- 좋아요 누른 회원 고유 ID (FK)
timeline_seq INT NOT NULL,                  -- 좋아요 대상 타임라인 글 ID (FK)
created_date DATETIME DEFAULT NOW(),        -- 좋아요 누른 시간
updated_date DATETIME DEFAULT NOW() ON UPDATE CURRENT_TIMESTAMP -- 수정일시
-- FOREIGN KEY (member_seq) REFERENCES member(member_seq) ON DELETE CASCADE,
-- FOREIGN KEY (timeline_seq) REFERENCES timeline(timeline_seq) ON DELETE CASCADE,
-- UNIQUE (member_seq, timeline_seq)        -- 중복 좋아요 방지
);

 

 

좋아요 (Likes) 엔터티 구현

package seulgi.bookbookclub.domain;

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

import java.time.LocalDateTime;

import static jakarta.persistence.FetchType.LAZY;

@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
public class Likes {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer likeSeq; // 좋아요 고유 ID (PK)

    @ManyToOne(fetch = LAZY, optional = false)
    @JoinColumn(name = "member_seq", nullable = false)
    private Member member; // 좋아요 누른 회원 (FK)

    @ManyToOne(fetch = LAZY, optional = false)
    @JoinColumn(name = "timeline_seq", nullable = false)
    private Timeline timeline; // 좋아요 대상 타임라인 글 (FK)

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdDate; // 좋아요 누른 시간

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedDate;
}
  • @UniqueConstraint를 사용하여 member_seq와 timeline_seq의 조합이 고유하도록 설정. (좋아요를 두번 누룰 수 없음으로)
  • 처음에 엔티티 이름을 Like로 했다가 아무리해도 안되길래 보니까 Like가 예약어라서 안되는 거였음. 당연한 건데 왜 몰랐을까. 이걸로 삽질 엄청함;

 

 


(+) 이렇게 엔터티를 모두 생성후 서버를 돌렸는데 테이블이 생성되거나 DDL문이 콘솔창에 뜨지 않는 문제 발생!

이부분은 아래의 포스팅으로 따로 정리 하였음.

 

[JPA] 데이터베이스 연결이 안된다, 콘솔창에 DDL문이 안보인다 등등

오늘의 문제사이드 프로젝트 중, 데이터베이스를 연결하고 엔터티를 만들고 Run 했는데 원래라면 데이터베이스가 생성되어야 하고, 콘솔창에 DDL문이 보여야 하는데 안 보임. 원인application.propert

ddururiiiiiii.tistory.com

 


엔터티 설계 구현 완!

728x90
320x100