Chapter 5. Spring Boot와 JPA로 Entity 설계하기

김지민·2024년 12월 26일

UMC springboot

목록 보기
4/9

지난주에는 데이터베이스 설계를 위한 ERD를 완성하며 데이터 구조를 정의했습니다.
이번 주차에서는 이 데이터를 활용하기 위한 Spring Boot와 JPA를 사용한 엔티티 설계를 다룹니다.

Spring Boot와 JPA를 통해 데이터를 쉽게 저장하고 조회하며, 프로젝트의 비즈니스 로직을 깔끔하게 구현할 수 있습니다.


학습 목표 🎯

1️⃣ JPA의 개념과 필요성을 이해합니다.
2️⃣ Spring Data JPA를 사용하여 ERD 기반으로 엔티티를 설계하고 매핑합니다.


🌟 1. JPA와 Spring Data JPA란?

객체 지향 프로그래밍의 목표는 캡슐화, 상속, 다형성을 활용하여 데이터를 관리하는 것이고,
RDBMS는 데이터를 정교하게 정규화하여 저장하는 것이 목표입니다.

이 패러다임 차이로 인해 아래와 같은 문제가 발생합니다:
1️⃣ 데이터를 SQL로 저장하고,
2️⃣ SQL 결과를 자바 객체로 변환해야 하는 번거로움.

해결책

  • JPA와 Spring Data JPA를 사용하여 반복 작업을 줄이고, 비즈니스 로직에만 집중합니다.

JPA(Java Persistence API)란?

JPA는 Java 진영에서 ORM(Object Relational Mapping) 기술의 표준 인터페이스입니다.
쉽게 말해, 애플리케이션의 객체를 관계형 데이터베이스(RDBMS) 테이블에 매핑해주는 기술입니다. 객체 지향적 언어인 자바는 궁극적으로 캡슐화/상속/다형성을 활용하는 것이 목표이고, RDBMS(관계형 데이터베이스)데이터를 정교하게 구성하는 것이 목표입니다. 두 언어가 목표가 다르기 때문에 원래는 개발자가 이를 하나하나 Mapping 해줘야 합니다. 이를 해결하기 위해 JPA가 사용됩니다!

ORM(Object Relational Mapping)이란?

객체 지향 언어와 관계형 데이터베이스 간의 불일치를 해결하는 기술입니다. 기술적인 측면에서는 애플리케이션의 객체를 RDB 클래스에 자동으로 연결시켜준다고 보시면 됩니다.
즉, 반복적인 CRUD SQL 쿼리문을 굳이 짤 필요 없이 자동으로 매핑하여 날려준다고 생각하시면 됩니다👍👍

  • 객체: 자바에서 클래스를 통해 데이터를 관리.
  • RDBMS: 테이블과 컬럼을 기반으로 데이터를 저장.

JPA는 SQL 작성 없이도 객체와 테이블 간의 변환을 자동으로 처리하여 개발자의 생산성을 높입니다.

🛠️ Spring Data JPA란?

Spring Data JPA는 JPA를 더 간편하게 사용할 수 있도록 지원하는 스프링 모듈입니다.

  • CRUD 쿼리 자동 생성: 단순한 쿼리는 메서드 선언만으로 자동 생성.
  • JPQL 지원: 복잡한 쿼리도 쉽게 작성 가능.
  • Repository 기반 개발: 데이터베이스와의 상호작용을 추상화.

✏️ 2. 프로젝트 구조 설계

🏗️ 프로젝트 디렉토리 구조

Spring Boot 프로젝트는 아래와 같은 디렉토리 구조를 권장합니다.
이 디렉토리 구조는 모듈화유지보수성을 높이기 위해 설계되었습니다.

src/main/java
│
├── controller        // HTTP 요청/응답 처리
├── service           // 비즈니스 로직
├── repository        // DB 접근 로직
├── domain            // Entity 클래스
├── dto               // 데이터 전송 객체
└── base              // 공통 클래스 (e.g., BaseEntity)

디렉토리 역할 설명

  1. controller

    • 사용자의 요청을 받아 처리하고, 결과를 응답으로 반환하는 계층입니다.
    • Spring MVC의 핵심 구성요소로 @RestController 또는 @Controller를 주로 사용합니다.
    • 예: 로그인 요청, 회원가입 요청 등을 처리.
  2. service

    • 비즈니스 로직을 처리하는 계층입니다.
    • Controller에서 받은 요청을 기반으로 데이터를 가공하거나, 여러 Repository를 조합해 복잡한 작업을 처리합니다.
    • 예: 회원 가입 시 이메일 중복 체크와 데이터 저장 처리.
  3. repository

    • 데이터베이스와의 직접적인 상호작용을 담당하는 계층입니다.
    • Spring Data JPA의 JpaRepository를 확장해 사용하며, CRUD 작업을 간편하게 처리할 수 있습니다.
  4. domain

    • 데이터베이스의 테이블과 1:1로 매핑되는 Entity 클래스들이 위치합니다.
    • JPA에서 사용되는 주요 어노테이션(@Entity, @Id, @GeneratedValue 등)을 사용합니다.
    • 예: User, Order, Product 등 주요 데이터를 표현.
  5. dto

    • 클라이언트와의 데이터 교환을 위한 객체(Data Transfer Object)입니다.
    • Entity와 달리 특정 요청이나 응답에 필요한 데이터만 포함합니다.
    • Entity 변경 시 클라이언트에 영향을 주지 않도록 데이터 전달을 추상화하는 역할을 합니다.
  6. base

    • 모든 Entity에서 공통으로 사용할 필드나 기능을 정의합니다.
    • 예: 생성일(created_at), 수정일(updated_at) 등을 처리하는 BaseEntity.

1️⃣ Entity 설계 및 매핑

엔티티(Entity)란?

  • 엔티티는 데이터베이스의 테이블과 1:1로 매핑되는 클래스입니다.
  • JPA를 사용하면 엔티티 클래스를 통해 SQL 없이 데이터베이스 작업을 수행할 수 있습니다.
  • Entity 클래스는 주로 데이터의 상태관계를 표현합니다.

ERD 기반 설계

ERD(Entity Relationship Diagram)란?

  • ERD는 데이터베이스 테이블 간의 관계를 시각적으로 표현한 다이어그램입니다.
  • JPA를 사용하여 엔티티를 설계하기 전, ERD를 기반으로 테이블 구조를 정의합니다.
  • 예제 ERD:

Member 엔티티 예제

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String name;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    @Column(nullable = false)
    private String email;
}

주요 어노테이션 설명
1. @Entity

  • JPA가 관리할 클래스임을 명시.
  • 데이터베이스 테이블로 매핑됩니다.
  1. @Id

    • 기본 키(primary key)를 설정합니다.
    • 모든 엔티티 클래스에는 반드시 하나의 기본 키가 있어야 합니다.
  2. @GeneratedValue

    • 기본 키의 값을 자동으로 생성하도록 설정합니다.
    • GenerationType.IDENTITY: MySQL의 AUTO_INCREMENT처럼 자동 증가하도록 설정.
  3. @Column

    • 테이블 컬럼의 속성을 설정합니다.
    • 예: nullable = false로 필수 입력 값을 설정하거나, length로 문자열 길이를 제한.
  4. @Enumerated

    • Enum 타입을 데이터베이스에 저장하기 위한 설정입니다.
    • EnumType.STRING: Enum 값을 문자열로 저장합니다.
    • EnumType.ORDINAL은 Enum의 순서를 저장하지만, 값 변경 시 오류가 발생할 수 있으므로 사용하지 않습니다.

2️⃣BaseEntity로 공통 필드 처리

공통 필드 처리 예제

모든 테이블에서 공통적으로 사용하는 생성일자와 수정일자를 관리하기 위해 BaseEntity를 작성합니다.

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

주요 어노테이션 설명
1. @MappedSuperclass

  • 이 클래스의 필드를 상속받는 엔티티에서 사용할 수 있도록 설정합니다.
  1. @EntityListeners(AuditingEntityListener.class)

    • Spring Data JPA에서 제공하는 감사(Auditing) 기능을 활성화합니다.
  2. @CreatedDate, @LastModifiedDate

    • 데이터 생성 및 수정 시간을 자동으로 기록합니다.

3️⃣ 연관관계 매핑

단방향 연관관계: Member와 MemberPrefer

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MemberPrefer extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

주요 어노테이션 설명
1. @ManyToOne

  • 다대일(N:1) 관계를 설정합니다.
  • 예: 여러 MemberPrefer가 하나의 Member에 연결됩니다.
  1. @JoinColumn

    • 외래 키 이름을 명시합니다.
  2. fetch = FetchType.LAZY

    • 지연 로딩 설정: 필요한 시점에 연관된 데이터를 가져옵니다.
    • 성능 최적화를 위해 기본값인 LAZY를 사용합니다.

양방향 연관관계 설정

양방향 연관관계는 객체 지향적인 설계에서 중요한 역할을 합니다.
데이터베이스 설계에서는 단방향 연관관계만으로도 모든 데이터 처리가 가능하지만,
JPA에서는 양방향 연관관계를 설정함으로써 추가적인 이점을 얻을 수 있습니다.

양방향 연관관계란?

양방향 매핑은 두 엔티티가 서로를 참조하는 관계를 의미합니다.

  • 예: Member 엔티티는 MemberPrefer를 참조하고,
  • MemberPrefer 엔티티는 다시 Member를 참조.

이러한 구조를 통해 데이터베이스의 데이터를 JPA가 관리하는 객체 그래프 탐색을 쉽게 할 수 있습니다.
즉, Member 객체를 통해 관련된 MemberPrefer 객체들을 탐색하거나,
반대로 MemberPrefer에서 Member를 탐색할 수 있습니다.

양방향 매핑의 장점

1️⃣ 객체 그래프 탐색 가능

  • 예: member.getMemberPreferList()와 같이 한쪽 엔티티에서 관련된 엔티티들을 쉽게 탐색할 수 있습니다.
  • 객체 지향적인 코드 작성이 가능하며, 데이터 간 관계를 명확히 표현할 수 있습니다.

2️⃣ Cascade 설정 가능

  • Cascade는 부모 엔티티의 변경이 자식 엔티티에도 자동으로 적용되도록 설정하는 기능입니다.
  • 예: Member 삭제 시, 연관된 MemberPrefer 엔티티도 자동으로 삭제되도록 설정할 수 있습니다.

3️⃣ 편리한 데이터 삭제 및 관리

  • 양방향 매핑이 없으면, 연관된 데이터를 수동으로 삭제하거나 관리해야 하는 번거로움이 있습니다.
  • Cascade 설정을 통해 부모와 자식 엔티티 간 데이터 관리의 편의성을 높입니다.

Cascade와 연관관계의 주인 문제 💥

데이터베이스에서는 Cascade 설정이 외래키(Foreign Key)를 소유한 테이블(연관관계의 주인)에 적용됩니다.
하지만 JPA에서는 Cascade 설정을 연관관계의 주인이 아닌 엔티티에서 설정해야 합니다.

이 점에서 JPA의 Cascade 설정은 단방향 매핑만으로는 올바르게 동작하지 않을 수 있는 문제를 포함하고 있습니다.

단방향 매핑 상태에서 Cascade를 설정할 경우:

  • 참조를 하는 쪽(자식 엔티티)에서 Cascade가 잘못 동작하여,
    자식 엔티티 삭제 시 부모 엔티티가 함께 삭제되는 문제가 발생할 수 있습니다.

예를 들어:

  • Member를 참조하는 MemberPrefer가 삭제될 때,
  • Member도 함께 삭제되는 의도치 않은 상황이 발생할 수 있습니다.

올바른 Cascade 적용을 위한 양방향 매핑

양방향 매핑을 사용하면 JPA가 연관관계의 전체 흐름을 명확히 이해하고, Cascade를 올바르게 처리할 수 있습니다.

@Entity
public class Member {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<MemberPrefer> memberPreferList = new ArrayList<>();

    public void addMemberPrefer(MemberPrefer memberPrefer) {
        memberPrefer.setMember(this);
        memberPreferList.add(memberPrefer);
    }

    public void removeMemberPrefer(MemberPrefer memberPrefer) {
        memberPreferList.remove(memberPrefer);
        memberPrefer.setMember(null);
    }
}
  1. Cascade 설정

    • 부모 엔티티(Member) 삭제 시 자식 엔티티(MemberPrefer)도 함께 삭제됩니다.
    • CascadeType.ALL: 부모 엔티티의 모든 변경사항(저장, 삭제 등)이 자식 엔티티에도 적용됩니다.
  2. Orphan Removal 설정

    • 부모와의 관계가 끊어진 자식 엔티티를 자동으로 삭제합니다.
    • 예: memberPreferList.remove(memberPrefer)를 호출하면 해당 자식 엔티티가 삭제됩니다.
  3. 연관관계 편의 메서드 추가

    • 양방향 매핑에서는 연관관계 편의 메서드를 추가하여 양쪽 엔티티의 상태를 동기화해야 합니다.
    • 위 예제에서 addMemberPrefer()removeMemberPrefer()가 이에 해당합니다.
    • 예: member.addMemberPrefer(memberPrefer)를 호출하면,
      memberPrefer.setMember(member)도 자동으로 호출되어 양쪽 관계가 일치하게 됩니다.

양방향 매핑 시 주의점

1️⃣ 무한 루프 방지

  • 양방향 매핑에서는 한쪽에서 다른 쪽을 참조하는 형태로 무한 루프가 발생할 수 있습니다.
  • 예: MemberMemberPrefer → 다시 Member로 계속 참조.
  • 해결 방법: @JsonIgnore 또는 DTO를 사용해 필요 이상의 데이터 참조를 차단합니다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
@JsonIgnore
private Member member;

2️⃣ Cascade 설정 남용 금지

  • Cascade는 부모-자식 관계에서만 사용하는 것이 바람직합니다.
  • 서로 독립적인 엔티티 간에 Cascade를 설정하면 데이터 누락이나 불필요한 삭제가 발생할 수 있습니다.

3️⃣ 비즈니스 로직과의 협의

  • 특정 연관관계를 Cascade로 처리할지 여부는 기획 단계에서 결정해야 합니다.
  • 예: 회원 탈퇴 시 회원의 모든 게시글을 삭제할지 남겨둘지 기획자와 논의 필요.
    • 에브리타임 같은 서비스에서는 탈퇴해도 게시글이 삭제되지 않는 경우가 많습니다.

3️⃣ 칼럼 별 세부적인 설정

엔티티의 각 멤버 변수는 데이터베이스의 컬럼과 매핑되며, 이때 컬럼의 속성을 정확히 정의해야 합니다.
이를 통해 JPA가 테이블을 올바르게 생성하고 관리할 수 있습니다.

1) 칼럼 설정의 필요성

기본적으로 JPA는 엔티티 필드의 타입과 속성을 기반으로 데이터베이스 컬럼을 생성합니다.
예를 들어, String 타입의 필드는 데이터베이스에서 VARCHAR로 매핑됩니다.
하지만, 필드의 길이, 유니크 여부, 기본값 등 추가적인 제약 조건은 명시적으로 설정해야 합니다.

  • 문제점
    JPA를 사용할 때 별도의 설정 없이 String name 필드를 생성하면,
    기본적으로 데이터베이스에서는 VARCHAR(255)로 처리됩니다.
    하지만, 실제 요구사항에서는 name 칼럼의 최대 길이가 20자로 제한되어야 할 수 있습니다.

2) @Column 어노테이션

JPA에서는 @Column 어노테이션을 사용하여 칼럼의 속성을 상세히 정의할 수 있습니다.

@Column(
    name = "name",            // 데이터베이스 컬럼명 (기본값: 필드명과 동일)
    nullable = false,         // NOT NULL 제약 조건
    unique = true,            // UNIQUE 제약 조건
    length = 20               // VARCHAR 길이 제한
)
private String name;

3) 주요 속성

1️⃣ name

  • 데이터베이스 컬럼명을 지정합니다.
  • 기본값은 필드명과 동일하지만, 필요에 따라 별도의 이름을 지정할 수 있습니다.
    @Column(name = "member_name")
    private String name;

2️⃣ nullable

  • 해당 필드가 NULL을 허용하는지 여부를 설정합니다.
  • 기본값은 true로 설정되며, nullable = false로 지정하면 데이터베이스에 NOT NULL 제약 조건이 추가됩니다.
    @Column(nullable = false)
    private String email;  // NOT NULL

3️⃣ unique

  • 해당 컬럼에 고유 제약 조건(UNIQUE)을 추가합니다.
  • 동일한 값이 중복 저장되지 않도록 보장합니다.
    @Column(unique = true)
    private String email;  // UNIQUE

4️⃣ length

  • 문자열 필드의 최대 길이를 지정합니다.
  • 데이터베이스에서 VARCHAR 또는 CHAR 타입의 길이를 제한하는 역할을 합니다.
    @Column(length = 50)
    private String address;

5️⃣ columnDefinition

  • SQL을 직접 작성하여 데이터베이스 컬럼 정의를 세밀하게 조정할 수 있습니다.
  • 기본값을 설정하거나 데이터 타입을 명시적으로 지정할 때 유용합니다.
    @Column(columnDefinition = "VARCHAR(20) DEFAULT 'UNKNOWN'")
    private String status;

6️⃣ insertable, updatable

  • 해당 칼럼이 INSERT 또는 UPDATE 쿼리에 포함될지 여부를 설정합니다.
    @Column(insertable = false, updatable = false)
    private LocalDateTime createdAt;  // 자동 생성/수정 필드로 직접 변경 불가

예제: Member 엔티티의 세부 설정

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String name;  // 이름은 반드시 입력해야 하며, 최대 길이는 20자

    @Column(nullable = false, unique = true)
    private String email;  // 이메일은 고유하고 반드시 입력해야 함

    @Column(columnDefinition = "VARCHAR(10) DEFAULT 'ACTIVE'")
    private String status;  // 기본값은 'ACTIVE'

    @Column(updatable = false)
    private LocalDateTime createdAt;  // 생성 시에만 값 설정 가능

    @Column(nullable = true)
    private String phoneNumber;  // 전화번호는 입력하지 않아도 됨
}

결과적으로 생성되는 테이블 스키마 (MySQL 기준)

CREATE TABLE member (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    status VARCHAR(10) DEFAULT 'ACTIVE',
    created_at DATETIME,
    phone_number VARCHAR(255)
);

주의사항

1️⃣ length 기본값

  • @Columnlength 속성을 지정하지 않으면 기본값은 255로 설정됩니다.
  • 요구사항에 맞게 항상 적절한 길이를 지정해야 효율적인 테이블 설계가 가능합니다.

2️⃣ nullable과 기본값 충돌

  • nullable = falsecolumnDefinitionDEFAULT 값은 함께 사용할 때 주의해야 합니다.
  • 데이터베이스는 DEFAULT 값을 설정하더라도, JPA는 이를 인지하지 못하므로 삽입 시 명시적으로 값을 설정해야 합니다.

3️⃣ columnDefinition 사용 시 의존성

  • columnDefinition은 데이터베이스에 강하게 의존하므로, 다중 데이터베이스 환경에서는 호환성 문제가 발생할 수 있습니다.
  • 가능한 경우 JPA 표준 어노테이션을 사용하는 것이 권장됩니다.

4️⃣ Application.yml 설정

Spring Boot 애플리케이션에서 데이터베이스 및 JPA 설정을 관리하는 파일은 application.yml입니다.
이 파일을 통해 데이터베이스 연결 정보와 JPA 동작 방식을 설정할 수 있습니다.

예제: 데이터베이스 설정

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/study   # 연결할 데이터베이스 URL
    username: your_username                 # 데이터베이스 사용자명
    password: your_password                 # 데이터베이스 비밀번호
  jpa:
    hibernate:
      ddl-auto: update                      # 엔티티 변경 시 테이블 자동 업데이트
    show-sql: true                          # 실행되는 SQL 쿼리를 출력

주요 설정 설명

1️⃣ spring.datasource.url

  • 애플리케이션이 연결할 데이터베이스의 주소를 설정합니다.

    • jdbc:mysql://localhost:3306/study: MySQL 데이터베이스 study에 연결.
    • localhost 대신 클라우드 DB를 사용하면 해당 주소를 입력합니다.
    • 포트 번호(3306)는 MySQL 기본 포트입니다.
    spring:
      datasource:
        url: jdbc:mysql://aws-db-instance:3306/study

2️⃣ spring.jpa.hibernate.ddl-auto

  • 엔티티의 변경 사항이 데이터베이스 테이블에 반영되는 방식을 설정합니다.

    • update: 엔티티 변경 사항을 반영해 테이블 구조를 자동으로 수정.
    • validate: 엔티티와 테이블의 스키마를 비교해 일치 여부만 확인.
    • none: 테이블 생성 및 수정 작업을 수행하지 않음.

    💡 권장 사항

    • 개발 환경: update (편리한 수정 작업을 위해)
    • 운영 환경: validate 또는 none (데이터 손실 방지)

3️⃣ spring.jpa.show-sql

  • 실행되는 SQL 쿼리를 콘솔에 출력합니다.
  • 디버깅과 테스트 시 유용하며, 복잡한 쿼리 작성 시 쿼리 최적화에 도움을 줍니다.
    spring:
      jpa:
        show-sql: true

추가 옵션

1️⃣ spring.jpa.properties.hibernate.format_sql

  • SQL 쿼리를 가독성이 높게 포맷팅하여 출력합니다.
    spring:
      jpa:
        properties:
          hibernate.format_sql: true

2️⃣ 데이터베이스 연결 풀 설정 (HikariCP)

  • Spring Boot는 기본적으로 HikariCP를 데이터베이스 연결 풀로 사용합니다.
  • 연결 풀 크기 등을 설정할 수 있습니다.
    spring:
      datasource:
        hikari:
          maximum-pool-size: 10  # 최대 연결 수
          minimum-idle: 5        # 최소 유휴 연결 수

3️⃣ 데이터베이스 드라이버 설정

  • 데이터베이스에 맞는 드라이버 클래스를 명시적으로 설정할 수 있습니다.
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver

환경별 설정 관리

운영 환경과 개발 환경에 따라 설정을 분리하려면 application-{profile}.yml 파일을 사용합니다.
예: application-dev.yml (개발 환경), application-prod.yml (운영 환경).

spring:
  profiles:
    active: dev

정리

이번 시간에는 지금까지 배웠던 내용을 바탕으로 스프링 프로젝트를 시작하는 과정과 틀에 대해 배워봤습니다. 이제 혼자서도 스프링 프로젝트를 만들어낼 수 있게 된 겁니다!!!(와~~) 다음 시간에는 JPA를 더 구체적으로 살펴보면서 오늘 배운 내용의 심화 버전을 배워보겠습니다!🤗😊😁

profile
열혈개발자~!!

0개의 댓글