๐Ÿ’ 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... args๋Š” select * 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 ๊ตฌํ˜„
  • Repository๋Š” interface๋ฅผ ์ƒ์†ํ•œ๋‹ค.

JpaRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š”๋‹ค.

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

์ด์ œ Account๋ฅผ Jpa 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์™€ ์ผ๋Œ€๋‹ค ๋งคํ•‘์ด ๋˜์–ด์žˆ๋Š” Article๋„ Entity๋กœ ๋“ฑ๋กํ•ด์ค€๋‹ค.

  • 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 - ์Šคํ”„๋ง ์ธ ์•ก์…˜

๋ฐ˜์‘ํ˜•