[Spring] 데이터베이스

2023. 11. 9. 03:09학부 강의/웹프로그래밍 (Spring)

0. 출처

 

아직 배우고 있는 중이라 부정확한 정보가 포함되어 있을 수 있습니다!
주의하세요!

 

 

올인원 스프링 프레임워크 참고.

 

올인원 스프링 프레임워크 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 


 

1. 데이터베이스

 

데이터베이스로는 RDBMS인 MariaDB를 사용한다.

 

  • MariaDB
    : 오픈 소스 RDBMS
    : 오라클이 MySQL을 인수해면서 무료 및 오픈 소스 버전에 대한 미래가 불투명해졌고, 이러한 우려에서 MariaDB가 탄생했다.
    : MySQL과 동일한 소스 코드를 기반으로 하며, GPL v2 라이선스를 따른다.

 

또한 데이터베이스를 쉽고 직관적으로 관리하기 위해서 HeidiSQL을 추가적으로 사용한다.

 


 

가. MariaDB 설치

 

10.11.5 버전으로 설치한다.

 

 

Download MariaDB Server - MariaDB.org

REST API Release Schedule Reporting Bugs … Continue reading "Download MariaDB Server"

mariadb.org

 

 

  • root 유저의 비밀번호는 1234로 설정했다. (자유롭게 선택하세요)
  • 3306 포트를 선택했다.

 


 

나. HeidiSQL 설치

 

 

 


 

다. 테이블 만들기

 

CREATE DATABASE db_library;
USE db_library;

 

CREATE TABLE tbl_admin_member(
    a_m_no INT AUTO_INCREMENT,
    a_m_approval INT NOT NULL DEFAULT 0,
    a_m_id VARCHAR(20) NOT NULL,
    a_m_pw VARCHAR(100) NOT NULL,
    a_m_name VARCHAR(20) NOT NULL,
    a_m_gender CHAR(1) NOT NULL,
    a_m_part VARCHAR(20) NOT NULL,
    a_m_position VARCHAR(20) NOT NULL,
    a_m_mail VARCHAR(50) NOT NULL,
    a_m_phone VARCHAR(20) NOT NULL,
    a_m_reg_date DATETIME,
    a_m_mod_date DATETIME,
    PRIMARY KEY(a_m_no)
);

 


 

라. JDBC

 

  • JDBC
    : Java Database Connectivity
    : 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API 세트다.
    : JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.
    : DBMS가 자바 프로그램의 요청을 이해할 수 있도록 프로토콜을 변환한다.

 

JDBC는 다양한 데이터베이스와 호환되며, 데이터베이스 업체들은 자신들의 데이터베이스에 맞는 JDBC 드라이버를 제공한다.

 

데이터베이스와 독립적인 코드를 작성할 수 있게 해준다.

 

개발자는 다양한 데이터베이스를 같은 방식으로 다룰 수 있고, 이는 코드의 이식성이 증가 및 데이터베이스 교체의 복잡성이 감소로 이어진다.

 

JDBC를 사용하면 다음과 같은 과정을 거치게 된다.

 

JDBC 드라이버 로드 → 데이터베이스 연결 → SQL query 작성 → PreparedStatement 생성 → 쿼리 실행

 

 

스프링에서는 JdbcTemplate 클래스를 제공하기 때문에 JDBC 드라이버 로드, 데이터베이스 연결과 같은 반복적인 작업보다는 SQL query 작성 및 실행에 집중할 수 있다.

 

<!-- pom.xml 수정 -->    

    ...

    <!--  JDBC  -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${org.springframework-version}</version>
    </dependency>

    <!-- mariaDB -->
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
        <version>3.1.2</version>
    </dependency>    
</dependencies>

pom.xml에 mariaDB와 JDBC를 추가한다.

 

<!-- jdbc-context.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

    <!-- <context:property-placeholder location="/WEB-INF/spring/property/real.info.properties" /> -->
    <!-- MariaDB connection 객체 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.mariadb.jdbc.Driver"/>
        <property name="url" value="jdbc:mariadb://127.0.0.1:3306/db_library" />
        <property name="username" value="root"/>
        <property name="password" value="1234"/>
    </bean>

    <!-- JdbcTemplate 객체 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- TransactionManager 객체 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans:beans>

jdbc-context.xml 생성한다.

 

  • MariaDB connection 객체
    : MariaDB 드라이버 로드
    : 127.0.0.1:3306/db_library에 위치한 MariaDB 데이터베이스에 연결하도록 설정.
    : usernameroot, password1234다.
  • JdbcTemplate 객체
    : 앞서 정의한 dataSource 빈을 주입받아 데이터베이스 연결을 위해 사용한다.
  • TransactionManager 객체
    : 트랜잭션을 관리.

 

<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/spring/root-context.xml
            /WEB-INF/spring/jdbc-context.xml
            /WEB-INF/spring/security-context.xml
        </param-value>
    </context-param>

jdbc-context.xml의 존재와 위치를 web.xml에 추가한다.

 

프로젝트가 서버에서 실행될 때 jdbc-context.xml을 읽고 JdbcTemplate 객체를 생성한다.

 

@Component
public class AdminMemberDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    public boolean isAdminMember(String a_m_id) {
        System.out.println("[AdminMemberDao] isAdminMember()");

        String sql = "SELECT COUNT(*) FROM tbl_admin_member "
                    + "WHERE a_m_id = ?";
        int result = jdbcTemplate.queryForObject(sql, Integer.class, a_m_id);

        if(result > 0)
            return true;
        else
            return false;
    }
...
  • @Autowired JdbcTemplate jdbcTemplate;
    : DAO에서 JdbcTemplate를 사용하기 위해서 의존 객체 자동 주입을 한다.
  • public boolean isAdminMember(String a_m_id) { … }
    : 입력한 아이디가 중복인지 검사한다.
  • String sql = "SELECT COUNT(*) FROM tbl_admin_member " + "WHERE a_m_id = ?";
    : SQL query로 “tbl_admin_member 테이블에서 a_m_id?(= 외부에서 입력된 값)과 같은 row의 수”를 질의한다.
  • int result = jdbcTemplate.queryForObject(sql, Integer.class, a_m_id);
    : 결과가 Integer 타입의 단일 객체인 sql을 실행한다. 이때 a_m_idsql의 인자로 전달한다.

 

isAdminMember()로 중복 검사를 하고 통과하면 관리자 정보를 데이터베이스에 저장하기 위해서 insertAdminAccount()를 구현한다.

 

@Component
public class AdminMemberDao {
    ...
    public int insertAdminAccount(AdminMemberVo adminMemberVo) {
        System.out.println("[AdminMemberDao] insertAdminAccount()");

        List<String> args = new ArrayList<String>();

        String sql = "INSERT INTO tbl_admin_member(";
        if(adminMemberVo.getA_m_id().equals("super admin")) {
            sql += "a_m_approval, ";
            args.add("1");
        }

        sql += "a_m_id, ";
        args.add(adminMemberVo.getA_m_id());

        sql += "a_m_pw, ";
        args.add(adminMemberVo.getA_m_pw());

        sql += "a_m_name, ";
        args.add(adminMemberVo.getA_m_name());

        sql += "a_m_gender, ";
        args.add(adminMemberVo.getA_m_gender());

        sql += "a_m_part, ";
        args.add(adminMemberVo.getA_m_part());

        sql += "a_m_position, ";
        args.add(adminMemberVo.getA_m_position());

        sql += "a_m_mail, ";
        args.add(adminMemberVo.getA_m_mail());

        sql += "a_m_phone, ";
        args.add(adminMemberVo.getA_m_phone());

        sql += "a_m_reg_date, a_m_mod_date) ";

        if(adminMemberVo.getA_m_id().equals("super admin"))
            sql += "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())";
        else
            sql += "VALUES(?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())";

        int result = -1;

        try {
            result = jdbcTemplate.update(sql, args.toArray());
        }catch(Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    ...
  • String sql = "INSERT INTO tbl_admin_member(";
    : INSERT문 문법 → INSERT INTO 테이블_또는_뷰_이름 (컬럼1, 컬럼2, ...) *values* (값1, 값2, ...)
  • result = jdbcTemplate.update(sql, args.toArray());
    : sql 실행. 인자로 ArrayList<String> argsarray로 변환해서 전달한다.

 

최고 관리자 super admina_m_approval1로 설정한다.

 

보충

아래는 JdbcTemplate 클래스의 몇 가지 핵심 메서드를 표 형식으로 정리한 것입니다:

메서드 유형 메서드 이름 설명
쿼리 query(String sql, RowMapper rowMapper) 주어진 SQL을 실행하고 RowMapper를 사용하여 결과를 객체로 매핑
쿼리 queryForObject(String sql, Class requiredType) 결과가 단일 객체인 SQL 쿼리를 실행
업데이트 update(String sql) 데이터베이스에서 INSERT, UPDATE, DELETE 작업을 수행하는 SQL을 실행
배치 업데이트 batchUpdate(String... sql) 배치로 여러 SQL 업데이트를 실행
설정 setDataSource(DataSource dataSource) JdbcTemplate에 사용할 DataSource를 설정
설정 setQueryTimeout(int queryTimeout) 쿼리 실행의 최대 시간을 초 단위로 설정
트랜잭션 execute(String sql) SQL 문을 실행하며 복잡한 트랜잭션과 저장 프로시저 호출에 사용될 수 있음
유틸리티 queryForList(String sql) 쿼리 결과를 Map의 리스트로 반환
유틸리티 queryForMap(String sql) 단일 행 결과를 Map으로 반환

 

  • JdbcTemplate class reference
 

JdbcTemplate (Spring Framework 6.0.13 API)

Execute a query for a result object, given static SQL. Uses a JDBC Statement, not a PreparedStatement. If you want to execute a static query with a PreparedStatement, use the overloaded JdbcOperations.queryForObject(String, Class, Object...) method with nu

docs.spring.io

 

출처 : https://ko.wikipedia.org/wiki/JDBC

출처 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html

출처 : ChatGPT 4.0

 


 

2. 암호화

 

보안상 데이터베이스에 비밀번호를 평문으로 보관하는 것은 굉장히 위험하다.

 

이에 비밀번호를 암호화해서 보관하겠다.

 

스프링에는 데이터를 암호화하는 spring-security-core라는 모듈을 제공한다.

 

  • spring-security
    : Spring Security is a framework that provides authentication, authorization, and protection against common attacks.
    : 자바 기반 응용 프로그램을 위한 인증, 인가 및 보안 기능을 제공한다.

 

데이터를 암호화하기 위해서 사용해 보겠다.

 

출처 : https://docs.spring.io/spring-security/reference/index.html

 


 

가. 설정

<!-- pom.xml -->
<dependencies>
    ...
    <!--  Spring security -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>${org.springframework-version}</version>
    </dependency>    
</dependencies>

pom.xmlspring-security-core를 추가한다.

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

    <bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

</beans>

WEB-INF/spring/security-context.xml을 생성한다.

 

bcrypt 해시 알고리즘을 사용해서 암호화하는 것을 알 수 있다.

 

bcrypt는 단방향 암호화를 위해 만들어진 해시 함수로 복호화는 불가능하다.

 

사용자의 비밀번호는 암호화된 상태로만 저장되고, 로그인 시 사용자가 입력한 비밀번호는 다시 암호화하여 저장된 해시와 비교한다.

 

비교결과 둘이 일치한다면 비밀번호가 올바른 것으로 간주한다.

 

(참고로 보다 철저한 보안을 위해 salt도 사용되는 것으로 파악된다.)

 

[Node.js] 쿠키의 한계

1. 인증 부분의 한계 민감한 정보를 클라이언트 쪽에 저장한다는 것은 위험하다. 또한 쿠키는 쉽게 탈취당할 수 있기 때문에 민감한 정보를 저장하는 것에 어울리지 않는다. 그렇기 때문에 요즘

ramen4598.tistory.com

 

<!-- web.xml -->
...
<context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/spring/root-context.xml
            /WEB-INF/spring/jdbc-context.xml
            /WEB-INF/spring/security-context.xml
        </param-value>
    </context-param>

프로젝트가 서버에서 실행될 때 security-context.xml을 인식하도록 web.xml에 추가한다.

 


 

나. 암호화 구현

@Component
public class AdminMemberDao {

    @Autowired
    PasswordEncoder passwordEncoder;   // 추가

    public int insertAdminAccount(AdminMemberVo adminMemberVo) {

        ...

        sql += "a_m_pw, ";
        args.add(passwordEncoder.encode(adminMemberVo.getA_m_pw()));   // 추가

        ...

 

  • @Autowired PasswordEncoder passwordEncoder;
    : 암호화에 사용할 PasswordEncoder를 자동으로 주입한다.
  • args.add(passwordEncoder.encode(adminMemberVo.getA_m_pw()));
    : adminMemberVo.getA_m_pw()를 암호화해서 저장한다.

 

http://localhost:8090/library/admin/ 접속 → 회원가입 누르기.

 

 

최고 관리자 계정을 생성한다.

 

회원가입에 성공했다.

 

실제로 암호화된 비밀번호가 데이터베이스에 저장된 것을 확인할 수 있다.

 


 

다. 복호화? matches!

 

앞서 bcrypt 해쉬 알고리즘은 단방향 해시 알고리즘이라서 복호호가 불가능하다고 설명했다.

 

그러면 복호화하지 않고 어떻게 입력한 비밀번호 정보가 올바른 비밀번호인지 알 수 있을까?

 

if(!passwordEncoder.matches(adminMemberVo.getA_m_pw(), adminMemberVos.get(0).getA_m_pw()))

짧게 설명하면 다음과 같이 boolean PasswordEncorder.matches(CharSequence rawPassword, String encodedPassword);를 활용해서 비교한다.

 

입력된 비밀번호를 암호화해서 데이터베이스에 저장된 해시값과 비교한다.

 

같다면 올바른 비밀번호를 입력한 것으로 간주한다.