지난주에는 데이터베이스 설계를 위한 ERD를 완성하며 데이터 구조를 정의했습니다.
이번 주차에서는 이 데이터를 활용하기 위한 Spring Boot와 JPA를 사용한 엔티티 설계를 다룹니다.
Spring Boot와 JPA를 통해 데이터를 쉽게 저장하고 조회하며, 프로젝트의 비즈니스 로직을 깔끔하게 구현할 수 있습니다.
1️⃣ JPA의 개념과 필요성을 이해합니다.
2️⃣ Spring Data JPA를 사용하여 ERD 기반으로 엔티티를 설계하고 매핑합니다.
객체 지향 프로그래밍의 목표는 캡슐화, 상속, 다형성을 활용하여 데이터를 관리하는 것이고,
RDBMS는 데이터를 정교하게 정규화하여 저장하는 것이 목표입니다.
이 패러다임 차이로 인해 아래와 같은 문제가 발생합니다:
1️⃣ 데이터를 SQL로 저장하고,
2️⃣ SQL 결과를 자바 객체로 변환해야 하는 번거로움.
해결책
JPA는 Java 진영에서 ORM(Object Relational Mapping) 기술의 표준 인터페이스입니다.
쉽게 말해, 애플리케이션의 객체를 관계형 데이터베이스(RDBMS) 테이블에 매핑해주는 기술입니다. 객체 지향적 언어인 자바는 궁극적으로 캡슐화/상속/다형성을 활용하는 것이 목표이고, RDBMS(관계형 데이터베이스)는 데이터를 정교하게 구성하는 것이 목표입니다. 두 언어가 목표가 다르기 때문에 원래는 개발자가 이를 하나하나 Mapping 해줘야 합니다. 이를 해결하기 위해 JPA가 사용됩니다!
객체 지향 언어와 관계형 데이터베이스 간의 불일치를 해결하는 기술입니다. 기술적인 측면에서는 애플리케이션의 객체를 RDB 클래스에 자동으로 연결시켜준다고 보시면 됩니다.
즉, 반복적인 CRUD SQL 쿼리문을 굳이 짤 필요 없이 자동으로 매핑하여 날려준다고 생각하시면 됩니다👍👍
JPA는 SQL 작성 없이도 객체와 테이블 간의 변환을 자동으로 처리하여 개발자의 생산성을 높입니다.
Spring Data JPA는 JPA를 더 간편하게 사용할 수 있도록 지원하는 스프링 모듈입니다.
Spring Boot 프로젝트는 아래와 같은 디렉토리 구조를 권장합니다.
이 디렉토리 구조는 모듈화와 유지보수성을 높이기 위해 설계되었습니다.
src/main/java
│
├── controller // HTTP 요청/응답 처리
├── service // 비즈니스 로직
├── repository // DB 접근 로직
├── domain // Entity 클래스
├── dto // 데이터 전송 객체
└── base // 공통 클래스 (e.g., BaseEntity)
controller
@RestController 또는 @Controller를 주로 사용합니다. service
repository
JpaRepository를 확장해 사용하며, CRUD 작업을 간편하게 처리할 수 있습니다.domain
@Entity, @Id, @GeneratedValue 등)을 사용합니다. User, Order, Product 등 주요 데이터를 표현.dto
base
created_at), 수정일(updated_at) 등을 처리하는 BaseEntity.
@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
@Id
@GeneratedValue
GenerationType.IDENTITY: MySQL의 AUTO_INCREMENT처럼 자동 증가하도록 설정.@Column
nullable = false로 필수 입력 값을 설정하거나, length로 문자열 길이를 제한.@Enumerated
EnumType.STRING: Enum 값을 문자열로 저장합니다. EnumType.ORDINAL은 Enum의 순서를 저장하지만, 값 변경 시 오류가 발생할 수 있으므로 사용하지 않습니다.모든 테이블에서 공통적으로 사용하는 생성일자와 수정일자를 관리하기 위해 BaseEntity를 작성합니다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
주요 어노테이션 설명
1. @MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@CreatedDate, @LastModifiedDate
@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
MemberPrefer가 하나의 Member에 연결됩니다. @JoinColumn
fetch = FetchType.LAZY
LAZY를 사용합니다. 양방향 연관관계는 객체 지향적인 설계에서 중요한 역할을 합니다.
데이터베이스 설계에서는 단방향 연관관계만으로도 모든 데이터 처리가 가능하지만,
JPA에서는 양방향 연관관계를 설정함으로써 추가적인 이점을 얻을 수 있습니다.
양방향 매핑은 두 엔티티가 서로를 참조하는 관계를 의미합니다.
Member 엔티티는 MemberPrefer를 참조하고, MemberPrefer 엔티티는 다시 Member를 참조. 이러한 구조를 통해 데이터베이스의 데이터를 JPA가 관리하는 객체 그래프 탐색을 쉽게 할 수 있습니다.
즉, Member 객체를 통해 관련된 MemberPrefer 객체들을 탐색하거나,
반대로 MemberPrefer에서 Member를 탐색할 수 있습니다.
1️⃣ 객체 그래프 탐색 가능
member.getMemberPreferList()와 같이 한쪽 엔티티에서 관련된 엔티티들을 쉽게 탐색할 수 있습니다. 2️⃣ Cascade 설정 가능
Member 삭제 시, 연관된 MemberPrefer 엔티티도 자동으로 삭제되도록 설정할 수 있습니다. 3️⃣ 편리한 데이터 삭제 및 관리
데이터베이스에서는 Cascade 설정이 외래키(Foreign Key)를 소유한 테이블(연관관계의 주인)에 적용됩니다.
하지만 JPA에서는 Cascade 설정을 연관관계의 주인이 아닌 엔티티에서 설정해야 합니다.
이 점에서 JPA의 Cascade 설정은 단방향 매핑만으로는 올바르게 동작하지 않을 수 있는 문제를 포함하고 있습니다.
단방향 매핑 상태에서 Cascade를 설정할 경우:
예를 들어:
Member를 참조하는 MemberPrefer가 삭제될 때, Member도 함께 삭제되는 의도치 않은 상황이 발생할 수 있습니다. 양방향 매핑을 사용하면 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);
}
}
Cascade 설정
Member) 삭제 시 자식 엔티티(MemberPrefer)도 함께 삭제됩니다. Orphan Removal 설정
memberPreferList.remove(memberPrefer)를 호출하면 해당 자식 엔티티가 삭제됩니다. 연관관계 편의 메서드 추가
addMemberPrefer()와 removeMemberPrefer()가 이에 해당합니다. member.addMemberPrefer(memberPrefer)를 호출하면,memberPrefer.setMember(member)도 자동으로 호출되어 양쪽 관계가 일치하게 됩니다. 1️⃣ 무한 루프 방지
Member → MemberPrefer → 다시 Member로 계속 참조. @JsonIgnore 또는 DTO를 사용해 필요 이상의 데이터 참조를 차단합니다. @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
@JsonIgnore
private Member member;
2️⃣ Cascade 설정 남용 금지
3️⃣ 비즈니스 로직과의 협의
엔티티의 각 멤버 변수는 데이터베이스의 컬럼과 매핑되며, 이때 컬럼의 속성을 정확히 정의해야 합니다.
이를 통해 JPA가 테이블을 올바르게 생성하고 관리할 수 있습니다.
기본적으로 JPA는 엔티티 필드의 타입과 속성을 기반으로 데이터베이스 컬럼을 생성합니다.
예를 들어, String 타입의 필드는 데이터베이스에서 VARCHAR로 매핑됩니다.
하지만, 필드의 길이, 유니크 여부, 기본값 등 추가적인 제약 조건은 명시적으로 설정해야 합니다.
String name 필드를 생성하면,VARCHAR(255)로 처리됩니다.name 칼럼의 최대 길이가 20자로 제한되어야 할 수 있습니다. JPA에서는 @Column 어노테이션을 사용하여 칼럼의 속성을 상세히 정의할 수 있습니다.
@Column(
name = "name", // 데이터베이스 컬럼명 (기본값: 필드명과 동일)
nullable = false, // NOT NULL 제약 조건
unique = true, // UNIQUE 제약 조건
length = 20 // VARCHAR 길이 제한
)
private String name;
1️⃣ name
@Column(name = "member_name")
private String name;2️⃣ nullable
NULL을 허용하는지 여부를 설정합니다. true로 설정되며, nullable = false로 지정하면 데이터베이스에 NOT NULL 제약 조건이 추가됩니다. @Column(nullable = false)
private String email; // NOT NULL3️⃣ unique
@Column(unique = true)
private String email; // UNIQUE4️⃣ length
VARCHAR 또는 CHAR 타입의 길이를 제한하는 역할을 합니다. @Column(length = 50)
private String address;5️⃣ columnDefinition
@Column(columnDefinition = "VARCHAR(20) DEFAULT 'UNKNOWN'")
private String status;6️⃣ insertable, updatable
INSERT 또는 UPDATE 쿼리에 포함될지 여부를 설정합니다. @Column(insertable = false, updatable = false)
private LocalDateTime createdAt; // 자동 생성/수정 필드로 직접 변경 불가@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; // 전화번호는 입력하지 않아도 됨
}
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 기본값
@Column의 length 속성을 지정하지 않으면 기본값은 255로 설정됩니다. 2️⃣ nullable과 기본값 충돌
nullable = false와 columnDefinition의 DEFAULT 값은 함께 사용할 때 주의해야 합니다. DEFAULT 값을 설정하더라도, JPA는 이를 인지하지 못하므로 삽입 시 명시적으로 값을 설정해야 합니다. 3️⃣ columnDefinition 사용 시 의존성
columnDefinition은 데이터베이스에 강하게 의존하므로, 다중 데이터베이스 환경에서는 호환성 문제가 발생할 수 있습니다. 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
spring:
jpa:
show-sql: true1️⃣ spring.jpa.properties.hibernate.format_sql
spring:
jpa:
properties:
hibernate.format_sql: true2️⃣ 데이터베이스 연결 풀 설정 (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를 더 구체적으로 살펴보면서 오늘 배운 내용의 심화 버전을 배워보겠습니다!🤗😊😁