데이터베이스 구성 요소 살펴보기
이 책의 목적이 SQL이나 DBMS를 공부하기 위한 것은 아니므로 앞으로 학습하는데 데이터베이스 관련 용어가 친숙해지도록 간단히 내용을 알고 넘어가자.
우리가 관리할 데이터베이스는 기본적으로 2차원 표 형태로 저장하고 관리한다. 표 형태의 데이터 저장 공간을 테이블(table)이라고 하는데, 테이블은 가로줄과 세로줄 형태로 구성되어 있다. 이때 가로줄을 행(row), 로, 세로줄을 열(column), 컬럼이라고 한다.
또한 데이터베이스에서 중요한 용어 중 하나가 바로 기본키(primary key)이다. 기본키는 테이블의 데이터가 중복되어 저장되지 않게 한다. 어떤 열을 기본키로 설정하면 해당 열에는 동일한 값을 저장하지 못한다.

엔티티 속성 구성하기
이제 SBB에서 사용할 엔티티(entity)를 만들어 보며 개념을 이해해 보자. 엔티티는 데이터베이스 테이블과 매핑되는 자바 클래스를 말한다. 우리가 만들고 있는 SBB는 질문과 답변을 할 수 있는 게시판 서비스이므로 SBB의 질문과 답변 데이터를 저장할 데이터베이스 테이블과 매핑되는 질문과 답변 엔티티가 있어야 한다.
엔티티를 모델 또는 도메인 모델이라고도 한다. 이 책에서는 이것을 구분하지 않고 테이블과 매핑되는 클래스를 모두 엔티티라 지칭한다.
그렇다면 먼저, 만들어야 할 질문(Question)과 답변(Answer) 엔티티에는 각각 어떤 속성들이 필요한지 생각해 보자. 우리가 만들려는 SBB 게시판은 사용자가 질문을 남기고 답변을 받을 수 있는 웹 서비스이다. 이와 같은 서비스를 제공하기 위해서는 사용자가 입력한 질문을 저장해야 하고, 질문의 제목과 내용을 담을 수 있는 항목이 필요하다. 그러므로 질문의 ‘제목’과 ‘내용’ 등을 엔티티의 속성으로 추가해야 한다. 질문 엔티티에는 다음과 같은 속성이 필요하고, 이러한 엔티티의 속성은 테이블의 열과 매핑이 된다.
속성 이름설명| id | 질문 데이터의 고유 번호 |
| subject | 질문 데이터의 제목 |
| content | 질문 데이터의 내용 |
| createDate | 질문 데이터를 작성한 일시 |
마찬가지로 답변 엔티티에는 다음과 같은 속성이 필요하다.
속성 이름설명| id | 답변 데이터의 고유 번호 |
| question | 질문 데이터 (어떤 질문의 답변인지 알아야 하므로 이 속성이 필요하다.) |
| content | 답변 데이터의 내용 |
| createDate | 답변 데이터를 작성한 일시 |
이렇게 생각한 속성을 바탕으로 질문과 답변에 해당되는 엔티티를 작성해 보자.
질문 엔티티 만들기
다음과 같이 질문 엔티티를 만들어 보자. 먼저 src/main/java 디렉터리의 com.mysite.sbb 패키지에 Question.java 파일을 작성해 Question 클래스를 만들어 보자.
package com.mysite.sbb;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
}
이 책은 스프링 부트 3.x 버전을 기준으로 설명한다. 스프링 부트 2.x 버전을 사용해야 한다면 https://wikidocs.net/182149을 참고하자.
코드를 살펴보자. 엔티티로 만들기 위해 Question 클래스에 @Entity 애너테이션을 적용했다. 이와 같이 @Entity 애너테이션을 적용해야 스프링 부트가 Question 클래스를 엔티티로 인식한다.
Getter, Setter 메서드를 자동으로 생성하기 위해 롬복(Lombok)의 @Getter와 @Setter 애너테이션을 적용했다.
그리고 엔티티의 속성으로 고유 번호(id), 제목(subject), 내용(content), 작성 일시(create Date)를 작성했다. 각 속성에는 Id, GeneratedValue, Column과 같은 애너테이션이 적용되어 있는데 하나씩 자세히 알아보자.
@Id 애너테이션
id 속성에 적용한 @Id 애너테이션은 id 속성을 기본키로 지정한다. id 속성을 기본키로 지정한 이유는 id 속성의 고유 번호들은 엔티티에서 각 데이터들을 구분하는 유효한 값으로, 중복되면 안 되기 때문이다.
@GeneratedValue 애너테이션
@GeneratedValue 애너테이션을 적용하면 데이터를 저장할 때 해당 속성에 값을 일일이 입력하지 않아도 자동으로 1씩 증가하여 저장된다. strategy = GenerationType.IDENTITY는 고유한 번호를 생성하는 방법을 지정하는 부분으로, GenerationType.IDENTITY는 해당 속성만 별도로 번호가 차례대로 늘어나도록 할 때 사용한다.
strategy 옵션을 생략한다면 @GeneratedValue 애너테이션이 지정된 모든 속성에 번호를 차례로 생성하므로 순서가 일정한 고유 번호를 가질 수 없게 된다. 이러한 이유로 보통 strategy = GenerationType.IDENTITY를 많이 사용한다.
@Column 애너테이션
엔티티의 속성은 테이블의 열 이름과 일치하는데 열의 세부 설정을 위해 @Column 애너테이션을 사용한다. length는 열의 길이를 설정할 때 사용하고(여기서는 열의 길이를 200으로 정했다.), columnDefinition은 열 데이터의 유형이나 성격을 정의할 때 사용한다. 여기서 columnDefinition = "TEXT"는 말 그대로 ‘텍스트’를 열 데이터로 넣을 수 있음을 의미하고, 글자 수를 제한할 수 없는 경우에 사용한다.
점프 투 스프링부트엔티티의 속성 이름과 테이블의 열 이름의 차이를 알아보자엔티티의 속성은 @Column 애너테이션을 사용하지 않더라도 테이블의 열로 인식한다. 테이블의 열로 인식하고 싶지 않다면 @Transient 애너테이션을 사용한다. @Transient 애너테이션은 엔티티의 속성을 테이블의 열로 만들지 않고 클래스의 속성 기능으로만 사용하고자 할 때 쓴다.
Question 엔티티에서 작성 일시에 해당하는 createDate 속성의 이름은 데이터베이스의 테이블에서는 create_date라는 열 이름으로 설정된다. 즉, createDate처럼 카멜 케이스(camel case) 형식의 이름은 create_date처럼 모두 소문자로 변경되고 단어가 언더바(_)로 구분되어 데이터베이스 테이블의 열 이름이 된다.
카멜 케이스는 맨 첫 글자를 제외한 나머지 단어의 첫 글자를 대문자로 써 구분하는 작명 방식을 말한다.
엔티티를 만들 때 Setter 메서드는 사용하지 않는다
일반적으로 엔티티를 만들 때에는 Setter 메서드를 사용하지 않기를 권한다. 왜냐하면 엔티티는 데이터베이스와 바로 연결되므로 데이터를 자유롭게 변경할 수 있는 Setter 메서드를 허용하는 것이 안전하지 않다고 판단하기 때문이다. 그렇다면 Setter 메서드 없이 어떻게 엔티티에 값을 저장할 수 있을까?
엔티티는 생성자에 의해서만 엔티티의 값을 저장할 수 있게 하고, 데이터를 변경해야 할 경우에는 메서드를 추가로 작성하면 된다. 다만, 이 책은 복잡도를 낮추고 원활한 설명을 위해 엔티티에 Setter 메서드를 추가하여 진행함을 기억해 두자.
답변 엔티티 만들기
1) 이번에는 답변 엔티티도 만들어 보자. 먼저 src/main/java 디렉터리의 com.mysite.sbb 패키지에 Answer.java 파일을 작성해 Answer 클래스를 만들어 보자.
package com.mysite.sbb;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
private Question question;
}
id, content, createDate 속성은 질문 엔티티와 동일하므로 구체적인 설명은 생략한다.
질문 엔티티와 달리 답변 엔티티에서는 질문 엔티티를 참조하기 위해 question 속성을 추가했다.
2) 답변을 통해 질문의 제목을 알고 싶다면 answer.getQuestion().getSubject()를 사용해 접근할 수 있다. 하지만 이렇게 question 속성만 추가하면 안 되고 질문 엔티티와 연결된 속성이라는 것을 답변 엔티티에 표시해야 한다. 즉, 다음과 같이 Answer 엔티티의 question 속성에 @ManyToOne 애너테이션을 추가해 질문 엔티티와 연결한다.
package com.mysite.sbb;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@ManyToOne
private Question question;
}
게시판 서비스에서는 하나의 질문에 답변은 여러 개가 달릴 수 있다. 따라서 답변은 Many(많은 것)가 되고 질문은 One(하나)이 된다. 즉, @ManyToOne 애너테이션을 사용하면 N:1 관계를 나타낼 수 있다. 이렇게 @ManyToOne 애너테이션을 설정하면 Answer(답변) 엔티티의 question 속성과 Question(질문) 엔티티가 서로 연결된다(실제 데이터베이스에서는 외래키(foreign key) 관계가 생성된다.).
- @ManyToOne은 부모 자식 관계를 갖는 구조에서 사용한다. 여기서 부모는 Question, 자식은 Answer라고 할 수 있다.
- 외래키란 테이블과 테이블 사이의 관계를 구성할 때 연결되는 열을 의미한다.
3) 그렇다면 반대로 질문에서 답변을 참조할 수는 없을까? 물론 가능하다! 답변과 질문이 N:1 관계라면 질문과 답변은 1:N 관계라고 할 수 있다. 이런 경우에는 @ManyToOne이 아닌 @OneToMany 애너테이션을 사용한다. 질문 하나에 답변은 여러 개이므로 Question 엔티티에 추가할 Answer 속성은 List 형태로 구성해야 한다. 이를 구현하기 위해 Question 엔티티를 다음과 같이 수정해 보자.
package com.mysite.sbb;
import java.time.LocalDateTime;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
Answer 객체들로 구성된 answerList를 Question 엔티티의 속성으로 추가하고 @OneToMany 애너테이션을 설정했다. 이제 질문에서 답변을 참조하려면 question.getAnswerList()를 호출하면 된다. @OneToMany 애너테이션에 사용된 mappedBy는 참조 엔티티의 속성명을 정의한다. 즉, Answer 엔티티에서 Question 엔티티를 참조한 속성인 question을 mappedBy에 전달해야 한다.
게시판 서비스에서는 질문 하나에 답변이 여러 개 작성될 수 있다. 그런데 보통 게시판 서비스에서는 질문을 삭제하면 그에 달린 답변들도 함께 삭제된다. SBB도 질문을 삭제하면 그에 달린 답변들도 모두 삭제되도록 cascade = CascadeType.REMOVE를 사용했다. 이와 관련해 보다 자세한 내용을 알고 싶다면 https://www.baeldung.com/jpa-cascade-types을 참고하기 바란다.
테이블 확인하기
질문과 답변 엔티티를 모두 만들었다면 다시 H2 콘솔에 접속해 보자.

만약 테이블이 생성되지 않았다면 로컬 서버를 재시작해 보자.
이와 같이 엔티티를 통해 Question과 Answer 테이블이 자동으로 생성된 것을 확인할 수 있다.