[TIL - 0831] ORM으로 테이블의 복잡도 낮추기, Pagination, 커넥션풀 사용 표준 Datasource

JPA

ORM으로 테이블 복잡도 줄이기

  • 성능 상의 이점을 보기 위해서 두 테이블로 나누지않고 하나의 테이블에 관련 정보를 모음 : JOIN 하지않고 그냥 SELECT 하기위해서
    • @Embedded와 @Embeddable로 복잡도 줄이기 : 객체 분리해서 모듈 형태로 관리하기 - 하나의 큰 판(CarInfo)과 여러 모듈(Car, Owner)
  • 테이블
mysql> desc carinfo;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | NO   | PRI | NULL    |       |
| name  | varchar(16) | YES  |     | NULL    |       |
| oname | varchar(16) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
  • 객체 추상화
@Entity(name = "carinfo")
public class CarInfo extends AbstractEntity {

    @Embedded
    private Car car;

    @Embedded
    private Owner owner;
}

@Embeddable
public class Car {

    @Column(nullable = false)
    private String name;
}

@Embeddable
public class Owner {

    @Column(nullable = false, name = "oname")
    private String name;
}
  • 결과
    // http://localhost:8080/owners/20000
    {
    "name": "ajrr6etxjf"
    }
    
  • 문제 생긴 부분과 해결한 부분 혹은 정확하게 몰랐던 부분
    1. 클래스 추상화할 때 같은 name이여서 name으로 필드명을 지었는데, 테이블 상에서는 구분되는 이름 : 맵핑할 때 @Column 설정으로 name 테이블의 컬럼명으로 설정 해주기 혹은 @AttributeOverride도 있음
    2. 테이블과 맵핑해줄 때(@Entity) CarInfo와 같이 이어진 단어라면 car_info로 맵핑하려고함 : 이미 데이터가 있는 테이블의 명칭이 carinfo인 경우 @Entity(name = “carinfo”)로 설정해줘야함
    3. 테이블과 맵핑해둔 클래스에는 기본 생성자가 있어야함(@Embeddable까지도) : 자바빈 규약은 아님(필드에 대한 setter 없었는데도 초기화됨)
// (1)
@Column(nullable = false, name = "oname")
private String name;


// (2)
@Entity(name = "carinfo")
public class CarInfo

Spring-data-JPA - OSIV, Page/Pageable, N+1

  • 연관 객체를 바로 불러오지않고 Lazy하게 필요할 때 불러오도록 설정해둠 : JOIN 쿼리까지 날려야하니깐 프록시 객체(타입만 같고 대리 객체, 가짜 객체)를 할당해두었다가 필요할 때 직접 쿼리메소드(이외 쿼리 날리는 방법 모두 포함)를 실행시켜 가져오기
    • 트랜잭션 안에서 가져와야하는데(기본적으로 영속성 컨텍스트가 트랜잭션 범위와 같음), 트랜잭션 끝난 후 Lazy 설정된 연관 객체를 로딩할 경우 LazyInitializationException이 발생함
    • 스프링은 영속성 컨텍스트 범위를 더 넓혀서(커넥션 유지 - 그래야 데이터베이스에 쿼리를 날리고 데이터를 가져오지) 데이터를 가져올 수 있도록 함 : 읽기 전용 트랜잭션
  • 성능과 관련해서 본 글 그리고 생각해보니 맞는 이야기
    • DB 커넥션 유지 시간을 오래 붙잡고 있음 : 성능 이슈가 발생하는 지점
  • OSIV를 사용하지않는다면 필요한만큼 불러오도록 설정하는게 어떨까?
    • 문제점 : 최악의 경우 하나의 자동차(종류)가 33만명의 사람이 타고 다니는 차로 저장되어있다면? 캐시 있고 없고 상관없이 너무 많은 데이터를 한꺼번에 불러오는 것부터가 문제(메모리 로드까지 엄청난 시간이…) - Page/Pageable 사용
    • N+1이 발생하지않았다…. : 내가 잘 모르는 것 같다… 연관관계의 아이를 가져올 때 N+1이 무조건 발생할 것이라 생각하고 디버깅을 했지만 아무런 문제없이 쿼리 1회로 끝내버렸음
// 페이지 처리와 생성된 쿼리
Page<Car> carPage = carRepository.findAll(PageRequest.of(0, 100));
Hibernate: 
    select car0_.id as id1_0_, car0_.name as name2_0_ 
    from car car0_ limit ?

// 1개에 연관된 객체가 4개가 있는데, 4개 이름 값을 가져올 때 4개의 쿼리가 발생하지않고 1개의 쿼리만 발생함
16:55:06.800 [TRACE] [http-nio-8080-exec-1] [o.h.type.descriptor.sql.BasicBinder] - binding parameter [1] as [BIGINT] - [999]
16:55:06.801 [DEBUG] [http-nio-8080-exec-1] [jinbro.lazyloadingtest.domain.Car] - car owner names : hokcxv8yoe, y53g946nhn, 08rhfjzjqk, ipj40e9lw9

Hibernate: 
    select 
        persons0_.fk_person_car as fk_perso3_1_0_, 
        persons0_.id as id1_1_0_, persons0_.id as id1_1_1_, 
        persons0_.fk_person_car as fk_perso3_1_1_, 
        persons0_.name as name2_1_1_ 
    from person persons0_ 
    where persons0_.fk_person_car=?

Java Database, Spring-boot

DataSource

  • ConnectionPool을 사용하기위한 표준 인터페이스(방법) : 여러 커넥션풀이 있을텐데 하나의 사용방법으로 커넥션을 찾고 맺는 방법(하나의 메소드임 - Connection)
    • 커넥션풀 : 데이터베이스와 연결하기위한 커넥션을 일정 개수 만들어두고 보관하고 있는 곳(대기 중, 재사용) - 연결하기위해서는 드라이버 로드 -> 커넥션 객체 가져오고(생성) -> 종료(객체 제거되도록), 매번 앞 동작을 하게되면 비효율적임
    • javax.sql.DataSource
    • spring-boot-starter-data-jpa의 기본 커넥션풀 : Hikari CP - 성능 상으로 좋으면서도 커넥션이 안정적이라고 하는데, 솔직히 다른 커넥션풀을 사용해보지도 이전에는 의식을 해보지 않아서 와닿지는 않음
Connection getConnection() throws SQLException
Connection getConnection(String username, String password) throws SQLException


// spring-boot 문서
29.1.2 Connection to a Production Database
Production database connections can also be auto-configured by using a pooling DataSource. Spring Boot uses the following algorithm for choosing a specific implementation:

We prefer HikariCP for its performance and concurrency. If HikariCP is available, we always choose it.
Otherwise, if the Tomcat pooling DataSource is available, we use it.
If neither HikariCP nor the Tomcat pooling datasource are available and if Commons DBCP2 is available, we use it.
If you use the spring-boot-starter-jdbc or spring-boot-starter-data-jpa “starters”, you automatically get a dependency to HikariCP.