본문 바로가기

Spring Boot

Entity 설계

Entity 설계의 기초

 

선행 :

postgreSQL을 통한 데이터베이스 구성하기

Spring Data JPA 적용 : Spring Data JPA와 postgreSQL 의존성 추가 (build.gradle)

디버깅을 위해 SQL로그와 관련된 설정값을 application.yaml 파일에 작성하기

 

JPA를 이용해서 DB의 테이블과 상호작용(데이터 저장, 수정, 조회, 삭제 : POST, GET, PATCH, PUT, DELETE과 관련된 CRUD)을 하기 위해 제일 먼저 해야하는것

 - > 데이터베이스의 테이블과 엔티티 클래스간 매핑(mapping) 작업

 

JPA에 사용되는 mapping 애너테이션을 이용해서 DB의 단일 테이블과 엔티티 클래스간의 매핑작업 하기

매핑작업 : 

 - 객체와 테이블간의 매핑

 - 필드(멤버 변수)와 열 간의 매핑

 - 엔티티간의 연관 관계 매핑

 - 기타등등.. 

으로 나눌 수 있다고 한다.

 

 

엔티티와 테이블간 맵핑

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue
    private final UUID id;
}

 

@Entity 맵핑 애너테이션을 통해서 Entity 클래스와 테이블을 맵핑해줄 수 있다.

class User 위에 @Entity 애너테이션을 적어주면 JPA 관리 대상 엔티티가 된다고 합니다.

 

@Entity에 대해

name을 통해 entity 이름을 설정할 수 있음

name 애트리뷰트를 설정하지 않으면, 기본값으로 클래스의 이름을 엔티티 이름으로 사용함(User)

 

@Table도 마찬가지로 name 애트리뷰트를 통해 테이블명을 변경해줄 수 있다. 디폴트는 클래스명(User)이다.

@Table 애너테이션은 옵션이며, 추가하지 않을 경우 클래스의 이름을 테이블 이름으로 사용한다.

주로 테이블 이름이 클래스 이름과 달라야할 경우에 추가해준다고 한다. (테이블명 != 클래스명 일때 사용)

나의 경우 테이블명이 users 이고, 클래스명이 User 라서 name을 넣지 않으면 User 테이블이 되기 때문에 name 애트리뷰트를 써주었다.

 

* @Entity 애너테이션과 @Id 애너테이션은 필수이다. 

* @Entity 애너테이션과 @Id 애너테이션을 함께 사용해야한다. @Entity가 붙은 클래스에는 @Id가 필수임

* 파라미터가 없는 기본 생성자는 필수로 추가해주어야 한다..

  Spring Data JPA을 적용할 때 기본 생성자가 없는 경우, 에러가 발생할 수 있기 때문에 기본생성자는 적어주는게 좋음

* 중복되는 Entity 클래스가 없고, 테이블 이름이 클래스 이름과 같은 경우

  @Entity 애너테이션과 @Table 애너테이션에 name 애트리뷰트를 지정하지 않고 클래스 이름으로 사용되는게 권장된다.

 

 

 

기본키 매핑

DB의 테이블에 기본키 설정은 필수이다.

JPA에서 기본적으로 @Id 애너테이션을 추가한 필드가 기본키(Primary Key) 열이 된다.

이러한 기본키를 어떤 방식으로 생성하는가?? 에 대해서 JPA가 지원해준다고 함

 

 - 기본키를 애플리케이션 코드상에서 직접 할당해주는 방식

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
	@Id
	private final UUID id;
    
    public User(UUID id) {
    	this.id = id;
    }
}

 

단순하게 기본키로 설정할 필드에 @Id 애너테이션을 추가해주면 기본키를 직접적으로 할당해줄 수 있다.

 

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

public class JpaIdDirectMappingConfig {
	private EntityManager em;
    private EntityTransaction tx;
    
    public CommandLineRunner testJpaSingleMappingRunner(EntityManagerFactory emFactory) {
    	this.em = emFactory.createEneityManager();
        this.tx = em.getTransaction();
        
        return args -> {
        	tx.begin();
            em.persist(new User(1L)); // 만약 위에서 설정한 기본키의 타입이 Long이라면 1L가 들어감
            tx.commit();
            
            User user = em.find(User.class, 1L);
            System.out.println("# userId: " + user.getUserId());
        };
    }
}

 

아~

위의 코드는 기본키를 '직접' 할당해서 엔티티를 저장해주는것 같다..

메서드 testJpaSingleMappingRunner의 타입이 CommandLineRunner니까 return값이 저렇게 되는것 같은데..

잘 모르겠네..

 

만약, return의 em.persist(new User(1L)); 에서 기본키 없이 엔티티를 저장하면 에러가 발생한다고 한다.

 

 

 - 기본키 자동생성 : IDENTITY, 기본키 생성을 DB에게 위임하는 전략

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
	
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 여기
	private final UUID id;
    
    public User(UUID id) {
    	this.id = id;
    }
}

 

@GeneratedValue는 보통 @Id와 함께 쓰이는 애너테이션이다. 기본키에 달아주는거임

이 애너테이션의 strategy 애트리뷰트값을 GenerationType.IDENTITY로 지정해주면 된다.

- > @GeneratedValue(strategy = GenerationType.IDENTITY)

 

IDENTITY 전략을 통해 기본키를 대신 생성한다면,

위의 JpaIdentityMappingCconfig 클래스의 testJapSingleMappingRunner 메서드에는 어떤 일이 벌어지는가?

return args -> {
        	tx.begin();
            em.persist(new User()); // new User()의 인자에 기본키값이 들어가지 않는다
            tx.commit();

 

리턴값에 있는 em.persist(new User())의 new User()에 별도의 기본키값을 할당하지 않는다.

IDENTITY 전략에서 기본키를 생성해주기 때문

 

뿐만아니라 IDENTITY 전략에서 기본키를 대신 생성해주는 방식으로 MySQL의 create table이나 alter table의 명령어들을 통해 테이블의 값들을 생성하거나 변경할때 auto_increment 명령어를 통해 숫자를 자동으로 생성하면서 증가시키는 형태를 기본키로 설정할수도 있다.

 

 - SEQUENCE : DB에서 제공하는 시퀀스를 사용해서 기본키를 생성하는 전략

SEQUENCE 전략은 @GeneratedValue(strategy = GenerationType.SEQUENCE) 로 지정해주면 된다.

@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE) // 여기
	private final UUID id;

 

DB에서 제공하는 시퀀스를 사용해서 기본키를 생성한다고 했다.

따라서 영속성 컨텍스트에 저장되기 전에 데이터베이스가 시퀀스에서 기본키에 해당하는 값을 제공할 것이다..?

// 기본키 직접 할당 전략의 return값
return args -> {
	tx.begin();
    em.persist(new User(1L));
    tx.commit(); // 새로운 User를 만들고 commit을 먼저 한다
    User user = em.find(User.class, 1L);
    System.out.println("# userId: " + user.getUserId());
}

// IDENTITY 전략의 return값은 위 return값에서 new User에 기본키값을 할당하지 않는다

// SEQUENCE 전략의 return값
return args -> {
	tx.begin();
    em.persist(new User()); // 여기까진 똑같다
    User user = em.find(User.class, 1L); 
    // 순서가 달라졌음,  DB가 시퀀스에서 기본키값을 제공함
    System.out.println("# userId: " + user.getUserId());
    tx.commit(); // 마지막에 commit으로 영속성 컨텍스트에 저장
}

 

return 안에서의 순서가 달라진다!

기본키값을 직접 할당하거나 IDENTITY 전략을 사용할땐 영속성 컨텍스트에 저장하고 DB에 기본키를 할당하는데,

SEQUENCE 전략은 DB가 시퀀스에서 기본키에 해당하는 값을 먼저 제공하고 나서 영속성 컨텍스트에 엔티티를 저장한다.

 

 

 - TABLE : 별도의 키 생성 테이블을 사용하는 전략, AUTO 전략?

@Id 필드에 @GeneratedValue(strategy = GenerationType.AUTO)를 지정하면

JPA가 데이터베이스의 Dialect에 따라 적절한 전략을 자동으로 선택한다고 한다..

 

 

 

 

필드(멤버변수)와 열 간의 매핑

엔티티 매핑 완성하기

 

User

@Entity
@Table(name = "users")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(force=true)
public class User extends BaseUpdatableEntity implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L; // 직렬화
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private final UUID id;

    @Column(nullable = false, length = 50)
    private String username;

    @Column(nullable = false, unique = true, length = 100) // unique key
    private String email;

 

@Column 애너테이션은 필드와 열을 맵핑해주는 애너테이션임

만약 @Coulmn 애너테이션이 없고 단순하게 필드만 정의되어 있다면, JPA는 이 필드가 테이블의 열과 맵핑되는 필드라고 간주한다. 또한 @Column 애너테이션에 사용되는 애트리뷰트값은 우리가 적지 않아도 디폴트값이 모두 적용된다.

 

 - nullable : 열에 null값을 허용할지 말지의 여부, 디폴트값 true(null값을 허용함)

                    email의 경우 회원정보에서 id로 많이 사용되며 필수 항목이며 비어선 안되니까 nullable = false를 적어줌

 - updateable : 열 데이터를 수정할 수 있는지의 여부, 디폴트값 true(수정가능)

                        email 주소는 로그인할 때 id의 역할을 한다고 하면, 한 번 등록하면 수정이 불가능하도록 하기 위해

                        updateable의 값을 false로 지정할 수 있다.(수정불가)

 - unique : 하나의 열에 unique key 즉 유니크 제약 조건을 설정함, 디폴트값 false(유니크 아님)

                 email은 중복되지 않는 고유한 값이어야 하니까 unique = ture(unique 적용) 를 적어줄 수 있다..

 

* @Column 애너테이션이 생략되었거나 애트리뷰트가 기본값을 사용할 경우?

int나 long같은 원시타입의 경우 @Column 애너테이션이 생략되면 기본적으로 nullable = false(null 불가) 임.

그런데 가령 int price not null 이라는 조건으로 테이블 열을 설정하려고 하는데,

nullable에 대한 명시적 설정 없이 단순하게 @Column만 띡 하고 적으면 nullable = true가 기본값이 되어버린다.

그럼 int price not null 이 아니라 int price로 DB에 올라갈 것이다.

따라서 우리가 의도하는게 int price not null일 경우, @Column(nullable = false)라고 명확하게 명시하던지

혹은 아예 @Column 애너테이션 자체를 사용하지 않는 것이 권장된다.

 

Channel

@Entity
@Table(name = "channels")
@Setter
@Getter
@AllArgsConstructor // 모든 필드를 넣은 생성자가 정의되어 있다면 빨간줄이 뜸
@NoArgsConstructor(force = true)
public class Channel extends BaseUpdatableEntity implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    @Column(nullable = true, length = 100)
    private String name; // 채널의 이름

    @Column(nullable = true, length = 500)
    private String description; // 채널 설명

    @Enumerated(EnumType.STRING) // @Enumerated
    @Column(nullable = false)
    private ChannelType type; // 채널 타입

 

@Enumerated 애너테이션에 대해

enum타입과 맵핑할 때 사용하는 애너테이션

@Enumerated 애너테이션은 두 가지 타입을 가질 수 있다..

  1) EnumType.ORDINAL : @Enumerated(EnumType.ORDINAL)

      enum의 순서를 나타내는 숫자를 테이블에 저장한다. 숫~~자

  2) EnumType.STRING : @Enumerated(EnumType.STRING)

      enum의 이름을 테이블에 저장한다. 문~~자

 

* EnumType.ORDINAL로 지정할 경우,

기존에 정의되어 있는 enum 사이에 새로운 enum 하나가 추가된다면,

테이블에 이미 저장된 enum의 순서번호와 enum에 정의된 순서가 일치하지 않는 문제가 발생한다고 함

따라서 이런 문제가 발생하지 않도록, 애초에 EnumType.STRING을 사용하는게 좋다고 한다.

 

 

엔티티와 테이블 맵핑을 잘 사용하는법에 대해

 - 클래스 이름 중복등의 특별한 이유가 없다면 @Entity, @Id 애너테이션만 추가한다.

    - 엔티티 클래스가 테이블 스키마 명세의 역할을 하길 바란다면, @Table 애너테이션에 테이블명을 지정해줄 수 있다..

 - 기본키 생성 전략은 DB에서 지원해주는 auto_increment 또는 SEQUENCE를 이용할 수 있도록

    IDENTITY 또는 SEQUENCE 전략을 사용하는게 좋다고 한다.

 - @Column 정보를 명시적으로 모두 지정하는건 정말 번거롭지만, DB를 따로 확인하지 않아도 된다.

   다른 누군가가 Entity 클래스 코드를 확인해도 테이블 설계가 어떤식으로 되어있는지 한눈에 알 수 있다는 장점이 있음

 - Entity 클래스 필드의 타입이 Java의 원시타입(int, long 등) @Column 애너테이션을 생략하지 말고,

   최소한 nullable = false 설정을 하는게 좋다. @Column의 nullable 초기값은 true이기 때문.

 - @Enumerated 애너테이션 사용시

   EnumType.ORDINAL을 사용할 경우 enum의 순서가 뒤바뀔 가능성이 있다.

   따라서 처음부터 EnumType.STRING을 사용하는걸 권장한다고 함

 

 

정리

@Entity 애너테이션을 클래스 레벨에 추가하여 JPA 관리대상 Entity로 만들어줄 수 있다

@Table 애너테이션으로 Entity와 맵핑할 테이블을 지정할 수 있다

@Entity 애너테이션을 지정했다면 @Id 애너테이션을 필수로 추가해줘야한다. 기본키가 필요하니까..

 

JPA의 기본키 생성 전략 : 직접하기, IDENTITY(DB에 위임), SEQUENCE(DB의 시퀀스를 사용함),

    TABLE(별도 키 생성 테이블 사용), AUTO(JPA가 DB의 Dialect에 따른 기본키 자동선택)

 

Java 원시타입 필드에서 @Column 애너테이션이 없거나, @Column이 있지만 애트리뷰트를 생략한 경우

nullable = false를 왠만하면 설정해주는것이 좋다.

 

java.util.Date, java.uitl.Calendar 타입으로 매핑하기 위해선 @Temporal 애너테이션을 추가해야한다.

하지만 LocalDate, LocalDateTime 타입일 경우 @Temporal 애너테이션은 생략이 가능하다.

 

@Transient 애너테이션을 필드에 추가할 경우 JPA가 테이블 열과 맵핑하지 않겠다는 의미로 인식한다고 함.

 

테이블에 이미 저장된 enum 순서번호와 enum에 정의된 순서가 일치하지 않게 되는 문제가 발생할 수 있다..

따라서 @Enumerated 애너테이션을 사용할 때 EnumType.STRING을 사용하는게 좋다..

'Spring Boot' 카테고리의 다른 글

Spring Data JPA : 프로젝트 설정  (0) 2025.07.22
Spring Data JPA  (2) 2025.07.22
RESTful 구현 기본 : Controller에서 요청 처리  (1) 2025.07.18
RESTful API 기본 : @RestController  (6) 2025.07.18
REST : 제약조건  (0) 2025.07.10