네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'2022/03'에 해당되는 글 23건


반응형

인덱스(INDEX)

검색 속도를 높이기 위한 색인 기술이다.

보통 인덱스는 일반적으로 SELECT 쿼리의 WHERE에 사용할 컬럼에 대해 효율적인 검색을 위해 사용하거나, 다른 테이블과의 JOIN에 사용된다. (주로 효율적인 검색을 위해 사용된다.)

일반적으로 SQL 서버에 데이터를 저장할 때는 내부적으로 아무런 순서없이 저장한다. 이때, 데이터 저장영역은 Heap이다.

 

Heap에서는 인덱스가 없는 테이블의 데이터를 찾을 때, 전체 데이터 페이지의 처음 레코드부터 끝 페이지 마지막 레코드까지 모두 조회하게 된다.

이러한 방식을 풀 스캔(Full Scan) 또는 테이블 스캔(Table Scan) 검색 방식이라고 한다.

 

검색 속도 향상을위해서 인덱스를 사용해야 한다.

 

[ 어떤 컬럼에 Index를 설정해야 하는가? ]

1. 핵심 기준 4가지

- 카디널리티(Cardinality)

카디널리티가 높으면(한 컬럼이 갖고 있는 값의 중복도가 낮으면) 인덱스 설정에 좋은 컬럼이다. 

- 선택도 (Selectivity)

선택도가 낮으면(한 컬럼이 갖고 있는 값 하나로 적은 row가 찾아지면) 인덱스 설정에 좋은 컬럼이다.

- 조회 활용도

조회 활용도가 높으면 인덱스 설정에 좋은 컬럼이다.

- 수정 빈도

수정 빈도가 낮으면 인덱스 설정에 좋은 컬럼이다. (인덱스도 테이블이기 때문에 인덱스로 지정된 컬럼의 값이 바뀌게 되면 인덱스 테이블도 새롭게 갱신되어야 한다.)

 

2. 그 밖의 Index 명시 사항

- WHERE 절에 자주 사용되는 컬럼에 사용하기

- LIKE와 사용할 경우 %를 뒤에 사용하기

- ORDER BY 에 자주 사용되는 컬럼에 사용하기

- JOIN에 자주 사용되는 컬럼에 사용하기

- 데이터 변경이 잦은 컬럼에는 사용하지 않기

 

[ Index를 무조건 많이 설정하면? ]

1. 인덱스 설정 시, 데이터 베이스에 할당된 메모리를 사용하여 테이블 형태로 저장하게 된다. 즉, 인덱스가 많아지면 데이터베이스의 메모리를 많이 잡아먹게 된다.

2. 인덱스로 지정된 컬럼의 값이 바뀌게 되면 인덱스 테이블이 갱신되어야 하므로 느려질 수 있다.

 

전체적인 데이터베이스의 성능 부하를 초래할 수 있다.

 

[ 설정된 Index가 DML에 미치는 영향 ] 

* SELECT

Index는 주로 SELECT 쿼리에서 성능이 잘 나온다.

* UPDATE, DELETE

인덱스로 설정된 컬럼에 대해 조건(WHERE)을 사용할 수도 있는 UPDATE, DELETE 사용 시 조회에서는 성능이 크게 저하되지 않는다.

* INSERT

INSERT의 경우 효율이 좋지 않다. 새로운 데이터를 추가하면서 인덱스가 설정되어 있던 컬럼의 테이블도 같이 수정되어야 하기 때문이다.

 

[ Single Column Index와 Multi Column Index의 비교 ]

- Multi Column Index의 장점

질의(SQL) 컬럼이 모두 조합 인덱스에 있는 경우, 물리적인 데이터 블록을 읽을 필요가 없다. (인덱스 테이블만 읽으면 된다.)

- Multi Column Index를 고려해야 하는 경우

WHERE 절에서 AND 연산자에 의해 자주 같이 질의되는 컬럼인 경우

 

[ Index 특징 요약 ]

- 검색 (SELECT) 속도 향상

- 인덱스 테이블을 위한 추가 공간과 시간 필요

- INSERT, UPDATE, DELETE가 경우에 따라 성능 하락 발생

 

 출처 

https://velog.io/@jwpark06/%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9D%B8-DB-index-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

테이블의 PK가 복합키로 이루어져 있다면 엔티티를 설계할 때 고려해야 한다.

 

* 복합키 설정 방법은 두가지가 존재한다.

1. @Embeddable 이용 

2. @IdClass 이용

 

@Embeddable 이용

CREATE TABLE year_user_score (
    year CHAR(4) NOT NULL,
    user_id BIGINT NOT NULL,
    score INTEGER,
    PRIMARY KEY (year, user_id)
);

year_user_score 테이블은 PK는 year와 user_id 두 개의 복합키로 이루어져 있다.

 

@EmbededId를 이용하여 엔티티를 설계할 때는 우선 Serializable 인터페이스를 구현한 클래스를 선언하고 필드에 복합키로 사용되는 칼럼을 선언하면 된다.

@Embeddable
public class ScoreId implements Serializable {
    
    @Column(name = "year")
    private String year;
    
    @Column(name = "user_id")
    private Long userId;
}

복합키 클래스를 생성했으니 엔티티와 결합해준다.

@Table(name = "year_user_score")
@Entity
public class Score {
    
    @EmbededId
    private ScoreId id;
    
    private int score;
}

 

@IdClass 이용

위와 같은 테이블이 존재한다.

 

Serializable 인터페이스를 구현한 PK 클래스를 선언하고 필드를 정의한다.

public class ScoreId implements Serializable {
    
    private String year;
    private Long userId;
}

엔티티 클래스에 @IdClass(ScoreId.class) 설정해준다.

@Table(name = "year_user_score")
@Entity
@IdClass(ScoreId.class)
public class Score {
    
    @Id
    @Column(name = "year")
    private String year; // ScoreId의 필드 이름이 동일해야 함.
    
    @Id
    @Column(name = "user_id")
    private Long id;
    
    private int score;
}
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Batch Insert 

여러 건의 데이터를 입력할 때 아래와 같이 insert 구문을 실행하지 않고,

INSERT INTO test_data (user_id, uuid, created_at)
values ('jiyoon', '2efd2159-471d-4727-8eb0-20ae5a9810d3', '2022-03-19 04:41:12.59');
INSERT INTO test_data (user_id, uuid, created_at)
values ('nero', '49294b38-8afe-48a0-ab4b-4975ac333e56', '2022-02-05 09:14:00.04');

아래와 같이 멀티라인 insert 구문이 훨씬 효율적이다. 이를 Batch Insert라고 한다.

INSERT INTO test_data (user_id, uuid, created_at) values
('jiyoon', '2efd2159-471d-4727-8eb0-20ae5a9810d3', '2022-03-19 04:41:12.59'),
('nero', '49294b38-8afe-48a0-ab4b-4975ac333e56', '2022-02-05 09:14:00.04');

Batch Insert With JPA

- Hibernate의 Batch Insert 제약 사항

식별자 생성에 IDENTITY 방식을 사용하면 Hibernate가 JDBC 수준에서 batch insert를 비활성화한다.

Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator.
출처: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch-insert

비활성화를 진행하는 이유는 새로 할당할 Key 값을 미리 알 수 없는 IDENTITY 방식을 사용할 때 Batch Support를 지원하면 Hibernate가 채택한 flush 방식인 ' Transactional Write Behind'와 충돌하기 때문에 IDENTITY방식에서는 Batch Insert는 동작하지 않는다.

 

그렇다고 Batch Insert를 적용하기 위해 IDENTITY 방식말고 섣불리 SEQUENCE 방식이나 TABLE 방식을 잘못 사용하면 더 나쁜 결과를 불러올 수 있다. 채번에 따른 부하가 상당히 큰 SEQUENCE 방식이나 TABLE 방식을 별다른 조치 없이 사용하면 Batch Insert를 쓸 수 없는 IDENTITY 방식보다 더 느리다. (참고: JPA GenerationType에 따른 INSERT 성능 차이)

 

Spring Data JDBC

- jdbcTemplate.batchUpdate()

JdbcTemplate에는 Batch를 지원하는 batchUpdate() 메서드가 마련되어 있다. 여러가지로 Overloading 되어있어 편리한 메서드를 골라서 사용하면 된다. 여기서는 batch 크기를 지정할 수 있는 BatchPreparedStatementSetter를 사용하는 아래 메서드를 구현해보자.

batchUpdate(String sql, BatchPreparedStatementSetter pss);

 

- ItemJdbc 객체를 ITEM_JDBC 테이블에 Batch Insert로 저장한다고 가정하자.

- batchSize 변수를 통해 배치 크기를 지정하고 전체 데이터를 배치 크기로 나눠서 Batch Insert를 실행하자.

 

ItemJdbcRepository.java

public interface TestDataJdbcRepository {
    void saveAll(List<TestData> dataList);
}

 

ItemJdbcRepositoryImpl.java

@Repository
@RequiredArgsConstructor
public class TestDataJdbcRepositoryImpl implements TestDataJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    @Value("${batchSize}")
    private int batchSize;

    @Override
    public void saveAll(List<TestData> dataList) {
        int batchCount = 0;
        List<TestData> subItems = new ArrayList<>();
        for (int i = 0; i < dataList.size(); i++) {
            subItems.add(items.get(i));
            if ((i + 1) % batchSize == 0) {
                batchCount = batchInsert(batchSize, batchCount, subItems);
            }
        }
        
        // 나머지 subItems를 insert
        if (!subItems.isEmpty()) {
            batchCount = batchInsert(batchSize, batchCount, subItems);
        }
        
        System.out.println("batchCount: " + batchCount);
    }

    private int batchInsert(int batchSize, int batchCount, List<TestData> subItems) {
        // batchUpdate(String sql, BatchPreparedStatementSetter pss) 사용
        jdbcTemplate.batchUpdate("INSERT INTO TEST_DATA (`USER_ID`, `UUID`) VALUES (?, ?)",
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, subItems.get(i).getUserId());
                        ps.setString(2, subItems.get(i).getUuid());
                    }
                    @Override
                    public int getBatchSize() {
                        return subItems.size();
                    }
                });
                
        subItems.clear();
        batchCount++;
        return batchCount;
    }
}

 

요약

- 많은 데이터를 batch insert 하고 싶을 때는, Spring Data JDBC의 batchUpdate()를 활용하자.

- Sprind Data JPA를 사용해야 한다면 IDENTITY 방식 말고 SEQUENCE 방식을 사용하는 것이 좋다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Fetch Join 


JPQL(Java Persistence Query Language)에는 경로 표현식이 있다.

경로 표현식이란 점(.)으로 객체 그래프를 탐색하는 것이다.

SELECT m.name FROM Member m  // 상태 필드
SELECT m.team FROM Member t  // 단일 값 연관 경로
SELECT t.members FROM Team t  // 컬렉션 값 연관 경로

* 상태 필드(state field): 단순하게 값을 저장하는 필드

* 연관 필드: 연관 관계를 위한 필드

    - 단일 값 연관 필드: @ManyToOne, @OneToOne 처럼 xxxToOne 관계, 대상이 엔티티

    - 컬렉션 값 연관 필드: @ManyToMany, @OneToMany 처럼 xxxToMany 관계, 대상이 컬렉션

 

[경로 표현식 특징]

* 상태 필드: 경로 탐색의 끝으로, 더이상 탐색이 불가능하다.

* 단일 값 연관 경로: 묵시적 내부 조인이 발생하며 추가적인 경로 표현식으로 탐색이 가능

* 컬렉션 값 연관 경로: 묵시적 내부 조인이 발생하며 더이상 탐색이 불가하다.

    - From 절에서 명시적 조인을 통해 별칭(alias)을 얻으면 별칭을 통해 탐색이 가능하다.

 

** 되도록 묵시적 내부 조인이 되도록 JPQL을 작성하지 않도록 JPQL을 작성하지 말 것.

묵시적 내부 조인이 발생하면 의도치 않은 조인 쿼리가 발생할 수 있으며 발견하기 어렵다. 성능과 연관이 있을 수 있기 때문에 명시적 조인을 사용한다.

 

[상태 필드 경로 탐색]

List<Integer> members = em.createQuery("SELECT m.age FROM Member m", Integer.class).getResultList();

* Hibernate

select m.age from Member m

* SQL

select member0_.age as col_0_0_ from Member member0_

 

- 상태 필드는 추가적인 조인이 없다.

 

[단일 값 연관 경로 탐색]

List<Team> teams = em.createQuery("SELECT m.team FROM Member m", Team.class).getResultList();

* Hibernate

select m.team from Member m

* SQL

select

    team1_.team.id as team_id1_3_, team1_.name as name2_3_

from

    Member member0_

    inner join

        Team team1_ on member0_.team_id = team1_.team_id

 

- 단일 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생한다.

 

[컬렉션 값 연관 경로 탐색]

// 컬렉션 값을 조회 시, TypedQuery 제네릭을 Collection으로 지정해야 한다.
List<Collection> resultList = em.createQuery("SELECT t.members FROM Team t", Collection.class)
                                .getResultList();

* Hibernate

select t.members from Team t

* SQL

select 

    members1_.member_id as member_i1_0_, members1_.age as age2_0_, members1_.team_id as team_id4_0_, members1_.username as username3_0_

from 

    Team team0_

    inner join

        Member members1_ on team0_team_id = members1_.team_id

 

- 컬렉션 값 연관 경로는 탐색 시 묵시적 내부 조인이 발생하며 값을 받는 클래스 타입을 'Collection'으로 지정해야 한다.

 

** 묵시적 조인 시 주의사항

- 항상 내부 조인이 일어난다.

- 컬렉션은 경로 탐색의 끝으로, 더이상 점을 찍어 탐색이 불가하다. alias를 사용하여 경로 탐색을 계속 할 수 있다.

- 경로 탐색은 주로 select, where 절에서 사용되나, 묵시적 조인으로 인해 SQL의 FROM(Join)에 영향을 준다.

 

 


Fetch Join (페치 조인)

JPQL에서 지원하는 기능으로 '성능 최적화'를 위해 사용한다.

연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.

JOIN FETCH 명령어를 사용한다.

SELECT m FROM Member m JOIN FETCH m.team

 

- 즉시 로딩처럼 동작한다.

 

[페치 조인과 일반 조인 차이점]

 

 가정  Member:Team = N:1 다대일 관계일 때, fetch 전략을 EAGER (즉시 로딩)으로 설정했다

Member member = em.find(Member.class, 5L);
membe.getTeam().getName();

* SQL

select 

    member0_.member_id as member_i1_0_0_, member0_.age as age2_0_0_, member0_.team_id as team_id4_0_0_, member0_.username as username3_0_0_, team1_.team_id as team_id1_3_1_, team1_.name as name2_3_1_

from

    Member member0_

left outer join Team team1_

on member0_.team_id = team1_.team_id

where member0_.member_id = ?

 

=> 즉시 로딩으로 설정했기 때문에 em.find()할 때 Member에 연관된 Team까지 조회해서 영속성 컨텍스트에 저장한다.

 

 문제  일반 JOIN

Member member = em.createQuery("SELECT m FROM Member m JOIN m.team WHERE m.id = :id", Member.class)
                  .setParameter("id", 5L)
                  .getSingleResult();
                  
member.getTeam().getName();

* Hibernate

SELECT m FROM Member m JOIN m.team WHERE m.id = :id

 

* SQL

select 

    member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_

from

    Member member0_

left outer join Team team1_

on member0_.team_id = team1_.team_id

where member0_.member_id = ?

 

select

    team0_.team_id as team_id1_3_0_, team0_.name as name2_3_0_

from

    Team team0_

where

    team0_team_id = ?

 

JPQL로 명시적 조인을 사용하여 Member 와 Team을 한번에 가져왔다. Member와 Team은 즉시로딩으로 설정되어 있다.

하지만, SQL이 두 번 출력되는 문제가 발생한다.

 

JPQL에서 Member만 select 하고 Team은 select 하지 않았기 때문에 Member 엔티티에만 값이 채워져 있고 Team 엔티티는 비어있는 상태다.

team을 구하고 실제로 값을 사용할 때(위 코드에서 member.getTeam().getName()에서)

select 쿼리로 Team 엔티티를 가져온다.

=> 쿼리가 2번이 나간다 => N+1 문제가 발생한다.

 

 해결  페치 조인 사용 또는 명시적 조인

페치 조인

SELECT m from Member m JOIN FETCH m.team WHERE m.id = :id

명시적 조인

SELECT m, t FROM Member m JOIN m.team t WHERE m.id = :id

 

 결론 

일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다.

페치 조인 실행 시 연관된 엔티티를 함께 조회한다.

 

[페치 조인을 사용하는 이유]

- 대부분 엔티티 간의 연관관계는 지연로딩(LAZY)로 되어있다. 하지만 상황에 따라 연관된 엔티티를 한번에 함께 끌어외야 할 상황이 존재하기 때문에, 그럴 때 Fetch Join을 사용하여 즉시 로딩(EAGER)처럼 동작하게 한다. (*LAZY를 적용해도 페치 조인이 우선시 된다.)

 

[컬렉션 페치 조인]

- 컬렉션 페치 조인은 일대다 관계에서 사용할 수 있으며 데이터가 많아질 수 있다.

=> DISTINCT로 중복 제거가 가능하다.

 

* SQL의 DISTINCT

- ROW가 완벽히 일치해야 중복 제거

* JPQL의 DISTINCT

- SQL의 DISTINCT 기능 뿐만 아니라, 동일한 엔티티면 중복 제거(Application단에서 엔티티 중복을 제거한다.)

즉, 같은 식별자를 가진 Entity는 중복 제거가 가능하다.

 

[페치 조인의 특징과 한계]

 문제 

- 페치 조인의 대상에는 원칙상 별칭을 줄 수 없다.

    * Hibernate는 가능하지만, 가급적 사용을 지양한다.

- 둘 이상의 컬렉션은 페치 조인을 할 수 없다.

- 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

    * 일대일, 다대일같은 단일 값 연관 필드들은 페치 조인을 해도 페이징이 가능하다. (데이터 증복이 없음)

    * Hibernate는 경고로그를 남기며 메모리에서 페이징을 한다.

      일단, 전체 데이터를 가져와 메모리에 올려놓고, 메모리에서 N개씩 데이터를 결과로 보여준다. 자칫 OutOfMemory가 발생할 수 있고, 성능에 영향을 준다.

 

 해결 

1) @BatchSize

N+1 문제를 테이블 수 + 1로 줄인다. SQL의 IN 쿼리를 사용한다.

2) JPQL에서 DTO로 조회한다. SELECT new com.jiyoon.dto.TestDTO(...) from Test t ...

 

 결론 

- 모든 것을 페치 조인으로 해결할 수는 없다. QueryDSL을 사용하는 것이 바람직한 경우가 많다.

- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.

- 여러 테이블을 조인해 엔티티가 가진 모양이 다른 결과를 내야한다면, 페치 조인보다 일반 조인이 효과적이고, 필요한 데이터만 조회해 DTO로 반환하는 것이 좋다.

    * FETCH JOIN으로 엔티티 조회

    * Entity를 조회해 DTO로 변환

    * JPQL에서 바로 DTO를 조회

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# 아이템27. 비검사 경고를 제거하라

 

제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게될 것이다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등이다.
Set<String> set = new HashSet();
$ javac WarningTest.java -Xlint:unchecked
WarningTest.java:8: [unchecked] unchecked conversion
Set<String> set = new HashSet();
                      ^
required: Set<String>
found: HashSet
1 warning

 

* javac 명령 인수에 -Xlint:uncheck 를 추가하면 해당 에러를 볼 수 있다.
 
컴파일러가 알려준 대로 수정하면 경고가 사라진다. 사실 컴파일러가 알려준 타입 매개변수를 명시하지 않고, 자바 7부터 지원하는 다이아몬드 연산자(<>)만으로 해결할 수 있다. 그러면 컴파일러가 올바른 실제 타입 매개변수를 추론해준다.
Set<String> set = new HashSet<>();
제거하기 훨씬 어려운 경고도 있다. 할 수 있는 한 모든 비검사 경고를 제거하라. 모두 제거한다면 그 코드는 타입 안정성이 보장된다. 즉, 런타임에 ClassCastException이 발생할 일이 없고, 여러분이 의도한 대로 잘 동작하리라 확신할 수 있다.
경고를 제거할 수는 없지만 타입이 안전하다고 확신할 수 있다면 @SuppressWarning("unchecked") 어노테이션을 달아 경고를 숨기자.
단, 타입 안전함을 검증하지 않은 채 경고를 숨기면 스스로에게 잘못된 보안 인식을 심어주는 꼴이다. 그 코드는 경고없이 컴파일이 되겠지만, 런타임에는 여전히 ClassCastException을 던질 수 있다. 한편 안전하다고 검증된 비검사 경고를 그대로 두면, 진짜 문제를 알리는 새로운 경고가 나와도 눈치채지 못할 수 있다. 제거하지 않은 수많은 거짓 경고 속에 새로운 경고가 파묻힐 것이기 때문이다.
@SuppressWarnings 어노테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 @SuppressWarnings 어노테이션은 항상 가능한 좁은 범위에 적용하자. 보통은 변수 선언, 아주 짧은 메서드, 혹은 생성자가 될 것이다. 자칫 심각한 경고를 놓칠 수 있으니 절대로 클래스 전체에 적용해서는 안된다.
한줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 어노테이션을 발견하면 지역변수 선언 쪽으로 옮기자.
 

## 지역변수를 추가해 @SuppressWarning의 범위를 좁힌다.

public <T> T[] toArray(T[] a) {
  if (a.length < size) {
    // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환이다.
    @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
  }

  System.arraycopy(elements, 0, 1, 0, size);
  if (a.length > size)
    a[size] = null;
  return a;
}
이 코드는 깔끔하게 컴파일되고 비검사 경고를 숨기는 범위로 좁혔다.
@SuppressWarnings("unchecked") 어노테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !