데이터 모델링
- 업무 내용을 분석하여 이해하고 약속된 표기법에 의해 표현하는 것
- 데이터베이스의 골격을 이해하고, 그 이해를 바탕으로 SQL문장을 효율적으로 작성 가능
내가 만들고자 하는 것은 직전 학기에 만들어 봤던 만보기 앱 화면에 서버를 연동하여
수치와 프로필을 띄우는 것이 주된 목표이다.
- 업무 파악
- 걸음 수
- 사용자 이름
- 보유 포인트 (걸음 수에 따라 달라짐)
개념적 데이터 모델링
- 데이터 간의 관계 구상
- 피터 첸 표기법으로 ERD를 구상한다.

개체 타입이라 적힌 네모는 테이블을 의미한다.

이런 다이어그램은 처음이라 조금 미숙하다,,
논리적 데이터 모델링
- 구체화된 업무 중심의 데이터 모델
- 추상적인 데이터보다 구체화된 데이터로 작성한다.
- key, 속성, 관계 등을 표시하고 우리가 흔히 보는 ERD 그림을 생각하면 된다.
- 개념적 데이터 모델링 그림을 테이블 형태로 다시 그린다.
본격적으로 그려보기 전에 ERD 그리는 법에 대해 짚고 넘어간다.
- Entity Relationship Diagram
- entity개체와 relationship 관계를 중점적으로 표시하는 데이터베이스 구조
- 엔티티
- 정의 가능한 사물 또는 개념
- DB의 테이블이 엔티티로 표현된다.
- 엔티티 속성 (Attribute)
- 엔티티의 속성을 나타낸다.
- 회원 엔티티를 예로 들면, 회원 아이디나 이름, 비밀번호 등이 있다.
- DB 테이블의 각 컬럼(필드)들이 엔티티 속성이다.
- 엔티티 도메인(domain)
- Attribute의 타입을 명시
- 데이터베이스가 지원하는 타입을 작성해야 한다.
- 엔티티 종류
- 유형 엔티티: 물리적인 형태
- 무형 엔티티: 개념적으로만 존재(인터넷 장바구니, 구매 이력 등)
- 문서 엔티티: 절차상 사용되는 문서나 장부에 대한 엔티티
- 이력 엔티티: 반복적으로 이루어지는 행위나 사건을 시간별로 저장하기 위한 엔티티
- 코드 엔티티: 무형 엔티티의 일종, 각종 코드를 관리
- Primary Key
- 중복이 없고 NULL 값이 없는 유일한 값에 지정하는 식별자
- 주 식별자는 유일한 속성이다.
- NOT NULL : 해당 속성에 들어갈 값에 null을 비허용하면, N 을 표기한다.
- Foreign Key
- 외래식별자는 key의 일종으로 선을 이어주어 개체와의 관계를 표시한다.
엔티티들을 다 만들었으면 선을 이어 관계를 맺어준다.
두 개의 엔티티 관계에서 부모의 키를 자식에서 PK로 사용하는지 일반 속성으로 사용하는지에 따라 다르다.
- 실선: 강한 연결관계 표현 / 자식 주식별자의 구성에 포함 / 부모 엔티티에 종속
- 점선: 약한 연결관계 표현 / 자식 일반 속성에 포함 / 자식 주식별자 구성을 독립적으로 구성
- 식별관계(실선): 부모 자식 관계에서 부모의 주 식별자(PK)를 외래 식별자(FK)로 참조해서 자신의 주 식별자로 설정
- 비식별관계(점선): 부모 자식 관계에서 부모의 주 식별자를 외래 식별자로 참조해서 일반 속성으로 사용
- 일대일 관계
- 학생과 신체정보
- 일대다 관계
- 학생과 취미
- 회원과 오늘의 걸음 수 (2/6, 2/7의 걸음 수 등)
- 다대다 관계
- 제품과 제조업체
- 다대다 관계의 해소
- 두 개의 엔티티만으로 부족한 경우가 있다.
- 1:N , N:1로 조정하는 작업이 필요하다.
- 중간 엔티티가 두 엔티티의 공유 속성 역할을 한다.
- ERD관계의 참여도
- | (작대기) 표시가 있으면 필수적으로 있어야 하는 개체 --- 필수 관계
- O (동그라미) 표시가 있으면 없어도 되는 개체 --- 선택 관계
- 대응되는 객체가 있을 수도 있고 없을 수도 있을 때 선택 관계를 사용한다.
- 일과 다
- 하나만 존재하는 경우 작대기로 표현
- 다수일 경우 삼지창 모양으로 표현

물리적 데이터 모델링
지금까지 구성한 ERD를 바탕으로 SQL 코딩을 통해 직접 데이터베이스를 설계하는 일이다.
난 이번에 h2 DB를 사용하여 로컬에서 간단히 확인할 예정이다.
스프링 입문에서 경험했던 스프링 데이터 JPA를 사용해보고자 했다.
처음엔 SQL 을 잘 모르기 때문에 scheme 파일을 만들어서 직접 테이블을 생성해보았다.
-- 1. 회원 테이블 (모든 테이블의 부모)
CREATE TABLE MEMBER (
id VARCHAR(50) PRIMARY KEY, -- 회원 ID (문자열)
username VARCHAR(50) NOT NULL, -- 사용자 이름
nickname VARCHAR(50), -- 닉네임
user_point INT DEFAULT 0 -- 보유 포인트 (기본값 0)
);
-- 2. 걸음 수 정보 (자식: 회원과 1:N)
CREATE TABLE STEP_INFO (
step_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- 자동 증가 ID
user_id VARCHAR(50) NOT NULL, -- 누구의 걸음인가?
step_count INT NOT NULL, -- 걸음 수
record_date DATE NOT NULL, -- 날짜
FOREIGN KEY (user_id) REFERENCES MEMBER(id) -- 외래키 연결
);
-- 3. 포인트 이력 (자식: 회원과 1:N)
CREATE TABLE POINT_HISTORY (
history_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id VARCHAR(50) NOT NULL,
amount INT NOT NULL, -- 변동량 (+100, -3000)
description VARCHAR(255), -- 내용 (걷기보상, 쿠폰구매)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 발생 시간
FOREIGN KEY (user_id) REFERENCES MEMBER(id)
);
-- 4. 쿠폰 정보 (자식: 회원과 1:N)
CREATE TABLE COUPON_INFO (
coupon_id VARCHAR(50) PRIMARY KEY, -- 쿠폰 ID (문자열로 가정)
user_id VARCHAR(50) NOT NULL,
coupon_name VARCHAR(100) NOT NULL, -- 쿠폰 이름 (추가 권장)
valid_date DATE NOT NULL, -- 유효기간
is_used BOOLEAN DEFAULT FALSE, -- 사용 여부
FOREIGN KEY (user_id) REFERENCES MEMBER(id)
);
제미나이를 활용하여 테이블을 생성하는 쿼리를 만들어냈다.
하지만, JPA는 이런 테이블을 자동으로 만들어주고, 메서드 이름만으로 기능을 사용 가능하다!
데이터 모델을 자바 코드로 직관적으로 옮길 수 있다
package NetZero.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@NoArgsConstructor
@Getter
public class Member {
@Id
@Column(name = "member_id")
private String id;
private String username;
private String nickname;
private int userPoint;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<PointHistory> pointHistories = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<StepInfo> stepInfos = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<CouponInfo> couponInfos = new ArrayList<>();
public Member(String id, String username, String nickname, int userPoint){
this.id = id;
this.username = username;
this.nickname = nickname;
this.userPoint = 0;
}
}
domain 패키지를 만들어서 4가지의 엔티티 클래스를 만들었다. 위 코드는 엔티티들의 부모인 Member엔티티이다.
member 엔티티를 부모로 두고 일대다 로 연결하여 정보를 주고받고자 하였다.
새로 볼 수 있는 애노테이션들을 학습하고자
- @ NoArgsConstructor
- 스프링 기본 시간에 배웠던 롬복 라이브러리인데 Required와 다른 형태이다.
파라미터가 없는 기본 생성자를 자동으로 만들어준다. - JPA는 DB에서 데이터를 가져와서 객체를 만들 때, reflection을 이용하는데
일단 빈 객체를 만들고 값을 채워 넣는 방식이다. - 코드 상에 생성자를 만들어 둔 이유:
- @ NoArgsConstructor 는 JPA를 위해서, 직접 만든 생성자는 개발자를 위해 만든 것이다.
- 직접 만든 생성자가 없다면 set 메서드를 호출해야 하는 소요가 크다!
- member.setId("user1");
member.setUsername("홍길동");
member.setNickname("WalkingMan");
member.setUserPoint(0);
- member.setId("user1");
- 안전성
- 기본 생성자만 있다면 실수로 이름을 넣지 않아도 컴파일 에러가 나지 않는다.
- 필수 값을 받는 생성자를 직접 만들어두면 컴파일 에러를 통해 확인 가능하다!
-> 컴파일 에러는 가장 좋은 에러라고 배운 적이 있다!
- 테스트 코드 작성이 용이하다
- set 메서드로 도배될 일이 없다
- 스프링 기본 시간에 배웠던 롬복 라이브러리인데 Required와 다른 형태이다.
- @OneToMany
- 일대다 관계를 표현한다.
- mappedBy
- 양쪽에서 서로 참조하는 상황이 생길 수 있다. member.getCoupon(), coupon.getMember()
DB테이블의 외래 키는 오직 하나에만 존재한다.(Coupon쪽) - 외래키 FK를 가진 것은 coupon이기 때문에 연관관계의 주인으로 설정한다. 여기서
주인이 아닌 쪽은 "mappedBy" 를 써서 양보한다. 그리고 주인인 쪽은 "JoinColumn"을 활용한다. - mappedBy = "member" 는 Coupon클래스에 있는 member 필드에 의해 관리된다 라는 뜻이다!
따라서 mappedBy 가 붙은 쪽, 즉 주인이 아닌 쪽은 읽기 전용이 된다. -> 처음에 의도한대로 Member에 따른 걸음 수, 쿠폰, 쿠폰 이력 조회가 가능해진다!!
- 양쪽에서 서로 참조하는 상황이 생길 수 있다. member.getCoupon(), coupon.getMember()
- cascade (영속성 전이)
- 부모 엔티티에게 변화가 생겼을 때 그 변화를 자식 엔티티에도 전파시킨다.
- CascadeType.ALL: 모든 변화를 전파합니다. (저장, 삭제 등) => 아주 용이하지만 데이터가 싹 날아갈 수 있
- CascadeType.PERSIST: 부모를 저장(save)할 때 자식도 같이 저장합니다.
- CascadeType.REMOVE: 부모를 삭제(delete)하면 자식도 같이 삭제합니다.

h2 콘솔에서 확인했을 때 테이블이 정상적으로 생성되었음을 확인할 수 있다!
다음에는 지금 듣고 있는 HTTP 강의를 바탕으로 API를 설계하고 DTO,Converter를 구현해보는 시간을 가질 예정이다!
쫄지마!