개발자로 후회없는 삶 살기

spring PART.스프링 JdbcTemplate 적용 본문

[백엔드]/[spring | 학습기록]

spring PART.스프링 JdbcTemplate 적용

몽이장쥰 2023. 5. 14. 14:30

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com:443

 

본론

- jdbc 탬프릿 소개

sql을 직접 사용하는 경우 jdbc 탬프릿을 사용하면 매우 편리합니다.

 


-> 장점

jdbc 탬플릿은 이전에 spring jdbc 라이브러리에 포함되어 있어서 설정할게 없고 반복문제를 해결해 줍니다. 개발자는 sql을 작성하고 전달할 파라미터를 정의하고 응답값을 매핑하기만 하면됩니다.

 

-> 단점

동적 sql을 해결하기 어렸습니다. 이를 MyBatis에서 해결합니다.

 

- jdbc 탬플릿 적용

jdbc 레포지토리를 만들어보겠습니다. 레포지토리 패키지 밑에 메모리 패키지가 있었는데 이번엔 jdbc 탬플릿 패키지를 만들고 하위에 구현체를 만듭니다. 이렇게 패키지 구조를 잡고 인터페이스 구현체를 구현합니다.

 

@Slf4j
@Repository
public class JdbcTemplateItemRepository implements ItemRepository {
    private final JdbcTemplate template;

    public JdbcTemplateItemRepository(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

탬플릿은 생성해야합니다. 생성자로 데이터 소스를 넣어줘야합니다. 커낵션을 내부에서 하기 때문입니다. 탬플릿을 빈 등록이 아닌 이 방법을 관례로 사용합니다.

 

-> 이전과 비교

con = getConnection(); 
pstmt =  con.prepareStatement(sql);
pstmt.setString(1, memberId);

rs = pstmt.executeQuery();
if (rs.next()) {
    Member member = new Member();
    member.setMemberId(rs.getString("member_id"));
    member.setMoney(rs.getInt("money"));
    return member;
} else {
    throw new NoSuchElementException("member not found memberId = " + memberId);
}

jdbc를 직접 사용할 때와 비교해보면 con을 열고 con으로 pstmt를 만들고 set으로 ?에 sql로 db에 전달할 값을 넣었습니다. ps는 sql을 db에 전달하는 인터페이스입니다. ps.execute하면 db로 sql이 전달되고 실행되고 다른 crud는 끝나는데 조회의 경우 db로부터 rs로 응답을 받습니다.

 

> rs에는 응답 결과가 들어가 있는데 rs.next()로 rs를 한 행씩 읽으면서 읽은 값을 객체에 set하고 조회는 자바 코드로 findById이면 set한 객체를 반환합니다. 레포에서 하는 메서드가 sql 그 자체이고 근데 그걸 자바 코드로 사용하니 인터에 정의한 대로 Item을 반환하도록 하는 것입니다.

 

-> 재정의

crud 메서드를 새로 작성합니다. 

 

1. 삽입

@Override
public Item save(Item item) {
    String sql = "insert into item (item_name, price, quantity) values (?, ?, ?)";
    KeyHolder keyHolder = new GeneratedKeyHolder();
    template.update(connection -> {
        //자동 증가 키
        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;
}

이전에 jdbc를 직접 사용할 때도 그렇지만 시작은 무조건 먼저 sql을 적습니다. 보면 id를 넣지 않는데 db가 자동으로 부여하기 때문입니다. 반환을 저장한 item을 하는데 매개변수 item은 id가 없으므로 db에서 생성해준 id 값을 가져와서 item에 set하고 반환해야 합니다. 가져오려면 키 홀더를 사용해야합니다. 가져오기 전에 먼저 insert sql을 실행해야하니 update를 하고 커낵션하고 db에 sql을 전달하는 ps 로직을 짜야합니다.

 

> 로직에 자동 증카 키를 적용하는 ps 로직을 짭니다. id를 인자로 주면서 ps를 만들고 ps에 전달할 파라미터를 상품명, 가격, 수량 넣습니다. ps가 sql ?에 파라미터를 동적으로 맵핑하는 것입니다. ps에 원래는 ?에 있는 파라미터만 넣으면 되는데 save의 경우 id도 필요합니다. 근데 id의 경우 자동 생성이라서 ps를 생성할 때 new로 id를 넣어주는 것입니다.

> 원래 이렇게 복잡하지 않고 탬플릿 메서드에 sql 넣고 파라미터 넣고 실행하면 끝인데 키 홀더 때문에 그렇습니다. 자동 키 증가의 경우 탬플릿을 이렇게 쓰는 구나 하면됩니다.

+ 키 홀더를 넘겼기에 꺼낼 수 있습니다. getKey해서 키 값을 꺼내고 item에 넣고 반환하면 됩니다. 삽입인데 item을 반환하는 이유는 언제나 그렇듯 인터페이스에서 save 메서드가 item을 반환하도록 해서 그렇습니다. 각 구현체마다 다르게 구현하지만 item을 파라미터로 받고 item을 반환하도록 하면 됩니다. 키 홀더는 반환할 때 id가 필요해서 사용하는 것입니다. 이것을 jdbcSimpleInsert를 쓰면 편리하게 쓸 수 있습니다.

 

2. 수정

@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
    String sql = "update item set item_name = ?, price = ?, quantity = ? where id = ?";

    template.update(sql,
            updateParam.getItemName(),
            updateParam.getPrice(),
            updateParam.getQuantity(),
            itemId);
}

역시 먼저 sql을 만들고 탬플릿의 update 메서드에 sql 넘기고 파라미터 지정해주면 됩니다. "개발자는 sql을 작성하고 전달할 파라미터를 정의하고 응답값을 매핑(mapper)하기만 하면 됩니다."라고 했는데 이렇게 하면 나머지 복잡한 con 연결, 반복, 동기화 매니저, 스프링 제공 예외는 탬플릿이 다 해결해 줍니다. 이렇게 탬플릿 메서드에 sql과 파라미터만 넣으면 됩니다. 저장에서는 pstmt를 썼는데 Id 자동 생성 때문에 한 것이지 pstmt는 필요없고  그냥 파라미터를 updateParam.get으로 넣으면 됩니다.

 

3. id로 조회

@Override
public Optional<Item> findById(Long id) {
    String sql = "select id, item_name, price, quantity from member where id = ?";

    try {
        Item item = template.queryForObject(sql, itemRowMapper(), id);
        return Optional.of(item);
    } catch (EmptyResultDataAccessException e) {
        return Optional.empty();
    }
}

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;
    });
}

역시 sql을 먼저 만들고 QueryForObject로 객체 하나를 뽑을 수 있습니다. id로 찾으면 원래는 rs 통해 일일히 가져오는데 맵퍼에 rs에서 값을 가져와서 객체를 반환하도록 작성해서 바로 객체를 가져올 수 있습니다. 인자로 sql 먼저 넣고 '?' 파라미터 넣는 게 기본입니다.

> 이번에는 itemRowMapper 라는게 필요한데 이전에는 sql 결과가 rs로 넘어오면 getString("itemName")이런 식으로 꺼내서 new Item에 set했는데 이제는 rs 결과를 바로 item 객체로 바꾸는 코드가 필요합니다.

> item 맵퍼를 만듭니다. RowMapper에서 Item을 반환합니다. rs를 람다로 받고 item을 만들고 id, 이름, 가격, 수량을 rs로 부터 받는 코드를 여기에 작성하고 query를 실행하면 맵퍼를 통해서 rs로부터 객체를 만드는 것입니다. 얘를 opt로 변환해서 of로 null이 아니어야만 한다고 반환하면 됩니다. 

 

> Null일 수도 있는 것 아닌가 싶은데 QueryForObject는 조회시에 결과가 없으면 EmptyResult 예외가 터져서 그때는 empty()로 반환합니다. 정리해보면 조회때만 rs가 필요하니 template 메서드에 sql, 맵퍼, 파라미터를 넣고 그 외 저장, 수정은 sql, 파라미터만 넣습니다.

+ rowMapper는 rs를 객체로 변환하는 코드입니다. QueryForObject에서 복잡한 jdbc 코드를 다 하고 rs를 가져올 텐데 거기서 rs 넘기면서 rowMapper를 호출해서 생성된 item 객체를 반환해줍니다. 맵퍼의 반환값이 QueryForObj의 반환값이 됩니다.

 

4. 모두 조회

@Override
public List<Item> findAll(ItemSearchCond cond) {
    String itemName = cond.getItemName();
    Integer maxPrice = cond.getMaxPrice();

    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());
}

findAll은 메모리에서 한 조건 검색을 한 후 조건에 만족하는 것을 모두 조회하는 코드를 짜면됩니다. 얘는 QueryForObject가 아닌 query 메서드를 하는데 QueryForObject는 하나 가져올 때 query는 여러개 가져올 때 씁니다. 역시 sql, 맵퍼, 파라미터를 전달해야 합니다.

반환을 탬플릿 메서드를 바로 합니다. 수정이나 저장은 jdbc를 직접 사용할 때 rs를 사용하지 않았고 조회에서만 사용했습니다. 따라서 여기서도 맵퍼를 사용하고 findById는 객체 하나만 Optional로 반환하는데 all에서는 객체 여러개를 반환합니다.

> 그리고 동적 쿼리를 작성합니다. 이걸 하는게 엄청 힘듭니다. 동적 쿼리를 만든다는 것이 상황에 따라서 where가 들어가고 안들어가고 합니다. 조건에 상품명이 있을 때와 가격도 있을 때 다르게 적용해야 해서 그렇습니다.

 

※ 탬플릿 메서드
1. 삽입, 삭제, 수정 : update()

반환값으로 이전에 jdbc의 pstmt.executeUpdate를 했을 때처럼 변환되는 row 수를 반환합니다. jdbc를 직접 쓰거나 탬플릿을 써도 삽입, 삭제, 수정은 update()(executeUpdate)이고 조회는 query()(excuteQuery)입니다.

2. 조회 : query*()

조회는 2개의 메서드로 queryObj와 query로 반환 결과가 하나면 queryObj, 하나 이상이면 query를 씁니다. 맵퍼는 조회에서 rs를 변환하도록 할 때 결과가 하나이거나, 하나 이상이나 둘 다 사용합니다.

 

- 정리

1. 탬플릿과 데이터 소스는 관례입니다.
2. update는 excuteUpdate처럼 row를 반환합니다.
3. id 자동 증가는 save에서 신경써야하고 그 값을 가져오려면 키 홀더를 써야합니다.
4. rowMapper는 rs를 객체로 변환하는 코드입니다.
5. QueryForObject는 결과가 없으면 EmptyResultDataAccessException, 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생하여 로그인이나 회원가입시 예외처리할 수 있습니다.


6. 로우맵퍼는 db 조회 결과를 객체로 변환할 때 사용합니다. jdbc를 직접 사용할 때는 rs를 findById 메서드 내부에 직접 써서 변환을 했습니다. 그 부분을 따로 메서드로 뺐다고 생각하면 됩니다. 차이가 있다면 rs.next()를 if로 하거나 while로 하는 것을 탬플릿이 대신 해줍니다. 따라서 findAll로 여러 객체를 조회하든 findByID로 한 객체만 조회하든 개발자는 rs에서 데이터를 추출하고 객체로 item을 만들고 반환하는 맵퍼 메서드만 만들고 template.query(sql, 맵퍼, param)만 하면 됩니다.

 

- 동적쿼리 문제

all에서 검색 조건에 따라 sql이 동적으로 달라집니다. 만약 검색 조건이 없으면 전체를 가져오는 sql이 실행되어야하고

 

상품명이 있으면 다른 sql이 실행되어야 합니다. 가격만 있을 경우, 가격 상품 조건이 있을 경우 2의 제곱으로 경우의 수가 기하급수적으로 늘어납니다. 

 

> 결과적으로 동적 쿼리를 작성해야합니다. 근데 동적 쿼리도 모든 경우의 수를 다루기 때문에 짜는게 너무 어렵습니다. 이후에 MyBatis의 가장 큰 장점이 동적 쿼리를 지금보다 훨씬 쉽게 적용할 수 있다는 것입니다.

 

- 실행

@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV1Config {
    private final DataSource dataSource;

    @Bean
    public ItemService itemService() {
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository() {
        return new JdbcTemplateItemRepositoryV1(dataSource);
    }
}

작성한 코드를 실행해보겠습니다. Config를 만듭니다. jdbc 레포를 등록할 것이라서 데이터 소스를 주입받습니다.

 

@Import(JdbcTemplateV1Config.class)

App에 Import에 Config를 바꿉니다.

 

프로퍼티스에 데이터 소스 설정 정보를 적습니다. 실행해보면 정상적으로 동작합니다. 서버를 재시작할 때마다 DB에 초기화 데이터가 계속 추가됩니다. 등록해 놓고 컨테이너가 준비가 끝나면 실행되는 이벤트를 걸어놓은 initData가 계속 호출됩니다.

 

logging.level.org.springframework.jdbc=debug를 프로퍼티스에 등록하면 쿼리 로그를 jdbc 탬플릿에서 남겨줍니다.

 

- jdbc 탬플릿 버전업

1. 이름지정 파라미터

이전에 jdbc를 쓸 때 ? 파라미터를 순서대로 바인딩된다고 했습니다. 여기서는 이름, 가격, 수량 순으로 이 순서만 잘 지키면 됩니다. 이게 보통은 문제가 안되는데 누군가 순서를 바꿀 때 문제가 됩니다.

 

누군가 수량을 앞으로 옮긴 상황이면 update 메서드에 이름, 가격, 수량으로 파라미터 바인딩을 했기에 가격에 수량이 수량에 가격이 들어갑니다. 실무에서는 파라미터가 10 ~ 20 개가 넘어가는 일도 많고 db에 새로운 필드를 추가하면서 이런 문제가 충분히 발생할 수 있습니다.

 

> 실무에서 버그 중에서 제일 고치기 힘든 버그가 db에 데이터가 잘못 들어가는 버그로 왜냐면 어플리케이션 문제는 코드만 코치면 되는데 db문제는 데이터 복구를 해야해서 그냥 코드 코치는 수준이 아니라 db다 열어서 데이터 다 보정하고 넣어야합니다. 따라서 개발을 할 때는 코드를 몇 줄 줄이는 것도 중요하지만 모호함을 제거해서 코들 명확하게 만드는 것이 유지보수 관점에서 훨씬 중요합니다.

 

- 이름지정 파라미터

이런 문제를 보완하기 위해 순서가 아닌 이름을 지정해서 파라미터 바인딩하는 기능을 제공합니다. 정말 파라미터 바인딩만 이전에는 save에서는 pstmt로 직접하고 update에서는 pstmt 없이 직접한 것을 이름기반으로만 바꾸는 것입니다.

 

@Repository
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
    private final NamedParameterJdbcTemplate template;

    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }

새로운 탬플릿 레포를 만듭니다. NamedParmeterJdbcTemplate를 쓸 것입니다. NamedParmeterJdbcTemplate를 선언합니다. 역시 데이터 소스가 필요하고 주입은 소스를 받고 탬플릿은 내부에서 생성하는 방식을 관례상 사용합니다. 이러면 파라미터 바인딩을 순서가 아니라 이름 기반으로 하게 됩니다.

 

1) save

// 순서 기반
@Override
public Item save(Item item) {
    String sql = "insert into item (item_name, price, quantity) values (?, ?, ?)";

    KeyHolder keyHolder = new GeneratedKeyHolder();
    template.update(connection -> {
        //자동 증가 키
        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;
}

// 이름 기반
@Override
public Item save(Item item) {
    String sql = "insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)";

    KeyHolder keyHolder = new GeneratedKeyHolder();
    SqlParameterSource param = new BeanPropertySqlParameterSource(item);
    template.update(sql, param, keyHolder);

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

이름 기반을 쓰면 pstmt가 싹 사라져서 save가 훨씬 단순해집니다. sql이 달라집니다. ?로 하는게 아니고 이름 기반으로 써야하고 ps.set하는 복잡한 코드를 없앨 수 있습니다. update에 sql, 파리미터, 키홀더를 넘깁니다. 이제 순서가 아닌 이름 기반 파라미터를 넣어야합니다. BeanPropertySqlParameterSource를 쓰는데 넘어온 매개변수 item 객체를 가지고 파라미터를 만드는 것입니다. 

 

> 정말 pstmt로 직접하는 역할을 자동화해줍니다. item 객체에 필드에 초기화된 값으로 파라미터 바인딩을 할 텐데 넘어온 item 객체의 필드 명으로 파라미터를 바로바로 맞춰서 만드는 것입니다. 따라서 sql을 이름 기반으로 바꿀 때 ':'하고 item 객체의 필드명으로 해야합니다.(키, 벨류 매칭) 이렇게 하면 끝입니다. 키 홀더를 위한 pstmt 코드가 전부 사라집니다. ':'뒤에 변수 명에 item 필드의 값이 들어갑니다.

 

2) update

// 이전
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
    String sql = "update item set item_name = ?, price = ?, quantity = ? where id = ?";

    template.update(sql,
            updateParam.getItemName(),
            updateParam.getPrice(),
            updateParam.getQuantity(),
            itemId);
}

// 이후
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
    String sql = "update item set item_name = :itemName, price = :price, quantity = :quantity where id = :id";

    SqlParameterSource param = new MapSqlParameterSource()
            .addValue("itemName", updateParam.getItemName())
            .addValue("price", updateParam.getPrice())
            .addValue("quantity", updateParam.getQuantity())
            .addValue("id", itemId);

    template.update(sql, param);
}

sql 쿼리를 ?에서 이름 기반으로 바꿉니다. 여기서도 update 메서드에 이름기반으로 파라미터를 넘겨야하는데 MapSqlParameterSource를 씁니다. 이렇게도 쓸 수 있다는 여러가지 케이스를 보여주는 것입니다. BeanPropertySqlParameterSource쓰고 template.update(sql, param)해도 됩니다.

 

> BeanPropertySqlParameterSource는 저절로 바인딩이 됐는데 얘는 addValue로 add 해줘야합니다. 수정의 경우 where가 있어서 매개변수로 넘어온 DTO의 필드가 아닌 itemId를 써야하는데 이 경우 sql에 id=:id로 이름을 맞춰주고 add에 itemId를 줍니다.

 

+ 이렇게 파라미터를 만들고 update 메서드에 sql, 파라미터를 똑같이 넣으면 됩니다. 지금 이전과 하는 작업은 똑같은데 필드 순서 변경 문제를 대비하여 순서기반을 이름기반으로 바꾸는 것일 뿐입니다. ':'뒤에 변수 명에 updateParam get의 값이 들어갑니다.

 

3) id로 찾기

@Override
public Optional<Item> findById(Long id) {
    String sql = "select id, item_name, price, quantity from item where id = :id";

    try {
        Map<String, Object> param = Map.of("id", id);
        Item item = template.queryForObject(sql, param, itemRowMapper());
        return Optional.of(item);
    } catch (EmptyResultDataAccessException e) {
        return Optional.empty();
    }
}

id만 ?을 :id로 바꾸고 queryForObj 메서드에 param을 맵퍼 앞으로 넣어줘야합니다. 이름 기반 파라미터 맵핑을 자바 Map으로도 할 수 있습니다.

 

4) 다 찾기

@Override
public List<Item> findAll(ItemSearchCond cond) {
    String itemName = cond.getItemName();
    Integer maxPrice = cond.getMaxPrice();
    SqlParameterSource param = new BeanPropertySqlParameterSource(cond);
    String sql = "select id, item_name, price, quantity from item";

    //동적 쿼리
    if (StringUtils.hasText(itemName) || maxPrice != null) {
        sql += " where";
    }

    boolean andFlag = false;
    if (StringUtils.hasText(itemName)) {
        sql += " item_name like concat('%',:itemName,'%')";
        andFlag = true;
    }

    if (maxPrice != null) {
        if (andFlag) {
            sql += " and";
        }
        sql += " price <= :maxPrice";
    }

    log.info("sql={}", sql);
    return template.query(sql, param, itemRowMapper());
}

여기서도 query 메서드에 param이 맵퍼 앞에 있어야합니다. cond의 필드 명을 파라미터로 가지고 있고 ?를 :itemName, :price로 하면 param에 cond의 초기화 값이 들어가 있기에 자동으로 바인딩이 됩니다.

 

5) 맵퍼 메서드

// 이전
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;
    });
}

// 이후
private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class);
}

맵퍼도 파라미터를 쓰는게 비슷합니다. BeanPropertyRowMapper를 스프링이 제공해주는데 newIntance에서 Item.class를 하면 끝납니다. 이러면 rs를 가지고 new item 객체에 set으로 값을 넣는 원래 동작을 다 해줍니다.

 

- 설명

파라미터를 전달하려면 Map처럼 key, value 데이터 구조를 만들어서 전달해야 합니다. key는 : 이름과 매칭이 되고 value에 그 값이 들어값니다. 크게 3개의 파라미터 종류가 있습니다. Map을 쓰거나 SqlParameterSource 인터페이스를 쓰고 그 구현체를 씁니다.

 

@Override
public Optional<Item> findById(Long id) {
    String sql = "select id, item_name as itemName, price, quantity from item where id = :id";
    
	 try {
        Map<String, Object> param = Map.of("id", id);
        Item item = template.queryForObject(sql, param, itemRowMapper());

id로 찾기에서 sql의 :id 치환하기 위해서 Map에 키, 벨류를 id를 만듭니다. 키가 :id와 이름이 매칭되고 value가 들어갑니다.

 

2. MapSqlParameterSource

@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
    String sql = "update item set item_name = :itemName, price = :price, quantity = :quantity where id = :id";

    SqlParameterSource param = new MapSqlParameterSource()
            .addValue("itemName", updateParam.getItemName())
            .addValue("price", updateParam.getPrice())
            .addValue("quantity", updateParam.getQuantity())
            .addValue("id", itemId);

    template.update(sql, param);
}

Map과 유사한 키, 벨류 구조로 SQL에 좀 더 특화된 기능을 제공하고 .addValue인 메서드 체인 사용법도 제공합니다. 이것을 보면 키, 벨류가 바로 이해가 됩니다. SqlParamterSource에 키, 벨류 형태로 파라미터가 있어야하고 sql의 :이름에 맞는 벨류값이 들어갑니다.

 

3. BeanPropertySqlParameterSource

@Override
public Item save(Item item) {
    String sql = "insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)";

한 줄로 끝낼 수 있는 것으로 인자인 item을 읽어 들여서 파라미터를 바로 만듭니다. 파라미터를 만든다는 것이 이전에 ps에 ?로 다 일일히 넣어줬던 것을 한번에 채웁니다. 얘는 프로퍼티 규약을 맞춰서 item 객체에 get 메서드가 있으면 이름, 가격, 수량 값을 다 꺼내서 키, 벨류로 데이터를 다 Map처럼 만듭니다. 

 

> save를 보면 item이 매개변수로 저장할 데이터가 초기화 되어있을 텐데 item에 있는 get 메서드의 id, 이름, 수량, 가격이 다 파라미터로 만들어지고 :로 맵핑되는 것은 쓰고 아닌 것은 안쓰면 끝입니다.

 

4. BeanPropertyRowMapper

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;
    });
}

맵퍼에서 이전에는 rs로 직접 다 가져왔습니다. 이것도 "item의 필드와 id, 상품명, 가격, 수량이 똑같고 DB도 똑같으니 맵핑하면 되는 것이 아닌가?"해서 그렇게 BeanPropertyRowMapper가 해줍니다. DB의 rs 결과를 받아서 데이터를 get하고 new item 객체에 set해 줍니다.

 

-> 무엇이 제일 좋은지

BeanPropertySqlParameterSource이 젤 좋은 것 같은데 항상 쓸 수 있는게 아닙니다. update를 보면 DTO에 id가 없어서 where가 해결이 안 됩니다. 따라서 BeanPropertySqlParameterSource을 못 써서 Map 종류를 써야합니다. id가 있었으면 BeanPropertySqlParameterSource을 썼을 것입니다.

 

- 별칭

근데 BeanPropertyRowMapper를 쓸 때는 DB값을 가져오기 때문에 별칭에 신경써야 합니다. rs를 안 쓸 때는 상관없는데 조회는 DB값을 가져오니 DB의 필드 값을 가져오는데 Item_Name의 경우 프로퍼티 규약에 따르면 setItem_Name이라는 메서드가 없어서 고치아픕니다. rowMapper 메서드를 보면 db의 item_name을 rs의 get하고 그에 맞는 set을 itemName으로 해서 초기화했는데 BeanPropertyRowMapper쓰면 이것을 프로퍼티 접근으로 봐서 DB 컬럼은 고정하고 set을 Item_Name으로 찾는다는 것입니다.

이 경우 개발자가 as로 sql을 고쳐야합니다. 이런 별칭을 자바에서 자주 사용합니다. db는 member_name인데 객체는 user_name이면 as username으로 바꾸기도 합니다. 탬플릿은 물론이고 mybatis에서도 많이 씁니다.

+ 관례 불일치

근데 지금 as로 안 고쳤습니다. 자바는 카멜 표기법을 사용합니다. db는 언더바를 사용하는 표기법을 많이 씁니다. 근데 하도 이런 관례를 많이 사용하니깐 이 둘간의 변환은 개발자가 as를 사용하지 않아도 해주면 좋겠습니다. 따라서 BeanPropertyRowMapper가 _ 표기법과 카멜을 자동 변환해줍니다. 

 

> 따라서 select Item_name으로 조회해도 ItemName에 문제 없이 값이 들어갑니다. 정리하자면 _ 는 자동으로 해결하니 그냥 두면 되고 컬럼 이름과 객체 이름이 완전히 다른 경우 as를 사용합니다.

 

- 실행

새로운 Config를 만들어서 새로운 레포를 빈등록하고 Import도 새로운 Config로 바꿉니다. 실행해보면 됩니다. 이렇게 하여 SQL ? 파라미터에서 순서가 바뀌면 끝장나는 문제를 이름기반으로 해결하였습니다.

 

- SimpleJdbcInsert

Jdbc 탬플릿은 insert를 직접 작성하지 않아도 되도록 SimpleJdbcInsert라는 편리한 기능을 제공합니다.

 

@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
    private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
        this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName("item")
                .usingGeneratedKeyColumns("id");
                // .usingColumns("item_name", "price", "quantity"); 생략가능
    }

SimpleJdbcInsert을 선언하고 생성자에서 생성하고 소스를 넣어야하고 withTableName과 자동 생성 키와 어떤 컬럼을 사용하는지를 체인 메서드로 넣어줍니다. 어떤 컬럼에 insert 할지는 생략할 수 있는데 심플이 데이터 소스를 읽고 DB의 테이블 명 메타 데이터를 읽어서 자동으로 사용할 컬럼을 적용합니다.

 

> 생략하면 모든 컬럼에 다 insert하는 것이고 지정하면 원하는 컬럼에만 데이터를 넣습니다. 테이블 명, 자동 생성키를 다 정해줘야해서 jdbcinsert는 빈 등록하지 않고 사용할 클래스의 생성자에서 직접 생성합니다.

 

@Override
public Item save(Item item) {
    String sql = "insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)";
    SqlParameterSource parma = new BeanPropertySqlParameterSource(item);
    Number key = jdbcInsert.executeAndReturnKey(parma);
    item.setId(key.longValue());
    return item;
}

이렇게 하면 insert 쿼리를 다 지울 수 있고 jdbcInsert의 excuteAndReturnkey메서드에 파라미터를 넘기면됩니다. 파라미터는 save의 item 매개변수를 넣으니 jdbcinsert와는 별개로 param를 다 알고 있습니다. 이렇게 하면 되고 key를 반환합니다. 키 홀더도 처리하는 것입니다. item에 setId로 넣고 반환하면 끝입니다.

 

 

- 탬플릿 기능 정리

1. 단건 숫자 조회

하나의 로우를 조회할 때는 QueryForObj를 쓴다고 했습니다. 근데 조회가 id로 찾기처럼 객체가 아니라 단순타입이면 타입을 지정해줍니다.

 

2. 단건 객체 조회

@Override
public Optional<Item> findById(Long id) {
    String sql = "select id, item_name, price, quantity from item where id = :id";

    try {
        Map<String, Object> param = Map.of("id", id);
        Item item = template.queryForObject(sql, param, itemRowMapper());

객체를 조회할 때는 rs를 사용해야해서 rowMapper를 사용합니다. 단건 조회라서 QueryForObj로 조회한 것입니다.

 

3. 목록 객체 조회

return template.query(sql, param, itemRowMapper());

여러개의 객체는 Query로 조회하고 동일하게 맵퍼를 사용합니다.

 

4. 변경

template.update(sql, param);

데이터 변경은 update를 쓰면 되고 영향을 받은 db의 row 수를 변환합니다.

 

5. 임의의 SQL

create 등 임의의 SQL을 쓴 때는 execute()를 씁니다.

Comments