6. Spring/Spring Boot를 이용한 로그인 페이지 만들기 : DB 설치, 설정, 날 것 그대로 DB를 사용하는 방법
시작에 앞서서 해당 실습은 인프런에서 제공되는 무료 강좌에서 다룬 실습입니다. 강좌를 진행하신 튜터 분의 설명을 듣고 싶다면 해당 강좌 링크를 남겨둘테니 들으시기를 바랍니다.(개인적으로는 한 번 듣는 것을 추천드립니다!!)
[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
이전 글을 보고 오시지 않았다면 한 번 보고 오시는 것을 추천드립니다.
https://messy-developing-diary.tistory.com/54
5. Spring/Spring Boot를 이용한 로그인 페이지 만들기 : 테스트
시작에 앞서서 해당 실습은 인프런에서 제공되는 무료 강좌에서 다룬 실습입니다. 강좌를 진행하신 튜터 분의 설명을 듣고 싶다면 해당 강좌 링크를 남겨둘테니 들으시기를 바랍니다.(개인적으
messy-developing-diary.tistory.com
본격적으로 실습을 시작해보자. 마침내 본 서비스에 사용될 데이터베이스가 선정되었다. 우리는 H2라는 데이터베이스를 사용할 것이다. 사용하는 이유라면 간단한데, 데이터 베이스 치고는 용량이 작다. 굳이 간단한 실습을 하는데 많은 기능이 필요한 것도 아니고, 몇 기가에 달하는 Oracle DB를 깔 필요도 없지는 않는가 ? 설치 방법은 간단하다. 우선 밑의 링크를 통해서 H2 데이터베이스를 다운받는다. 단 데이터베이스 버전을 최신 버전이 아닌 1.4.2.00 버전으로 준비하기를 바란다.
https://www.h2database.com/html/download-archive.html
Archive Downloads
www.h2database.com
다운을 받았으면 cmd 창을 열고 해당 파일의 bin 폴더로 이동해준다. 그 후 윈도우 기준으로 h2w.bat 파일을 실행시켜주면 된다. 그러면 다음과 같은 화면이 뜰 것이다.
연결 버튼을 누르면 데이터베이스가 작동하기 시작한다. 당연하지만 실습을 위해서는 이 데이터베이스를 종료하면 안된다. 시험 겸, 실습을 위한 테이블을 만들기 위해 다음의 쿼리를 작성하여 동작시키자.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
쿼리에 대해 간단하게 설명하자면 member 라는 테이블을 생성할 것이고, name이라는 string 타입의 열과 식별자 역할을 하는 id 열을 가진다. 이 때 id는 데이터베이스가 자동으로 부여하도록 설정하였다.
화면 그대로 설정해 주면 된다. JDBC url에 jdbc:h2:tcp://localhost/~/test를 입력해주면 된다. 이제는build.gradle 파일에 들어가 데이터베이스와 통신하기 위한 설정을 해주어야 한다. 방법은 간단하다. 다음 코드를 추가해주기만 하면 된다.(중요 : build.gradle 파일을 수정한 후 반드시 build.gradle을 한반 refresh 해주어야 한다!! 이 걸 안해주어서 한동안 실습하는데 곤란함을 겪었다...)
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
그러면 build.gradle은 다음과 같을 것이다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
}
tasks.named('test') {
useJUnitPlatform()
}
또한 application.properties 파일을 수정해주어야 하는데, 이 파일은 resource 파일에 들어 있다. 다음과 같이 수정해주면 된다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
자 이제 길고 긴 설정을 마쳤으니 본격적으로 코드를 짜보자. 우리는 JdbcMemberRepository 클래스를 만들 것이다. 해당 클래스는 MemberRepository 인터페이스를 이식받는다. 코드는 다음과 같다.
package com.example.loginpage.repository;
import com.example.loginpage.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository{
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from member where name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
{
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
코드가 상당히 길다. 이 코드를 전부 설명하지는 않겠다. 간단히만 요약하자면 DB로 작업하기 위해서 필요한 과정들이 있다. 이는 파일을 읽고쓰는 것과 상당히 비슷하다. 파일을 읽거나 쓸때, 우선 파일을 열어야 하고, 해당 파일에 대해서 어떤 작업을 할 지 알려주어야 한다. 알려준 대로 읽거나, 쓰는 등 작업을 한 후 파일을 닫아주어야 한다. 똑같이 데이터베이스와 작업하기 위해서는 데이터 베이스와 연결해주어야 한다. 그 후 모드에 대한 정보와 쿼리를 보내게 된다. 모든 작업이 끝나게 되면 반드시 연결을 끊어주어야 한다. 그 이유는 간단히만 설명하자면 데이터베이스와 연결되기 위해서는 포트(Port)를 사용하게 되는데, 이는 하나의 프로세스가 독점하게 된다. 이를 반환하지 않게 되면, 다른 프로세스들의 작업을 지연시키는 등 문제가 발생하게 된다.(서버가 맛이 가버리는 원인 중 하나이니 잘 알아두어야 한다)
자 이제 임시로 사용하였던 저장소를 바꾸어주기로 하자. 방법은 간단하다. 더도 말고 덜도 말고 SpringConfig 클래스를 다음과 같이 변경해주기만 하면된다.
package com.example.loginpage;
import com.example.loginpage.repository.JdbcMemberRepository;
import com.example.loginpage.repository.MemberRepository;
import com.example.loginpage.repository.MemoryMemberRepository;
import com.example.loginpage.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new JdbcMemberRepository(dataSource);
}
}
아마 별문제 없이 잘 실행될 것이다. 별도로 테스트 코드를 적어두었다. 저번처럼 test 폴더에 추가하면 된다.
package com.example.loginpage.service;
import com.example.loginpage.domain.Member;
import com.example.loginpage.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));//예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
@SpringBootTest라는 어노테이션은 통합적인 환경 테스트를 하겠다는 뜻이고, @Transactional은 여러 기능을 제공하기는 하지만 여기서는 DB에 전송된 쿼리들을 Commit하지 않겠다고 선언하는 기능을 한다. DB의 경우 쿼리들은 Commit을 해야 실제 DB에 적용이 된다. 이렇게 해야 테스트를 한 것이 실제 DB에 영향을 주지 않는다. 이 것으로 글을 마친다.
PS. DB를 다시 깔아야 하는 상황이 온다면 uninstall.exe로 지우면 된다. 그 외에도 C 드리이버 user에서 h2_server.properties, test.mv.db, test.trace.db를 반드시 지워주어야 한다. 연결이 되지 않는다면 그때는 url에서 ip 주소가 적혀있는 부분을 localhost로 바꾸어주자. 그 외에도 db 파일을 찾을 수 없다며 문제가 발생하기도 한다. 그때는 새로운 H2 db를 연결해주고 테스트 없이 연결하면 된다.
이 글이 도움이 되었기를 바라며 글을 마친다.