티스토리 뷰

공부/JPA

JPA 값 타입 구현하기

JStack 2024. 7. 27. 20:07

이전 글: https://gojs.tistory.com/48

 

JPA 프록시와 페치 전략

특정 엔티티를 조회할 때 연관된 엔티티를 항상 같이 조회하는 것은 아니다.한번에 모든 연관관계를 로딩하게 되면 너무 많은 데이터를 조회하게 되어서 OOM이 발생할 수도 있다.그렇다면 JPA에

gojs.tistory.com

 

JPA는 Java 엔티티와 DB 테이블 간의 매핑을 도와주는 프레임워크이다.

따라서 엔티티의 필드 타입과 테이블의 컬럼 타입도 매핑해준다.

엔티티의 각 필드에 정의할 수 있는 타입들에 대해서 알아보자.

 

기본값 타입

기본값 타입은 Java의 기본 타입, 기본 타입의 Wrapper 클래스 타입, String 타입 등이다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int age;
    ...
}

 

임베디드 타입 (복합 값 타입)

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

    // 근무 기간
    @Temporal(TemporalType.DATE) 
    Date startDate;
    
    @Temporal(TemporalType.DATE) 
    Date endDate;

    // 집 주소
    private String city;
    private String street;
    private String zipcode;
    ...
}

위의 Member 엔티티는 이름, 근무 기간, 집 주소를 속성으로 가진다.

그렇지만 그 내용을 일일히 나열하여 풀어두었고 이처럼 상세한 내용을 가지는 것은 전혀 객체지향적이지 못하다.

 

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

    // 근무 기간
    @Embedded
    Period workPeriod;

    // 집 주소
    @Embedded
    Address homeAddress;

    ...
}
@Embeddable
public class Period {
    @Temporal(TemporalType.DATE)
    Date startDate;
    
    @Temporal(TemporalType.DATE)
    Date endDate;
    ...

    public boolean isWork(Date date) {
        ...
    }
}
@Embeddable
public class Address {
    @Column(name = "city")
    private String city;
    private String street;
    private String zipcode;
}

위와 같이 Embedded 타입으로 구현하니 훨씬 응집도 있는 코드로 변화했다. 새로 정의한 클래스들을 재사용할 수도 있다. (객체지향적)

  • @Embeddable : 값 타입을 정의하는 곳
  • @Embedded : 값 타입을 사용하는 곳

Embedded 타입은 기본 생성자가 필수이며 엔티티에 의존하므로 엔티티와 composition 관계가 된다. (component)

 

@AttributeOverride (속성 재정의)

@Entity
public class Member {
    @Id @GenereatedValue
    private Long id;
    private String name;

    @Embededded 
    Address homeAddress;

    @Embededded
    @AttributeOverrides({
        @AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")),
        @AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")),
        @AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE"))
    }) 
    Address companyAddress;
}
CREATE TABLE MEMBER (
	COMPANY_CITY varchar(255),
	COMPANY_STREET varchar(255),
	COMPANY_ZIPCODE varchar(255),
	city varchar(255),
	street varchar(255),
	zipcode varchar(255)
	...
);

만약 임베디드 타입이 null이면 매핑한 컬럼은 모두 null이 된다.

 

값 타입과 불변 객체

값 타입 공유 참조

임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.

member1.setHomeAddress(new Address("OldCity"));
Address address = member.getHomeAddress();

address.setCity("NewCity");
member2.setHomeAddress(address);

위 코드에서 member2의 주소를 NewCity로 변경하였는데, member1의 주소도 같이 NewCity로 바뀌었다.

member1,2 모두 address 인스턴스를 참조하기 때문에 영속성 컨텍스트가 둘 모두 바뀐 것으로 판단하였기 때문이다.

 

member1.setHomeAddress(new Address("OldCity"));
Address address = member.getHomeAddress();

Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);

위와 같이 값을 복사해서 사용하면 해당 문제 발생하지 않지만 원본을 직접 넘기는 경우를 방지할 수는 없다.

원본을 수정해서 넘기는 경우를 방지하기 위해서 setter를 없애는 방법이 있다. (불변 객체로 설계)

 

member1.setHomeAddress(new Address("OldCity"));
Address address = member.getHomeAddress();

Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);

setter 메서드가 없으니 위와 같이 Address 값 타입을 새로 만들어서 사용할 수 밖에 없다.

 

값 타입 컬렉션

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name="FAVORITE_FOODS", joinColumns=@JoinColumn(name="MEMBER_ID"))
    @Column(name="FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name="ADDRESS", joinColumns=@JoinColumn(name="MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();

    ...
}
@Embeddable
public class Address {
    @Column
    private String city;
    private String street;
    private String zipcode;
    ...
}

Member 엔티티에서는 favoriteFoods, addressHistory 필드를 값 타입 컬렉션으로 지정했다.

 

 

favoriteFoods의 타입은 String 타입 컬렉션이다. @ElementCollection으로 String 타입의 컬럼을 가지는 별도 테이블을 지정한다. 또한 그 컬럼의 이름을 @Column으로 지정한다.

addressHistory의 타입은 Address 타입 컬렉션이다. 이에 따라 @ElementCollection을 사용하여 테이블 정보를 지정하며 테이블 매핑정보는 @AttributeOverride를 사용해서 재정의한다.

 

Member member = new Member();

member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123"));

member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");

member.getAddressHistory().add(new Address("서울", "강남", "123-123"));
member.getAddressHistory().add(new Address("서울", "강북", "000-000"));

em.persist(member);
INSERT INTO MEMBER (ID, CITY, STREET, ZIPCODE)
VALUES (1, '통영', '몽돌해수욕장', '660-123');

INSERT INTO FAVORITE_FOODS (MEMBER_ID, FOOD_NAME)
VALUES (1, '짬뽕');
INSERT INTO FAVORITE_FOODS (MEMBER_ID, FOOD_NAME)
VALUES (1, '짜장');
INSERT INTO FAVORITE_FOODS (MEMBER_ID, FOOD_NAME)
VALUES (1, '탕수육');

INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE)
VALUES (1, '서울', '강남', '123-123');
INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE)
VALUES (1, '서울', '강북', '000-000');

실행코드에 따라 발생하는 쿼리이다.

값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수로 가진다.

 

Member member = em.find(Member.class, 1L);

Address homeAddress = member.getHomeAddress();

Set<String> favoriteFoods = member.getFavoriteFoods();
for (String food : favoriteFoods) {
    System.out.println("favoriteFoods = " + food);
}

List<Address> addressHistory = member.getAddressHistory();
addressHistory.get(0);

그리고 값 타입 컬렉션은 기본적으로 지연 로딩을 페치 전략으로 가진다. 따라서 위의 코드를 실행하면 아래와 같은 순서로 데이터베이스를 조회하게 된다.

  1. member.getHomeAddress()를 실행할 때 member와 homeAddress를 하나의 쿼리로 조회한다.
  2. member.getFavoirteFoods()를 실행할 때 favorite_food 테이블의 해당 회원 데이터 목록을 조회한다.
  3. member.getAddressHistory()를 실행할 때 address 테이블의 해당 회원 데이터 목록을 조회한다.

 

주의사항

엔티티는 엔티티의 값을 변경하더라도 식별자 값으로 데이터베이스의 데이터를 찾아 변경할 수 있다.

그러나 값 타입은 식별자 개념이 없기 때문에 값이 변경되더라도 데이터베이스의 데이터를 찾기가 힘들다.

따라서 값 타입 컬렉션의 경우 특정 값이 변경되면 모든 값을 삭제하고 새로운 값 타입 컬렉션을 다시 저장한다.

 

실무에서는 값 타입 대신 일대다 관계의 엔티티를 고려한다.

별도 엔티티와의 연관관계에 영속성 전이와 고아 객체 제거 기능 적용하여 값 타입처럼 사용할 수 있다.

 

DDD에서 값 타입

https://gojs.tistory.com/66

 

JPA를 활용한 DDD 적용

JPA를 사용하지 않고 MyBatis나 JDBC를 활용하더라도 의존성을 잘 관리한다면 충분히 DDD를 적용할 수 있다.그러나 JPA와 같은 ORM을 사용한다면 도메인 모델과 데이터 모델을 매핑하는 다양한 기능을

gojs.tistory.com

DDD로 엔티티를 설계하는 경우 값 타입을 구현하는 레퍼런스는 위 포스팅을 참고하시라..

 

다음 글: https://gojs.tistory.com/54

 

객체지향 쿼리 언어

객체지향 쿼리JPA에서 하나의 식별자로 하나의 엔티티를 조회할 수 있고, 조회한 엔티티를 기준으로 삼아 연관된 엔티티를 찾을 수 있다.- 식별자로 조회: EntityManager.find(id)- 객체 그래프 탐색으

gojs.tistory.com

'공부 > JPA' 카테고리의 다른 글

Spring Data JPA 활용하기  (0) 2024.09.22
객체지향 쿼리 언어 알아보기  (0) 2024.08.26
JPA 프록시와 로딩 전략  (0) 2024.07.27
JPA Entity 간 연관관계 매핑 (2)  (0) 2024.07.27
JPA Entity 간 연관관계 매핑 (1)  (0) 2024.07.26
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함