[강의 정리] 값 타입

나무·2023년 12월 4일

JPA 

목록 보기
9/11
post-thumbnail

JPA 에서는 데이터 타입은 크게 두 가지로 분류되는데, 1) 엔티티 타입 그리고 2) 값 타입 으로 나눌 수 있다. 여기서 오해하지말아야할게 지금 말한 JPA에서 말하는 데이터 타입은 자바에서 나누고 있는 원시타입, 참조변수 타입과 다른 개념이다.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    @Embedded
    private Address address;
    
    @ElementCollection
    @CollectionTable(
    	name ="ADDRESS",
    	joinColumns = @JoinColumn(name="MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList();
}

위와 같이 회원 엔티티가 정의 되어 있을때 각 필드들의 데이터 타입을 한번 확인해보자.

엔티티 타입

TEAM -> @Entity 로 정의 되어진 타입으로 JPA가 관리해주는 엔티티 타입 이다.

값 타입

Long, String, int
이 필드들은 자바에서 기본적으로 제공해주는 원시타입 혹은 래퍼클래스들이며 JPA에서는 이를 기본값 타입 이라 부른다.

Address
'이것도 엔티티 타입 아니야?' 라는 생각이 들 수 있지만, 이것 또한 값 타입이다. 다른 말로는 임베디드 타입 이라고도 부르는데 자세한 설명은 뒤에서 하겠다.

List<Address>
마찬가지로 값타입이며 컬렉션 값 타입 이라 부른다.

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

임베디드 타입(Embedded Type)은 엔터티(Entity)에서 여러 속성들을 그룹화하여 관리 할 수 있도록 하는 방법

회원이라는 엔티티가 가져야할 속성들 중에는 '주소' 가 있을 수 있다. 하지만 '주소' 는 또 다시 시/군/구, 우편번호, 아파트/건물 이름 등 다양한 속성들로 구분되어질 수 있다.

문제는 이렇게 될 경우 2가지 큰 문제점이 발생한다.

첫번째는 엔티티 필드가 너무 많아져서 엔티티 객체 코드가 지저분해진다.

두번째는 필드 매핑 작업에 손이 너무 많이 가게 된다 는 것이다.

아래 예시 코드를 확인해보자.

예시 : 회원 엔티티

@Entity
public class Member{
	private Long id;
    
    // 주소 영역
    private String city;
    private String gu;
    private String dong;
    private String street;
    private String zipcode;
    private String buildingName;
    
    // 나머지 영역
     private int age;
    private String name;
    private Integer phoneNo;
    ...
}

현재 회원엔티티는 주소 정보 필드 때문에 코드가 굉장히 길고 복잡해졌다. 물론 "겨우 이정도가지고?" 라는 생각을 할 수 있겠지만, 주소 말고도 다른 타입들이 더 생길 수 있다는걸 생각해보자.

예시 : 회원 엔티티 리팩토링

Address 클래스

@Embeddable
public class Address{
    private String city;
    private String gu;
    private String dong;
    private String street;
    private String zipcode;
    private String buildingName;
}

Member 클래스

@Entity
public class Member{
	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
	private Long id;
    
    // 주소 영역
    @Embedded
    private Address address;
    
    private String name;
    private int age;
    private int phoneNo;
    ...
}

이렇게 작성하면 회원엔티티가 한결 더 깔끔해진걸 확인 할 수 있다.

테스트 코드

테이블과의 매핑

임베디드 타입은 결국 값 타입이지 테이블을 할당받은 엔티티가 아니다.

그렇기 때문에 JPA가 쿼리를 날릴때 Address 라는 테이블을 찾아서 JOIN 하거나 그럴일 없이 그냥 Address의 필드들과 매핑되는 컬럼들로 다시 분해해서 INSERT 쿼리를 날리게된다.

※ 물론 컬렉션 값타입을 사용하면 실제 테이블을 생성하기도 한다.


2. 값 타입 주의 사항

임베디드 타입은 결국 참조 타입이다. 참조 타입의 특징은 주소를 통해 값들을 공유 할 수 있다는 것이다.

다음과 같은 요구사항이 있다고 가정해보자.

모든 회원의 주소 정보를 "홍길동" 회원의 주소 정보와 동일하게 저장해라. 단, 도시는 각각 다르게 저장해야한다.

값 공유

// 1. "홍길동" 의 주소 정보 추출
Member hong = em.find("홍길동");
Address hongAddress = m1.getAddress();

// 2."서울" 로 변경
hongAddress.setCity("서울");
Member m1 = new Member("회원1", hongAddress) 

// 3. "대전" 으로 변경
hongAddress.setCity("대전");
Member m2 = new Member("회원2", hongAddress)

// 4. "판교" 로 변경
hongAddress.setCity("판교");
Member m3 = new Member("회원3", hongAddress)

System.out.println("회원1 도시 : "+m1.getAddress().getCity());
System.out.println("회원2 도시 : "+m2.getAddress().getCity());
System.out.println("회원3 도시 : "+m3.getAddress().getCity());

// 출력 결과
// 회원1 도시 : 판교
// 회원2 도시 : 판교
// 회원3 도시 : 판교

요구 사항대로 도시를 제외한 나머지 주소 정보는 모두 같게 저장하기 위해 "홍길동" 회원엔티티에서 주소를 추출 한뒤 도시의 값만 setter 로 바꾸면서 각 회원들에게 저장을 하였다.

하지만 보시다시피 저장된 값은 모든 회원들의 도시정보가 가장 마지막에 세팅 해주었던 "판교" 였다. 왜냐하면 hongAddress 를 공유 하고 있었기 때문 이다.

리팩토링 : 값 복사

요구사항대로 개발을 하기위해서는 hongAddress 인스턴스를 복사해야한다.

// 1. "홍길동" 의 주소 정보 추출
Member hong = em.find("홍길동");
Address hongAddress = m1.getAddress();

// 2."서울" 로 변경
m1Address = hongAddress.clone();
m1Address.setCity("서울");
Member m1 = new Member("회원1", m1Address) 

// 3."대전" 로 변경
m2Address = hongAddress.clone();
m2Address.setCity("대전");
Member m1 = new Member("회원2", m1Address)

// 4."판교" 로 변경
m3Address = hongAddress.clone();
m3Address.setCity("판교");
Member m1 = new Member("회원3", m1Address)


System.out.println("회원1 도시 : "+m1.getAddress().getCity());
System.out.println("회원2 도시 : "+m2.getAddress().getCity());
System.out.println("회원3 도시 : "+m3.getAddress().getCity());

// 출력 결과
// 회원1 도시 : 서울
// 회원2 도시 : 대전
// 회원3 도시 : 판교

하지만 어떤 어리숙한 개발자가 인스턴스를 복사해서 사용해야한단 사실 자체를 까먹거나 알지 못한경우 이를 막을 방도가 없다. setter 를 없애기전까진 말이다,,,

리팩토링 : 불변 객체

원본의 값이 공유되어 도중에 값이 변경되는것을 막기위해서는 근본적인 문제점을 제거해야한다.

Address 의 경우 현재 setter 메서드를 구현하고 있다. 하지만 setter의 경우 앞에서 보았다시피 객체의 값을 도중에 언제든지 변경할 수 있다는 것이다.

그래서 실무에서는 정말 웬만한 경우가 아닌 이상 아예 setter 를 private으로 놓던가 아니면 아예구현 조차 하지 않는다.

즉, 값을 수정 못하게 아예 불변 객체로 만들어 버리는 것이다.

그러면 도대체 값을 어떻게 복제해야하는가?

코드를 살펴보자.

// 1. "홍길동" 의 주소 정보 추출
Member hong = em.find("홍길동");
Address hongAddress = m1.getAddress();

// 2."서울" 로 변경
Member m1 = new Member("회원1", 
new Address("서울", hongAddress.getGu(), hongAddress.getStreet())) 

// 2."대전" 로 변경
Member m2 = new Member("회원2", 
new Address("대전", hongAddress.getGu(), hongAddress.getStreet())) 

// 2."판교" 로 변경
Member m3 = new Member("회원3", 
new Address("판교", hongAddress.getGu(), hongAddress.getStreet())) 

System.out.println("회원1 도시 : "+m1.getAddress().getCity());
System.out.println("회원2 도시 : "+m2.getAddress().getCity());
System.out.println("회원3 도시 : "+m3.getAddress().getCity());

// 출력 결과
// 회원1 도시 : 서울
// 회원2 도시 : 대전
// 회원3 도시 : 판교

보시다시피 주소 정보를 생성자를 통해 주입하고 있다. 즉, 세터로 값을 바꾸는게 아니고 생성자를 이용해 아예 처음에 값을 정해버리는 것이다.

그럼 만일 도시를 또 수정해야 하는 상황이 온다면? 그때도 마찬가지로 새로운 인스턴스를 하나 또 생성한다음 생성자를 통해 위와 같이 값을 옮겨담아야 한다.

비록 불변이라는 작은 제약이 있지만 예상치 못하게 값의 변경되어버리는 큰 재앙을 막을 수 있다.

리팩토링 : 값 타입 비교

이번엔 다음과 같은 요구사항이 또 들어왔다고 가정하자.

"홍길동" 회원과 주소가 일치하는 회원을 찾으시오

// 서울, 동작구, 상도동
Address hong = em.find("홍길동").getAddress();

// 서울, 동작구, 상도동 (일치)
Member m1 = em.find("오길동").getAddress();

// 성남, 분당구, 판교동 (불일치)
Member m2 = em.find("박길동").getAddress();

// 서울, 관악구, 봉천동(불일치)
Member m3 = em.find("김길동").getAddress();

for(Member m : List.of(m1, m2, m3)){
	if(hong.getAddress() == m1.getAddress()){
    	System.out.println(m.getName());
    }
}
System.out.println("0명");

// 출력 결과
// 0명

예상대로라면 "오길동" 이 출력이되어야하지만 실제로는 그렇지 않다.

왜냐하면 == 연산자는 객체의 속성 값들을 비교하는게 아닌, 참조하고 있는 주소값을 비교하기 때문이다. (동일성 비교)

그래서 객체 타입의 경우 equals()hashCode() 를 따로 재정의해준다음 그걸로 객체를 비교해줘야한다. (동등성 비교)

본 포스트는
김영한의 자바 ORM 표준 JPA프로그래밍 기본 강의 및 도서를 참고하여 정리했습니다.

profile
🍀 개발을 통해 지속 가능한 미래를 만드는데 기여하고 싶습니다 🍀

0개의 댓글