카테고리 없음

6. Spring/Spring Boot를 이용한 로그인 페이지 만들기 : DB 설치, 설정, 날 것 그대로 DB를 사용하는 방법

잡학다식을꿈꾼다 2023. 1. 28. 00:14
반응형

 시작에 앞서서 해당 실습은 인프런에서 제공되는 무료 강좌에서 다룬 실습입니다. 강좌를 진행하신 튜터 분의 설명을 듣고 싶다면 해당 강좌 링크를 남겨둘테니 들으시기를 바랍니다.(개인적으로는 한 번 듣는 것을 추천드립니다!!)

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 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를 연결해주고 테스트 없이 연결하면 된다.

 

 이 글이 도움이 되었기를 바라며 글을 마친다.

반응형