잡다한 프로그래밍 :: 잡다한 프로그래밍
반응형

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] 상속관계 매핑  (2) 2025.07.03
[JPA] 다양한 연관관계 매핑  (2) 2025.06.30
[JPA] 양방향 연관관계와 연관관계의 주인[1]  (1) 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] 다양한 연관관계 매핑  (2) 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
반응형

이전글에서 객체와 테이블의 매핑에 대해서 배웠는데,

이번에는 JPA가 애플리케이션을 실행할 때, 엔티티 클래스 정의를 기반으로 DB 테이블을 자동 생성하는 기능에 대해 정리한다.

 

쉽게말해, 엔티티를 작성하면 JPA가 알아서 테이블을 생성해준다!

 

객체 중심 개발 패러다임

과거에는 테이블 설계(DDL) → 객체 매핑 순이었다면,
JPA는 반대로 객체 정의(Entity) → 테이블 생성(DDL) 흐름을 지원합니다.

즉, "객체 중심 설계"로 전환하면서, JPA가 엔티티 정의를 읽고 적절한 테이블 스키마를 생성해줍니다.

 

DB 방언(Dialect)에 따라 DDL 자동 생성

JPA는 사용하는 DBMS에 맞는 방언(Dialect) 을 기반으로
적절한 SQL DDL을 자동 생성합니다.

예:

  • Oracle: varchar2
  • MySQL: varchar

 

⚙️ DDL 생성 전략 설정 (hibernate.hbm2ddl.auto)

DDL 자동 생성 기능의 핵심 설정은 다음 속성으로 제어합니다.

spring.jpa.hibernate.ddl-auto=create
설정 값 설명
create 기존 테이블 삭제 후, 매번 새로 생성 (애플리케이션 시작 시 DROP → CREATE)
create-drop create와 동일하지만, 애플리케이션 종료 시 테이블을 DROP
update 변경된 부분만 반영 (기존 데이터 보존) → 주의: 운영에서 사용 금지
validate 테이블과 엔티티 매핑이 일치하는지 검사만 수행 (DDL 실행 ❌)
none 아무 작업도 수행하지 않음 (기본값 또는 운영에서 권장)

 

언제 어떤 옵션을 써야 할까?

개발 환경 create, update 빠른 테스트 및 개발을 위해 테이블 자동 생성/수정
테스트 서버 update, validate 데이터 보존하면서 매핑 검증
운영 환경 validate, none DDL 실행 ❌, 수동 스키마 관리 필수


참고: DDL 자동 생성의 역할

  • @Table, @Column(length = 100), @UniqueConstraint, @Index 등
    엔티티에 설정한 메타데이터를 기반으로 테이블/컬럼/제약조건 생성
  • JPA 실행 로직에는 영향을 주지 않음
    → DDL은 시작 시점에만 수행되고, 이후 쿼리 동작과는 무관
반응형

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

[JPA] 기본키 매핑  (0) 2025.06.19
[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
반응형

지난 시간에는 JPA의 영속성 컨텍스트(Persistence Context)와 1차 캐시에 대해 살펴보았습니다.

 

이번 글에서는 실제 설계된 도메인 객체를 RDB 테이블과 매핑하는 방법,

즉 JPA 엔티티 매핑의 기본 개념과 어노테이션 사용법을 정리 하겠습니다.

 

1. JPA 엔티티(@Entity)란?

@Entity

  • JPA가 관리하는 영속성 대상 클래스임을 선언합니다.
  • 이 어노테이션이 붙은 클래스만이 JPA의 CRUD, 변경 감지, 지연 로딩 등의 기능을 활용할 수 있습니다.
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    // ... getter, setter, 기본생성자 등
}

 

엔티티 클래스 제약 사항

  1. 기본 생성자
    • 파라미터가 없는 public 또는 protected 생성자가 반드시 필요합니다. (프록시 객체를 만들기 위해)
  2. final 클래스, enum, interface, inner 클래스 사용 불가
    • 엔티티는 런타임에 프록시로 확장될 수 있어야 하므로 final 클래스나 enum/interface/내부 클래스(inner class)로 선언할 수 없습니다.
  3. final 필드 사용 불가
    • JPA는 필드 값을 변경 감지하기 위해 리플렉션을 사용하므로, final 필드는 사용할 수 없습니다.
더보기

JPA의 지연 로딩과 프록시 객체

  1. 지연 로딩(Lazy Loading) 개념
    • 엔티티 조회 시, 연관된 다른 엔티티를 즉시 함께 가져오지 않고
    • 실제로 해당 연관 데이터를 사용할 때(getter 호출 시) 한 번 더 DB 조회를 수행하는 방식
@Entity
public class Post {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY) // ← 지연 로딩 설정
    private User author;               // ← 이 부분이 지연 로딩 대상
}

프록시 객체(Proxy Object)

  • 지연 로딩을 구현하기 위해 JPA가 생성하는 “가짜” 엔티티 객체
  • 실제 엔티티 클래스를 상속해 만들고, 초기에는 최소한의 자리만 차지하다가
  • 필요한 시점에 원본 데이터를 로딩

- final class가 안 되는 이유

  • 프록시 객체를 만들려면 엔티티 클래스를 상속해야 하는데,
  • final class는 상속이 금지되어 있어 프록시 생성이 불가
  • 결과적으로 지연 로딩 기능 자체를 사용할 수 없으므로,
  • JPA 엔티티에는 final class를 허용하지 않음

- final 필드가 안 되는 이유

  • JPA는 리플렉션과 기본 생성자 + setter 호출을 통해 프록시 객체의 내부 값을 채움
  • final 필드는 한번 초기화된 뒤 변경이 불가능하므로,
  • 리플렉션을 통한 값 설정이 실패하고 정상 동작할 수 없음
  • 따라서 엔티티의 속성(컬럼 매핑 필드)에도 final 키워드를 사용하지 말아야 함

 

※ JAVA record 클래스와 JPA

Java 14에 도입된 record는 불변 데이터 캐리어(Immutable Data Carrier) 로, 컴파일러가 자동으로 equals/hashCode/toString/모든 필드를 초기화하는 생성자 등을 생성해 줍니다. 일견 JPA 엔티티처럼 데이터 보관용 DTO로 유용해 보이지만, 다음 이유로 엔티티로 사용할 수 없습니다.

  1. 불변성(final 필드)
    record의 모든 구성 요소(component)는 암묵적으로 private final 필드로 선언됩니다.
    → JPA는 영속성 컨텍스트에서 변경 감지를 위해 필드 값을 바꿀 수 있어야 하지만, final 필드이므로 불가능합니다.
  2. 기본 생성자 부재
    record는 전체 필드를 인자로 받는 생성자만 제공하고, 파라미터 없는 생성자를 생성하지 않습니다.
    → JPA 사양에서는 “파라미터 없는 기본 생성자”를 요구합니다.
  3. 프록시(subclass) 생성 불가
    JPA 구현체(예: Hibernate)는 프록시 생성이나 바이트코드 조작을 위해 엔티티 클래스를 상속하거나 바이트코드를 재작성해야 합니다.
    record는 final로 간주되어 상속이 불가능합니다.

 

2. 테이블 매핑 어노테이션: @Table

@Entity만 선언하면 엔티티 이름과 같은 테이블에 매핑됩니다. 테이블명, 스키마, 카탈로그, 제약 조건 등을 세밀하게 지정하려면 @Table 을 함께 사용합니다.

@Entity
@Table(
    name = "users",            // 매핑할 테이블명
    schema = "public",         // 스키마
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"email"})  // 이메일 컬럼에 유니크 제약 걸기
    }
)
public class User {

 

옵션 종류

 

  • name: 매핑할 테이블명
  • schema / catalog: DB 스키마/카탈로그
  • uniqueConstraints: DDL 생성 시 추가할 복합 유니크 제약 조건

 

 

반응형

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

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
[JPA] JPA 동작 방식  (2) 2025.06.08

+ Recent posts