본문 바로가기

개발/프로젝트

[스프링부트 게시판] 7. 테이블 생성

이번 게시글에서 JPA를 이용한 테이블을 생성하도록 하겠습니다.

 

ToyProject에서 사용할 테이블은 User, Board, Reply로 3개로 나뉘어 생성할 것입니다.


우선 Entity들을 모을 패키지를 새로 생성하여 3가지의 Entity 클래스를 생성해주었습니다

 

Entity 생성

 

먼저 사용자 엔티티, User 부터 설정해주었습니다.

 

[User]

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class User {
	
	@Id @GeneratedValue
	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;
	
	@ColumnDefault("user")
	private String role;
	
	@CreatedDate
	@Column(updatable=false)
	private LocalDateTime regDt;
	
	@LastModifiedDate
	@Column(insertable=false)
	private LocalDateTime modDt;
}

전에 올려놓은 Lombok 게시물에서 @Data, @AllArgsConstructor, @NoArgsConstructor를 제외한 나머지 어노테이션에 대해 설명드리겠습니다.

 

@Id - 해당 엔티티(테이블)에서 기본키(Primary Key)로 사용

@GeneratedValue - 기본키(Primary Key) 값에 대한 생성 전략을 제공, 기본적으로 Auto를 제공하는데 현재 사용하는 MySQL에서 auto_increment(추가할 때마다 자동 값 증가)를 제공합니다.

@Column - 해당 테이블에서 사용하는 열(Column)을 뜻합니다. 여기서 nullable(빈 값처리 유무), length(해당 열의 최대 크기), unique(유일한 데이터 처리 유무), insertable / updatable (삽입/수정 시 처리 유무) 등등 이 존재합니다.

@CreatedDate / @LastModifiedDate - JPA 생성 / 마지막 수정 시 실시간 정보를 저장

@ColumnDefault - 따로 처리하지 않으면 기본값으로 정한 값을 저장, 이와 비슷한 기능으로 @Column에서 columnDefination에서 상세하게 사용할 수도 있다.

 

테이블 클래스를 구현하였고 실제로 테이블이 생성이 되는지 확인을 해야합니다.

 

application.properties에서 JPA 관련 설정에 ddl-auto=create가 있습니다. 이 때 서버를 작동하면  @Entity로 설정한 클래스 파일을 테이블로 만들어줍니다. ddl-auto에 create말고 update 등 여러가지 있지만 나중에 설명드리겠습니다.

 

# JPA(Java Persistense API)
spring.jpa.hibernate.ddl-auto=create
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

 

Console창에서 테이블 생성 쿼리문 확인

 

정상적으로 추가가 된 것을 DBeaver에서 확인할 수 있었습니다.

 

DBeaver

 

만약 컬럼명을 수정하고 싶다면 클래스 파일의 수정하고 싶은 변수명을 바꾸면 됩니다.


이번에는 Board 테이블을 생성하도록 하겠습니다.

 

[Board]

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Board {
	
	@Id @GeneratedValue
	private Long id;
	
	@Column(nullable=false, length=100)
	private String title;
	
	@Lob
	private String content;
	
	@ColumnDefault("0")
	private int count;
	
	@ManyToOne
	@JoinColumn(name="userid")
	private User user;
	
	@CreatedDate
	@Column(updatable=false)
	private LocalDateTime regDt;
	
	@LastModifiedDate
	@Column(insertable=false)
	private LocalDateTime modDt;
}

 

위에서 설명한 어노테이션을 제외한 추가된 어노테이션에 대해 설명드리겠습니다.

 

@Lob - Lob(Large Object)의 약자로 대용량 데이터를 받아들이기 위한 어노테이션이다. 주로 게시물의 게시글을 작성하는 경우, 즉 필자가 지금 쓰고 있는 게시글 같은 경우가 Lob을 이용하여 저장이 가능하다는 것이다.

@ManyToOne - Many(현재 테이블) To One(대상 테이블)로써 N:1 관계를 뜻합니다. 여기서 한 UserBoard를 여러 개 작성할 수 있기 때문에 이와 같은 관계 맵핑을 처리하는 것입니다. 비슷한 관계 맵핑 어노테이션으로는 @OneToMany(1:N) @ManyToMany(N:M) 등이 있습니다.

@JoinColumn - @ManyToOne과 같은 관계 맵핑에서 연결할 Key값을 의미합니다. 여기서 Board 테이블은 User의 Id 값을 외래키(Foreign Key)로 설정하는 것입니다.

 

그리고 JPA의 특징으로 이전에 외래키를 참고하려면 같은 타입의 값을 이용하여 직접 따로 쿼리문을 짜는 형식으로 하였지만 JPA의 ORM이란 기능으로 JAVA 혹은 다른 모든 언어로 작성된 Object를 테이블로 매핑합니다. 데이터베이스에서는 오브젝트로 저장하진 않고 JoinColumn으로 설정한 외래키의 데이터를 사용합니다.

 

@ManyToOne에 대상으로 User에 적용합니다. '한 유저가 여러 게시물을 생성할 수 있다.' 라는 개념입니다.

 

DBeaver
외부키로 User의 Id를 참조하고 있는 것을 확인이 되었다.


이번엔 Reply 테이블을 생성하도록 하겠습니다.

 

[Reply]

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Reply {
	
	@Id @GeneratedValue
	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;
	
	@CreatedDate
	@Column(updatable=false)
	private LocalDateTime regDt;
	
	@LastModifiedDate
	@Column(insertable=false)
	private LocalDateTime modDt;
}

 

Board 테이블과 동일하게 @ManyToOne에 대상으로 BoardUser에 적용합니다. '한 유저가 한 게시물의 댓글을 여러 번 생성 할 수 있다.' 라는 개념입니다.

 

DBeaver
Reply의 userId와 boardId에 외래키 기능으로 들어갔습니다.

 

이제 여기서 BoardReply@ManyToOne 을 이용하여 외래키(Foreign Key)가 설정되었지만 이제 여러 번을 쓸 수 있는 One에 해당하는 Board@OneToMany를 적용해야합니다. (User는 예외)

 

Board

 

여기에서 @JoinColumn을 설정하지 않은 이유는, Board 테이블은 댓글 또한 출력해야하기 때문에 Reply 테이블과 연결되는 것이 맞지만 이를 Join을 통해 외래키로 설정을 한다면 하나의 게시물에는 여러개의 댓글이 작성될 수 있기 때문에 Board 테이블에 외래키로 생성된 열에 둘 이상의 값을 저장되게 되면서 데이터베이스에서 데이터의 원자성을 해치는 결과를 초래하게 되기 때문입니다.

 

추가로 @OneToMany 어노테이션의 'mappedBy' 속성을 사용하여 해당 변수는 연관관계의 주인이 아니다. 즉, 외래키가 아니라는 것을 알려주어 데이터베이스에서 열을 따로 생성하지 않도록 설정해주었습니다.

 

참고로 mappedBy 속성에 지정된 값은 기존 Reply 테이블에서 설정한 필드 이름입니다.

 

Reply

 

여기서 @ManyToOne@OneToMany에서 기본 전략이 각각 'EAGER', 'LAZY'로 설정이 되어있습니다.

 

ManyToOne
OneToMany

 

'EAGER' - 즉시 로딩

  • 연관관계가 설정된 모든 테이블에 대해 조인이 일어남.
  • JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용, 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회.

주의점

  1. 컬렉션을 하나 이상 즉시 로딩하는 것을 권장하지 않음.
  2. 컬렉션 즉시 로딩은 항상 외부조인(OUTER JOIN)을 사용한다

 

'LAZY' - 지연 로딩 -> 명시되지 않은 한 연관관계가 설정된 다른 테이블에 대해서 조인하지 않음.

 

JPA 기본 Fetch 전략

  • @ManyToOne, @OneToOne : 즉시 로딩
  • @OneToMany, @ManyToMany : 지연 로딩
  • 다만, 아직 JPA에 익숙하지 않아 덜 이해된 상태이기에 사용하면서 이해가 되면 추가로 작성하겠습니다...

 

참고하고 있는 유튜브를 토대로 토이 프로젝트의 블로그를 만들면서 BoardReply 데이터를 불러올 때 'EAGER'를 사용하도록 하겠습니다.

 

 

Board

 


[참고자료]

 

스프링부트 강좌 22강(블로그 프로젝트) - 연관관계의 주인

https://www.youtube.com/watch?v=DtMmXQl4_hw&list=PL93mKxaRDidECgjOBjPgI3Dyo8ka6Ilqm&index=24