jpa 사용하기
sts + springboot + mybatis 환경에서 최대한 도메인 모델링 방식을 구현하고자 하였고
계속 작업을 하다보니 mybatis와 도메인모델링 방식의 괴리가 살짝쿵 다가왔다.(사실 예전부터)
mybatis를 orm 처럼 사용하기 위해 다음과 같은 준비과정을 거쳤었다.
- typehandler를 통해 DB 데이터와 enum객체를 매핑하기 ex) database value :1 <-> java enum : FIRSTENUM
- 엔티티와 대조되는 도메인 모델 클래스를 정의하고 에그리거트와 연관객체 / vo 정의해보기
- fetch lazy와 join을 사용한 N+1 이슈 해결하기
2,3번은 jpa 환경에서도 발생할 수 있는 이슈였고 mybatis에서 해결할 수 있는 방안으로 진행했다.
하지만 기본적인 crud와 더불어 기능을 위한 쿼리문은 불가피하게 이전과 같이 작성해야했다.
자바 코드, 객체지향적 패러다임에서 조금 벗어난 듯한 느낌..
아무튼 그래서 일정 기능을 마무리(리펙토링)하고 기존환경을 튜토리얼 삼아 새로운 환경을 쌓아올리기로 하였다.
intellij + spring boot + jpa (소위 말해 현업에서 쓸 것 같은..)
크게 보면 이러한 목표다.
JPA를 사용하여 rest api 서버를 만든다.
rest api서버는 어플리케이션 / 웹에서 사용되도록 한다.
어드민 서비스는 rest api와 통신하여 구현한다.
인프라적 관점에서는 msa환경을 모방해보기 위해..
docker와 spring boot이미지를 활용해 각 서버가 통신할 수 있는 환경을 나눈다.
spring cloud의 config server를 사용하여 properties를 컨트롤한다.
spring cloud의 api gateway를 사용하여 각 서비스에 라우팅 될 수 있도록 한다.(zuul?)
이렇게 되면 좋겟다!!
프로젝트는 git에 올려두었지만 일단 보는걸로
내가 사용하고 있는 DB에 테이블은 다 대문자로 되어있다.
jpa는 자동으로 소문자 취급하더라.. 그래서 properties에 다음과 같이 추가
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
가장 먼저 해봤던건
이전과 마찬가지로 도시정보 국가정보를 매핑해보는것
도시와 국가를 엔티티로 갖고
양방향 N:1 , 1:N 관계를 갖도록 하였다.
package com.guiving.domain.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@ToString(exclude = "cityList")
@NoArgsConstructor
@Getter
@Entity
@Table(name="TB_COUNTRY")
public class Country {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="country_idx")
private Long id;
@Column(name="country_nm")
private String name;
@Column(name="country_code")
private String code;
@OneToMany(mappedBy="country", fetch = FetchType.LAZY)
private List<City> cityList = new ArrayList<City>();
}
package com.guiving.domain.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@ToString(exclude = "companyList")
@NoArgsConstructor
@Getter
@Entity
@Table(name="TB_CITY")
public class City {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="city_idx")
private Long id;
@Column(name="city_nm")
private String name;
@Column(name="city_code")
private String code;
@Column(name="city_currency")
private String currency;
@ManyToOne
@JoinColumn(name = "country_idx")
private Country country;
@OneToMany(mappedBy = "city", fetch = FetchType.LAZY)
private List<Company> companyList = new ArrayList<>();
}
다음은 테스트 코드
package com.guiving.repository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CityRepositoryTest {
@Autowired
CityRepository cp;
@Test
public void findAllTest() throws Exception{
cp.findAll().stream()
.forEach(x-> System.out.println("element : " + x));
}
}
테스트를 하던 중 계속 난관에 봉착했던 게 initializer문제와 stack overflow문제였다.
결론부터 이야기하자면 테스트코드에서 system.out.println부분에 객체를 toString으로 찍는 부분인데
lombok의 toString을 사용하게 되다보니까 모든 필드를 문자열로 찍으려했다.
fetch lazy 케이스에서는 toString을 하다보니 객체가 로드되지 않은 상태의 참조라 initializer 문제가 발생하였고
fetch Eager 케이스는 즉시로드를 하고 연관객체를 문자열 출력하는 와중, 양방향 참조를 하다보니 무한 참조가 발생해버린 것이다.
그래서 entity클래스를 보면 ToString부분에 exclude부분을 추가하여 lazy에 대처할 수 있도록 했다.
참조링크
https://mia-dahae.tistory.com/92
두번째는 company entity
package com.guiving.domain.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
;
@ToString
@NoArgsConstructor
@Getter
@Entity
@Table(name="TB_COMPANY")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="com_idx")
private Long id;
@Column(name="com_name")
private String comName;
@Column(name="com_owner_name")
private String comOwnerName;
@Column(name="com_build_date")
private String comBuildDate;
@Column(name="com_biz_num")
private String comBizNum;
@Column(name="com_addr")
private String comAddr;
@Column(name="com_biz_license_url")
private String comBizLicenseUrl;
@Column(name="com_auth_code")
private String comAuthCode;
@Column(name="com_addr_city")
private String comAddrCity;
@Column(name="com_addr_state")
private String comAddrState;
@ManyToOne(fetch = FetchType.EAGER)
@JoinTable(name = "TB_COM_CITY"
, joinColumns = @JoinColumn(name="com_idx")
,inverseJoinColumns = @JoinColumn(name = "city_idx"))
private City city;
}
company entity는 city를 연관객체로 갖고 있다.
원래는 업체 하나가 여러 도시에서 운영이 가능한 것을 염두하고 N:M 관계의 테이블 구조를 두었다.
막상 데이터를 보니 모든 업체가 하나의 도시에서만 운영이 되고 있어 ManyToOne 관계로 두었다.
(city를 list로 두면 나중에 참조가 불편하기도 하고..)
처음에는 JPA사용법 미숙으로 JoinTable이 ManyToMany만 가능한 줄 알았으나 다행이 ManyToOne도 가능하여 위와같이 정의
Fetch 정책은 즉시로드 Eager를 두었다.
하지만 이렇게 되면 어떤 문제가 있다?
N+1 바로 이것..
mybatis에서는 직접 쿼리를 작성하다보니 inner및 outer조인을 적절히 사용하여 하나의 쿼리와 resultMap으로 컨트롤 하였는데
jpa환경은 이 부분을 직접 컨트롤 해주어야 했다.
그렇다고 그리 어려운 것은 아니었으니
package com.guiving.repository;
import com.guiving.domain.entity.Company;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface CompanyRepository extends JpaRepository<Company,Long> {
@EntityGraph(attributePaths ={"city","city.country"} )
@Query("select DISTINCT a from Company a")
List<Company> findAll();
}
이게 다다.
EntityGraph를 사용하여 join이 될 객체를 알려주고
카티션곱 방지를 위해 Query에 Distinct를 걸면 되는 것
한가지 헤멘 점은 Query상 from 에 TB_COMPANY를 적었었다.
이미 Entity Company와 TB_COMPANY를 매핑되어 있는 상태였고 JPA는 TB_COMPANY를 무조건 Company로 받아 들이고 있는 것.
참조링크
https://jojoldu.tistory.com/165
테스트 돌려보니 잘나온닷
@Test
public void findAllTest() throws Exception{
cp.findAll().stream()
.forEach(x->{
System.out.println("element : " + x );
}
);
}
처음으로 사용해보는 jpa생각보다 어렵지 않지만
차근차근 알아보면서 진행하는 걸로…