Spring Boot: 스프링 부트 기초 [2]

hyeppy·2025년 8월 27일

Spring Boot

목록 보기
3/6
post-thumbnail

엔티티(Entity)


질문 엔티티 만들기

엔티티(Entity)란 데이터베이스의 테이블과 매핑되는 자바 클래스를 의미한다. 즉, 데이터를 관리하는 데 사용하는 ORM의 자바 클래스가 바로 엔티티.

컨트롤러 클래스에 @Controller 어노테이션을 추가해 스프링 컨테이너에게 웹 요청을 처리하는 컨트롤러 역할을 한다고 알려 줬던 것처럼, 엔티티 클래스에도 @Entity 어노테이션을 적용해야 한다.

// 질문 엔티티
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;
}

@Entity

  • JPA에게 이 클래스가 데이터베이스 테이블과 매핑되는 엔티티임을 알려 주는 어노테이션
  • 이 어노테이션이 적용된 클래스는 JPA가 관리하는 엔티티가 됨

@Getter, @Setter

  • Getter, Setter 메서드를 자동으로 생성하기 위한 롬복(Lombok)의 어노테이션
  • 수동으로 메서드를 작성할 필요 없이 자동으로 생성됨

@Id

  • 기본키(Primary Key)로 지정하는 어노테이션
  • id 속성은 각 데이터를 구분하는 고유한 값으로, 절대 중복되면 안 됨
  • 테이블에서 각 행을 식별하는 데 사용

@GeneratedValue

  • 데이터 저장 시 해당 속성 값들을 자동으로 생성해 주는 어노테이션
  • 개발자가 일일이 값을 입력하지 않아도 자동으로 1씩 증가하여 저장됨
  • strategy = GenerationType.IDENTITY
    • 고유한 번호를 생성하는 전략을 지정하는 옵션

    • IDENTITY: 데이터베이스의 AUTO_INCREMENT 기능을 사용하여 자동으로 증가흔 값 생성

    • 이 옵션을 지정하지 않으면 모든 엔티티에서 번호를 공유하게 되어 예측 가능한 순서를 보장할 수 없음

      // AUTO: JPA가 데이터베이스에 맞게 자동 선택
      @GeneratedValue(strategy = GenerationType.AUTO)
      
      // SEQUENCE: 데이터베이스 시퀀스 사용
      @GeneratedValue(strategy = GenerationType.SEQUENCE)
      
      // TABLE: 별도 테이블을 사용하여 키 생성
      @GeneratedValue(strategy = GenerationType.TABLE)

@Column

  • 테이블 컬럼의 세부 설정을 위한 어노테이션
  • 엔티티의 속성은 기본적으로 테이블의 주요 컬럼과 일치하며, 이 어노테이션으로 추가 설정 가능
    // 컬럼 길이 설정 (기본값: VARCHAR(255))
    @Column(length = 200)
    private String subject;
    
    // 컬럼 데이터 타입 직접 지정
    @Column(columnDefinition = "TEXT")
    private String content;
    
    // null 허용 여부 (기본값: true)
    @Column(nullable = false)
    private String title;
    
    // 유니크 제약 조건
    @Column(unique = true)
    private String email;

@Transient

  • 엔티티의 속성을 데이터베이스 테이블의 컬럼으로 매핑하지 않을 때 사용
  • 해당 속성은 클래스 내에서만 사용되고 데이터베이스에는 저장되지 않음
    @Entity
    public class User {
        @Id
        private Long id;
        
        private String password;
        
        @Transient  // 데이터베이스에 저장되지 않음
        private String confirmPassword;  // 비밀번호 확인용 임시 필드
    }

답변 엔티티 만들기

보통의 게시판 서비스에서는 하나의 질문에 여러 개의 답변이 달릴 수 있다. 게시물이 지워진다 해서 하단의 답변이 지워지지 않지만, 게시물이 지워진다면 그 게시물의 아래에 있던 답변들은 연쇄적으로 사라진다. 따라서 답변 엔티티는 질문 엔티티를 참조하는 구조가 필요하다.

// 기본 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;  
}

위와 같이 단순히 private Question question만 추가하면 JPA가 이 속성의 관계를 이해할 수 없다. JPA에게 이것이 어떤 종류의 관계인지 알 수 있도록 명시적으로 작성해 주어야 한다.

@ManyToOne

  • Many(답변) : One(질문) ⇒ N:1 관계
  • 하나의 질문에 여러 개의 답변이 달릴 수 있음을 표현
  • 부모(Question)-자식(Child) 관계에서 자식 엔티티에 생성
    → 외래키(Foreign Key)가 자식 테이블(Answer)에 생성
// @ManyToOne으로 관계 설정한 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 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; 
}
-- 생성되는 테이블 구조
CREATE TABLE answer (
    id INTEGER NOT NULL AUTO_INCREMENT,
    content TEXT,
    create_date DATETIME,
    question_id INTEGER,  -- 외래키: Question 테이블의 id를 참조
    PRIMARY KEY (id),
    FOREIGN KEY (question_id) REFERENCES question(id)
);

@OneToMany

  • One(질문) : Many(답변)1:N 관계
  • 하나의 질문이 여러 개의 답변을 가질 수 있음을 표현
  • 답변 목록을 List<Answer> 형태로 관리
  • 주요 속성
    • mappedBy = “question”
      // Answer 엔티티의 question 속성과 연결
      public class Answer {
          @ManyToOne
          private Question question;  // ← 이 속성명과 mappedBy 값이 일치해야 함
      }
      
      // Question 엔티티에서 참조
      @OneToMany(mappedBy = "question")  // Answer의 "question" 속성을 참조
      private List<Answer> answerList;
    • Cascade
      // 모든 연산을 연쇄적으로 수행
      @OneToMany(cascade = CascadeType.ALL)
      
      // 삭제 시 연쇄 수행
      @OneToMany(cascade = CascadeType.REMOVE)
      
      // 저장 시에만 연쇄 수행
      @OneToMany(cascade = CascadeType.PERSIST)
      
      // 여러 옵션 동시 적용
      @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
// @OneToMany으로 관계 설정한 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 엔티티 클래스는 Question 엔티티 클래스의 자식 관계이므로 반드시 Question을 참조하여야 하지만, Question 엔티티는 Answer 엔티티를 무조건 참조할 필요는 없다. 이때 Question에서 Answer를 참조하느냐에 따라 단방향 관계와 양방향 관계가 결정된다.

// 단방향
// Answer에서만 Question 참조 가능
Answer answer = answerRepository.findById(1);
Question question = answer.getQuestion();  // ✅ 가능

// 양방향
// Question에서 Answer 목록 조회 가능
Question question = questionRepository.findById(1);
List<Answer> answers = question.getAnswerList();  // ✅ 가능

// Answer에서 Question 조회도 가능
Answer answer = answerRepository.findById(1);
Question question = answer.getQuestion();  // ✅ 가능

JPA에서는 양방향 관계에서 데이터 정합성을 보장하기 위해 하나의 엔티티만 외래키를 관리하도록 설정하고 있다. 만약 양쪽 모두 외래키를 관리한다면 데이터 불일치가 발생할 수 있기 때문에, 일반적으로는 외래키를 실제로 가지고 있는 테이블(= Answer)의 엔티티가 연관관계의 주인이 된다.

  • 주인 (Owner): 외래키를 관리하는 엔티티 → Answer (외래키 보유)
  • 피주인: 단순히 조회만 하는 엔티티 → Question (mappedBy 사용)

2-04 엔티티로 테이블 매핑하기


profile
Backend

0개의 댓글