💐 Spring/Spring in Action

Jdbc, JdbcTemplate, Spring Data Jpa에 대해서

2022. 3. 21. 00:54
목차
  1. Goal
  2. 순수 JDBC
  3. Spring에서 제공하는 JdbcTemplate
  4. DataSource를 Bean으로 등록
  5. application.yml 등록
  6. 테스트 해보기
  7. 연관관계 매핑일 때, 데이터를 저장하는 2가지 방법 (JdbcTemplate vs SimpleInsert)
  8. JdbcTemplate 사용
  9.  
  10. SimpleJdbcInsert 사용
  11. Spring Data JPA 사용
  12.  
  13. AccountService 생성
  14.  
  15. CRUD 테스트
  16. REFERENCES
반응형

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 - 스프링 인 액션

반응형
저작자표시 (새창열림)
  1. Goal
  2. 순수 JDBC
  3. Spring에서 제공하는 JdbcTemplate
  4. DataSource를 Bean으로 등록
  5. application.yml 등록
  6. 테스트 해보기
  7. 연관관계 매핑일 때, 데이터를 저장하는 2가지 방법 (JdbcTemplate vs SimpleInsert)
  8. JdbcTemplate 사용
  9.  
  10. SimpleJdbcInsert 사용
  11. Spring Data JPA 사용
  12.  
  13. AccountService 생성
  14.  
  15. CRUD 테스트
  16. REFERENCES
iseunghan
iseunghan
꾸준하게 열심히..
iseunghan
iseunghan

공지사항

  • 어제보다 나은 오늘이 되기 위해 🔥
  • 분류 전체보기 (262)
    • 💐 Spring (14)
      • 개념 및 이해 (2)
      • Spring 핵심 기술 (24)
      • Spring REST API (8)
      • Spring MVC, DB 접근 기술 (7)
      • Spring Security (23)
      • Spring in Action (1)
    • 🌻 JAVA (84)
      • 자바 ORM 표준 JPA 프로그래밍 (20)
      • 알고리즘, 자료구조 (13)
      • 디자인 패턴 (7)
      • 정리정리정리 (43)
      • JUnit (1)
    • 🔖 Snippets (3)
      • Javascript (3)
    • ⚙️ Devops (22)
      • ⛏ Git (11)
      • 🐳 Docker (6)
      • 🐧 Linux (3)
      • 🌈 Jenkins (1)
      • 📬 Kafka (1)
    • 💬 ETC.. (4)
      • 💻 macOS (2)
    • 🌧️ ORM (2)
      • JPA (2)
    • 🐍 Python (3)
    • 📚 Databases (15)
      • 오라클로 배우는 데이터베이스 개론과 실습(2판) (3)
      • RealMySQL 8.0 (8)
    • 🔥 Computer Science (5)
      • 📡 네트워크 (5)
    • 🏷️ 협업 (1)
    • 📜 코딩테스트 (38)
      • BAEKJOON\수학 1, 수학 2 (8)
      • BAEKJOON\재귀 (5)
      • BAEKJOON\브루트 포스 (3)
      • BAEKJOON\정렬 (1)
      • BAEKJOON\백트래킹 (5)
      • BAEKJOON\BFS, DFS (6)
      • BAEKJOON\이분탐색 (1)
      • BAEKJOON\다이나믹 프로그래밍 (9)
      • BAEKJOON\그리디 알고리즘 (0)
    • ✨ ISEUNGHAN (1)

인기 글

최근 글

전체
오늘
어제
반응형
hELLO · Designed By 정상우.
iseunghan
Jdbc, JdbcTemplate, Spring Data Jpa에 대해서
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.