냐냐한 IT/냐냐한 Spring Boot

Spring Boot + H2 + Jdbc로 사용해보기

소소하냐 2019. 9. 20. 16:14

Spring Boot + MyBatis 프로젝트 연습 목록 

- 01. 신규 Spring Boot 프로젝트 만들기

- 02. Thymeleaf, spring-boot-devtools 추가

- 03. Spring Boot에 H2 추가 

- 04. Spring Boot + H2 + Jdbc로 사용해보기 (현재 포스트)

 

 


현재 포스팅하는 시리즈는 Spring Boot와 MyBatis를 결합한 프로젝트를 연습하는 부분입니다. 하지만 이번 포스팅은 MyBatis를 설정하기 전에 먼저 Jdbc로 사용하는 방법을 확인하고 싶어 진행한 곁가지입니다. 01~03번 포스팅까지는 동일하게 진행되고 사용함에 있어 04.Jdbc / 05.MyBatis(이후 정리 예정) 이렇게 나뉘어집니다. 

 

JDBC 사용하기

 

1. pom.xml에 아래 내용 추가 (앞선 포스팅 H2 설정하기에서 이미 추가하였다) 

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    </dependencies>

 

2. account 테이블 생성 및 데이터 입력 

: schema.sql 내용 변경 (이전에는 h2 설정이 잘 되었는지 확인하기 위한 테스트 테이블 생성이 들어있었다.)

DROP TABLE IF EXISTS accounts;
 
CREATE TABLE accounts (
  id INT AUTO_INCREMENT  PRIMARY KEY,
  email VARCHAR(255) NOT NULL
);
 
INSERT INTO accounts (email) VALUES
  ('email1@email.com'),
  ('email2@email.com'),
  ('email3@email.com');

 

3. Account 도메인 객체 생성

: /src/main/java 하위에 com.sosohanya.leveldiary.account 패키지를 만들고 Account 클래스 생성 
  (패키지 및 파일 구조는 본인이 선호하는 바에 따라)

package com.sosohanya.leveldiary.account;

public class Account {

	private long id;
	private String email;
	
	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public Account(long id, String email) {
		this.id = id;
		this.email = email;
	}
	
	public Account(String email) {
		this.email = email;
	}

	@Override
	public String toString() {
		return "Account [id=" + id + ", email=" + email + "]";
	}	
}

 

4. 데이터 처리 인터페이스 정의
: /src/main/java 하위에 com.sosohanya.leveldiary.account 패키지에 AccountRepository 인터페이스 생성
  (인터페이스를 사용하지 않고 바로 클래스로 구현할 수도 있겠지만..나중을 위해서 그러지 않겠습니다. 인터페이스(Interface) 개념을 간단히 잘 소개한 생활코딩-인터페이스 페이지 보기 [새창]

AccountRepository.java 

더보기
package com.sosohanya.leveldiary.account;

import java.util.List;

public interface AccountRepository {

	int count();
	
	long save(Account account);
	
	int update(Account account);
	
	int deleteById(Long id);
	
	void deleteAll();
	
	List<Account> findAll();
	
	Account findById(Long id);
	
	Account findByEmail(String email);
}

 

5. 데이터 처리 구현

: /src/main/java 하위에 com.sosohanya.leveldiary.account 패키지에 AccountRepository를 구현하는 JdbcAccountRepository 클래스 생성 

JdbcAccountRepository.java 소스 

package com.sosohanya.leveldiary.account;

//... import 구문 생략 ...

@Repository
public class JdbcAccountRepository implements AccountRepository {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;  

	@Override
	public int count() {
		return jdbcTemplate
				.queryForObject("select count(*) from accounts", Integer.class);
	}

	@Override
	public long save(Account account) {
		//자동 생성되는 키 리턴받기 
		KeyHolder keyHolder = new GeneratedKeyHolder();
		
		jdbcTemplate.update(connection -> {
				PreparedStatement ps = connection.prepareStatement("insert into accounts (email) values (?)", Statement.RETURN_GENERATED_KEYS);
				ps.setString(1, account.getEmail());
				return ps;
	        }, keyHolder);
		
		return (long)keyHolder.getKey();
		
		//자동 생성되는 키를 받을 수 없다. 성공 또는 실패. 0 또는 1만 반환 
		//return jdbcTemplate.update("insert into accounts (email) values (?)", 
		//		account.getEmail());
	}
	

	@Override
	public int update(Account account) {
		return jdbcTemplate.update("update accounts set email = ? where id = ?", 
				account.getEmail(),
				account.getId());
	}

	@Override
	public int deleteById(Long id) {
		return jdbcTemplate.update("delete accounts where id = ?",
				id);
	}

	@Override
	public List<Account> findAll() {
		return jdbcTemplate.query("select * from accounts", 
				(rs, rowNum) -> new Account(
							rs.getLong("id"),
							rs.getString("email")
						)
				);
				
	}

	@Override
	public Account findById(Long id) {
		try {
			return jdbcTemplate.queryForObject("select * from accounts where id = ?",
					new Object[] {id}, 
					(rs, rowNum) -> Optional.of(new Account(
							rs.getLong("id"),
							rs.getString("email")
						))
					).orElse(null);
		}catch(EmptyResultDataAccessException e) {
			return null;
		}
	}

	@Override
	public Account findByEmail(String email) {
		try {
			return jdbcTemplate.queryForObject("select * from accounts where email = ?", 
					new Object[] {email}, 
					(rs, rowNum) -> Optional.of(new Account(
							rs.getLong("id"),
							rs.getString("email")
						))
					).orElse(null);
		}catch(EmptyResultDataAccessException e) {
			return null;
		}
	}

	@Override
	public void deleteAll() {
		jdbcTemplate.update("delete from accounts");
		
	}
}

 

6. 단위 테스트로 확인 : 생성한 JdbcAccountRepository에 대한 테스트 작성 및 테스트 

(부끄럽지만 실무를 하면서 단위 테스트를 적용해본적이 없어 아래 단위 테스트 내용은 단위 테스트 생초보의 작성내용이라는 점 감안해주시길 바랍니다. 저는 Java 생초보, 단위 테스트 생초보. 저는 여러 가지로 게으른 개발자였습니다. ㅠㅠ)

JdbcAccountRepositoryTest.java 소스

더보기
package com.sosohanya.leveldiary.account;

//... import 구문 생략 ...

@RunWith(SpringRunner.class)
@SpringBootTest
public class JdbcAccountRepositoryTest {

	@Autowired
	JdbcAccountRepository jdbcAccountRepository;
	
	String defaultEmail = "add@email.com";
	
	@Before
	public void setUp() {
		jdbcAccountRepository.deleteAll();		
	}
	
	@Test
	public void count() {
		assertEquals(0, jdbcAccountRepository.count());
		jdbcAccountRepository.save(new Account("add1@email.com"));
		jdbcAccountRepository.save(new Account("add2@email.com"));
		jdbcAccountRepository.save(new Account("add3@email.com"));
		assertEquals(3, jdbcAccountRepository.count());
	}
	
	@Test
	public void saveAndFindAll() {
		jdbcAccountRepository.save(new Account("add1@email.com"));
		jdbcAccountRepository.save(new Account("add2@email.com"));
		jdbcAccountRepository.save(new Account("add3@email.com"));
		
		List<Account> getAll = jdbcAccountRepository.findAll();
		assertEquals(3, getAll.size());
	}
	
	@Test
	public void saveAndFindById() {
		long resultId = jdbcAccountRepository.save(new Account(defaultEmail));
		Account getAccount = jdbcAccountRepository.findById(resultId);
		
		assertNotNull(getAccount);
		assertEquals(defaultEmail, getAccount.getEmail());
	}
	
	@Test
	public void saveAndFindByEmail() {
		jdbcAccountRepository.save(new Account(defaultEmail));
		Account getAccount = jdbcAccountRepository.findByEmail(defaultEmail);
		
		assertNotNull(getAccount);
		assertEquals(defaultEmail, getAccount.getEmail());
	}
	
	@Test
	public void update() {
		long resultId = jdbcAccountRepository.save(new Account(defaultEmail));
		Account getAccount = jdbcAccountRepository.findById(resultId);
		
		getAccount.setEmail("update@email.com");
		jdbcAccountRepository.update(getAccount);
		Account updatedAccount = jdbcAccountRepository.findById(resultId);
		assertEquals("update@email.com", updatedAccount.getEmail());
	}
	
	@Test
	public void deleteById() {
		long resultId1 = jdbcAccountRepository.save(new Account("add1@email.com"));
		long resultId2 = jdbcAccountRepository.save(new Account("add2@email.com"));
		long resultId3 = jdbcAccountRepository.save(new Account("add3@email.com"));
		
		jdbcAccountRepository.deleteById(resultId2);
		assertEquals(2, jdbcAccountRepository.count()); 
		
		assertNotNull(jdbcAccountRepository.findById(resultId1));
		assertNull(jdbcAccountRepository.findById(resultId2));
		assertNotNull(jdbcAccountRepository.findById(resultId3));
	}
}
  • 오류 발생 : org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database may be already in use: null. Possible solutions: close all other connection(s);
    • 로컬 서버가 실행된 상태에서 JUnit test를 실행할 때 나온 현상. (이미 로컬 서버에서 Database를 사용중이라 생긴 문제로 파악됨)
    • 해결 : application.properties 파일에서 spring.datasource.url 속성에 ;AUTO_SERVER=TRUE 내용 추가
    • AUTO_SERVER=TRUE 
      : 서버를 수동으로 시작하지 않고도 여러 프로세스가 동일한 데이터베이스에 액세스 가능. 데이터베이스가 이미 열려있는지 여부에 관계없이 동일한 데이터베이스 URL 사용이 가능. (이 기능은 in-memory 데이터베이스에서는 작동하지 않음) 자세한 내용은 H2 공식 웹사이트 - Features : Automatic Mixed Mode 확인 [새창]
spring.datasource.url=jdbc:h2:~/leveldiary;AUTO_SERVER=TRUE

 

7. 위에서 생성한 JdbcAccountRepository를 사용하는 목록/내용보기/추가/수정/삭제 기능을 간단하게 구현
(프로젝트를 만들어보고 돌아가는 것을 보기 위한 것이기에 디자인적인 부분은 전혀 없습니다. )

    7-1. AccountController.java 생성 (/scr/main/java 폴더내의 com.sosohanya.leveldiary.controllers)

더보기
package com.sosohanya.leveldiary.controllers;

//... import 구문 생략 ...

@Controller
@RequestMapping("/account")
public class AccountController {
	
	@Autowired
	JdbcAccountRepository jdbcAccountRepository;

	@GetMapping("/list") //목록 
	public String list(Model model) {
		
		model.addAttribute("accounts", jdbcAccountRepository.findAll());
		model.addAttribute("count", jdbcAccountRepository.count());
		
		return "account/list";
	}
	
	@GetMapping("/add") //추가 Form 
	public String add() {
		return "account/add";
	}
	
	@PostMapping("/add") //추가 처리 
	public String addProcess(String email){
		jdbcAccountRepository.save(new Account(email));
		
		return "redirect:/account/list";
	}
	
	@GetMapping("/{id}") //상세보기 
	public String view(@PathVariable Long id, Model model) {
		model.addAttribute("account", jdbcAccountRepository.findById(id));
		
		return "account/view";
	}
	
	@GetMapping("/{id}/modify") //수정 Form
	public String modify(@PathVariable Long id, Model model) {
		
		model.addAttribute("account", jdbcAccountRepository.findById(id));
		
		return "account/modify";
	}
	
	@PostMapping("/{id}/modify") //수정 처리 
	public String modifyProcess(@PathVariable Long id, String email) {
		Account account = jdbcAccountRepository.findById(id);
		account.setEmail(email);
		jdbcAccountRepository.update(account);
		
		return String.format("redirect:/account/%d", id) ;
	}
	
	@PostMapping("/{id}/delete") //삭제 처리 
	public String deleteProcess(@PathVariable Long id) {
		jdbcAccountRepository.deleteById(id);
		
		return "redirect:/account/list";
	}
	
}

    7-2. src/main/resources/templates/account 폴더 생성 후 해당 폴더에 list.html / add.html / view.html / modify.html 파일 생성 (thymeleaf 템플릿을 사용)

- list.html

더보기
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.com"> <!-- xmlns:th를 설정하지 않으면 에디터에서 경고가 표시되어 신경쓰임 -->
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div>
account count : <span th:text="${count}"></span> 
</div>
<br/><br/>
<div>[account list]</div><br/>
<table border="1">
	<tr>
		<th>id</th>
		<th>email</th>
	</tr>
	<tr th:each="account : ${accounts}">
		<td th:text="${account.id}"></td>
		<td><a href="/account/1" th:href="@{/account/{id}(id=${account.id})}" th:text="${account.email}"></a></td>
	</tr>
</table>

<a th:href="@{/account/add}" href="/account/add">account 추가</a>
</body>
</html>

- add.html 

더보기
...생략...

<form method="post" th:action="@{/account/add}">
	Email : <input type="text" name="email" />
	<button type="submit">추가</button>
</form>
<br/>
<a href="/account/list" th:href="@{/account/list}">account list</a>

...생략...

- view.html

더보기
...생략...

<div>
	email : <span th:text="${account.email}"></span>
</div>

<div>
	<a href="/account/list" th:href="@{/account/list}">목록</a> <br/>
	<a href="/account/1/modify" th:href="@{/account/{id}/modify(id=${account.id})}">수정하기</a> <br/>
	<form method="post" th:action="@{/account/{id}/delete(id=${account.id})}">
		<button type="submit">삭제하기</button>
	</form>
</div>

...생략...

- modify.html 

더보기
...생략...

<form method="post" th:action="@{/account/{id}/modify(id=${account.id})}">
	Email : <input type="text" name="email" th:value="${account.email}" />
	<button type="submit">수정</button>
</form>
<br/>
<a href="/account/list" th:href="@{/account/list}">account list</a>

...생략...

       

프로젝트 폴더 구조 및 list/add/view/modify 화면입니다. 

 

지금까지의 내용은 github에 branch:004-using-jdbc[새창]로 확인하실 수 있습니다

 

참고 사이트 : 

- Spring 공식사이트 - Accessing Relational Data using JDBC with Spring

- Mkyong.com - Spring Boot JDBC Examples

 


Spring Boot + MyBatis 프로젝트 연습 목록 

- 01. 신규 Spring Boot 프로젝트 만들기

- 02. Thymeleaf, spring-boot-devtools 추가

- 03. Spring Boot에 H2 추가 

- 04. Spring Boot + H2 + Jdbc (현재 포스트)