Spring

[인프런/스프링 DB 2편] 2. 데이터 접근 기술 (1) JdbcTemplate

주니어주니 2023. 6. 28. 22:07

 

1. JdbcTemplate

 

1) JdbcTemplate 장단점

 

장점

  • 반복 문제 해결
    • 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 반복 작업 대신 처리
    • 개발자는 SQL 작성, 전달할 파라미터 정의, 응답 값 매핑
      • 커넥션 획득
      • statement 준비, 실행 
      • 결과 반복
      • 커넥션 종료, statement, resultset 종료
      • 커넥션 동기화 
      • 예외 발생시 스프링 예외 변환기 실행
  • 설정 편리
    • 스프링으로 JDBC를 사용할 때 기본으로 사용되는 spring-jdbc 라이브러리에 포함되어 있음

 

단점 

  • 동적 SQL 해결하기 어려움

 

 

2) JdbcTemplate 설정 

 

* build.gradle 

dependencies {

	//JdbcTemplate 추가
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	//H2 데이터베이스 추가
	runtimeOnly 'com.h2database:h2'
    
}
  • JdbcTemplate 추가 -> JdbcTemplate이 들어있는 spring-jdbc가 라이브러리에 포함됨 
  • H2 데이터베이스에 접속 -> H2 데이터베이스의 클라이언트 라이브러리 (Jdbc Driver) 추가

 

 

 

2. JdbcTemplate 적용

 

2-1. 기본

 

0) ItemRepository 인터페이스 

package hello.itemservice.repository;

import hello.itemservice.domain.Item;

import java.util.List;
import java.util.Optional;

public interface ItemRepository {

    Item save(Item item);

    void update(Long itemId, ItemUpdateDto updateParam);

    Optional<Item> findById(Long id);

    List<Item> findAll(ItemSearchCond cond);

}

 

 

1) ItemRepository 인터페이스 구현 

package hello.itemservice.repository.jdbctemplate;

/**
 * JdbcTemplate
 */
@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }
  • JdbcTemplateItemRepositoryV1은 ItemRepository 인터페이스를 구현함
  • JdbcTemplate을 사용하기 위해 DataSource가 필요함 (의존 관계 주입, 생성자 내부에서 JdbcTemplate 생성)
    -> 관례상 이렇게 많이 씀
    ( * JdbcTemplate을 스프링 빈으로 직접 등록하고 @Autowired 등으로 주입받아도 됨) 

 

 

2) 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;
}
  • template.update()
    • update : 데이터 변경 시 (Insert, Update, Delete)
    • template.update()의 반환값은 int - 영향 받은 로우 수 반환

  • KeyHolder (그냥 이런 방식이 있구나 참고) 
    • connection.prepareStatement(sql, new String[]{"id"})
      • 데이터를 저장할 때 PK 생성에 identity (auto increment) 방식을 사용중 
        -> PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장 
        -> 데이터베이스가 ID를 대신 생성

    • long key = keyHolder.getKey().longValue() 
      • 데이터베이스에 INSERT가 완료되어야 생성된 PK ID값을 확인할 수 있음
      • PK인 "id" 지정 -> INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID값 조회할 수 있음

 

 

3) 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);
}
  • template.update()
    • ?에 바인딩할 파라미터를 순서대로 전달
    • 반환값은 쿼리의 영향을 받은 로우 수 (int) 
      -> 여기서는 where id=? 를 지정했기 때문에 1개만 영향받음

 

 

4) findById() 상품 1개 조회 

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

(1) template.queryForObject(SQL, 반환타입(RowMapper<T>), 인자)

  • 결과값이 1개일 때
  • 반환타입 : 데이터 형만 가능 (Long, Integer, String 등)
    -> 그래서 RowMapper를 사용해서 원하는 형태의 결과값 반환 (반환 결과인 ResultSet을 객체로 변환)
  • 결과가 없으면 EmptyResultDataAccessException 예외
  • 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외

 

 

(2) RowMapper<T>

  • ResultSet에 담긴 쿼리 결과값을 원하는 형태로 변환 
  • ResultSet은 직접 next()를 확인해서 루프를 돌렸지만, RowMapper는 내부 코드만 채우면 루프를 알아서 돌려줌 
  • rowNum : 현재 행의 수 ( null 가능, 행의 수만큼 ResultSet 값과 객체를 매핑시킴 )
  • -> 쿼리 결과값이 ResultSet에 담김
    -> 원하는 객체를 생성해서 거기에 ResultSet의 값을 매핑시킴 
    -> 객체 반환
    -> 이걸 rowNum 만큼 실행 

 

 

(3) Optional<T>

  • ItemRepository.findById() 인터페이스는 결과가 없을 때 Optional 반환해야 함 
    -> 결과가 없으면 예외를 잡아서 Optional.empty() 로 반환
  • null이 올 수 있는 값을 감싸는 Wrapper 클래스 -> NPE(Null Point Exception)가 발생하지 않도록 해줌
    • Optional.empty()
      • 값이 null 일 수 있을 때
        -> Optional 빈 객체 생성
        -> Optional로 감싸진 객체를 반환하기 때문에 객체를 get하기 전에 null 체크해야 하는 것을 인식할 수 있음
    • Optional.of()
      • 값이 절대 null이 아닐 때
        -> 객체를 Optional로 Wrapping 해줌
    • Optional.ofNullable()
      • 값이 null일 수도, 아닐 수도 있을 때
        -> orElse, orElseGet 등으로 값이 없는 경우에도 안전하게 가져올 수 있음
        -> null 이면 Optional.empty() 를 반환

 

💡 Optional 사용 예

if ( ~~ != null)
    if ( ~~ != null)
         if (~~ != null)

같이 복잡하지 않게 Optional 사용 
Optional.ofNullalble(~~) . orElse( ~~~) . ~~ 

 

 

5) findAll() 검색 결과 목록 조회 

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

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;
    });
}
  • template.query(SQL, 반환타입(RowMapper<T>), 인자)
    • 결과가 1개 이상일 때
    • 결과가 없으면 빈 컬렉션 반환
    • 동적 쿼리 -> JDBC Template에서 동적 쿼리를 작성하기 매우 복잡함
      (어떤 경우 where넣고, 어떤 경우 and넣고 이런 경우를 모두 계산해야 함)

 

 

2-2. DB 연결 설정 

 

1) JdbcTemplateV1Config 

  • JdbcTemplateItemRepositoryV1을 스프링 빈에 등록
package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV1;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@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);
    }
}
  • 메모리 저장소가 아니라 실제 DB에 연결하는 JdbcTemplate이 사용되도록 설정 Configuration
  • itemService 인터페이스, itemRepository 구현체로 JdbcTemplateRepository가 사용되도록 설정
  • DataSource는 스프링 부트가 자동 등록하도록 application.properties에서 설정 -> dataSource를 주입받음

 

 

2) ItemServiceApplication 변경 

package hello.itemservice;

import hello.itemservice.config.*;
import hello.itemservice.repository.ItemRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;


@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ItemServiceApplication.class, args);
	}

	@Bean
	@Profile("local")
	public TestDataInit testDataInit(ItemRepository itemRepository) {
		return new TestDataInit(itemRepository);
	}

}
  • 원래 @Import(MemoryConfig.class) -> @Import(JdbcTemplateV1Config.class) 변경

 

 

3) application.properties 데이터베이스 접근 설정

spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
  • 스프링부트가 해당 설정을 사용해서 커넥션 풀, DataSource, 트랜잭션 매니저를 스프링 빈으로 자동 등록 !!

 

 

4) 로그 추가 

#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug

  • SQL 로그 확인 -> main, test 둘다 확인하려면 두 곳에 모두 추가 

 

 

 


 

3. NamedParameterJdbcTemplate - 이름 지정 파라미터 

 

 

3-1. 이름 지정 파라미터 바인딩

 

 

* 원래 순서대로 파라미터 연결 

String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
        updateParam.getItemName(),
        updateParam.getPrice(),
        updateParam.getQuantity(),
        itemId);

 

순서 꼬이거나 데이터 추가되는 경우 -> 버그 

-> 이름 지정 파라미터 바인딩 사용

 

 

 

1) NamedParameterJdbcTemplate 주입 

/**
 * NamedParameterJdbcTemplate
 */
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {

    private final NamedParameterJdbcTemplate template;

    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }
  • 생성자 내부에서 DataSource 주입받음 (JdbcTemplate => DataSource 필요), NamedParameterJdbcTemplate 생성
  • NamedParameterJdbcTemplate를 스프링 빈으로 직접 등록하고 주입받아도 됨 

 

 

2) SQL 수정 

  • ? -> :파라미터이름
// save
String sql = "insert into item(item_name, price, quantity) values (:itemName, :price, :quantity)";

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

 

 

3) 파라미터 바인딩 

 

* 파라미터 종류 3가지 

  • Map
  • SqlParameterSource 인터페이스
    • MapSqlParameterSource
    • BeanPropertySqlParameterSource

 

 

(1) Map 사용

 

* findById()

@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();
    }
}
  • new HashMap() 으로 해도 되지만, 간단히 넣기 위해 Map.of() 사용
  • 파라미터 전달하려면 key, value 구조를 만들어서 전달
  • key = :파라미터이름 으로 지정한 파라미터의 이름 , value = 해당 파라미터의 값 
  • queryForObject(sql, param, itemRowMapper());

 

 

(2) MapSqlParameterSource 사용 

 

* update()

@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);
}
  • SqlParameterSource 인터페이스의 구현체 
  • Map과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 더 특화된 기능 
  • 메소드 체인을 통한 편리한 사용법

 

 

(3) BeanPropertySqlParameterSource 사용 

 

* save(), findAll()

@Override
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;
}

@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());
}
  • SqlParameterSource 인터페이스의 구현체
  • 자바빈 프로퍼티 규약을 통해 자동으로 파라미터 객체 생성
  • 예 ) new BeanPropertySqlParameterSource(item)
    • Item 객체의 getItemName(), getPrice() 를 사용해 파라미터 자동으로 생성
    • key = itemName , value = item 객체의 상품명 값
    • key = price, value = item 객체의 가격 값

  • 가장 좋아보이지만, 항상 사용할 수 있는 것 X
    • update() 에서는 SQL에 :id를 바인딩 해야 하는데, ItemUpdateDto에는 itemId가 없음
      -> BeanPropertySqlParameterSource 사용 X
      -> MapSqlParameterSource 사용해서 따로 추가

 

 

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

 

* BeanPropertyRowMapper 사용

private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class);   // camel 변환 지원
}
  • ResultSet의 결과를 받아서 자바빈 규약에 맞춰 데이터 변환
    • 쿼리 조회 결과 "id" 값 -> setId() 호출 -> Item 객체의 Id 값에 적용
    • 쿼리 조회 결과 "item_name" 값 -> setItemName() 호출 -> 카멜 표기법으로 자동 변환 -> Item 객체의 itemName 값에 적용

  • 관례의 불일치 
    • 자바 객체 : 카멜 표기법(camelCase) 
    • 관계형 데이터베이스 : 언더스코어 표기법(snake_case)
    • -> BeanPropertyRowMapper는 언더스코어 표기법을 카멜로 자동 변환해줌 
      -> select item_name 값이 setItemName() 에 잘 들어감

 

 

3-2. DB 연결 설정 

 

1) JdbcTemplateV2Config 

  • JdbcTemplateItemRepositoryV2 를 스프링 빈에 등록
package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV1;
import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV2;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV2Config {

    private final DataSource dataSource;

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

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

 

 

2) JdbcTemplateV2Config를 사용하도록 변경 

@Import(JdbcTemplateV2Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
...
}
  • @Import(JdbcTemplateV1Config.class) -> @Import(JdbcTemplateV2Config.class)

 

 


 

4. SimpleJdbcInsert - INSERT SQL 작성해주는 기능 

 

* Repository - SimpleJdbcInsert 주입

/**
 * SimpleJdbcInsert
 */
@Slf4j
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");  // 생략 가능
    }
  • DataSource를 의존성 주입 받음
  • 생성자 내부에서 SimpleJdbcInsert를 생성함 
    • withTableName : 테이블 지정
    • usingGenerateKeyColumns : key를 생성하는 PK 컬럼명 지정
    • usingColumns : INSERT SQL에 사용할 컬럼 지정 (생략 가능 -> 특정 값만 저장하고 싶을 때 사용)
      -> 생성 시점에 테이블의 데이터를 조회해서 알아서 컬럼 확인함

 

 

* save() 

@Override
public Item save(Item item) {
    SqlParameterSource param = new BeanPropertySqlParameterSource(item);
    Number key = jdbcInsert.executeAndReturnKey(param);
    item.setId(key.longValue());
    return item;
}
  • jdbcInsert.executeAndReturnKey(param) : INSERT SQL 생성, 생성된 키 값 편리하게 조회

 

 

* Config 객체JdbcTemplateItemRepositoryV3 스프링 빈 등록

 

* Application 객체@Import(JdbcTemplateV3Config.class) 를 설정으로 사용하도록 변경

 

 

 


 

5. JdbcTemplate 기능 정리 

 

1) JdbcTemplate이 제공하는 주요 기능 

  • JdbcTemplate
    • 순서 기반 파라미터 바인딩 지원 

  • NamedParameterJdbcTemplate
    • 이름 기반 파라미터 바인딩 지원 
  • SimpleJdbcInsert
    • INSERT SQL 편리하게 사용

 

 

 

2) JdbcTemplate 사용법

 

(1) 조회 

 

* 1건 조회 - 숫자

  • 1건 조회 - queryForObject()
  • 조회 대상이 객체가 아니라 단순 데이터 하나 - Integer.class, String.class 처럼 지정
int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

 

 

* 1건 조회 - 숫자, 파라미터 바인딩 

int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
 "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

 

 

* 1건 조회 - 문자 

String lastName = jdbcTemplate.queryForObject("select last_name from t_actor where id = ?", String.class, 1212L);

 

 

* 1건 조회 - 객체 

  • RowMapper - 결과를 객체로 매핑
Actor actor = jdbcTemplate.queryForObject(
    "select first_name, last_name from t_actor where id = ?",
    (resultSet, rowNum) -> {
    	Actor newActor = new Actor();
        newActor.setFirstName(resultSet.getString("first_name"));
        newActor.setLastName(resultSet.getString("last_name"));
        return newActor;
    },
    1212L);

 

 

* 목록 조회 - 객체 

  • 여러 행 - query()
List<Actor> actors = jdbcTemplate.query(
    "select first_name, last_name from t_actor",
    (resultSet, rowNum) -> {
        Actor actor = new Actor();
        actor.setFirstName(resultSet.getString("first_name"));
        actor.setLastName(resultSet.getString("last_name"));
    return actor;
 });
  • RowMapper 분리
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
	return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}

 

 

(2) 변경

 

* 등록

jdbcTemplate.update(
     "insert into t_actor (first_name, last_name) values (?, ?)",
     "Leonor", "Watling");

 

* 수정

jdbcTemplate.update(
    "update t_actor set last_name = ? where id = ?",
    "Banjo", 5276L);

 

* 삭제

jdbcTemplate.update(
    "delete from t_actor where id = ?",
    Long.valueOf(actorId));

 

 

(3) 기타 

임의의 SQL 실행할 때는 execute() 사용

 

* DDL

jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

 

 

 

=> 간단한 방법으로 SQL 사용

 

하지만, 동적 쿼리 해결 X + SQL을 자바 코드로 작성(코드가 라인 넘어갈 때마다 + 로 연결)