database

데이터베이스 데이터 접근 기술

HJHStudy 2024. 4. 20. 05:22
728x90

자바에서 사용하는 데이터베이스 접근 기술로는 jdbctemplate, mybatis, jpa, 스프링데이터 jpa, querydsl 등이 있다.

이번 포스팅에서 다룰 내용은  jdbctemplate, mybatis이다. jpa 부분은 다른 카테고리에서 자세하게 다룰 예정이다.

 

jdbctemplate에 대해 알아보자.

JdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있는데, 이 라이브러리는 스프링으로 JDBC를
사용할 때 기본으로 사용되는 라이브러리이다. 그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.

템플릿 콜백 패턴을 사용해서 직접 jdbc를 직접 사용할 때 발생하는 반복 작업을 대신 처리해준다.

sql을 작성하고 전달할 파라미터를 정의하고 응답 값을 매핑하면 된다.

단점으로는 동적 sql을 해결하기 어렵다.

//JdbcTemplate 추가
 implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'

 

gradle에 이 부분을 추가해서 JdbcTemplate과 h2 데이터베이스를 사용할 준비를 한다.

 

여기서 h2 데이터베이스를 사용하는 설정을 알아보자.

h2의 홈페이지에서 h2를 다운로드하여야 한다.

아래의 링크는 h2를 다운로드할 수 있다.

https://www.h2database.com/html/download-archive.html

 

Archive Downloads

 

www.h2database.com

 

다운로드한 후 설치된 디렉터리로 이동해서 실행권한을 주고 ./h2 sh로 실행하면 된다. (mac)

윈도우 사용자는 h2.bat으로 실행해 주면 된다.

실행하면 h2데이터베이스 서버가 켜진다.

 

실행한 후 데이터베이스 파일 생성 방법은 사용자명은 sa로 두고 첫 연결에서 jdbc url은

jdbc:h2:~/test 입력 후 연결을 눌러준 후 ~/test.mv.db 파일 생성이 되었는지 확인하고 위 이미지에 보이는 url로 접속하면 된다. 비밀번호는 필요하지 않다.

 

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    username: sa
    password:
    driver-class-name: org.h2.Driver

 

application.yml에 h2의 설정하는 부분을 넣어주면 h2를 사용할 수 있다.

yml 설정은 들여 쓰기가 중요하므로 잘 지켜야 한다.

 

JdbcTemplate으로 다시 돌아와서

jdbc에 대한 

 

drop table if exists item CASCADE;
 create table item
 (
     id        bigint generated by default as identity,
     item_name varchar(10),
     price     integer,
     quantity  integer,
     primary key (id)
 );

우선 예제를 통해 알아보기 위해 item 테이블을 만들어준다.

 

 

데이터를 저장하는 예제로 기능들을 알아보자.

 

@Data
public class Item {

    private Long id;

    private String itemName;
    private Integer price;
    private Integer quantity;

}

테이블 매핑과 객체로 사용할 Item이다. id, 이름, 가격, 수량을 필드로 가지고 있다.

 

@Data
public class ItemSearchCond {

    private String itemName;
    private Integer maxPrice;

    public ItemSearchCond() {
    }

    public ItemSearchCond(String itemName, Integer maxPrice) {
        this.itemName = itemName;
        this.maxPrice = maxPrice;
    }
}

item의 검색 조건으로 사용되는 객체이다.

 

@Data
public class ItemUpdateDto {
    private String itemName;
    private Integer price;
    private Integer quantity;

    public ItemUpdateDto() {
    }

    public ItemUpdateDto(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

상품을 수정할 때 사용하는 객체로 데이터를 전송할 목적으로 만들어서 dto를 붙였다.

data transfet object로 기능은 없이 데이터를 전달할 목적으로 사용되는 객체이다.

꼭 dto를 붙이지 않아도 되고 이전에 만든 cond도 dto라고 할 수 있다. 이름을 다르게 하고 굳이 cond, dto를 붙여서 기능을 다르게 설정하는 것은 명시적으로 어떤 목적을 가졌는지 나타내기 위함으로 규칙은 일관성 있게 관리하는 것이 좋다.

추후 API 설명에서도 하겠지만 이미 객체가 있지만 dto를 만들고 cond를 만들어서 사용하는 이유는 실제 객체의 변경 즉, set을 막아두는 것이 좋다. 자세한 설명은 JPA 부분에서 더 하겠지만 실제 객체가 여러 군데에서 수정하고 생성되면 어디서 변경되었는지 추적도 어려워지며 실제 객체의 코드도 난잡해진다. 실제 객체의 사용보다 사용목적이 뚜렷한 객체를 만들어서 사용하는 편이 좋기 때문이다.

 

 

@Slf4j
@Repository
public class JdbcTemplateItemRepository{

    private final JdbcTemplate template;

    //JdbcTemplate은 datasource가 필요하다. datasource를 의존관계로 주입 받아 내부에서
    //JdbcTemplate을 생성해 사용한다. 직접 스프링 빈으로 등록하고 사용해도 되지만 이 사용법을 주로 사용한다.
    public JdbcTemplateItemRepository(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    public Item save(Item item) {
        String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
        //db에서 생성된 아이디 값을 가져오기 위한 구문
        KeyHolder keyHolder = new GeneratedKeyHolder();
        //데이터 변경시 update() 사용
        template.update(connection -> {
            //자동 증가 키
            //KeyHolder와 connection.prepareStatement(sql, new String[]{"id"})를 사용해서 id를 지정해주면 INSERT쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다.
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, item.getItemName());
            ps.setInt(2, item.getPrice());
            ps.setInt(3, item.getQuantity());
            return ps;
        }, keyHolder);

        long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

    public void update(Long itemId, ItemUpdateDto updateParam) {
        //dto를 사용해 수정할 데이터를 ?에 바인딩할 파라미터를 순서대로 전달하면 된다.
        String sql = "update item set item_name=?, price=?, quantity=? where id=?";
        template.update(sql,
                updateParam.getItemName(),
                updateParam.getPrice(),
                updateParam.getQuantity(),
                itemId);
    }

    public Optional<Item> findById(Long id) {
        //데이터 결과 로우를 하나를 반환한다.
        String sql = "select id, item_name, price, quantity from item where id =?";
        try {

            Item item = template.queryForObject(sql, itemRowMapper(), id);
            return Optional.of(item);
            //결과가 없을 때 Optional을 반환하기 때문에 예외를 잡아 empty로 반환.
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    public List<Item> findAll(ItemSearchCond cond) {
        //데이터를 리스트로 조회한다. 검색 조건으로 적절한 데이터를 찹는다.
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        //작성된 sql에 뒤에 if문을 사용해서 동적으로 sql += 사용해 검색하는 값에 따라 분기점을 두어 쿼리를 완성되게 해 동적 sql을 작성하게 했다.
        String sql = "select id, item_name, price, quantity from item";
        //동적쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += " where";
        }
        boolean andFlag = false;
        List<Object> param = new ArrayList<>();
        if (StringUtils.hasText(itemName)) {
            sql += " item_name like concat('%',?,'%')";
            param.add(itemName);
            andFlag = true;
        }
        if (maxPrice != null) {
            if (andFlag) {
                sql += " and";
            }
            sql += " price <= ?";
            param.add(maxPrice);
        }
        log.info("sql={}", sql);


        return template.query(sql, itemRowMapper(), param.toArray());
    }
    private RowMapper<Item> itemRowMapper() {
        //조회결과를 객체로 변환할 때 사용
        return ((rs, rowNum) -> {
            Item item = new Item();
            item.setId(rs.getLong("id"));
            item.setItemName(rs.getString("item_name"));
            item.setPrice(rs.getInt("price"));
            item.setQuantity(rs.getInt("quantity"));
            return item;
        });
    }

}

동작에 필요한 부분은 주석을 사용해 설명해 두었다.

 

이 코드에 문제점은 파라미터를 순서대로 바인딩하는 것은 편리하지만 순서가 맞지 않게 되면 의도와 다른 값이 들어가게 된다.

이 문제를 해결하기 위해서 NamedParameterJdbcTemplate를 사용해 이름을 지정해 바인딩하는 기능을 제공한다.

 

save를 예시로 들어보도록 하자.

public class NamedParameterRepository {

    private final NamedParameterJdbcTemplate template;
    public NamedParameterRepository(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }

    public Item save(Item item) {
        String sql = "insert into item (item_name, price, quantity) " +
                "values (:itemName, :price, :quantity)";
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(sql, param, keyHolder);
        Long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }
    
}

위에 예시와 다르게? 대신 :파라미터 이름을 받는 것을 볼 수 있다.

 

파라미터를 전달할 때 key와 value 구조를 통해 전달할 수 있다.

단순히 map을 사용할 수 있고 MapSqlParameterSource를 통한 메서드 체인을 통해서도 가능하다. 마지막으로 BeanPropertySqlParameterSource 자바빈 프로퍼티 규약을 통해서도 자동으로 파라미터 객체를 생성할 수 있다.

 

 

insert sql을 직접 작성하지 않도록 SimpleJdbcInsert를 제공한다.

 

 	private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepository(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
        this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName("item")
                .usingGeneratedKeyColumns("id");
    }

 
    public Item save(Item item) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }

스프링에서 JdbcTemplate을 사용할 때 이 방법을 많이 사용한다. SimpleJdbcInsert를 스프링빈으로 직접 등록하고 주입받아도 된다.

withTableName : 데이터를 저장할 테이블 명을 지정한다.

usingGeneratedKeyColumns : key를 생성하는 PK 컬럼 명을 지정한다.

usingColumns: INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다. 생략할 수 있다.

 

스프링 JdbcTemplate 사용 방법 공식 매뉴얼은 아래에 링크에서 확인할 수 있다.

https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate

 

다음은 MyBatis에 대해 알아보자.

JdbcTemplate보다 더 많은 기능을 제공하는 SQL Mapper이다.

장점으로 SQL을 XML에 편리하게 작성할 수 있다. 동적쿼리도 포함

 

JdbcTemplate

String sql = "update item " +
         "set item_name=:itemName, price=:price, quantity=:quantity " +
         "where id=:id";

 

Mybaits

<update id="update">
     update item
     set item_name=#{itemName},
         price=#{price},
         quantity=#{quantity}
     where id = #{id}
</update>

두 코드의 차이를 보면 라인이 길어져도 불편함이 없다.

 

<select id="findAll" resultType="Item">
     select id, item_name, price, quantity
     from item
     <where>
         <if test="itemName != null and itemName != ''">
             and item_name like concat('%',#{itemName},'%')
         </if>
         <if test="maxPrice != null">
             and price &lt;= #{maxPrice}
         </if>
     </where>
 </select>

동적 쿼리를 작성하는 부분도 훨씬 깔끔하다.

 

MyBatis 공식 사이트 아래에 링크에서 기능들을 찾아 사용할 수 있다.

https://mybatis.org/mybatis-3/ko/index.html

 

mybatis – 마이바티스 3 | 소개

마이바티스는 무엇인가? 마이바티스는 개발자가 지정한 SQL, 저장프로시저 그리고 몇가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크이다. 마이바티스는 JDBC로 처리하는 상당부분의 코드와

mybatis.org

 

 

mybatis gradle 설정이다.

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'

 

자바 객체와 데이터베이스의 관례 불일치에 대해 알아보도록 하겠다.

자바 객체는 카멜 표기법을 사용한다. 하지만 관계형 데이터베이스는 언더스코어를 사용한 snake_case 표기법을 사용한다.

map-underscore-to-camel-case 기능 활성화시 언더스코어 표가법을 카멜 표기법으로 자동으로 변환해 준다.

 

컬럼 이름과 객체 이름이 다를 경우 SQL에서 별칭을 사용하면 된다.

ex) selet item_name as name

 

mybatis도 예제 하나 확인해 보도록 하겠다.

 

@Mapper
public interface ItemMapper {
		void save(Item item);
        
		void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto
	updateParam);
    
    Optional<Item> findById(Long id);
    
    List<Item> findAll(ItemSearchCond itemSearch);
}

 

마이바티스 매핑 XML을 호출해 주는 매퍼 인터페이스이다.

이 인터페이스에는 @Mapper애노테이션을 붙여주어야 한다. 그래야 MyBatis에서 인식할 수 있다.

 

같은 위치에 실행할 SQL이 있는 XML 매핑 파일을 만들어주면 된다.

자바 코드가 아니기 때문에 src/main/resources 하위에 만들되, 패키지 위치는 맞추어 주어야 한다.

 

<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
     <insert id="save" useGeneratedKeys="true" keyProperty="id">
         insert into item (item_name, price, quantity)
         values (#{itemName}, #{price}, #{quantity})
</insert>
</mapper>

 

id 에는 매퍼 인터페이스에 설정한 메서드 이름을 지정하면 된다. 여기서는 메서드 이름이 save()이므로 save로 지정하면 된다.
파라미터는 #{} 문법을 사용하면 된다. 그리고 매퍼에서 넘긴 객체의 프로퍼티 이름을 적어주면 된다.  #{} 문법을 사용하면 PreparedStatement를 사용한다. JDBC의 ?를 치환한다 생각하면 된다.

useGeneratedKeys는 데이터베이스가 키를 생성해 주는 IDENTITY전략일 때 사용한다.

keyPropert는 생성되는 키의 속성 이름을 지정한다. Insert가 끝나면 item객체의 id속성에 생성된 값이 입력된다.

 

XML에서 특수 문자를 사용할 때 <>는 태그에서 사용하기 때문에

< : &lt;

> : &gt;

& : &amp;

이외에는 xml에서 지원하는 CDATA 구문 문법을 사용해야 한다.

 <![CDATA[
             and price <= #{maxPrice}
             ]]>

 

이 코드 내에서는 단순 문자로 인식된다.

 

where 문을 사용한 동적 쿼리이다.

<select id="findActiveBlogLike"
      resultType="Blog">
   SELECT * FROM BLOG
   <where>
     <if test="state != null">
          state = #{state}
     </if>
     <if test="title != null">
         AND title like #{title}
     </if>
     <if test="author != null and author.name != null">
         AND author_name like #{author.name}
     </if>
   </where>
</select>

 

MyBatis 동작 원리는 

@Mapper를 조회하고 동적 프록시 객체를 생성하고 생성된 동적 프록시 객체를 스프링 빈으로 등록하는 동작방식이다.

Mapper를 사용한 클래스를 출력하면 프록시가 적용된 것을 확인할 수 있다.

 

 

여기까지 JdbcTemplate과 mybatis를 알아보았다. 아주 간단한 사용법과 과정을 알아보았다.

사실 최근 거의 자바에서의 데이터 접근 기술은 JPA와 QueryDSL을 사용하기에 간단한 사용법을 알아보았고 JPA와 QueryDSL을 사용하고도 구현하기 어려운 쿼리도 있는데 그 부분을 JdbcTemplate과 mybatis가 해결해 주기도 한다.

 

 

본 포스팅은 인프런의 김영한 강사님의 스프링 스프링 DB 2편의 코드, 내용을 일부 발췌하여 사용했습니다.

https://inf.run/wREBZ

 

스프링 DB 2편 - 데이터 접근 활용 기술 | 김영한 - 인프런

김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드

www.inflearn.com

 

728x90

'database' 카테고리의 다른 글

CDC (Change Data Capture) 변경 데이터 캡처  (0) 2025.05.11
PostgreSQL에 대해서  (0) 2024.06.25
springboot 트랜잭션 AOP와 트랜잭션 전파  (0) 2024.04.21
Transaction에 대해서  (1) 2024.04.19
데이터 베이스와 JDBC, ORM  (1) 2024.04.18