💐 Spring/Spring in Action

Jdbc, JdbcTemplate, Spring Data Jpa에 대해서

iseunghan 2022. 3. 21. 00:54
반응형

Goal

  • 스프링 JdbcTemplate 사용해보기
  • SimpleJdbcInsert를 사용해서 데이터 추가해보기
  • 스프링 데이터를 사용해서 JPA 선언하고 사용해보기

 

순수 JDBC

Jdbc란, Java DataBase Connectivity, 데이터베이스를 연결하기 위한 API이다.

Jdbc를 이용해 select 쿼리 작성해보면 아래와 같다.

@Override
public Account findById(String id) {
    Connection conn = null;
    PreparedStatement st = null;
    ResultSet rs = null;

    String sql = "select id, name from Account where id = ?";

    try {
        conn = datasource.getConnection();
        st = conn.prepareStatement(sql);
        st.setString(1, id);    // ?에는 id가 들어간다.

        ...
    } catch (SQLException e) {
        ...
    } finally {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {}
        }
        if (st != null) { ... }      // st.close
        if (conn != null) { ... } // conn.close()
    }
    return null;
}

JDBC를 사용하면 DB 연결부터 마지막에 연결해제까지 일일히 다 해줘야한다.
이런 불편함을 해소하기 위해 Spring에서 제공하는 JdbcTemplate 클래스가 있다.

Spring에서 제공하는 JdbcTemplate

  • dependency 추가
  • <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
  • JdbcTemplate
    • query("sql", RowMapper)
    • queryForObject("sql", RowMapper, Object... args)
    • update("sql", Object... args)

Object... argsselect * from Account where id = ? 와 같이 ?에 해당하는 값을 인자로 넘겨주는 것이다.

@Repository
public class JdbcAccountRepository implements AccountRepository{

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Long save(Account account) {
        jdbcTemplate.update("insert into Account values(?, ?)",
                            account.getId(),
                            account.getUsername());
        return account.getId();
    }

    @Override
    public List<Account> findAll() {
        return jdbcTemplate.query("select * from Account", this::rowToAccount);
    }

    @Override
    public Account findById(Long id) {
        return jdbcTemplate.queryForObject("select * from Account where id = ?",
                                            this::rowToAccount,
                                            id);
    }

    @Override
    public Account findByUsername(String username) {
        return jdbcTemplate.queryForObject("select * from Account where username = ?",
                                            this::rowToAccount,
                                            username);
    }

    private Account rowToAccount(ResultSet rs, int rowNum) throws SQLException {
        return new Account(rs.getLong("id"),
                    rs.getString("username"));
    }
}

아래 처럼 익명함수로 만들어도 된다.

jdbc.queryForObject(sql, new RowMapper<Account>() {
    public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
            return new Account(rs.getString("id"),
                            rs.getString("username"));

    }
}

아까 순수 JDBC 사용한 select 쿼리랑 비교해보자.

엄청나게 편리해졌다.

순수 Jdbc에서는 DB 커넥션을 생성하고 했다면, 이제 JdbcTemplate을 사용할 때에는 DataSource라는 정보를 설정해줘야 한다.

아래 두가지 방법을 사용할 수 있다. (더 많을수도 있다.)

  • DataSource를 직접 Bean으로 등록
  • application.yml에 설정

DataSource를 Bean으로 등록

@Configuration이 붙은 Class 아래에 작성해준다.

@Bean
public BasicDataSource source() {
    BasicDataSource source = new BasicDataSource();
    source.setUrl("jdbc:h2:tcp://localhost/~/databaseName");
    source.setDriverClassName("org.h2.Driver");
    source.setUsername("sa");
    source.setPassword("");

    return source;
}

@Bean
public JdbcTemplate dataSource(BasicDataSource source) {
    return new JdbcTemplate(source);
}

빈으로 주입받아서 사용하면 된다.

@Autowired
private JdbcTemplate jdbc;

application.yml 등록

  • Spring boot에서 제공해주는 방법이다. 편리한 방법이다.
# DataSource 설정
spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:tcp://localhost/~/prac_jdbc
    username: sa
    password:

# SQL 생성을 위한 설정
  sql:
    init:
      schema-locations: classpath:scheme.sql
      mode: always

테스트 해보기

  • 간단한 Controller 생성
// AccountController.java

@RestController
public class AccountController {

    @Autowired
    private AccountRepository jdbcAccountRepository ;

    @GetMapping("/account")
    public List<Account> findAll() {
        return jdbcAccountRepository.findAll();
    }

    @GetMapping("/account/{id}")
    public Account findById(@PathVariable Long id) {
        return jdbcAccountRepository.findById(id);
    }

    @PostMapping("/account")
    public Long save(@RequestBody Account account) {
        return jdbcAccountRepository.save(account);
    }
}

실행결과

insert 문은 생략.

 

  • GET /account

  • GET /account/{id}

  • POST /account

연관관계 매핑일 때, 데이터를 저장하는 2가지 방법 (JdbcTemplate vs SimpleInsert)

  • JdbcTemplate.update
  • 편리한 SimpleInsert

JdbcTemplate 사용

  • Account와 Article은 서로 연관관계가 있다고 가정한다.
    • 하나의 Account는 여러 개의 Article을 가진다.

연관관계가 있는 두 테이블을 저장하기 위해 PreparedStatementCreator, KeyHolder를 사용한다.

public class Account {
    ...
    List<Article> articles;
    ...
}

public class Article {
    Long id;
    String title;
    Date createdAt;
    ...
}
@Repository
public class JdbcAccountRepository implements AccountRepository {

    @Autowired
    private JdbcTemplate jdbc;

    public Long save(Account account) {
        Long accountId = saveAccountInfo(account);
        account.setId(accountId);

        for (Article article : account.getArticles()) {
            saveArticleToAccount(article, accountId); 
        }
        return account.getId();
    }

    public Long saveAccountInfo(Account account) {
        PreparedStatementCreator psc = new PreparedStatementCreatorFactory(
                "insert into Account (name, age) values (?, ?)",
                Types.VARCHAR, Types.INTEGER
        ).newPreparedStatementCreator(
                Arrays.asList(
                        account.getUsername(),
                        account.getAge()
                )
        );

        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(psc, keyHolder);

        return keyHolder.getKeyAs(Long.class);
    }

    private void saveArticleToAccount(Article article, Long accountId) {
        jdbcTemplate.update(
            "insert into Account_Article (account, article) values (?, ?)",
                accountId, article.getId());
    }
}
  • 코드가 복잡하다.
  • 생성된 Account ID값을 얻기 위해서는 KeyHolder가 필요한데, 이 KeyHolder를 사용하기 위해서는 꼭 PreparedStatementCreator 객체가 필요하다.

 

SimpleJdbcInsert 사용

JdbcTemplate 래퍼 클래스이다.

SimpleAccountRepository.java

@Repository
public class SimpleAccountRepository implements AccountRepository{

    private final ObjectMapper objectMapper;
    private final SimpleJdbcInsert accountInserter;
    private final SimpleJdbcInsert accountArticleInserter;

    public SimpleAccountRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
        this.accountInserter = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("Account")
                .usingGeneratedKeyColumns("id");

        this.accountArticleInserter = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("Account_Articles");

        this.objectMapper = objectMapper;
    }

    @Override
    public Long save(Account account) {
        Long accountId = saveAccountInfo(account);
        account.setId(accountId);

        for (Article article : account.getArticles()) {
            saveArticleToAccount(article, accountId);
        }
        return null;
    }

    private void saveArticleToAccount(Article article, Long accountId) {
        // key 값은 테이블의 열 이름과 동일
        HashMap<String, Object> values = new HashMap<>();
        values.put("article", article.getId());
        values.put("account", accountId);

        accountArticleInserter.execute(values);
    }

    private Long saveAccountInfo(Account account) {
        Map<String, Object> values = objectMapper.convertValue(account, Map.class);

        return (Long) accountInserter.executeAndReturnKey(values);
    }

    ...
}
  • SimpleJdbcInsert는 더욱 편리한 기능을 제공한다.
  • 이전에는 key를 돌려받기 위해 KeyHolder + PrepareStatementCreator를 사용했다면, 이제는 SimpleJdbcInsert클래스의 executeAndReturnkey를 이용해 키를 돌려받을 수 있다.

Spring Data JPA 사용

Spring Jdbc보다 훨~씬 편리한 기능들 제공

  • dependency 추가
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
</dependency>
  • 간단한 CRUD 구현
  • Repositoryinterface를 상속한다.

JpaRepository 인터페이스를 상속받는다.

@Repository
public interface JpaAccountRepository extends JpaRepository<Account, Long> {
}

이제 AccountJpa Entity로 등록하기 위해서 다음 3가지가 필요하다.

  • Jpa Entity인 것을 알려주기 위해 클래스 레벨에 @Entity 어노테이션을 붙여준다.
  • 기본 생성자 (매개변수가 없는 생성자)
  • id 필드에 @Id 어노테이션을 붙여준다.
  • @GeneratedValue(strategy=GenerationType.****)를 같이 사용한다.
    • 기본키 생성에는 4가지 전략이 있다.
    • IDENTITY : 기본키를 null로 넣으면, DB가 알아서 생성해서 넣어준다. (ex: Mysql, PostgreSQL,,)
    • SEQUENCE : 데이터베이스 시퀀스는 유일한 값을 순서대로 생성한다. (ex. Oracle, PostgreSQL, H2..)
    • TABLE : 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
    • AUTO : 데이터베이스 방언(dialect)마다 위 세가지 전략을 자동으로 지정한다.
@Data
@RequiredArgsConstructor
@Builder
@Entity
public class Account {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String username;
    private int age;
    @OneToMany(mappedBy = "account")
    private List<Article> articles;
}

Account와 일대다 매핑이 되어있는 ArticleEntity로 등록해준다.

  • Account(1) 입장에서는 Article(N)은 다(N)이기 때문에, @ManyToOne을 붙여준다.
  • @ManyToOne에는 항상 JoinColumn으로 1(일)에 해당하는 Entity를 지정해줘야한다.
  • @JoinColumn (생략가능) : EntityName + _ + ID, 생략하면 JPA가 알아서 구문 분석을해서 매핑해준다.
@Data
@Builder
@NoArgsConstructor
@Entity
public class Article {
  @Id @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  private String title;
  private Date date;
  @ManyToOne
  @JoinColumn(name = "ACCOUNT_ID")    // 생략가능
  private Account account;
}

 

 

AccountService 생성

@Service
public class Account1Service {

    @Autowired
    private JpaAccountRepository accountRepository;

    public Account save(Account account) {
        return accountRepository.save(account);
    }

    public List<Account> findAll() {
        return accountRepository.findAll();
    }

    public Account findById(Long id) {
        return accountRepository.findById(id)
                .orElseThrow(IllegalStateException::new);
    }

    public Account update(Long id, Account account) {
        Account oldAccount = this.findById(id);

        if(account.getUsername() != null) {
            oldAccount.setUsername(account.getUsername());
        }
        if (account.getAge() != 0) {
            oldAccount.setAge(account.getAge());
        }
        return accountRepository.save(oldAccount);
    }

    public void delete(Long id) {
        accountRepository.deleteById(id);
    }
}

 

CRUD 테스트

TEST 코드

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class Account1ServiceTest {

    @Autowired
    private Account1Service accountService;

    @Test
    @Order(1)
    void account_저장() {
        for (int i = 1; i <= 3; i++) {
            Account account = Account.builder()
                    .username("name" + i)
                    .age(20 + i)
                    .build();

            Account save = accountService.save(account);

            assertEquals(save.getUsername(), account.getUsername());
            assertEquals(save.getAge(), account.getAge());
        }
    }

    @Test
    @Order(2)
    void 전체_account_조회() {
        List<Account> accounts = accountService.findAll();

        assertEquals(accounts.size(), 3);
    }

    @Test
    @Order(3)
    void 하나의_account_조회() {
        Account account = accountService.findById(1L);

        assertEquals(account.getId(), 1L);
        assertEquals(account.getUsername(), "name1");
        assertEquals(account.getAge(), 21);
    }

    @Test
    @Order(4)
    void account_업데이트() {
        Account account = Account.builder()
                .username("user100")
                .age(100)
                .build();

        Account update = accountService.update(2L, account);

        assertEquals(update.getUsername(), account.getUsername());
        assertEquals(update.getAge(), account.getAge());
    }

    @Test
    @Order(5)
    void account_삭제() {
        accountService.delete(3L);

        assertThrows(IllegalStateException.class, () -> accountService.findById(3L));
    }
}

테스트 결과


REFERENCES

Spring in Action - 스프링 인 액션

반응형