시작하며

Index 정리 및 예시를 통해 학습한 내용을 정리하였다.

인덱스란?

 - 인덱스는 데이터베이스 테이블에 대한 검색 성능 속도를 향상시켜주는 자료 구조이다. 인덱스는 특정 컬럼(여러컬럼의 조합)에 대한 정렬된 값의 집합으로, 데이터베이스 엔진이 데이터를 빠르게 찾을수 있도록 도와준다.

Ex) 책의 목차라고 생각하면 이해가 쉽다. 책의 목차에서는 내가 찾고자 하는 페이지가 어디 있는지 빠르게 찾을 수 있도록 해준다.

 

인덱스는 데이터베이스 테이블의 특정 컬럼(여러 컬럼)의 값을 사전 순서 또는 정렬 순서로 저장하여 데이터 접근과 검색을 최적화한다.

일반적으로 B-트리(B-tree)나 해시 테이블 등의 자료구조를 사용하여 인덱스를 관리한다.

 

실생활에서의 인덱스 사용예제)

 

인덱스의 자료구조

해시 테이블(Hash Table)

- 해시 테이블은 Key, Value를 한쌍으로 데이터를 저장하는 자료구조이다. key값을 이용해 대응된는 value값을 구하는 방식이다. 평균적으로 0(1)의 매우 빠른 시간만에 원하는 데이터를 찾을 수 있지만, 실제 인덱스에서 잘 사용하지 않는다. 해시 테이블은 등호(=) 연산에 최적화 되어있기 떄문이다. 

- 데이터베이스에선 부등호(<. >) 연산이 자주 사용되는데, 해시 테이블 내의 데이터들은 정렬되어 있지 않으므로 특정 기준보다 크거나, 작은 값을 빠른시간내에 찾을 수가 없다.

B-Tree

- B-Tree는 자식 2개만을 갖는 이진트리(Binary Tree)를 확장하여 N개의 자식을 가질 수 있도록 고완된것이다. 

- B-Tree는 생성당시는 균형 트리이지만, 테이블 갱신의 반복을 통해서 균형이 깨지고, 성능이 약화된다.

 

B+ Tree

- B+ Tree는 B-Tree의 확장으로서, 오직 leaf node에만 데이터를 저장하고 leaf node가 아닌 node에서는 자식 포인터만 저장한다.

- Leaf node에만 데이터를 저장하므로, B-Tree에 비해 같은 node에 더 많은 키를 저장 할 수 있다.

- 데이터를 찾기 위해 leaf node 까지 탐색을 해야 하는데, Lined list로 연결되어 있기 때문에 full scan시 leaf node들만 순차 탐색하면
  되기때문에 B-Tree보다 탐색에 유리하다.

 

mysql의 경우 사용하는 엔진에 따라 다른 구조를 가진다.

Clustered Index vs Non Clustered Index

Clusterd Index 특징

  • 테이블 당 1개만 존재
  • PK 제약조건으로 컬럼을 생성시 자동 적용
  • 데이터가 정렬된 상태

Ex) 해당 DB Index 페이지에서 DDD라는 컬럼이 추가가 되는 상황이다.

       현재의 리프페이지에는 더이상 데이터가 추가될수 없기에 페이지 분할이 일어나며 DDD라는 컬럼이 추가가 되었다.

현재에서 KKK라는 컬럼이 추가시 루트페이지 및 리프페이지가 부족하다 리프페이지의 페이지 분할 및 루트추가 작업이 발생한다.

 

Non Clusterd Index 특징

  • Secondary Index(보조 인덱스) 라고도 한다.
  • 테이블에 여러개 존재 할 수 있다.
  • Unique 제약조건으로 컬럼을 생성시 자동 적용
  • 정렬되지 않아도 된다.
  • 리프페이지에서 데이터가 있는 곳의 주소를 가진다.
  • Clusterd Index에 비해 조회 속도가 느리지만, Insert, Update, Delete 시 부하가 적다.

※ PK를 Clusterd Index가 아닌 Non Clusterd Index로 적용도 가능하다.

CREATE TABLE TB_STUDENT (
  ID INT PRIMARY KEY NONCLUSTERED
  ...
)

어떠한 컬럼에 Index를 설정해야하는가?

  1. 핵심적인 기준 4가지
    • 카디널리티가 높은(↑) 컬럼
      - 카디널리티가 높다란, 한 컬럼이 갖고 있는 값의 중복도가 낮다는 뜻이다.
      Ex) 사람이라는 테이블을 기준으로 Gender라는 성별은 남, 여 만 존재한다. 이보다, 주민등록번호와 같은 고유값을 설정하는게 좋다.
    • 선택도가 낮은(↓) 컬럼
      - 선택도 = 카디널리티 / 전체레코드수, 선택도가 1이면 모든 데이터가 unique하다는 뜻이다.
    • 조회 활용도가 높은(↑) 컬럼 
      - Where의 대상 컬럼으로 많이 활용되어야 Index설정의 의미가 있다.
    • 수정빈도가 낮은(↓) 컬럼
  2. 그 밖의 Index 명시 사항
    • WHERE에 자주 사용되는 컬럼
    • LIKE와 사용할 경우에는 %가 뒤에 사용되도록 하기(%가 앞에 사용될 경우 Full Table Scan을 함.)
      Ex) LIKE 'index%'
    • ORDER BY에 자주 사용되는 컬럼에 사용하기
    • JOIN에 자주 사용되는 컬럼에 사용하기
    • 데이터의 변경이 없는 컬럼에 사용하기
    • WHERE 절 컬럼에 연산을 사용할 경우 Index를 사용하지 않는다.
      Ex) SELECT * FROM TB_TABLE WHERE COLUMN * 10 < 100              (X)
             SELECT * FROM TB_TABLE WHERE COLUMN < 100 * 10              (O)

그렇다면 Index는 몇개 설정해야 좋을까?

Index 설정 시 데이터베이스에 할당된 메모리를 사용하여 테이블 형태로 저장하게된다. 그렇기 때문에 무분별한 Index 설정은 메모리를 많이 사용하게되며, Index로 지정된 컬럼의 값이 바뀌게 되면 Index 테이블이 갱신되어야 하므로 느려 질 수 있다.

그렇기에 Index는 한 테이블당 3~5개가 적당하다.

Index 설정 및 삭제

-- Clusterd Index 설정
ALTER TABLE '테이블명' ADD CONSTRAINT '인덱스명' PRIMARY KEY ('컬럼명');

-- Non Clusterd Index 설정
ALTER TABLE '테이블명' ADD CONSTRAINT '인덱스명' UNIQUE ('컬럼명');

-- Clusterd Index 설정(단 PK가 아닌 값을 지정하기 때문에 Unique여야만 한다.) 
CREATE FULLTEXT INDEX '인덱스명' ON '테이블명'('컬럼명');

-- Non Clusterd Index 설정(중복 허용) 
CREATE INDEX '인덱스명' ON '테이블명' ('컬럼명');

-- Non Clusterd Index 설정(중복 비허용)
CREATE UNIQUE INDEX '인덱스명' ON '테이블명' ('컬럼명');

-- Non Clusterd Index 설정(다중 컬럼 인덱스 생성)
CREATE UNIQUE INDEX '인덱스명' ON '테이블명' ('컬럼명', '컬렴명');

-- Index 삭제
DROP INDEX '인덱스명' ON '테이블명'
ALTER TABLE '테이블명' DROP INDEX '인덱스명'

Index 성능 비교

예제)

CREATE TABLE TB_STUDENT(
ID INT AUTO_INCREMENT PRIMARY KEY, -- ID : PK(Clusterd Index)
NAME VARCHAR(20),
AGE INT,
CTN VARCHAR(20) UNIQUE -- CTN : Unique(Non Clusterd Index)
);

테이블에 등록된 Index 확인

show index from '테이블명';

테이블의 index 크기 확인

show table status like '테이블명';

SELECT

-- Clusterd Index
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';
select * from TB_STUDENT ts WHERE ID = 52345;
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';

Clusterd Index의 경우 하나의 데이터를 찾기까지 20건 이하의 페이지를 찾았다.

-- Non Clusterd Index
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';
select * from TB_STUDENT ts WHERE CTN = 9965-8467-0135;
SHOW GLOBAL STATUS LIKE 'Innodb_pages_read';

반면, Non Clusterd Index의 경우 상당히 많은 페이지를 찾았다.

해당 Test를 통해 Clusterd Index의 조회성능을 알 수 있다.

 

단일 컬럼 인덱스와 다중 컬럼 인덱스 차이

-- 단일 컬럼 인덱스 테이블
CREATE TABLE TB_STUDENT10(
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20) NOT NULL,
AGE INT NOT NULL,
CTN VARCHAR(20) NOT NULL,
PRIMARY KEY(ID),
INDEX idx_name(NAME),
INDEX idx_CTN(CTN)
);

-- 다중 컬럼 인덱스 테이블
CREATE TABLE TB_STUDENT20(
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20) NOT NULL,
AGE INT NOT NULL,
CTN VARCHAR(20) NOT NULL,
PRIMARY KEY(ID),
INDEX idx_NAME_CTN(NAME, CTN)
);

각각의 테이블에 프로시저를 이용하여 100만건의 데이터를 삽입하였다.

이후 SELECT 실행에 따른 EXPLAIN 결과이다.

EXPLAIN SELECT * FROM TB_STUDENT10 ts 
WHERE NAME = 'NAME934540' AND CTN = 'CTN934540';

EXPLAIN SELECT * FROM TB_STUDENT20 ts 
WHERE NAME = 'NAME934540' AND CTN = 'CTN934540';

다중 컬럼 인덱스가 단일 컬럼인덱스에 비해 검색 소요시간이 더욱 짧다.

1. DB 접근 기술

JDBC(Java Database Connectivity)

  • JDBC API를 이용하여 JDBC Driver의 변경에따른 DB 접근
  • 동작원리

  • 예제)
package sec01.ex01.dao;

import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;

import sec01.ex01.dto.MemberDTO;

public class MemberDAO {
   
   String driver = "oracle.jdbc.OracleDriver";
   String url = "jdbc:oracle:thin:@localhost:1521:xe";
   String user = "user1";
   String pwd = "1234";
   
   private Connection conn = null;
   private Statement stmt = null;
   private PreparedStatement pstmt = null;
   private ResultSet rs = null;
   
   private DataSource ds;

   public List<MemberDTO> listMember(){
      
      List<MemberDTO> list = new ArrayList<>();
      
      try {
         Class.forName(driver);
         System.out.println("Oracle 드라이버 로딩 성공");
         
         conn = DriverManager.getConnection(url, user, pwd);
         System.out.println("Connection 성공");
         
         stmt = conn.createStatement();
         System.out.println("Statment 생성 성공");
         
         String sql = "SELECT *FROM MEMBERTEST";
         
         rs =  stmt.executeQuery(sql);
         
         while(rs.next()) {
            String id = rs.getString("id");
            String pwd = rs.getString("pwd");
            String name = rs.getString("name");
            String email = rs.getString("email");
            Date joinDate = rs.getDate("joinDate");
            
            MemberDTO dto = new MemberDTO();
            dto.setId(id);
            dto.setPwd(pwd);
            dto.setName(name);
            dto.setEmail(email);
            dto.setJoinDate(joinDate);
            
            list.add(dto);
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {if(rs != null)  rs.close();} catch (Exception e2) {}
         try {if(stmt != null)stmt.close();} catch (Exception e2) {}
         try {if(conn != null)conn.close();} catch (Exception e2) {}
      }
      return list;
   }
}
  •  단점
    1. 쿼리를 실행하기 전, 후 많은 코드를 작성해야한다.
      • DB와의 연결 설정 및 객체 자원을 사용후 종료(close)해야한다.
    2.  DB 관련 로직에서 예외처리코드를 작성해야한다.
      • DB연결 설정에서 예외처리가 필수가 된다.
    3. 중복코드가 많다.
    4. 가독성이 좋지않다.
    5. 유지,보수가 어렵다.

SQL Mapper

  • 객체와 테이블 간의 관계를 직접 매핑하는 것이 아닌, SQL문을 실행해 쿼리 수행 결과를 어떤 객체에 바인딩 하는 방법이다.
  • 따라서 DBMS에 종속적인 방법이라고 할 수 있으며, 대표적으로 MyBatis가 있다

ORM

  • ORM 기술은 객체(Object)와 DB테이블을 매핑하여 데이터를 객체화하는 기술이다.
  • 개발자가 직접 SQL을 작성하지 않아도 자동으로 SQL문을 만들어내기 때문에 DBMS에 종속적이지 않다. 대표적으로 JPA가 있다.

2. MyBatis

  • Concept :  SQL과 Java 코드를 분리

MyBatis는 개발자에게 유연하고 효율적인 데이터베이스 액세스를 제공한다. SQL 중심적인 접근 방식과 매핑 기능을 통해 개발자는 데이터베이스 조작을 세밀하게 제어할 수 있으며, 성능 최적화와 단순성을 통해 개발 생산성과 코드의 가독성을 높인다.

 

3. JPA

    • Concept : DB 테이블을 매핑해서 데이터를 하나의 객체로 간주하고자 하며, 이러한
                         사상을
      통해서  DB와 객체지향의 패러다임 불일치를 해결하고자 한다.

JPA는 객체 지향적인 프로그래밍 모델을 사용하여 생산성을 향상시키고 유지보수를 용이하게 한다.

또한, 데이터 베이스에 대한 종속성을 줄이고 이식성을 높여서 유연성을 제공한다.

 

 

4. MyBatis 동작과정 및 장단점

동작과정

MyBatis데이터베이스 액세스 흐름

MyBatis를 사용하기 위한 기본적인 자바 인터페이스는 SqlSession이다. 이 인터페이스를 통해 명령어를 실행하고, 메퍼를 얻으며, 트랜잭션을 관리 할 수 있다. SqlSession은 인터페이스로 구현체인 SqlSessionTemplate이 있다. SqlSessionTemplate은 필요한 시점에 세션을 닫고, 커밋하거나 롤백하는 것을 포함한 세션의 생명주기를 관리한다.

 MyBatis공식문서

장단점

예시) 간략한 Team, Member라는 테이블이 있다.

DB테이블 구성도

Java Entity

두 테이블을 Join하여 모든 컬럼을 가져올때 객체지향적이지 않은, DB에 종속적인 방법을 사용하게된다.

이때 조회 결과값을 담을 Entity가없기때문에, 하나의 Entity를 만들어 사용하게된다.

※ 물론 Join 결과값을 Result <resultMap> 이라는 방법으로 1:1, 1:N. N:N  사용이 가능하나,

   실무 프로젝트 기준으로 생각하였을때 매우 복잡하다. 그렇기에 대다수의 사람들이 MyBatis를 이러한 방식으로 사용한다.

또한 insert시 Team에 대한 insert 후 useGeneratedKey방식을 이용하여 PK값을 얻어온이후 다시 Member에 대한 insert를 사용해야한다.

  • 장점
    1. SQL 리를 직접 작성하므로, 최적화된 쿼리를 구현 할 수 있다.
    2. 복잡한 쿼리도 구현 가능하다.
  • 단점
    1. 컴파일 시 오류를 확인 할 수없다.
    2. 쿼리를 직접 작성하기 때문에 JPA에 비해 생산성이 떨어진다.
    3. DB, Entity 변경 시 소스 및 쿼리 수정이 필요하다.
    4. 중복 쿼리가 발생하기 쉽다.

★ MyBatis 사용시 객체 지향적인 Java와 데이터 지향적인 DB의 DB에 종속적인 패러다임의 불일치가 발생한다.

 

 

5. JPA 동작과정 및 장단점

EntityManger

EntityManager는 데이터베이스와 상호 작용하는 주요한 인터페이스로, 엔티티의 영속성과 일관성을 유지하며, 데이터베이스 작업을 단순화하고 효율적으로 처리하는 데 도움을 준다.

  1. Entity 관리
    • EntityManager를 사용하여 엔티티를 영속성 컨텍스트에 등록하고, 상태 변경을 추적한다.
    •  엔티티를 조회, 저장, 수정, 삭제할 수 있는 기능을 제공한다.
  2. Transaction 관리
    • 트랜잭션의 시작과 종료를 관리하고, 트랜잭션 내에서 엔티티 작업을 수행한다.
    •  트랜잭션을 커밋 또는 롤백하여 데이터베이스의 일관성을 보장한다.
  3. 영속성 컨텍스트 관리

영속성 컨텍스트

영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

영속성 컨텍스트는 내부적으로 1차 캐시영역과 쓰기지연 SQL 저장소 영역이 있다.

영속성 컨텍스트 조회

  • 1차 캐시를 조회하여, 1차 캐시에 저장된 데이터가 있을경우 그대로 조회
  • 1차 캐시를 조회하여, 1차 캐시에 저장된 데이터가 없을경우 DB 조회 후, 1차 캐시에 저장한다.

엔티티 등록 - 쓰기 지연

엔티티 매니저는 데이터 변경 시 반드시 트랜잭션을 시작해야 한다.

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

// 트랜잭션 시작
tx.begin();

// 비영속
Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 영속
em.persist(member);

// 엔티티 등록
tx.commit();
  • em.persist(member); : member 엔티티를 영속 컨텍스트에 저장하지만, 데이터베이스에는 반영되지 않는다.
  • tx.commit(); : 트랜잭션을 커밋하는 순간 데이터베이스에 INSERT SQL을 보내 저장하게 된다.
  • em.persist()를 실행할 때, 영속 컨텍스트의 1차 캐시에는 member 엔티티가 저장되고, 쓰기 지연 SQL 저장소에는 member 엔티티의 INSERT SQL 쿼리문이 저장된다.
  • tx.commit()을 실행하는 순간 쓰기 지연 SQL 저장소에 저장된 INSERT SQL 쿼리를 보내 데이터베이스에 저장하는 것이다.

엔티티 수정 - 변경 감지

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

// 트랜잭션 시작
tx.begin();

// member 조회
Member member = em.find(Member.class, "member");
member.setUsername("hello");
member.setAge("20");

// 엔티티 등록
tx.commit();
  • 영속 컨텍스트의 1차 캐시에는 member의 초기 데이터가 저장되어 있을 것이다.
  • 이후 set 메서드를 통해 데이터를 변경한다.
  • 트랜잭션 커밋 시 flush()가 발생하면서 1차 캐시에서 엔티티와 스냅샷을 비교하여 변경에 대한 감지를 한다.
  • 이후 SQL UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에서 쿼리를 보낸다.

엔티티 삭제

Member member = em.find(Member.class, "member");

// 엔티티 삭제
em.remove(member);
  • 엔티티 삭제는 remove() 메서드를 통해 데이터를 삭제할 수 있다.
  • 영속성 컨텍스트와 데이터베이스에서 모두 제거된다.

엔티티 생명주기

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed): 영속성 컨텍스트에 저장된 상태
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed): 삭제된 상태

장단점

  • 장점
    1. 객체 지향적 프로그래밍 설계가 가능하다.
    2. 기본적인 SQL문법이 지원되기 때문에 생산성이 향상된다.
    3. 데이터베이스 방언이 지원되기 때문에 특정 데이터베이스에 종속되지 않는다.
  • 단점
    • QueryDsl을 사용하여도 MyBatis에 비해 복잡한 쿼리문이 구현하기 힘들다.
    • 러닝커브가 높다.

 

JPA의 경우 개발 생산성이 향상되는 장점은 있으나, 복잡한 쿼리문(통계)의 경우 myBatis 가 직관적이고 효율적이라고 생각하여

프로젝트의 특성에 맞게 사용하는것을 추천한다.

 

 

 

 

JPA

JPA는 Java Persistence API의 약자로, '자바 애플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스'이다.

자바 ORM(Object Relational Mapping) 기술에 대한 API 표준 명세이다. 나는 지금껏 '라이브러리' 정도로 생각하며, 사용하였다.

JPA는 단순한 인터페이스 이기 때문에 구현체가 없다. 이런 JPA의 구현체는 아래그림과 같다.

 

JPA를 사용하기 위해서는 JPA를 구현한 Hibernate, DataNucleus, EclipseLink 같은 ORM 프레임워크를 사용해야한다.

그중 Hibernate가 가장 범용적으로 다양한 기능을 제공한다고 한다.

3가지 ORM 프레임워크는 다음에 찾아보자.

 

Hibernate

Hibernate는 JPA 구현체 중 하나이며, 다음은 JPA와 Hibernate의 상속 및 구현 관계이다. 

JPA의 핵심인 EntityManagerFactory, EntityManager, EntityTransaction 인터페이스를 Hibernate는 각각 SessionFactory, Session, Transaction으로 상속받고 Impl로 구현하였다.

Hibernate는 JPA의 구현체이기때문에 다른 ORM프레임워크를 사용해도 되고, JPA는 인터페이스이기 때문에 본인이 구현하여 사용 할 수도 있다.

Hibernate 장점

  1. ORM 방식 사용
    • Hibernate은 자바 객체와 관계형 데이터베이스 간의 매핑을 간단하게 처리한다.
    • 개발자는 SQL 쿼리를 직접 작성하지않고도 객체를 데이터베이스에 저장 및 검색 등이 가능하다.
  2. 성능 최적화
    • Hibernate은 지연 로딩(Lazy loading) 및 캐시 기능을 지원하여 애플리케이션의 성능을 향상시킬 수 있다.
    • 지연 로딩은 필요한 시점에만 데이터를 로드하여 데이터베이스 액세스를 줄여준다.
  3. 트랜잭션 관리
    • Hibernate은 ACID (원자성, 일관성, 고립성, 지속성) 트랜잭션을 지원한다.
    • 트랜잭션 관리를 쉽게 할 수 있으며, 롤백과 커밋 기능을 제공한다.

Hibernate 단점

  1. 성능
    • 물론 SQL을 직접 작성하는 것보다 메서드 호출만으로 쿼리를 수행한다는 것은 성능이 떨어질 수 있다.
    • 실제로 초기의 ORM은 쿼리가 제대로 수행되지 않았고, 성능도 좋지 못했다한다.
    • 그러나 지금은 많이 발전하여, 좋은 성능을 보여주고 있고 계속 발전하고 있다.
  2. ​세밀함
    • 메서드 호출로 DB 데이터를 조작 하기 때문에 세밀함이 떨어진다.
    • 복잡한 통계 분석 쿼리를 메서드만으로 해결하는 것은 힘든 일이다. 이것을 보완하기 위해 SQL과 유사한 기술인 JPQL을 지원한다.
  3. ​러닝커브
    • JPA를 잘 사용하기 위해서는 알아야 할 것이 많습니다. 즉 러닝커브가 높습니다.

Spring Data JPA

Spring Data JPA는 Spring에서 제공하는 모듈 중 하나로 JPA를 쉽고 편하게 사용하도록 도와준다.

기존 JPA의 경우 EntityManager를 주입 받아 사용해야하지만, Spring Data JPA는 JPA를 한단계 더 추상화하킨 Repository 인터페이스를 제공한다.

 

Repository 구현 규칙

Method

 method  기능
 save()  레코드 저장 (insert, update)
 findOne()  primary key로 레코드 한건 찾기
 findAll()  전체 레코드 불러오기. 정렬(sort), 페이징(pageable) 가능
 count()  레코드 갯수
 delete()  레코드 삭제

Keyword

메서드 이름 키워드  샘플  설명
 And  findByEmailAndUserId(String email, String userId)  여러필드를 and 로 검색
 Or  findByEmailOrUserId(String email, String userId)  여러필드를 or 로 검색
 Between  findByCreatedAtBetween(Date fromDate, Date toDate)  필드의 두 값 사이에 있는 항목 검색
 LessThan  findByAgeGraterThanEqual(int age)  작은 항목 검색
 GreaterThanEqual  findByAgeGraterThanEqual(int age)  크거나 같은 항목 검색
 Like  findByNameLike(String name)  like 검색
 IsNull  findByJobIsNull()  null 인 항목 검색
 In  findByJob(String … jobs)  여러 값중에 하나인 항목 검색
 OrderBy  findByEmailOrderByNameAsc(String email)  검색 결과를 정렬하여 전달

 

 

Hibernate와 Spring Data JPA 차이점

Spring Data JPA의 경우 DB에 접근하는 상황에서는  Repository를 정의하여 사용한다. 개발자가 Repository 인터페이스에 정해진 규칙대로 메소드를 입력하면 Spring이 메소드 이름에 적합한 쿼리를 만든다. 또한 Hibernate의 경우 EntityManager를 통해 관리한다. 하지만 Spring Data JPA의 경우 EntityManger를 직접 다루지않고, SimpleJpaRepository의 내부적으로 EntityManager를 사용하고 있다. 

 

Spring Data JPA 공식문서

 

시작하며

현재 사용하고 있는방법을 기록해두고 더 좋은 코드를 찾을시 비교하기위한 기록용이다.

 

ex)

 @RequestPart(required = false, value = "img_file") MultipartFile imgFile
            , @Valid @RequestPart(value = "data") InsertRequest insertRequest
            , BindingResult bindingResult

1차. 입력값의 유무

if (insertRequest == null){
            throw  new RootException(ApiStatusCode.BAD_REQUEST, "잘못된 정보입니다.");
        }

2차. 입력값의 이상유무

       BidingResult를 사용하여 최초의 Request값에 대한 검증을 한다.

 

Request객체의 검증 어노테이션을 활용한다.

@NotNull, @NotBlank, @NotEmpty, @Size, @Min, @Max 등 

 if (bindingResult.hasErrors()) {
            log.info("BindingResult hasErrors");
            throw new RootException(ApiStatusCode.BAD_REQUEST,  "잘못된 정보입니다.");
        }

 

3차. Service 계층 세부 내용검증

 if (insertRequest.getStartDt().compareTo(insertRequest.getEndDt()) >= 1) {
            //0이면같음, 음수면 정상
            throw new RootException(ApiStatusCode.BAD_REQUEST, "노출 시작 일자가 종료 일자 보다 늦을 수 없습니다.");
        }

 

시작하며

Spring을 사용하며 @RequestParam, @RequestBody, @ModelAttribute 3가지 어노테이션을 활용하였다.

대충 어떻게 사용하는지는 알지만 한번 정리의 필요성을 느꼈다. 정리 고고

 

@RequestParam

public String getTest(@RequestParam("name") String name){
	System.out.println(name);
}

@RequestParam은 1개의 HTTP 요청 파라미터를 받기위해서 사용한다.

 

- value

- defaultValue

- required = true(기본설정 true / )false 설정가능

 

required = false 설정을 하지않고 파라미터를 전송하지않을시 400 에러가 발생한다.

@RequestBody

public String getTest(@RequestBody Test test){
	System.out.println(test);
}

Json형태의 HTTP Body내용을 Java 객체로 변환시켜주는 역할을 한다.

Get과 Post 방식 모두 사용은 가능하지만, @RequestBody는 Body안에 Json을 포함해야한다.

하지만 Get은 QueryParameter방식으로 보내기때문에 Get은 Get답게 Post는 Post답게 사용해야한다.

 

 

@ModelAttribute 

@Getter
@Setter
public class TestDto {
	private String name; 
	private int age; 
} 

public class TestController { 

	@RequestGetMapping(value = "/hi") 
	public void getTest(@ModelAttribute("test") TestDto test){ 
		System.out.println("이름 : " + test.getName());
		System.out.println("나이 : " + test.getAge()); 
	} 
}

@ModelAttribute는 Form형태 기반의 요청들을 받는다. Get Post 둘다 사용가능하다. Get의 경우는 Query Parameter로 요청데이터를 보내고, Post의 경우 x-www-from-urlencoded형태로 요청데이터를 보낸다.단, DTO에 바인딩시 Setter를 통해 바인딩된다. Setter가 없으면 바인딩되지않음!

myBatis 프로젝트를 사용하며 배운점들을 정리한다.

 

Dynamic Query는 상황에 다라 분기 처리를 통해 동적인 SQL문을 작성할 수 있다.

 

myBatis는 XML에서 쿼리를 작성하기 때문에 별도의 표기법이 필요하다.

정적쿼리

  <select id="findByFaq" resultType="FaqTotalEntity">
        SELECT  A.FAQ_ID
                , A.IS_VIEW
                , A.FAQ_TITLE
                , A.FAQ_CONTENT
                , A.START_DT
                , A.END_DT
                , B.IMAGE_ID
                , B.NAME
                , B.PATH
        FROM TB_FAQ A
            LEFT JOIN TB_FAQ_IMAGE B
            ON A.FAQ_ID = B.FAQ_ID
        WHERE A.FAQ_ID = #{faqId}
    </select>

if, where를 활용한 동적쿼리

<select id="findByTopList" resultType="FaqTotalEntity">
         SELECT  A.FAQ_ID
              , A.VIEW_COUNT
              , A.IS_VIEW
              , A.ADMIN_ID
              , A.FAQ_TITLE
              , A.IS_TOP
              , A.TOP_ORDER
              , A.START_DT
              , A.END_DT
              , B.PATH
         FROM TB_FAQ A
            LEFT JOIN TB_FAQ_IMAGE B
            ON A.FAQ_ID = B.FAQ_ID
        WHERE A.IS_TOP = 'Y'
        <if test='isView == "N" or isView == "Y"'>
            AND A.IS_VIEW = #{isView}
        </if>
        <if test="faqTitle != null">
            AND A.FAQ_TITLE LIKE CONCAT('%',#{faqTitle},'%')
        </if>
        <if test="startDt != null">
            AND A.START_DT <![CDATA[>=]]> #{startDt}
        </if>
        <if test="endDt != null">
            AND A.END_DT <![CDATA[<=]]> #{endDt}
        </if>
        ORDER BY
        <choose>
            <when test="order != null and orderType == 'ASC'">
                ${order} ASC
            </when>
            <when test="order != null and orderType == 'DESC'">
                ${order} DESC
            </when>
            <when test="order == null and orderType == null">
                A.TOP_ORDER
            </when>
        </choose>
    </select>

그외 choose - when - otherwise , trim, where, set 등 다양한 조건식이있다.

#{ }, ${ }의 차이점

위 동적쿼리문에서 orderby의 ${order}를 #{order}로 기존에는 사용하였다.

PostMan으로 API Request를 호출하였다.

그런데 쿼리문이 order by 정렬이 되지않은 상태로 출력되었다.

즉, DB의 결과값고 PostMan의 결과값이 달랐다.

 

Log상 myBatis의 쿼리문도 정상적으로 날라가는게 보였는데 원인을 찾지 못해 삽질을 시작했다.

 

그결과

#{ }는 파라미터 값이 ' '로 감싸져들어가고, ${ }는 파라미터 값이 그대로 들어간다.

${ }로 변경하니 정상 출력되었다.

 

 

참고사이트 : myBatis 공식홈페이지

getGeneratedKeys

프로젝트시 myBatis를 사용하여, API 요청시 2개의 테이블을 Insert 해야한다.

 

  • EX) Order라는 A 테이블과 OrderStatus라는 두개의 테이블이 존재한다.

A라는 테이블은 OrderID라는 PK값을 가지고, B라는 테이블은 OrderStatusID라는 PK를 가진다.

여기서 A테이블의 OrderID PK를 B테이블의 FK로 연결하지는 않았다.

※ OrderID의 PK이므로 중복이 존재하지않기에 JOIN 및 서브쿼리를 사용

이떄 하나의 API요청으로 두테이블은 모두 Insert하려한다.

 

API 요청시 Order 테이블의 Insert로 OrderID의 PK(auto_increment)가 생성이될때,

해당 PK값을 반환하여 OrderStatus 테이블을 Insert하려한다.

 

mapper.xml 설정

useGeneratedKeys="true" keyProperty="Entity의 PK 필드명"

 

myBatis 공식문서 의 내용으로 "생성키에 대한 JDBC 지원을 허용. 지원하는 드라이버가 필요하다. true 로 설정하면 생성키를 강제로 생성한다. 일부 드라이버(예를들면, Derby)에서는 이 설정을 무시한다." 라고 명시되어있다.

 

해당 내용을 해석해보면 Insert 후 PK값을 반환하는것이 아닌, PK값을 키를 먼저 반환한다고 생각이된다.(개인의 생각)

 

Insert 부분에는 당연히 PK는  자동생성이기에 입력되지않는다.

해당 설정을 사용하면 Entity에 PK값이 반환되어 .get시 PK값이 출력이된다.

 

참으로 신기하다.

 

참고 사이트 : myBatis 공식문서

 

 

 

시작하며

@Transactional JPA를 활용한 프로젝트를 개발하였지만, 깊이 있는 이해도는 없었다. 명확하게 설명하지 못한다면,

제대로 알고있는게 아니라 생각한다. 공부내용 정리글이다.

 

Transactional 정의

데이터베이스에서 트랜잭션은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다.

여기서 단위는 더이상 쪼개질 수 없는 최소의 연산이다.

Transactional 특징

  • 원자성(Atomicity): 트랜잭션의 모든 작업이 완전히 성공하거나 완전히 실패하는 단일 단위로 처리되도록 보장하는 능력이다. 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.
  • 일관성(Consistency): 각 데이터 트랜잭션이 데이터베이스를 일관성 있는 상태에서 일관성 있는 상태로 이동해야 함을 의미한다. 즉, 트랜잭션이 성공적으로 완료하면 언제나 동일한 데이터베이스 상태로 유지하는 것을 의미한다.
  • 격리성(Isolation): 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것을 의미한다. 이것은 트랜잭션 밖에 있는 어떤 연산도 중간 단계의 데이터를 볼 수 없음을 의미한다. 즉, 데이터베이스는 스트레스 테스트를 통과해야 한다. 과부하로 인해 잘못된 데이터베이스 트랜잭션이 발생하지 않아야 한다.
  • 지속성(Durability): 성공적으로 수행된 트랜잭션은 영원히 반영(기록)되어야 함을 의미한다. 트랜잭션은 로그에 모든 것이 저장된 후에만 commit 상태로 간주 될 수 있다. 데이터베이스내의 데이터는 트랜잭션의 결과로만 변경되어야 하며, 외부 영향에 의해 변경 될 수 없어야 한다.
  • 데이터베이스 트랜잭션 처리에 있어서 중요한 요소이며, 데이터베이스가 제공하는 일관성과 안정성을 보장합니다.

REST 구성

  • 자원(Resource) - URI
  • 행위(Verb) - HTTP Method
  • 표현(Representations)

REST 특징

  1. 자원(Resource) - URI를 통해 자원을 명시하고, HTTP 메소드를 사용하여 해당 자원에 대한 CRUD 작업을 수행합니다.
  2. 메시지(Message) - RESTful API에서는 HTTP 프로토콜의 메시지 포맷을 이용하여 요청과 응답을 처리합니다. 예를 들어, JSON, XML 등의 포맷을 사용할 수 있습니다.
  3. Stateless(무상태성) - RESTful API는 서버 측에서 상태 정보를 유지하지 않고, 요청에 대한 응답만 전달합니다. 이러한 특징을 가지기 때문에, 서버의 확장성과 성능을 높일 수 있습니다.
  4. 캐시(Cache) - RESTful API에서는 HTTP 프로토콜의 캐시 기능을 이용하여 캐시를 적극적으로 사용할 수 있습니다. 이를 통해, 클라이언트와 서버 간의 통신을 최적화할 수 있습니다.
  5. 인터페이스(Interface) - RESTful API는 클라이언트와 서버 간의 인터페이스를 일관성 있게 유지하도록 설계됩니다. 이를 통해, 클라이언트와 서버 간의 상호 작용을 단순화하고, 상호 운용성(interoperability)을 향상시킬 수 있습니다.
  6. 계층형 구조(Layered System) - RESTful API에서는 서버와 클라이언트 사이에 다양한 계층을 둘 수 있습니다. 이를 통해, 시스템의 확장성과 보안성을 향상시킬 수 있습니다.
  7. 자체 표현(Self-descriptive) - RESTful API는 메시지 자체에 대한 정보를 포함하고 있기 때문에, 메시지를 이해할 수 있는 자체 표현 형식을 사용합니다. 이를 통해, 클라이언트와 서버 간의 상호 작용을 간소화하고, 확장성을 향상시킬 수 있습니다.

RESTful API 디자인 가이드

1. URI 마지막에 슬래시(/)를 포함하지 않는다.

 - URI 경로 마지막은 반드시 문자여야 한다. 슬래시(/) 다음에 의미 있는 값을 추가하지 않으면 혼동이 올 수 있다.

X : http://api.test.com/users/
O : http://api.test.com/users

2. 슬래시(/)를 사용하여 계층적 관계를 나타낸다.

 - 슬래시 문자는 리소스 간의 계층적 관계를 나타내기 위해 URI 경로에 사용된다.

ex) http://api.test.com/users/1/posts

3. URI 가독성을 높이려면 하이픈(-)을 사용해야한다.

 - URI를 사람들이 쉽게 스캔하고 해석 할 수 있도록 하이픈(-) 문자를 사용하여 가독성을 높인다. 영어로 공백이나 하이픈을 사용하면 URI에서는 모두 하이픈을 사용해야 한다.

X : http://api.test.com/users/posts_comments
O : http://api.test.com/users/posts-comments

4. URI는 소문자를 사용해야한다.

X : http://api.test.com/users/postsComments
O : http://api.test.com/users/posts-comments

5. 행위(Method)는 URI에 포함하지 않는다.

X : http://api.test.com/users/1/delete-post/1
O : http://api.test.com/users/1/posts/1

6. 파일 확장자는 URI에 포함하지 않는다.

X : http://api.test.com/users/1/posts/1/photo.jpg
O : http://api.test.com/users/1/posts/1/photo

 

HTTP 응답 상태코드

상태코드

200 클라이언트의 요청을 정상적으로 수행함
201 클라이언트가 어떠한 리소스 생성을 요청, 해당 리소스가 성공적으로 생성됨
(POST를 통한 리소스 생성 작업 시)

상태코드

400 클라이언트의 요청이 부적절 할 경우 사용하는 응답 코드
401 클라이언트가 인증되지 않은 상태에서 보호된 리소스를 요청했을 때 사용하는 응답 코드
  (로그인 하지 않은 유저가 로그인 했을 때, 요청 가능한 리소스를 요청했을 때)
403 유저 인증상태와 관계 없이 응답하고 싶지 않은 리소스를 클라이언트가 요청했을 때 사용하는 응답 코드
  (403 보다는 400이나 404를 사용할 것을 권고. 403 자체가 리소스가 존재한다는 뜻이기 때문에)
405 클라이언트가 요청한 리소스에서는 사용 불가능한 Method를 이용했을 경우 사용하는 응답 코드

상태코드

301 클라이언트가 요청한 리소스에 대한 URI가 변경 되었을 때 사용하는 응답 코드
  (응답 시 Location header에 변경된 URI를 적어 줘야 합니다.)
500 서버에 문제가 있을 경우 사용하는 응답 코드

Servlet => Spring => Spring Boot 순서로 공부를하며 DTO와 Entity의 차이점은 간략하게 파악하였다.

하지만 DTO와 VO는 어떠한 차이점이 있을까?

여러 참고 사이트를 비교하며 정리해본다.

DTO(Data Transfer Object)

DTO는 데이터를 전달하기 위한 객체이다. 계층간의 Getter / Setter를 이용하여 데이터를 주고 받는다.

여러 레이어에서 사용할 수 있지만, 주로 View와 Controller사이에서 사용한다.

DTO는 Getter / Setter 메소드를 포함하고, 그 외의 비즈니스 로직은 포함하지 않는다.

EX)

public class MemberDto {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Setter가 아닌 생성자를 이용해서 초기화하는 경우 불변 객체로 활용 할 수 있다. 불변 객체로 만들시 데이터를 전달하는 과정에서 데이터가 변조되지 않는다.

public class MemberDto {
    private final String name;
    private final int age;

    public MemberDto(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

VO(Value Object)

VO는 값 자체를 표현하는 객체이다. VO는 객체들의 주소가 달라도 값이 같으면 동일한것으로 여긴다. 

 EX) 고유번호가 다른 만원 2장이 있다. 이 둘은 고유번호(주소)는 다르지만 값(10,000원)은 동일하다.

VO는 Getter 메소드와 함께 비즈니스 로직도 포함 할 수 있다. 단, Setter 메서드는 포함하지 않고, 값의 비교를 위해

equals()와 hashCode() 메소드를 오버라이딩 해줘야한다.

public class Money {
    private final String currency;
    private final int value;

    public Money(String currency, int value) {
        this.currency = currency;
        this.value = value;
    }

    public String getCurrency() {
        return currency;
    }

    public int getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return value == money.value && Objects.equals(currency, money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(currency, value);
    }
}

// MoneyTest.java
public class MoneyTest {
    @DisplayName("VO 동등비교를 한다.")
    @Test
    void isSameObjects() {
        Money money1 = new Money("원", 10000);
        Money money2 = new Money("원", 10000);

        assertThat(money1).isEqualTo(money2);
        assertThat(money1).hasSameHashCodeAs(money2);
    }
}

EX) equals()와 hashCode() 메서드를 오버라이딩 하지 않았을 때

EX) equals()와 hashCode() 메서드를 오버라이딩 하였을 때

Entity

Entity는 실제 DB 테이블과 매핑되는 핵심 클래스이다. 이를 기준으로 테이블이 생성되고 스키마가 변경된다.

따라서, 절대로 Entity를 요청이나 응답값을 전달하는 클래스로 사용해서는 안된다.

그리고 비즈니스 로직을 포함할 수 있다.

public class Member {
    private final Long id;
    private final String email;
    private final String password;
    private final Integer age;

    public Member() {
    }

    public Member(Long id, String email, String password, Integer age) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.age = age;
    }
}

세 객체 비교

분류 DTO VO Entity
정의 레이어간 데이터 전송용 객체 값 표현용 객체 DB 테이블 매핑용 객체
상태 변경 여부 Setter 존재시 가변,
Setter 비 존재시 불변
불변 Setter 존재시 가변,
Setter 비 존재시 불변
로직 포함 여부 로직을 포함 할 수 없다. 로직을 포함 할 수 있다. 로직을 포함 할 수 있다.

 

참고 사이트 : 인비의 DTO vs VO

+ Recent posts