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와 필요시 생성자 추가
}
- @EntityListeners(AuditingEntityListener.class) 사용하여 Auditing 을 활성화 함
- 엔터티 클래스에서 @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문이 콘솔창에 뜨지 않는 문제 발생!
이부분은 아래의 포스팅으로 따로 정리 하였음.
엔터티 설계 구현 완!
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지#005] 좋아요 도메인 개발 및 테스트 (0) | 2025.01.20 |
---|---|
[개발일지#004] 팔로우 도메인 개발 및 테스트 (0) | 2025.01.20 |
[개발일지#003] 타임라인 도메인 개발 및 테스트 (0) | 2025.01.20 |
[개발일지#002] 회원 도메인 개발 및 테스트 (0) | 2025.01.16 |
[개발일지#000] 프로젝트 생성 (요구사항 분석, 프로젝트 생성, MySQL 연결, 개발 편의 설정 등) (0) | 2025.01.03 |