본문 바로가기
TIL

2023-10-30-TIL

by raphael3 2023. 10. 31.
반응형

스프링 DB 접근 기술

순수 JDBC → 스프링 JDBC로 발전함: 쿼리를 사용하여 DB에 접근, 데이터 저장/조회/수정 등을 수행함

JPA: 객체를 쿼리 없이 DB에 바로 저장할 수 있는 기술

JPA → 스프링 JPA: JPA를 더 편하게 쓸 수 있도록 스프링으로 한 번 감싼 것


H2

H2 Database Engine

→ All platform → unzip → bin

h2.sh의 권한: 664 ⇒ chmod 755 h2.sh

alias 추가:

sudo vi ~/.zshrc
> alias h2=”h2.sh까지의 경로”
source ~/.zshrc

→터미널에서 “h2” 입력하여 실행

→ “연결” 클릭

“연결 끊기”버튼으로 나갈 수 있음

홈 디렉토리에 test.mv.db가 생성됨

이후부터는, JDBC URL “jdbc:h2:tcp://localhost/~/test”인 소켓 형식으로 접속 (파일 형태의 데이터베이스에 접속하면 각 애플리케이션들이 DB 접속할 때마다 충돌이 날 수 있기 때문)

쿼리

테이블 생성을 위해 아래 쿼리를 실행한다.

drop table if exists member CASCADE;
create table member
(
    id   bigint generated by default as identity,
    name varchar(255),
    primary key (id)
);
  • generated by default as identity: 해당 컬럼이 DB에 의해 자동으로 값을 생성하고 관리되는 것을 의미한다. 구체적으로는, 값 없이 DB로 들어오면 DB가 알아서 값을 채워달라는 의미다. 따라서, 테이블에 새로운 레코드가 삽입될 때마다 자동으로 증가하는 일련번호(또는 ID)를 부여한다.
    • generated by default: 컬럼 값이 개발자의 직접적인 값 입력 없이도 데이터베이스에 의해 자동으로 생성된다.
    • as identity: 해당 컬럼은 IDENTITY 컬럼이다. IDENTITY 컬럼은 자동으로 증가하는 값을 가지며, 일반적으로 프라이머리 키(primary key)로 사용된다. 주로 테이블의 기본 키(primary key)로 사용되는 컬럼에 사용된다.

값을 넣는다.

insert into MEMBER(name) values('Spring1');
insert into MEMBER(name) values('Spring2');
SELECT * FROM MEMBER

id를 생략하고 insert를 해도 자동으로 들어가는 것을 확인할 수 있다.


쿼리는 보통 프로젝트에 파일로 만들어 관리한다.


순수 JDBC

Java Database Connectivity의 약자 자바 프로그램이 데이터베이스와 통신할 수 있도록 해주는 자바 API(응용 프로그래밍 인터페이스) 이 API로 데이터베이스에 접속하고 SQL 쿼리를 실행할 수 있다.

필요한 API 설치: build.gradle

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

→ src/main/resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

→ src/main/java/hello/hellospring/repository/JdbcMemberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository{

    @Override
    public Member save(Member member) {
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @Override
    public List<Member> findAll() {
        return null;
    }
}

이제부턴 인메모리 데이터베이스가 아닌 h2 데이터베이스를 이용하는 방식의 새로운 구현체를 만들 것이다.

이제, DataSource를 주입받고, 주입받은 DataSource를 통해 DB connection 소켓을 얻어낸 다음, 쿼리를 날리면 된다.

package hello.hellospring.repository;

import hello.hellospring.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);
    }
}
  • 연결할 때, 그리고 연결된 자원을 release할 땐 항상 DataSourceUtils을 통해야 한다.
  • close(…) {…} : 자원의 해제는 자원의 할당과 역순이다.
  • getConnection, preparedStatement 등은 메서드 호출 시 마다 해줘야 한다.
  • 순수 JDBC 방식은 try-catch를 이용하여 예외처리를 잘 처리해줘야 한다.

MemoryMemberRepository를 Spring Bean으로 등록한 것과 같이, 순수 JDBC를 이용한 JdbcMemberRepository 역시 Spring Bean으로 등록해줘야 한다. (→ 자바 코드로 직접 스프링 빈 등록하는 방식)

// src/main/java/hello/hellospring/SpringConfig.java
package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

    // 1
    @Autowired
    DataSource dataSource1;
    
    // 2
    @Autowired
    private DataSource dataSource2;

    public SpringConfig(DataSource dataSource) {
        this.dataSource2 = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {

//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource2);

    }
}

application.properties에서 지정해준 덕에 스프링이 DataSource를 알고 있다. 따라서 이를 그대로 주입해줄 수 있다.

MemberRepository 인터페이스의 다형성을 활용하였다.


JdbcMemberRepository.java를 새로 생성하고, 이에 따라 SpringConfig.java 코드를 수정한 것 외에는 그 어떤 코드도 건들지 않았다. 특히, MemberService를 수정하지 않아도 된다.

 

 

 

이러한 설계 방식이 SOLID 원칙 중, OCP 원칙을 준수한다고 볼 수 있다. Open-Closed Principle(개방-폐쇄 원칙): 개방된 확장, 폐쇄된 수정(확장에는 열리고, 수정에는 닫힌다.) ⇒ 기능을 완전히 변경해도, 거의 수정하지 않는다.

반응형

'TIL' 카테고리의 다른 글

2023-11-01-TIL | Spring  (1) 2023.11.03
2023-10-31-TIL  (1) 2023.11.01
2023-10-28-TIL  (1) 2023.10.31
2023-10-27-TIL  (1) 2023.10.27
2023-10-26-TIL  (0) 2023.10.27