
MySQL
show database;
show tables;
Postgresql
\l
\dt

A: 약한 엔티티 , B: 강한 엔티티| 관계형 데이터베이스 | NoSQL | |
|---|---|---|
| DBMS 예시 | MySQL, PostgreSQL | Redis, MongoDB |
| 구조 | 레코드 - 테이블 - 데이터베이스 | 도큐먼트 - 컬렉션 - 데이터베이스 |
레코드 - 테이블 - 데이터베이스의 구조

이름, 아이디, 주소, 전화번호, 성별남 또는 여
데이터베이스에서 필드와 레코드로 구성된 테이블을 만들 수 있음
| name | ID | address | phonenumber |
|---|---|---|---|
| 큰돌 | kundol | 서울 | 112 |
| 가영 | kay | 대전 | 114 |
| 빅뱅 | big | 카이루 | 119 |
| … | … | .. | … |
name, ID, address, phonenumber가 필드에 해당member 라는 테이블로 속성인 이름, 아이디 등을 가지고 있으며, name , ID, address, phonenumber 등의 필드를 가짐💡
책의 아이디 : INT
책의 제목 : VARCHAR(255)
책의 저자 아이디 : INT
책의 출판년도 : VARCHAR(255)
책의 장르 : VARCHAR(255)
생성 일시 : DATETIME
업데이트 일시 : DATETIME
위와 같은 엔터티를 MySQL로 구현하려면, 다음과 같은 쿼리를 입력하면 됨
CREATE TABLE book (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
author_id INT,
publishing_year VARCHAR(255),
genre VARCHAR(255),
created_at DATETIME,
updated_at DATETIME,
PRIMARY KEY (id)
);
MySQL 기준으로 설명숫자 타입으로는 TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 등이 있음
| 타입 | 용량 | 최솟값(부호 있음) | 최솟값(부호 없음) | 최댓값(부호 없음) | 최댓값(부호 있음) |
|---|---|---|---|---|---|
| TINYINT | 1 | -128 | 0 | 127 | 255 |
| SMALLINT | 2 | -32768 | 0 | 32767 | 65535 |
| MEDIUMINT | 3 | -8388608 | 0 | 8388607 | 16777215 |
| INT | 4 | -2147483648 | 0 | 2147483647 | 4294967295 |
| BIGINT | 8 | -263 | 0 | 263-1 | 264-1 |
날짜 타입으로는 DATE, DATETIME, TIMESTAMP 등이 있음
DATE
1000-01-01 ~ 9999-12-31DATETIME
1000-01-01 00:00:00 ~ 9999-12-31 23:59:59TIMESTAMP :
1970-01-01 00:00:01 ~ 2038-01-19 03:14:07문자 타입으로는 CHAR, VARCHAR, TEXT, BLOB, ENUM, SET 이 있음
CHAR와 VARCHAR
CHAR와 VARCHAR 모두 그 안에 수를 입력하여 몇 자까지 입력할 지 결정CHAR은 고정 길이 문자열, 0 ~ 255의 길이VARCHAR은 가변 길이 문자열, 0 ~ 65535 사이의 값으로 지정 가능VARCHAR로 저장하는 것이 효율적TEXT와 BLOB
ENUM과 SET
ENUM와 SET 모두 문자열을 열거한 타입
ENUM은 ENUM('x-small', 'small', 'medium', 'large', 'x-large')과 같은 형태로 쓰임
SET은 ENUM과 비슷하지만
ENUM이나 SET을 쓰면 공간적으로 이점을 볼 수 있지만, 애플리케이션의 수정에 따라 데이터베이스의 ENUM이나 SET 에서 정의한 목록을 수정해야 한다는 단점이 있음
| MySQL 타입 | JPA 타입 |
|---|---|
| TINYINT | @Column(columnDefinition = "TINYINT") 또는 byte |
| SMALLINT | @Column(columnDefinition = "SMALLINT") 또는 short |
| MEDIUMINT | @Column(columnDefinition = "MEDIUMINT") (직접 지정 필요) |
| INT | int |
| BIGINT | long |
@Column의 columnDefinition을 활용하면 특정 DBMS의 고유한 데이터 타입을 사용할 수 있음
| MySQL 타입 | JPA 타입 |
|---|---|
| DATE | java.time.LocalDate |
| DATETIME | java.time.LocalDateTime |
| TIMESTAMP | java.time.Instant 또는 java.time.LocalDateTime |
@Temporal 어노테이션은 이제 자바 8부터는 잘 사용되지 않아서, 자바의 java.time API로 처리해주는 게 더 나은 선택
| MySQL 타입 | JPA 타입 |
|---|---|
| CHAR | @Column(columnDefinition = "CHAR(n)") 또는 String |
| VARCHAR | String |
| TEXT | @Lob를 붙인 String |
| BLOB | @Lob를 붙인 byte[] 또는 Blob |
| ENUM | @Enumerated(EnumType.STRING) |
| SET | @Column(columnDefinition = "SET('value1', 'value2', ...)") 또는 직접 커스텀 타입으로 구현 |
ENUM을 매핑할 때는 @Enumerated 어노테이션을 사용해서 JPA에서 매핑해줘야 함
데이터베이스의 테이블은 하나만 있는 것이 아니라, 여러 개의 테이블이 있고, 이러한 테이블들은 서로의 관계가 정의될 수 있음
예시: 사용자와 프로필
User (사용자 ID)Profile (프로필 ID, 사용자 ID)Profile 테이블에 user_id를 외래키로 사용하여 1:1 관계를 정의JPA 구현:
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "user")
private Profile profile;
}
@Entity
public class Profile {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
mappedBy를 사용하여 양방향 관계에서 주인을 명확히 해야 순환 참조 문제가 발생하지 않음예시: 사용자와 게시물
User (사용자 ID)Post (게시물 ID, 사용자 ID)Post 테이블에 user_id를 외래키로 설정JPA 구현:
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Post> posts;
}
@Entity
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
CascadeType.ALL을 설정하면, 부모 엔티티(User)가 저장되거나 삭제될 때 관련된 자식 엔티티(Post)도 함께 처리됨예시: 학생과 수업
Student (학생 ID)Course (수업 ID)Student_Course (학생 ID, 수업 ID)로 N:M 관계를 표현할 수 있음JPA 구현 (중간 테이블 자동 생성):
@Entity
public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private Set<Course> courses = new HashSet<>();
}
@Entity
public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
@ManyToMany로 중간 테이블을 자동 생성하게 만들 수 있지만,JPA 구현 (중간 테이블 엔티티로 관리):
@Entity
public class StudentCourse {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private LocalDate enrollmentDate;
}
지연 로딩(Lazy Loading)과 즉시 로딩(Eager Loading):
@OneToMany와 같은 관계에서는 지연 로딩(Lazy)이 기본값으로 설정된다.N+1 문제:
@Query("SELECT u FROM User u JOIN FETCH u.posts WHERE u.id = :id")
User findUserWithPosts(@Param("id") Long id);
- N+1 문제와 Fetch Join에 대한 추가 설명
N+1 문제
N+1 문제는 ORM (Object-Relational Mapping)에서 자주 발생하는 성능 문제로, 한 번의 쿼리로 메인 엔티티를 가져온 후, 그 엔티티와 관련된 N개의 하위 엔티티를 가져오기 위해 추가로 N번의 쿼리가 발생하는 현상
예시 상황
1. N+1 문제 발생 예시
User테이블에서 10명의 사용자를 가져온다고 가정한다.- 각 사용자는 여러 개의
Post(게시물)을 가지고 있다.
1차 쿼리: 사용자를 가져오는 쿼리SELECT * FROM user;2차 쿼리: 각 사용자의 게시물을 가져오기 위한 추가 쿼리로, 사용자 10명에 대해 10번 실행된다.
SELECT * FROM post WHERE user_id = 1; SELECT * FROM post WHERE user_id = 2; ... SELECT * FROM post WHERE user_id = 10;
- 이렇게 되면 사용자 수(N명)에 대해 추가로 N번의 쿼리가 실행되니까, 총 N+1개의 쿼리가 실행되는 문제
- 데이터가 많아질수록 쿼리 횟수가 기하급수적으로 늘어나서 성능에 큰 부담을 줄 수 있다.
Fetch Join
- Fetch Join은 N+1 문제를 해결하기 위한 방법 중 하나
- JPA는 기본적으로 연관된 데이터를 지연 로딩(Lazy Loading)하게 되는데, Fetch Join을 사용하면 연관된 데이터를 즉시 로딩(Eager Loading)하면서 한 번의 쿼리로 필요한 데이터를 모두 가져오게 된다.
Fetch Join을 사용한 쿼리:
@Query("SELECT u FROM User u JOIN FETCH u.posts") List<User> findAllUsersWithPosts();이 쿼리는
User와 관련된Post들을 한 번의 쿼리로 모두 가져와서 N+1 문제를 방지한다.
실행되는 SQL 쿼리:SELECT u.*, p.* FROM user u JOIN post p ON u.id = p.user_id;

키들의 관계
💡
유일성, 최소성?
유일성은 테이블 내에서 중복될 수 없다는 의미
최소성은 그 속성의 조합이 최소한으로 데이터의 고유성을 보장해야 한다는 의미
💡 유저 테이블을 만든다고 가정하면, 주민등록번호, 이름, 성별 등의 속성이 존재한다.
이 중 이름, 성별 등은 중복된 값이 들어올 수 있으므로 부적절하고, 남는 것은 주민등록번호이다.
이런 식으로 중복된 값들을 제외하며 중복되지 않는 것을 ‘자연스럽게’ 뽑다가 나오는 키를 자연키라고 함
💡 유저 테이블을 만든다고 했을 때 회원 테이블을 생성한다고 가정하면 주민등록번호, 이름, 성별 등의 속성이 존재한다.
여기에 인위적으로 유저 아이디를 부여한다. 이를 통해 고유 식별자가 생겨난다.
Oracle은 Sequence,MySQL은 Auto Increment 등으로 설정한다.
이렇게 인위적으로 생성한 키를 인조키라고 함. 자연키와는 대조적으로 변하지 않는데, 따라서 보통 기본키는 인조키로 설정함
@Id 어노테이션을 사용해 기본키를 설정 가능@GeneratedValue(strategy = GenerationType.IDENTITY)는 MySQL에서 Auto Increment처럼 인조키를 자동으로 생성하는 것@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;