4 minute read


jpa는 하면 할수록 헷갈리는 것이군
company 엔티티를 작성하다가 OneToMany 관계의 연관객체를 하나 더 추가했다.


@ToString(exclude = {"operatorList","guiverList"})
@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 name;

    @Column(name="com_owner_name")
    private String ownerName;

    @Temporal(TemporalType.DATE)
    @Column(name="com_build_date")
    private Date buildDate;

    @Column(name="com_biz_num")
    private String bizNum;

    @Column(name="com_auth_code")
    private String comAuthCode;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "state",column = @Column(name = "com_addr_state")),
            @AttributeOverride(name = "city",column = @Column(name = "com_addr_city")),
            @AttributeOverride(name = "street",column = @Column(name = "com_addr"))
    })
    private Address address;


    @ManyToOne
    @JoinTable(name = "TB_COM_CITY"
            , joinColumns = @JoinColumn(name="com_idx")
            ,inverseJoinColumns = @JoinColumn(name = "city_idx"))
    private City city;

    @OneToMany(mappedBy = "company")
    private List<Operator> operatorList = new ArrayList<>();

    @OneToMany(mappedBy = "company")
    private List<Guiver> guiverList = new ArrayList<>();

    public int getOpCount(){
        return operatorList.size();
    }
    public int getGuiverCount(){
        return guiverList.size();
    }
}


operatorList와 guiverList. 이 두가지 연관 객체는 나에게 MultipleBagFetchException 을 선사해주었으니
무엇인가 찾아보면 다음과 같았다.


참조링크
https://jojoldu.tistory.com/457?fbclid=IwAR132BRMYHrL4D5Pu25YUglIcEN1FGTE2tacFcsVOPAT0MAzwoMX6Flzbe0

갓동욱님이 말하시길..
단건의 select – from where a=b절을
select – from where a in (..) 로 바꾸는 전략이 있다고 한다.

spring.jpa.properties.hibernate.default_batch_fetch_size=1000

application.properties 에 위 설정을 추가하면 1000개 단위의 in절 조건들을 나누어 쿼리로 돌리게되고 이로써 N+1이 어느정도 해소된다는 내용.


public interface CompanyRepository extends JpaRepository<Company,Long> {

    @EntityGraph(attributePaths ={"operatorList"} )
    @Query("select DISTINCT a from Company a join fetch a.city ct ")
    List<Company> findAll();

}


그래서 나도 따라 바꿨다.
@EntityGraph는 left join이 되고 @Query의 join fetch는 inner join이 되는 관계로 어쩔 수 없이 EntityGraph에 추가
company entity의 guiverList 요소는 지연로딩 설정으로 추후에 쿼리문이 돈다.

이 설정 관련해서 갓동욱님의 말은 이렇다.

  • hibernate.default_batch_fetch_size를 글로벌 설정으로 사용해 N+1 문제를 최대한 in 쿼리로 기본적인 성능을 보장하게 한다.
  • @OneToOne, @ManyToOne과 같이 1 관계의 자식 엔티티에 대해서는 모두 Fetch Join을 적용하여 한방 쿼리를 수행한다.
  • @OneToMany, @ManyToMany와 같이 N 관계의 자식 엔티티에 관해서는 가장 데이터가 많은 자식쪽에 Fetch Join을 사용한다.
  • Fetch Join이 없는 자식 엔티티에 관해서는 위에서 선언한 hibernate.default_batch_fetch_size 적용으로 100~1000개의 in 쿼리로 성능을 보장한다.

    내가 주목해 본 것은 세번째 부분인데 필연적으로인지 두 가지의 OneToMany join fetch는 불가하기 때문에 어느정도 타협을 보아야한다는 것.


    그리고 오지게 따라해도 안되던데 그 이유 하나.


  @Test
  @Transactional
  public void findAllTest() throws Exception{
      cp.findAll().stream()
                  .forEach(x->{
                          System.out.println("element : " + x +"  opcount  : " + x.getOperatorList().size());
                          System.out.println("element : " + x +"  gvcount  : " + x.getGuiverList().size());
                          //System.out.println("element : " + x);
                      }
                  );
  }


lazy load initializer 익셉션이 떨어지는데 이를 해결하려면 @Transactional 어노테이션을 써줘야하는 것…
(이걸 몰라서 한참 헤멨다.. Eager 로드쓸거 아니면 꼭 달아주도록 해야지)
















다른 작업은 country와 city의 타입을 정해둔것
이전 프로젝트에서 Enum형태로 city_idx를 매핑했었다.
허나 지금은 entity로 작성되어있어서 다음과 같이 나타났다


@ToString
@Getter
@AllArgsConstructor
public enum CityCode implements CodeEnum {

    SEL("SEL",CountryCode.KR,"서울"),
    PUS("PUS",CountryCode.KR, "부산"),
    CEB("CEB",CountryCode.PH, "세부"),
    MNL("MNL",CountryCode.PH, "마닐라"),
    BOR("BOR",CountryCode.PH, "보라카이"),
    DAN("DAN",CountryCode.VN, "다낭"),
    HCM("HCM",CountryCode.VN, "호치민"),
    NHA("NHA",CountryCode.VN, "나트랑"),
    TAG("TAG",CountryCode.PH, "보홀");

    private final String code;
    private final CountryCode parentCode;
    private final String comment;


    @Override
    public String getKey() {
        return name();
    }

    @Override
    public String getComment() {
        return comment;
    }


    @Override
    public String getCode() {
        return code;
    }
}


@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;



    @Enumerated(EnumType.STRING)
    @Column(name="city_code")
    private CityCode code;

    /*
    @Column(name="city_nm")
    private String name;
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "country_idx")
    private Country country;
     */

    @Column(name="city_currency")
    private String currency;



    @OneToMany(mappedBy = "city", fetch = FetchType.LAZY)
    private List<Company> companyList = new ArrayList<>();
}



CityCode enum객체를 사용하여 city_idx와 매핑되게 하고 싶었으나 (어제 알게된 converter와 같이)
생각처럼 잘 되지 않아 일단 후퇴. enum정보와 중복되는 필드들을 주석 처리했다.
(찾아보니까 Enum을 id로 쓰는 경우는 없더라능)









마지막으로 아주 고약한 녀석을 만났다.


@ToString(exclude = {"company","guiver"})
@NoArgsConstructor
@Getter
@Entity
@Table(name="TB_VEHICLE")
public class Vehicle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="vehicle_idx")
    private Long id;

    @Column(name = "vehicle_number")
    private String number;

    @Column(name = "vehicle_year")
    private String year;

    @Column(name = "vehicle_color")
    private String color;

    @ManyToOne
    @JoinColumn(name = "vehicle_car_model_idx")
    private CarModel carModel;

    @ManyToOne
    @JoinColumn(name = "vehicle_guiver_idx")
    private Guiver guiver;

    @ManyToOne
    @JoinColumn(name = "vehicle_com_idx")
    private Company company;

}


@ToString
@NoArgsConstructor
@Getter
@Entity
@Table(name = "TB_CAR_MODEL")
public class CarModel {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "cm_idx")
    private Long id;

    @Column(name = "cm_make")
    private String brand;

    @Column(name = "cm_model")
    private String model;

    @Column(name = "cm_seater")
    private String seats;


}


@ToString(exclude = "city")
@NoArgsConstructor
@Getter
@Entity
@Table(name="TB_CAR")
public class Car {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="car_idx")
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "city_idx")
    private City city;

    @Enumerated(EnumType.STRING)
    @Column(name="car_title")
    private CarType type;

    @Column(name="car_desc")
    private String desc;



}



이 세 가지의 연관 관계는 대략 이렇다.


</img>

결과론 적으론 Vehicle이 ManyToOne으로 Car를 가지고 있어야하고
Car역시 OneToMany로 Vehicle을 가지고 있으면 될 것인데.
idClass를 사용하더라도 공통적으로 가져올 수 있는 필드가 없다..
(제일 문제가 되는 부분은 city가 외래키로 매핑 되어야하는데 city를 가진 company와 guiver 둘 중 하나는 nullable이다.)

mybatis에서 어거지로 쿼리를 사용해 가져올 때는 문제가 되질 않았다.
그런데 여기서 넣어보려니 미칠노릇이네..
비슷한 예제를 내가 못 찾는 건가 싶기도하고.

곰곰히 생각해봤더니 추후에 비지니스로 처리 가능할 거 같아서 일단 넘어갔다..
하지만 꼭 처리해보고싶은 부분



마지막으로 어쩌다 본 애그리거트에 관련된 글


참조링크
https://stylishc.tistory.com/146

한 가지 염두할 부분은 애그리거트 루트가 주체가되는 트랜잭션을 구성하는 것이다.
애그리거트는 하나의 도메인 모델을 표현하고 있으므로, 도메인 모델의 영속성을 처리하는 레파지토리 또한 하나만 존재하는 것이 맞다.
물리적으로 별도의 테이블에 저장한다고 해서, 별도의 레파지토리를 각각 만들지는 않는다.

라고 한다..
지금의 나는 엔티티별로 레파지토리가 존재한다.
이 또한 애그리거트 단위로 바꿔야할 것 ..ㅠㅠㅠ


부족함이 많구나