'프로그래밍/JPA' 카테고리의 글 목록 :: 잡다한 프로그래밍
반응형

JPA의 @MappedSuperclass 정리

@MappedSuperclass는 공통 매핑 정보만 제공하는 용도의 클래스에 붙이는 JPA 애노테이션입니다.
실제 엔티티나 테이블로 매핑되지 않으며, 자식 엔티티가 이 클래스의 필드를 그대로 자신의 필드처럼 포함하여 테이블 컬럼으로 매핑되도록 합니다.

 

언제 사용하는가?

  • 여러 엔티티에 공통된 필드(ex. 생성일, 수정일, 삭제일 등)가 반복될 때
  • 상속 관계를 표현하려는 것이 아니라, 중복 제거 및 일관성 유지가 목적일 때
  • JPA의 상속 전략(@Inheritance) 을 사용하지 않고 테이블은 각각 따로 유지하고 싶을 때

@MappedSuperclass
pulbic abstract class BaseEntity {
	private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private LocalDateTime deletedAt;
}
@Entity
public class Member extends BaseEntity {
	private String name;
}

 

 

  • BaseEntity는 엔티티도 아니고 테이블도 생성되지 않습니다.
  • 대신 Member, Seller 등의 하위 클래스 테이블에 created_at, updated_at, deleted_at 컬럼이 자동으로 포함됩니다.
  • 이 방식은 ORM의 매핑 정보를 재사용하는 것이지, DB 상속 전략을 구현하는 것은 아님.
  • 따라서 직접 조회, 검색 불가
  • 직접 테이블 생성할 일 없으므로 추상클래스 권장
반응형
반응형

JPA의 상속 매핑 전략

JPA에서는 객체지향 언어에서 제공하는 상속(Inheritance) 개념을 관계형 데이터베이스 위에서도 그대로 표현할 수 있도록 상속 매핑 전략을 제공합니다.

 

하지만 DB는 상속 개념이 없기 때문에, 이를 다음의 세 가지 전략으로 풀어냅니다.

 

1. JOINED 전략 (@Inheritance(strategy = JOINED))

가장 정규화된 방식

 

  • 공통 속성은 Item 테이블에 저장하고
  • 개별 속성은 Album, Book, Movie 테이블에 저장
  • 각 자식 테이블은 ITEM_ID를 외래키로 가지며
  • 조회 시 JOIN으로 연결
  • ITEM 입장에서 어떤 타입의 데이터인지 알 수있게 DTYPE으로 구분할 수 있음 (넣는것을 권장)

 

@Entity
@Ingeritance(strategy = IngeritanceType.JOINED) // 해당 값이 없으면 기본 한테이블에 다 넣는 2번방식
@DiscriminatorColumn // 해당 값이 있으면 DTYPE이 생김
public abstract class Item { // 추상클래스로 안하면 > ITEM을 독단적으로 쓰는 경우가 있다고보고 생성하는것 추상클래스로 만들자
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    private int price;
}
@Entity
// @DiscriminatorValue("A") 값으로 DTYPE값을 변경 가능
public class Album extends Item {
	private String artist;
}
@Entity
pulbic class Moview extends Item {
	private String director;
    private String actor;
}

장점

  • 테이블이 정규화되어 있어 무결성 보장
  • NOT NULL, FK, 유니크 제약조건 사용 가능
  • 저장공간 효율화

단점

  • JOIN이 많아져 조회 성능 저하 가능 (조회 쿼리가 복잡)
  • INSERT 시 부모 테이블과 자식 테이블에 2번 INSERT

 

2. 단일 테이블 전략 (@Inheritance(strategy = SINGLE_TABLE))

모든 데이터를 한 테이블에 넣는 방식 (주로 프로젝트가 단순하거나, 복잡한 테이블 구조가 필요없을 때 사용)

 

  • DTYPE으로 구분
  • 공통 컬럼 + 각 타입별 컬럼 모두 포함됨
  • 관련 없는 데이터는 NULL 처리됨

 

@Entity
@Ingeritance(strategy = IngeritanceType.SINGLE_TABLE) //혹은 생략
@DiscriminatorColumn // 해당 값이 없어도 무조건 생김 = DTYPE이 없으면 누가 누군지 몰라서 필수임
public abstract class Item {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    private int price;
}

장점

  • 조인이 없기 때문에 조회 성능 우수
  • 쿼리 단순함

단점

  • null 칼럼 다수 발생
  • 테이블이 점점 넓어짐 (column 수 증가) → 인덱싱 비효율
  • 구조상 제약 조건 사용 제한

3. 구현 클래스마다 테이블 전략 (@Inheritance(strategy = TABLE_PER_CLASS))

자식 클래스마다 완전한 테이블을 따로 생성 (비추천)

@Entity
@Ingeritance(strategy = IngeritanceType.TABLE_PER_CLASS)
public abstract class Item {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    private int price;
}

※ 추상클래스를 사용하지 않으면, ITEM 테이블이 단독으로 생길수 있으니 주의!

 

이 전략은 절대 실무에서 쓰지 말자

  • 부모 타입으로 조회하면 UNION ALL 쿼리 발생
    → SELECT * FROM album UNION ALL SELECT * FROM movie ...
Item item = em.find(Item.class, move.getId());
System.out.println("Item = " + item);
  • DTYPE 사용하지 않음

장점

  • 서브타입 명확히 분리
  • 각 테이블 독립적 → NOT NULL, 유니크 키 사용 가능

단점

  • 통합 조회 어렵고 성능 저하
  • 변경 시 확장성, 유지보수 최악 (테이블 변경시 비즈니스 로직 변경되어야함)
반응형
반응형

1. 다대일 단방향 (@ManyToOne)

 

실무에서 가장 많이 사용하는 구조
외래키가 N(다)쪽 테이블에 존재
DB와 객체 매핑이 가장 자연스럽고, 성능/유지보수/조회 모두 장점

코드로는?

@Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;

     @ManyToOne
     @JoinColumn(name = "TEAM_ID")
     private Team team;
 }

정리

 

DB입장에서 다에 외래키가 가야함, @ManyToOne 맴버 입장에서 팀찾기 위해 Team을 매핑함

그니까 즉외래키가 있는곳에 (ManyToOne) 매핑하면됨

  • DB 기준: N쪽에 외래키가 가야함 (member 테이블에 team_id 외래키)
  • @ManyToOne 어노테이션 사용

 

2. 다대일 양방향 (@ManyToOne + @OneToMany)

Member 뿐 아니라, Team에서 member 조회 필요할 때
실제 연관관계의 주인외래키가 있는 곳(N쪽)
즉, @ManyToOne이 항상 주인, @OneToMany(mappedBy = "team_id")는 읽기용

코드 예시

 @Entity
 public class Member {
 	@Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     //@Column(name = "TEAM_ID")
     //private Long teamId;
     
     @ManyToOne
     @JoinColumn(name = "TEAM_ID")
     private Team team;
 }
@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // 읽기전용, 주인 아님
    private List<Member> members = new ArrayList<>();
}

 

3. 일대다 단방향 (@OneToMany Only)

1쪽(Team)이 연관관계의 주인 (멤버입장에서 팀을 알필요가 없을 때)
1쪽(Team)에 외래키 관리 책임이 생김 → 비효율
JPA 표준 스펙은 지원하지만, 실무에서는 추천 안함

 

하지만 DB 설계 자체는 Member쪽에 외래키가 들어가야함

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();    
}
 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
 }
Member member = new Member();
member.setUsername("member1");

em.persist(member); // insert member

Team team = new Team(); // insert team
team.setName("teamA");

team.getMembers().add(member); // update Member set TEAM_ID = ? WHERE MEMBER_ID=?

 

  • 동작:
    • team.getMembers().add(member) 시
    • 실제로는 update 쿼리 1번 더 나감
      (update Member set TEAM_ID = ? where MEMBER_ID = ?)
  • 비추천 이유:
    • 실질적으로 외래키는 항상 N쪽(Member)에 있으므로 update쿼리가 한번 더 나가서 성능상 손해
    • 1쪽(Team)이 외래키 관리 = 성능 손해, 객체지향적으로도 어색 (team을 수정했는데 > member테이블을 수정하는 쿼리 요청)
  • 조인컬럼을 꼭 써야함, 그렇지 않으면 조인 테이블 방식을 사용해버림
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    private String name;

    // 조인컬럼 명시 안 함!
    @OneToMany
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String username;
}

 

create table team (
    id bigint not null,
    name varchar(255),
    primary key (id)
);

create table member (
    id bigint not null,
    username varchar(255),
    primary key (id)
);

-- <== JPA가 만든 중간 테이블!
create table team_members (
    team_id bigint not null,
    members_id bigint not null,
    primary key (team_id, members_id)
);

4. 일대다 양방향 (거의 안 씀, 읽기전용 트릭)

사실상 쓸 일 없음. 읽기전용 컬렉션이나, 조인 쿼리/화면용
insertable=false, updatable=false 옵션 활용

 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     @ManyToOne
     @JoinColumn(name = "TEAM_ID", insertable = false, updateable = false)
     private Team team;
 }

이런 식:

  • insertable, updateable 옵션으로 읽기전용으로 사용 가능 (필수)
    • 읽기 전용으로 사용하지 않을경우 양방향으로 수정이 가능해서 운영상 큰 문제가 생길 수 있음
    • 하지만 그냥 이구조는 사용하지 말자
  • 사실상 데이터 일관성 보장 어렵고, 양방향 필요하면 다대일 양방향 구조 사용 권장

 

5. 일대일 매핑

DB입장에서 일대일 매핑의 경우 외래키는 양쪽 모두 가능하다.

즉 일대일 관계의 반대도 일대일 매핑이다. (외래키에는 UNI 제약조건이 필요하다)

 

외래키를 주 테이블(Member)에 둘 경우

 

 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     @ManyToOne
     @JoinColumn(name = "TEAM_ID", insertable = false, updateable = false)
     private Team team;
     
     @OneToOne
     @JoinColumn(name = "LOCKER_ID")
     private Locker locker;
 }
@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    //@OneToOne(mappedBy = "locker")  양방향 매핑이 필요한 경우 mappedBy로 읽기전용 매핑
    //private Member member;
}

 

정리:

  • 다대일 양방향 매핑처럼 외래키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy 사용

 

외래키를 대상 테이블(Locker)에 둘 경우

JPA에서 Member에 Locker를 두는 경우는 지원하지 않는다.

즉, 단방향 연관관계에서는 항상 주 테이블이 외래 키를 가지고 있어야 합니다.

 

 

하지만 이런 양방향 관계는?

가능하다 Locker의 멤버를 외래키의 주인으로 둔다 (위쪽을 반대로 바꾼상황)

 

그럼 외래 키를 어디에 두는 게 좋을까?

DBA 관점

  • 정책이 바뀔 가능성 고려 (ex. 나중에 Member가 여러 개의 Locker를 가질 수도 있음)
    • 이 경우 외래 키를 **대상 테이블(Locker)**에 두고, UNIQUE 제약만 제거하면 1:N 구조로 쉽게 확장 가능
    • 반면 Member 쪽에 FK가 있으면 컬럼 삭제 및 스키마 변경 필요 → 유지보수 부담 증가
  • 데이터 모델의 유연성 확보에 유리

📌 선호: 외래 키를 **대상 테이블(Locker)**에 둔다.

ORM/JPA 개발자 관점

  • 대부분의 비즈니스 로직은 **Member(주 테이블)**를 중심으로 조회됨
  • Member에서 Locker 존재 유무에 따라 로직 분기가 많음
    쿼리 하나로 Locker 존재 여부 확인 가능
  • JPA 매핑이 쉬움
  • 성능상 이점 (불필요한 조인 생략 가능)
  • 📌 권장: 외래 키를 **주 테이블(Member)**에 둔다. (하지만 업무환경에 맞게...)

 

정리

 

주 테이블(주로 많이 접근하는 테이블)의 외래키

  • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래키를 두고 대상 테이블을 찾음
  • JPA 매핑 편리
  • 주 테이블만 조회해도 대상 테이블의 데이터가 있는지 확인 가능 (member만 조회해도 locker의 유무를 알 수 있음)

만약 Locker값이 없으면 외래 키에 null을 허용해야한다는 단점이 있음

 

 

대상 테이블에 외래 키

  • 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지가능

프록시 기능의 한계로 지연로딩으로 설정해도 항상 즉시 로딩된다.

더보기

 

 

6. 다대다 매핑 (N:M) (실무에서 쓰면 안됨)

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음

연결 테이블을 추가해서 일대다, 다대일 관계로 작성해야함

 

 

하지만 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능 (즉 객체관계를 아래 테이블 구조로 만들어준다)

 

예제 코드

 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     @ManyToOne
     @JoinColumn(name = "TEAM_ID", insertable = false, updateable = false)
     private Team team;
     
     @OneToOne
     @JoinColumn(name = "LOCKER_ID")
     private Locker locker;
     
     // 해당 부분
     @ManyToMany
     @JoinTable(name = "MEMBER_PRODUCT")
     private List<Product> products = new ArrayList<>();
 }
@Entity
public class Product {
	@Id @GeneratedValue
    private Long id;
    private String name;
    
    // 양방향으로 쓸경우 해당 부분 작성
    @ManyToMany(mappedBy = "roducts")
    private List<Member> members = new ArrayList<>();
}

실무에서 사용하면 안 되는 이유

굉장히 편해보이는데?

실제 운영 환경에서는 연결테이블이 단순하게 연결만 하고 끝나지 않고, 추가정보들을 필요로 한다.

ex) ORDERAMUNT, ORDERDATE...등

 

1. ManyToMany의 경우 중간 테이블에 추가 정보를 넣을 수 없다

2. 쿼리 구조가 비직관적

JPA가 생성하는 SQL이 다음과 같은 형태로 나가게 됩니다

Member 자체를 조회후 아래와 같은 2번의 쿼리를 실행한다.

 
select * from member_product where member_id = ?
select * from product where id in (?, ?, ?, ...)

 

  • join 없이 2번의 쿼리 실행
  • 추가 필터 조건이나 정렬이 어렵고,
  • 중간 테이블을 조작할 수 없음
왜 JOIN이 아닌가?

JPA에서 @ManyToMany는 **중간 테이블(member_product)**을 직접 엔티티로 취급하지 않고, 그냥 연결 매핑용 메타 정보만 활용하기 때문에 join하지 않고 2단계 접근합니다.

JPA는 연관 관계를 객체 기준으로 관리하기 때문에 단방향/양방향 여부와 Lazy/Eager 여부에 따라 내부 쿼리가 달라지지만, 대부분 @ManyToMany + LAZY 조합은 중간 테이블 → 대상 테이블 IN 절 조회 구조로 됩니다

 

 

 

실무에서의 해결 방법: 중간 엔티티로 승격

중간 테이블을 엔티티(MemberProduct)로 만들어 명시적으로 다뤄야 합니다.

 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;

     // 해당 부분
     @OneToMay(mappedBy="member")
     private List<MemberProduct> memberProducts = new ArrayList<>();
 }
@Entuty
public class MemberProduct {
	@Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name="PRODUCT_ID")
    private Product product;
@Entity
public class Product {
	@Id @GeneratedValue
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProduct = new ArrayList<>();
}

장점

  • orderDate, orderAmount 등 추가 컬럼 관리 가능
  • 비즈니스 로직에서 MemberProduct 자체를 조작 가능
  • 복잡한 조회 조건, 정렬 등도 자유롭게 작성 가능
  • 쿼리 최적화 가능 (JOIN, INDEX, FETCH 등)
반응형
반응형

양방향 연관관계 정리

 

양방향 매핑시 가장 많이 하는 실수 (연관관계의 주인을 입력하지 않음)

1. 연관관계의 주인 (owner)에 데이터를 변경해야함

 

  • @ManyToOne 또는 @JoinColumn이 주인
  • @OneToMany(mappedBy = "xxx")는 주인이 아님

[잘못된 예시]

// ManyToOne
Member member = new Member();
member.setUsername("member1");
em.persist(member);

// OneToMany
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(memeber);
em.persist(team);

결과 = member의 team_id컬럼이 null

 

 

2. 양방향 매핑 시에는 양쪽 모두 값을 세팅하자

  • 객체의 일관성을 위해 필수
  • Hibernate 1차 캐시에 의한 착각을 방지

flush, clear를 하지 않고 그대로 쓰면 1차캐시에서 가져오기 때문에 null일 수 있음

// OneToMany
Team team = new Team();
team.setName("TeamA");
em.persist(team);

// ManyToOne
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
// team.getMembers().add(member);

Team findTeam = em.find(Team.class, team.getId()); // 1차캐시에서 조회함
findTeam.getMembers() = 없음

 

 

팁: 별도의 메소드 또는 정적 팩토리 메소드로 분리하자

@Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     @Column(name = "TEAM_ID")
     private Long teamId;

	// 분리
	public void changeTeam(Team team) {
    	this.team = team;
        team.getMembers().add(this);
    }
 }

 

3. 무한 루프를 조심하자

3-1. toString() 무한 루프 주의 (롬복 toString 사용을 하지말자)

 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     @Column(name = "TEAM_ID")
     private Long teamId;
	
    @public String toString() {
    ...
	}
 }
 
 @Entity
 public class Team {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     private String name;
     
         @public String toString() {
    ...
	}
 }

3-2. JSON 생성 라이브러리

엔티티를 JSON 변환시 ToString 처럼 무한 루프에 빠질수 있다.

 

따라서 DTO로 변환 하여 사용한다

 

정리 마무리

  • 단방향 매핑으로도 이미 연관관계 매핑은 완료 
  • 양방향은 편의 기능이며, 주인은 꼭 @ManyToOne
  • 객체와 DB의 일관성을 위해 양쪽 다 set해야 함
  • 무한 루프 방지: DTO, @ToString.Exclude, @JsonIgnore 등 사용
  • 초기엔 단방향 설계 → 필요할 때만 양방향 추가
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 상속관계 매핑  (0) 2025.07.03
[JPA] 다양한 연관관계 매핑  (0) 2025.06.30
[JPA] 양방향 연관관계와 연관관계의 주인[1]  (0) 2025.06.24
[JPA] 단방향 연관관계  (0) 2025.06.24
[JPA] 기본키 매핑  (0) 2025.06.19
반응형

이번엔 양방향 연관관계에 대해 정리해본다.

 

단방향 연관관계와 양방향 연관관계의 차이점

Team team = new Team();
team.setName("A");
em.persist(team);

Member m = new Member();
member.setUsername("member1");
member.setTeam(team);

Member m = em.find(Member.class, member.getId());
Team t = m.getTeam(); // 팀 찾을 수 있음

지난 번 단방향 연관관계에서는 member에서 team을 객체지향적으로 찾을 수 있었다.

 

하지만 team에서 반대로 member를 찾을 수 없다, 양방향 매핑이 안되어 있기 때문임!

 

 

양방향 연관관계란?

 

JPA에서 객체와 테이블의 연관관계는 다르게 동작한다.

  • DB 테이블은 외래키(예: MEMBER.TEAM_ID) 하나로 양방향 조인 가능
  • 객체는 서로 참조해야만 양방향이 된다 → 단방향 두 개의 조합

즉, JPA에서 양방향 연관관계란 서로를 참조하는 단방향 관계 2개를 만드는 것과 같다.

 

member 엔티티

 @Entity
 public class Member {
 	@Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     //@Column(name = "TEAM_ID")
     //private Long teamId;
     
     @ManyToOne
     @JoinColumn(name = "TEAM_ID")
     private Team team;
 }

 

team 엔티티

 

@Entity
 public class Team {
     @Id @GeneratedValue
     private Long id;
     private String name;
     
     @OneToMany(mappedBy = "team")
     List<Member> members = new ArrayList<Member>();
 }

 

이렇게 명시적으로 양방향 관계를 맺을경우, team에서도 member를 get할 수 있다.

 

JPA 양방향 연관관계에서 연관관계의 주인이란?

앞서 말했듯 객체와 테이블의 관계에는 차이가 있다.

 

  • 객체 세계에서는 연관관계를 2개의 단방향으로 표현한다.
  • 반면 DB 세계에서는 외래키 하나로 양방향 조인이 가능하다.

 

연관관계 주인이 왜 필요할까?

이제 이런 상황을 생각해보자.

나는 A라는 멤버의 팀정보를 바꾸고 싶을때, member에 있는 Team 객체를 수정해야할까? 아니면 팀에 있는 member 객체를 수정해야할까?

 

이런 애매한 상황이 생길 수 있으므로 명확한 연관관계의 주인이 필요하다

연관관계의 주인이란?

🔑 정의

DB의 외래키를 관리하는 객체

즉, 외래키를 직접 가지고 있는 쪽, 대부분 @ManyToOne이다.

만약 oneToMany를 주인으로 사용할경우 Team에 있는 member를 변경했는데 쿼리는 member를 수정하는 쿼리가 나가게된다.

즉 Team 객체를 수정했는데 쿼리는 member를 수정(?) 복잡해지고 객체 지향적이지 못함 (성능이슈도 있음)

규칙 정리

항목 설명
연관관계의 주인 외래키가 있는 객체 (대개 @ManyToOne)
외래키 관리 여부 연관 관계의 주인만이 외래키를 관리 (등록 수정 = INSERT/UPDATE 쿼리 발생)
mappedBy 주인이 아닌 쪽에서 사용, 주인이 아닌경우 읽기만 가능
연관관계 설정 방식 주인만 세팅하면 DB에 반영됨

mappedBy의 의미

@OneToMany(mappedBy = "team")
private List<Member> members;

mappedBy = "team" 은 다음과 같은 뜻이다:

  • "나는 주인이 아니다"
  • "연관관계 관리는 Member.team이 한다"
  • "나는 읽기 전용이며 insert/update 대상이 아니다"
 

 

반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계 매핑  (0) 2025.06.30
[JPA] 양방향 연관관계와 연관관계의 주인[2]  (0) 2025.06.25
[JPA] 단방향 연관관계  (0) 2025.06.24
[JPA] 기본키 매핑  (0) 2025.06.19
[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
반응형

객체 지향적 설계의 시작인 단방향 연관관계에 대해서 학습해보자

배경: Member와 Team의 관계

회원(Member)은 하나의 팀(Team)에만 소속될 수 있습니다.

즉, 다대일(N:1) 관계이며, DB에서는 다음과 같은 테이블 구조를 가집니다.

관계형 DB에서는 TEAM_ID 외래 키를 통해 팀과 연결됩니다. 그런데 이 구조를 JPA로 그대로 옮기면 아래처럼 됩니다.

 

비객체지향적 JPA 매핑

Member 엔티티

 @Entity
 public class Member {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     @Column(name = "TEAM_ID")
     private Long teamId;

 }

 

TEAM 엔티티

@Entity
 public class Team {
     @Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     private String name;
 }

 

문제점

Team team = new Team();
team.setName("A");
em.persist(team);

Member m = new Member();
member.setUsername("member1");
member.setTeamId(team.getId()); // 객체 지향적이지 못함 setTeam이여야 객체 지향적이지 않을까?

 

 

실제 저장 결과

MEMBER_ID TEAM_ID USERNAME
1 1 member1

 

TEAM_ID NAME
1 A
  • 저장은 되지만, setTeamId()는 객체지향스럽지 않습니다.
  • Member는 Team 객체와 연관이 없음
  • 조회 시도도 번거로움:
Member m = em.find(Member.class, memberId);
Team team = em.find(Team.class, m.getTeamId()); // 직접 Team ID로 또 조회해야 함
객체지향 세계에서는 "객체가 객체를 참조"해야 자연스럽습니다.
반면, 위 방식은 단지 외래 키 숫자만 저장하고 있어 테이블 중심 사고에 머물러 있습니다.

 

즉 객체를 테이블에 맞추어 데이터 중심으로 모델링하면 협력 관계(연관 관계)를 만들 수 없음

해결: 단방향 연관관계 매핑

Member 객체가 Team 객체를 직접 참조하게 만듭니다. 즉, 연관 관계를 객체 간 참조로 표현합니다.

 

- N이되는 쪽에 @ManyToOne 어노테이션을 사용한다

- DB JOIN 컬럼에 @JoinColumn 어노테이션을 사용한다.

 @Entity
 public class Member {
 	@Id @GeneratedValue
     @Column("MEMBER_ID")
     private Long id;
     
     @Column(name = "USERNAME")
     private String name;
     
     //@Column(name = "TEAM_ID")
     //private Long teamId;
     
     @ManyToOne
     @JoinColumn(name = "TEAM_ID")
     private Team team;
 }

 

수정된 코드

Team team = new Team();
team.setName("A");
em.persist(team);

Member m = new Member();
member.setUsername("member1");
member.setTeam(team);

조회할 때도 깔끔하게 객체를 통해 참조 가능합니다:

 

정리: 왜 단방향 연관관계가 중요한가?

설계 패턴 테이블 중심 객체지향 중심
코드 복잡도 조회 시 매번 ID → DB 재조회 필요 객체 내부 필드에서 바로 접근 가능
유지보수 ID와 객체 동기화 이슈 발생 가능 일관성 있는 모델링
장점 단순, 초기 진입 쉬움 JPA와 객체지향 철학에 부합
반응형
반응형

이번 시간에는 JPA에서 엔티티의 기본 키(@Id) 를 어떻게 매핑하고 생성 전략을 어떻게 선택할지에 대해 알아보자.

기본 키 매핑이란?

JPA에서 @Entity 클래스는 반드시 식별자(PK) 를 가져야 하며, 이 식별자를 통해 엔티티를 구분하고 영속성 컨텍스트에서 관리한다.

기본적으로 다음과 같은 방식으로 식별자를 지정할 수 있다.

@Entity
public class Member {
    @Id
    private Long id; // 직접 지정 (직접 할당 방식) 거의 사용하지 않음
}

 

기본 키 자동 생성 전략 (@GeneratedValue)

IDENTITY DB에 위임 (MySQL 등)
SEQUENCE DB 시퀀스 오브젝트 사용 (Oracle, Postgres 등), @SequenceGenerator 필요
TABLE 키 생성 전용 테이블 사용 (모든 DB 지원, but 느림), @TableGenerator 필요
AUTO 방언(Dialect)에 따라 자동 선택 (기본값)
 

IDENTITY 전략 (DB 너가 알아서 해줘!)

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

 

  • DB에 ID 생성 위임 (AUTO_INCREMENT)
  • 대표 DB: MySQL, MariaDB, SQL Server

특징:

  • em.persist() 즉시 INSERT SQL 실행됨 → PK값을 DB에서 받아오기 때문
    • DB에  ID값 생성을 위임 하기 때문에 최초에 ID값이 없어 영속성 컨텍스트에 먼저 저장할 수 없음
    • 따라서 INSERT 쿼리를 먼저 날린후 해당 ID값 으로 영속성 컨텍스트에 저장
    • 따라서 batch insert 지원하지 않음 (하지만 같은 트랜잭션 내에서 성능차이가 그렇게 크진않음..)

 

SEQUENCE 전략

@Entity
@SequenceGenerator(
 name = “MEMBER_SEQ_GENERATOR",
 sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
 initialValue = 1, allocationSize = 1)
public class Member {
 @Id
 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
 private Long id;

 

  • DB의 시퀀스 오브젝트 사용 → 성능이 우수함
  • 시퀀스 제너레이터를 통해서 시퀀스를 만들어낼 수 있고, 테이블별로 만드는걸 권장함
  • 대표 DB: Oracle, PostgreSQL, DB2, H2

동작 방식:

  1. 먼저 select nextval('member_seq') 로 시퀀스 값을 조회
  2. 해당 값으로 INSERT 실행
  3. 같은 트랜잭션에서 insert 여러 번 하더라도 미리 받은 값에서 순차 사용 가능
  • allocationSize=50이 기본 → 메모리 미리 할당
  • 최초에 조회 후 같은 트랙잭션 내에서 insert 여러번 실행시 50개의 PK를 가지고 있으므로 bulk insert 가능

@SequenceGenerator 주요 속성 정리

속성명 설명 기본값 필수여부
name 식별자 생성기 이름 (@GeneratedValue(generator = "...")와 연결) 없음 O
sequenceName 매핑할 DB 시퀀스 객체 이름 hibernate_sequence X
initialValue 시퀀스 시작 값 (DDL 생성 시에만 사용) 1 X
allocationSize 한 번에 미리 증가시킬 수량 (성능 최적화용, JPA는 메모리 캐싱함) 50 X
  → DB 시퀀스가 1씩 증가하도록 설정되어 있다면 반드시 1로 맞춰야 함    
catalog 시퀀스가 속한 catalog 이름 DB 기본값 X
schema 시퀀스가 속한 schema 이름 DB 기본값 X

 

TABLE 전략

  • 키 생성용 테이블을 직접 만들어서 시퀀스처럼 사용하는 방식
  • 장점: 모든 DB에서 사용 가능
  • 단점: 성능이 매우 느림 → 실무에선 거의 사용 X

이런 방식은 레거시 DB 호환성 때문에 어쩔 수 없을 때만 고려

 

AUTO 전략 (기본값)

  • 방언(Dialect)에 따라 적절한 전략 자동 선택

예:

  • MySQL → IDENTITY
  • Oracle → SEQUENCE
  • H2 → SEQUENCE
 

권장하는 식별자 전략

 

  • PK는 절대 변하지 않는 대리키(대체키)를 Long 타입으로 사용
  • 자연키(주민번호, 이메일 등)는 PK로 부적합 (변경 가능성, 유출 위험 등)
  • 전략 선택:
    • Oracle, PostgreSQL 등 → SEQUENCE + allocationSize 1
    • MySQL → IDENTITY
    • 운영 DB 성능 최적화 필요 → 직접 관리 (e.g., UUID 전략, UUID+time 등)

 

반응형
반응형

지난 시간에는 엔티티와 테이블 매핑을 배웠다면,
이번에는 엔티티의 필드와 데이터베이스 컬럼을 어떻게 매핑하는지에 대해 정리합니다.

 

실습 요구사항

아래 조건에 맞는 회원(Member) 엔티티를 만들어 봅니다.

  1. 회원은 일반 회원과 관리자 두 가지 역할이 있다.
  2. 회원 가입일과 수정일을 저장해야 한다.
  3. 회원을 설명할 수 있는 필드가 있으며, 길이 제한은 없다.

회원 Entity

 
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
public class Member {
	@Id
	private Long id;
 
	@Column(name = "name") // 필드는 username 이지만, 컬럼명은 name일 때
	private String username;
 
 	private Integer age;
 
	@Enumerated(EnumType.STRING)
	private RoleType roleType;
 
 	@Temporal(TemporalType.TIMESTAMP)
 	private Date createdDate;
 
 	@Temporal(TemporalType.TIMESTAMP)
 	private Date lastModifiedDate;
 	
	@Lob
 	private String description;
}

 

생성되는 테이블 예시

create table Member {
	id bigint not null,
    age integer,
    createdDate timestamp,
    description clob,
    lastModifiedDate timestamp,
    roleType varchar(255),
    name varchar(255)
    primary key (id)
}

 

매핑 어노테이션 정리

1. @Column

  • 기본적으로 필드명 = 컬럼명 이지만, @Column(name="...")으로 커스텀 가능
  • 주요 속성:
속성
name 컬럼명 지정
nullable false면 NOT NULL 제약조건 생성 (default true)
unique 간단한 유니크 제약조건 부여 (운영에선 비권장, 유니크의 이름이 랜덤값으로 생성되어 알아보기 불편함, 멀티 컬럼 선택 불가 등)
insertable/updatable INSERT, UPDATE 가능 여부
columnDefinition 직접 DDL 정의 (예: "varchar(100) default 'EMPTY'")
length 문자열 길이 제한 (기본 255)
precision, scale BigDecimal 등 정밀 수치 지정 (precision=19, scale=2)

2. @Enumerated

  • Enum 타입 매핑 시 사용
  • 옵션:
    • EnumType.ORDINAL: enum 순서를 저장 (비추천)
    • EnumType.STRING: enum 이름을 저장 (필수 권장)

ORDINAL 사용 시 문제 예시

public enum RoleType {
    USER, ADMIN
}

이렇게 사용중일 때 DB에 USER = 0, ADMIN = 1로 저장

 

public enum RoleType {
    GUEST, USER, ADMIN
}

만약 이후에 GUEST라는 타입을 추가했다면

GUEST = 0, USER = 1, ADMIN = 2

기존 USER가 GUEST로 해석될 수 있음 ⇒ 데이터 망가짐

 

3. @Temporal (최신 자바 미사용)

  • java.util.Date, java.util.Calendar 타입 매핑 시 사용

4. @Lob

  • 큰 데이터(CLOB, BLOB)를 저장할 때 사용
  • 필드 타입에 따라 자동 결정:
    • String, char[] → CLOB
    • byte[] → BLOB

@Lob은 속성이 없음. 타입에 따라 자동으로 CLOB/BLOB 선택됨.

📌 그럼 CLOB vs TEXT 차이?

  • CLOB: 표준 ANSI SQL의 대용량 문자 타입
  • TEXT: DBMS에 따라 제공되는 비표준 타입 (MySQL 등)
  • JPA는 CLOB으로 매핑하며, 방언에 따라 TEXT 등으로 변환됨

5. @Transient

  • 해당 필드를 DB 컬럼으로 매핑하지 않음
  • 계산용, 로그 출력용 등 JPA에서 관리하지 않아야 할 필드에 사용
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 단방향 연관관계  (0) 2025.06.24
[JPA] 기본키 매핑  (0) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10

+ Recent posts