🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [49]일차
🚀49일차에는 JPA를 통해 상속 관계를 매핑할 수 있는 "상속 관계 매핑"에 대해 실습해보았다.
학습 목표 : 자바에서의 상속관계를 JPA 상속 매핑 전략들로 구현할 수 있도록 함
학습 과정 : 회고를 통해 작성
상속 매핑 전략
- JPA에서의 상속매핑 전략은 "객체지향모델에서 상속구조를 어떻게 관계형 DB 스키마에 매핑할지"를 정의
- 3가지 주요 상속 매핑 전략
1. 단일 테이블 전략 (SINGLE_TABLE)
2. 조인 테이블 전략 (JOINED)
3. 테이블 당 구체 클래스 전략 (TABLE_PER_CLASS) - 공통적인 것들은 따로 빼놓고 상속을 통해서 이 공통적인 것들을 상속받아 사용하도록 하는 것이 상속 매핑 전략
- 자바에는 상속이 있지만 관계형 DB에는 상속이 존재하지 않는다.
따라서 자바에서 상속으로 이루어진 엔티티들이 있을때
관계형 DB에서는 이러한 상속 관계를 어떻게 매핑할 것인지에 대한 전략을 가능하게 해주는 것이 “상속 매핑 전략”
1. 단일 테이블 전략
- 상속 계층의 모든 클래스를 하나의 테이블에 매핑
- “모든 필드가 하나의 테이블에 포함”되므로 다형적 쿼리의 성능이 빠름
- 테이블에는 상속계층의 모든 속성에 대한 컬럼이 존재하므로
특정 하위 클래스의 속성이 비어있는 경우가 많아져 데이터의 중복이나 낭비가 발생할 수 있음 - ex. 이동수단 - 버스만 가지는 속성들 (컬럼), 택시만 가지는 속성들 (컬럼) 등
하나의 큰 컬럼(=이동수단)에서 각기 다른 속성을 가질 수 있는 컬럼들(=버스, 택시)을 분리해서 컬럼을 정의
// #. 상속매핑전략 3가지 중 : 1. 단일 테이블 전략
// 모든 속성을 가진 Vehicle (부모클래스)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
@Getter@Setter
public abstract class Vehicle {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // id
private String manufacturer; // 제조회사
}
- @DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
➡️하나의 테이블에서 다른 타입의 컬럼을 관리하기 위한 어노테이션
테이블을 하나 가져갈때, 버스인지, 택시인지를 구분해낼 수 있는 컬럼에 대한 정보를 주는 것
type을 통해 DiscriminatorType.STRING : 문자열로 구분 - @Inheritance(strategy = InheritanceType.*SINGLE_TABLE*)
➡️싱글테이블 전략을 사용하겠다는 정의 (=단일 테이블 전략 사용)
- 각기 다른 클래스들의 컬럼들이 Vehicle 하나의 테이블에 선언되어 만들어짐
- type이 varchar 처럼 구분되어 선언됨
Main 테스트
// Car
Car car = new Car();
car.setManufacturer("Samsung");
car.setSeatCount(5);
// Bus
Bus bus = new Bus();
bus.setManufacturer("고속버스");
bus.setBusColor("Green");
//... Taxi, Truck 등
em.persist(car);
em.persist(bus);
em.getTransaction().commit();
- 각 테이블별 속하는 컬럼들의 값이 잘 들어가있는 것을 확인 가능
데이터 조회
// 데이터 조회
List<Vehicle> vehicles = em.createQuery("SELECT v FROM Vehicle v", Vehicle.class).getResultList();
for (Vehicle vehicle : vehicles) {
if (vehicle instanceof Car) {
Car car = (Car) vehicle;
System.out.println("Car: " + car.getManufacturer() + ", Seats: " + car.getSeatCount());
} else if (vehicle instanceof Truck) {
Truck truck = (Truck) vehicle;
System.out.println("Truck: " + truck.getManufacturer() + ", Payload Capacity: " + truck.getPayloadCapacity());
}
}
- createQuery()
JPQL의 문법으로, 엔티티를 대상으로 쿼리를 만듦
따라서 테이블이름이 아닌 엔티티 이름을 사용함 - SELECT v FROM Vehicle v
alias를 사용하여 Vehicle 엔티티를 v로 명명하고, SELECT로 그 v를 불러서 조회하겠다는 것
JPQL 사용 문법을 의미, 이처럼 alias로 지정한 이름의 필드들을 가져올 수 있고,
TYPE(v) 처럼 해당 엔티티의 타입을 조회할수도 있다.
ex. em.createQuery(”SELECT v.id, v.manufacturer, TYPE(v) FROM Vehicle v”, Vehicle.class) - getResultList() : 조회된 결과들을 리스트로 꺼내옴
- if(vehicle instanceof Car) { … } : 얻어온 vehicle의 타입이 Car인지 검사하는 것
Vehicle vehicle = em.find(Vehicle.class, 3L);
if(vehicle instanceof Car){ … }
- 모체인 Vehicle 타입을 find()로 가져와서 if문으로부터 instanceof로
그 타입이 어떤 자식을 가리키는지를 검사할 수도 있음
2. 조인 테이블 전략
- 각 클래스를 별도의 테이블로 매핑하고, 상속관계에 있는 클래스 간에는 조인을 사용
- 데이터의 정규화가 잘되어있어 데이터 중복이 없는 전략이지만
객체 로드,저장 시 여러 조인이 필요해 성능이 저하될 가능성이 있음
// #. 상속매핑전략 3가지 중 : 2. 조인 테이블 전략
// 모든 속성을 가진 Sports (부모클래스)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Getter@Setter
public class Sports {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String country;
}
@Entity
@Getter@Setter
class Football extends Sports{
private String uniformColor; // 유니폼 색깔
}
@Entity
@Getter@Setter
class Basketball extends Sports{
private int playerNumber; // 농구의 인원 수
}
// Football
Football fb = new Football();
fb.setCountry("England");
fb.setUniformColor("BlackStripe");
// Basketball
Basketball bb = new Basketball();
bb.setCountry("United States of America");
em.persist(fb);
em.persist(bb);
em.getTransaction().commit();
- 각각의 테이블이 만들어지며 id라는 공통적인 컬럼으로써 조인을 할 수 있는 형태로 구성되어있다.
- @Inheritance(strategy = InheritanceType.JOINED)
로써 테이블이 조인으로 연결되어있는 것이다.
데이터 조회
// 데이터 조회
Sports sports = em.find(Sports.class, 1L);
if(sports instanceof Football){
Football football = (Football)sports;
log.info("ID : " + football.getId() + "::: 유니폼 색깔 : " + football.getUniformColor());
}else if(sports instanceof Basketball){
Basketball basketball = (Basketball)sports;
log.info("ID : " + basketball.getId() + "::: 인원 수 : " + basketball.getPlayerNumber());
}
em.getTransaction().commit();
- 출력 : [ID : 1::: 유니폼 색깔 : BlackStripe]
정리하자면
Sports 테이블의 id가 자동으로 생성되며
이 id가 하위 테이블(Football, Basketball)의 기본키이자 외래키 역할을 함
- 각 엔티티(부모, 자식)별로 별도 테이블을 생성
- 자식 테이블의 id는 부모 테이블의 id를 참조(FK)하게됨
- 장점 : 부모 테이블에서 모든 공통 속성을 관리함
- 단점 : 조인을 해야만 전체 데이터를 조회 가능 (조회 시 항상 JOIN이 필요)
Football과 Basketball 엔티티는 Sports 엔티티를 상속받았지만,
각각 독립적인 테이블을 가지며, id를 통해 Sports 테이블과 관계 매핑이 되고 있음
3. 테이블 당 구체 클래스 전략
- 각 구체 클래스를 자신의 테이블로 매핑하는데 데이터 중복이 발생할 수 있고,
다형적 쿼리 수행 시 모든 관련 테이블을 조회해야하므로 성능이 떨어질 수 있다.
// #. 상속매핑전략 3가지 중 : 3. 테이블 당 구체 클래스 전략
// 모든 속성을 가진 Sports (부모클래스)
@Entity
@Getter@Setter
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class SquidGame {
@Id@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
private int survivor; // 모든 게임의 공통된 생존자
}
@Entity
@Getter@Setter
class Dalgona extends SquidGame{
private String shape; // 달고나 게임의 달고나 모양
}
@Entity
@Getter@Setter
class Gonggi extends SquidGame{
private int passMinNumber; // 공기놀이 게임의 잡은 돌의 최소 통과 기준
}
❓GenerationType.IDENTITY 대신 GenerationType.TABLE을 사용하는 이유
⚠️GenerationType.IDENTITY 사용 불가의 이유
➡️ IDENTITY 전략은 기본적으로 하나의 시퀀스만 관리하려고 하기 때문에 오류 발생
✅GenerationType.TABLE 사용의 이유
➡️TABLE_PER_CLASS는 부모 테이블(SquidGame)이 따로 존재하지 않고, 자식 테이블들이 별도의 테이블로 생성
ex. Dalgona와 Gonggi는 각각 개별적인 테이블이므로, 서로 ID Sequence 객체를 공유하지 않음
별도의 테이블(hibernate_sequences)을 생성하여 ID 값을 관리
따라서, 여러 개의 테이블에서 같은 ID 값을 사용하지 않도록 분리할 수 있다는 장점이 있음
- @GeneratedValue(strategy = GenerationType.TABLE)
Hibernate는 자동 증가하는 ID 값을 직접 관리하기 위해 hibernate_sequences라는 별도의 테이블을 생성
➡️각 테이블의 ID 증가 값을 따로 저장
➡️next_val 컬럼을 통해 다음에 사용할 ID 값을 추적
❓TABLE_PER_CLASS 전략에서 find() 메서드가 동작하지 않는 이유
SquidGame squidgame = em.find(SquidGame.class, 1L);
- 엔티티가 아니기때문에 EntityManager의 find()로 엔티티를 받지 못한다
- TABLE_PER_CLASS 전략에서는 부모 클래스(SquidGame) 자체가 엔티티로 존재하지 않기 때문에
find()가 동작하지 않음
즉 TABLE_PER_CLASS 전략은 부모 테이블을 생성하지 않고, 각 자식 클래스가 독립적인 테이블로 존재
(=SquidGame 테이블이 따로 존재하지 않으므로 SquidGame.class를 기준으로 엔티티를 찾을 수 없음)
✅ find()를 사용할 때 자식 엔티티로 조회해야함
Dalgona dalgona = em.find(Dalgona.class, 1L);
- 각각 테이블에서 (id, survivor) 같은 공통 컬럼들은 포함된채 각각의 테이블이 컬럼이 가진 컬럼도 포함되어서 출력
- 부모 테이블을 생성하지 않아도 각 자식 클래스가 독립적인 테이블로 존재할 수 있게 만드는 상속 매핑 전략
데이터 조회
// 데이터 조회
SquidGame sg = em.find(SquidGame.class, 1L);
if(sg instanceof Dalgona){
Dalgona dalgona = (Dalgona)sg;
log.info("[달고나 게임] 생존자 : " + dalgona.getSurvivor() + ", 달고나 모양 : " + dalgona.getShape());
}else if(sg instanceof Gonggi){
Gonggi gonggi = (Gonggi)sg;
log.info("[공기 놀이] 생존자 : " + gonggi.getSurvivor() + ", 통과 개수 : " + gonggi.getPassMinNumber());
}
출력 : [ [달고나 게임] 생존자 : 300, 달고나 모양 : Umbrella ]
❓불필요한 SquidGame 클래스를 생성하지 않는 방법
@MappedSuperclass 어노테이션 사용으로 부모 클래스(SquidGame)에 대한 테이블이 생성되지 않도록 가능
@Entity는 부모테이블이 생성되고, 자식테이블에서 JOIN을 해야하는 반면,
@MappedSuperclass는 부모테이블이 생성되지 않고, 자식 테이블이 부모 필드를 직접가지게됨
(=즉 자식 테이블(Dalgona, Gonggi)이 부모 클래스(SquidGame)의 필드를 직접 포함할 수 있음)
➡️@MappedSuperclass 사용의 장점 : 불필요한 테이블 생성을 막고, 조회 성능도 향상됨
상황 별 상속 매핑 전략 선택
- 단일 테이블 전략 : 다형적 쿼리 성능 중요시에 사용
- 조인 테이블 전략 : 데이터 정규화와 무결성 중요시에 사용
- 테이블 당 구체 클래스 전략 : 데이터 중복 최소화와 간단한 쿼리 성능 우선시에 사용
임베디드 타입
- (Embedded Types, =복합값 타입)
- JPA에서 엔티티의 일부로 정의된 재사용 가능한 도메인 모델의 일부를 표현할때 사용
- 엔티티 내 여러 속성을 논리적으로 그룹화하고, 중복없이 여러 엔티티 간 공통 구조를 공유가능
- @Embeddable 어노테이션으로 클래스 정의 후
@Embedded 로 엔티티내에서 이 타입의 인스턴스를 포함시킴
@Embeddable
@Getter@Setter
public class Address {
private String street;
private String city;
private String state;
private String zipCode; // 우편번호
private String country;
}
- @Embeddable : 이 클래스의 필드들이 다른 클래스에 포함될 수 있다는 것을 의미
@Entity
@Table(name = "companies")
@Getter@Setter
class Company{
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address; // 회사는 주소를 가지고 있을 것
}
- @Embedded : @Embeddable 어노테이션을 사용하는 클래스를 필드로 사용하는 클래스에서 사용을 명시
- Company 엔티티의 필드인 id, name과 함께 Address에서 공통된 속성으로 정의했던 필드들도 함께테이블로 생성됨
Main 테스트
Address address1 = new Address();
address1.setCity("Fukuoka");
address1.setCountry("Japan");
address1.setStreet("Daimyo");
address1.setZipCode("11111");
Company company1 = new Company();
company1.setName("복호두");
company1.setAddress(address1);
em.persist(company1);
em.getTransaction().commit();
🚀실습 - @Embeddable과 @Embedded
- Customer, ContactInfo
@Entity
@Table(name = "customers")
@Getter@Setter
public class Customer {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private ContactInfo contactInfo;
}
@Embeddable
@Getter@Setter
class ContactInfo{
private String email;
private String phoneNumber;
private String address;
private String country;
}
// main
ContactInfo contactInfo1 = new ContactInfo();
contactInfo1.setEmail("dominic@premier.com");
contactInfo1.setPhoneNumber("413-1111");
contactInfo1.setAddress("London");
contactInfo1.setCountry("England");
Customer customer1 = new Customer();
customer1.setName("Solanke");
customer1.setContactInfo(contactInfo1);
em.persist(customer1);
em.getTransaction().commit();
- ContactInfo 에 고객에 대한 정보를 담아놓고 @Embedded로써 그 클래스의 필드들을 사용할 수 있도록 했다.
- 영속성 컨텍스트에 적용 후 결과를 확인해보면 Cusomter클래스의 id, name 컬럼 제외하고도
address, country, email, phoneNumber 등도 컬럼에 포함되어있는 것을 확인할 수 있음
// 데이터 조회
List<Customer> customers = em.createQuery("SELECT c FROM Customer c", Customer.class).getResultList();
for(Customer c : customers){
log.info("Customer : " + c.getName());
log.info("Email : " + c.getContactInfo().getEmail());
log.info("PhoneNumber : " + c.getContactInfo().getPhoneNumber());
log.info("Address : " + c.getContactInfo().getAddress());
log.info("Country : " + c.getContactInfo().getCountry());
}
🚀실습 - 단일 테이블 전략
- Devices (부모 클래스)와 Phone, Laptop (자식 클래스들)간 상속관계
@Entity
@Getter@Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
public abstract class Devices {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String brand;
private int price;
}
@Entity
@Getter@Setter
@DiscriminatorValue("PHONE")
class Phone extends Devices{
private String operatingSystem;
private int batteryLife;
}
@Entity
@Getter@Setter
@DiscriminatorValue("LAPTOP")
class Laptop extends Devices{
private int ramSize;
private boolean hasTouchScreen;
}
- 단일 테이블 전략이므로 devices라는 하나의 테이블에서 모든 컬럼이 관리되고 있다.
🚀실습 - 조인 테이블 전략
// 2. 조인 테이블 전략
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
@Table(name = "DEVICES_JOINED")
public abstract class Devices {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String brand;
private int price;
}
//@Table(name = "PHONE_SINGLE")
@Entity
@Getter@Setter
@Table(name = "PHONE_JOIN")
@DiscriminatorValue("PHONE")
class Phone extends Devices{
private String operatingSystem;
private int batteryLife;
}
//@Table(name = "LAPTOP_SINGLE")
@Entity
@Getter@Setter
@Table(name = "LAPTOP_JOIN")
@DiscriminatorValue("LAPTOP")
class Laptop extends Devices{
private int ramSize;
private String hasTouchScreen;
}
- 각각 devices_joined, phone_join, laptop_join 테이블들을 조회해야함
- 기본키값인 id를 제외하고는 부모클래스 Devices의 필드인 brand, price들은 각각 테이블에 포함되어있지 않음
🚀실습 - 테이블 당 구체 클래스 전략
// 3. 테이블 당 구체 클래스 전략
@Entity
@Getter@Setter
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Table(name = "DEVICES_JOINED")
public abstract class Devices {
@Id@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
private String brand;
private int price;
}
//@Table(name = "PHONE_SINGLE")
//@Table(name = "PHONE_JOIN")
@Entity
@Getter@Setter
@Table(name = "PHONE_TABLE")
class Phone extends Devices{
private String operatingSystem;
private int batteryLife;
}
//@Table(name = "LAPTOP_SINGLE")
//@Table(name = "LAPTOP_JOIN")
@Entity
@Getter@Setter
@Table(name = "LAPTOP_TABLE")
class Laptop extends Devices{
private int ramSize;
private String hasTouchScreen;
}
- 부모클래스의 테이블은 필요하지 않고 자식클래스의 테이블들에서 모든 컬럼들의 정보를 확인할 수 있음
🚀회고 결과 :
이번 회고에서는 JPA에서의 상속 관계 매핑을 추가 실습해보았다.
다양한 데이터에서의 실습을 통해 각각 전략마다 어떤 결과값(조회값)을 보여주는지를 확인할 수 있었다.
또한 @Embeddable, @Embedded 개념을 통해 자바의 공통된 필드들을 사용할 수 있는 개념을 연습해볼 수 있었다.
- @Embeddable과 @Embedded의 위치 유의 (@Embeddable : 사용가능하다는 선언, @Embedded : 사용할 곳에서 선언)
- 테이블 당 구체 테이블 전략에 대한 추가 이해 필요
느낀 점 :
관계형 DB 모델에서 상속 관계를 표현해낼 수 있는 방법이 있다는 것을 생각해보지 못했는데
자바에서의 상속관계를 유사하게 표현하기 위하여 다양한 전략을 활용하여 표현해내는 것이 인상적이었다.
향후 계획 :
- 단순 JPA 사용이 아닌 Spring과의 JPA 사용을 알아보기
- 상속관계의 다양한 테이블 실습
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_51일차_"Criteria + hr DB" (0) | 2025.02.20 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_50일차_"Spring Data JPA" (0) | 2025.02.19 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_48일차_"JPA 관계형 테이블" (1) | 2025.02.14 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_47일차_"JPA 엔티티 매핑" (0) | 2025.02.13 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_46일차_"JPA" (0) | 2025.02.12 |