Spring

[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 1.JDBC 이해

gogi masidda 2024. 2. 20. 17:05

JDBC의 등장 이유

데이터베이스마다 커넥션을 연결하는 방법, SQL을 전달하는 방법, 결과를 응답받는 방법이 모두 달라서 원래 사용하면 데이터베이스 기술에서 다른 기술로 변경하면 데이터베이스 사용 코드도 함께 변경해야하고, 개발자도 각각의 데이터베이스 기술을 새로 학습하여야 한다. -> JDBC라는 자바 표준이 등장.

 

JDBC는 자바에서 데이터 베이스에 접속할 수 있도록 하는 자바 API이다. java.sql.Connection(연결), java.sql.Statement(SQL을 담은 내용), java.sql.ResultSet(SQL 요청 응답)의 인터페이스를 정의해두었고, 각각의 DB 회사에서 자신의 DB에 맞도록 구현해서 라이브러리로 제공한다. (MySQL JDBC 드라이버, Oracel JDBC 드라이버)

-> 애플리케이션은 JDBC 표준 라이브러리에만 의존하여 다른 종류의 데이터베이스 기술로 바꾸고 싶으면, JDBC 구현 라이브러리(드라이버)만 변경하면 된다. 그리고 개발자도 JDBC 표준 인터페이스 사용법만 학습하면 된다.

 

SQL Mapper

  • 장점: JDBC를 편리하게 사용하도록 도와줌
    • SQL 응답 결과를 객체로 편리하게 변환
    • JDBC의 반복 코드 제거
  • 단점: 개발자가 SQL 직접 작성
  • 대표 기술: 스프링 JdbcTemplate, Mybatis

ORM

  • 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술. 
  • 개발자는 SQL 구문을 직접 작성하지 않고, ORM이 개발자 대신에 SQL을 동적으로 만들어 실행해줌. 각각의 DB마다 다른 SQL을 사용하는 문제도 해결해줌.
  • 대표 기술: JPA, 하이버네이트, 이클립스 링크

JDBC가 제공하는 DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다.

 

@Slf4j
public class DBConnectionUtil {

    public static Connection getConnection() {
        try {
            Connection con = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            log.info("get connection = {}, class = {}", con, con.getClass());
            return con;
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }
}
  • getConnection은 URL, USERNAME, PASSWORD를 받는다.
  • URL을 jdbc:h2:tcp://localhost/~/MatnMut로 했다. 여기서 jdvc:h2는 h2 DB에 접근하기 위한 규칙으로, H2 드라이버는 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에게 반환한다.

프로젝트 수정 V0

강의처럼 한 버전씩 올라가면서 수정할 계획이다. 

우선 save만 고쳤다.

@Slf4j
@Repository
public class MemberRepositoryWithDB {
    private static long sequence = 0L;

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(id, register_date, name, loginid, password) values (?, ?, ?, ?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = DBConnectionUtil.getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setLong(1, ++sequence);
            pstmt.setDate(2, Date.valueOf(LocalDate.now()));
            pstmt.setString(3, member.getName());
            pstmt.setString(4, member.getLoginId());
            pstmt.setString(5, member.getPassword());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
            log.info("save : member = {}", member);
        }
    }
  • 원래 수정 전에는 Controller에서 회원가입 페이지를 불러와서 거기에 유저가 입력한 정보를 Member 객체에 담아서 가져와 store라는 HashMap을 따로 만들어서 저장했다.
  • 수정 후에는 DB와 연결하고, SQL을 작성하여 이름, 로그인 아이디, 비밀번호 Member객체로부터 가져오고, 나머지는 코드를 통해 파라미터에 넣었다.

테스트

@Slf4j
class MemberRepositoryWithDBTest {

    MemberRepositoryWithDB repository = new MemberRepositoryWithDB();

    @Test
    void crud() throws SQLException {
        //save
        Member member = new Member("cd", "gogi", "massida");
        repository.save(member);
    }
}
  • 이렇게 테스트를 수행했다. 원래는 Member 객체의 생성자가 필요없었는데 필요해진거 같아서 name, loginId, password를 파라미터로 하는 생성자를 만들었다.

결과

DB에서도 잘 저장되어 나오는 것을 확인할 수 있다.

 

ResultSet 

  • 보통 select 쿼리의 결과가 순서대로 들어간다.
  • ResultSet 내부에 있는 커서를 이동해서 다음 데이터를 조회할 수 있다. rs.next()를 호출하면 커서가 다음으로 이동한다. rs.next()의 반환값이 true이면 커서가 가리키는 곳에 데이터가 있다는 것이다.
  • 최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()를 한번은 호출해야 최초의 데이터를 조회할 수 있다. 

FindById 변경

public Member findById(Long id) throws SQLException {
        String sql = "select * from member where id = ?";

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = DBConnectionUtil.getConnection();
            pstmt = con.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"));
                member.setRegisterDate(rs.getDate("register_Date").toLocalDate());
                member.setLoginId(rs.getString("loginId"));
                member.setPassword(rs.getString("password"));
                return member;
            } else {
                throw new NoSuchElementException("member not found memberId = " + id);
            }
        } catch (SQLException e) {
            log.error("db error ", e);
            throw e;
        } finally {
            close(con, pstmt, rs);
        }
    }

이번에는 조회하는 것이라 쿼리의 결과를 담을 ResultSet이 필요하다. 

 

테스트

@Test
    void crud() throws SQLException {
        //save
        Member member = new Member("cd", "gogi", "massida");
        repository.save(member);

        //findById
        Member findMember = repository.findById(1L);
        log.info("findMember = {}", findMember);
    }
    
    //결과
    findMember = Member(id=1, registerDate=2024-02-20, name=cd, loginId=gogi, 
    					password=massida, nickname=null)

FindAllMember 변경

public List<Member> findAllMember() throws SQLException {
        String sql = "select * from member";

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = DBConnectionUtil.getConnection();
            pstmt = con.prepareStatement(sql);

            rs = pstmt.executeQuery();
            List<Member> allMember = new ArrayList<>();
            while(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                member.setRegisterDate(rs.getDate("register_Date").toLocalDate());
                member.setLoginId(rs.getString("loginId"));
                member.setPassword(rs.getString("password"));
                allMember.add(member);
            }
            return allMember;
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        } finally {
            close(con, pstmt, rs);
        }
    }

테스트

@Test
    void crud() throws SQLException {
        //save
        Member member1 = new Member("cd", "gogi", "massida");
        repository.save(member1);
        Member member2 = new Member("yc", "fish", "good");
        repository.save(member2);

        //findById
        Member findMember = repository.findById(1L);
        log.info("findMember = {}", findMember);

        //findAll
        List<Member> memberList = repository.findAllMember();
        log.info("memberList = {}", memberList);

        //delete
        repository.delete(1L);
        assertThatThrownBy(() -> repository.findById(1L))
                .isInstanceOf(NoSuchElementException.class);
    }
        
        //결과
        memberList = [Member(id=1, registerDate=2024-02-20, name=cd, loginId=gogi, password=massida, nickname=null), 
        Member(id=2, registerDate=2024-02-20, name=yc, loginId=fish, password=good, nickname=null)]

FindByLoginId 수정

public String findByLoginId(String loginId) throws SQLException {
        String sql = "select name from member where loginId = ?";

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            con = DBConnectionUtil.getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, loginId);

            rs = pstmt.executeQuery();

            if(rs.next()){
                String findUsername = rs.getString("name");
                return findUsername;
            } else {
                throw new NoSuchElementException("member not found loginId = " + loginId);
            }
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        } finally {
            close(con,pstmt,rs);
        }
    }

테스트는 앞의 findById 테스트의 로직과 같다.


Delete 만들기

회원을 삭제하는 기능은 이전에 만들지 않았지만, 필요할 것 같아 만들었다.

public void delete(Long id) throws SQLException {
        String sql = "delete from member where id = ?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try{
            con = DBConnectionUtil.getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setLong(1, id);

            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

 


 

강의에서처럼 확실히 불필요한 반복이 많은 것을 깨달았다. 거의 모든 코드에서 sql과 Resultset을 받는 것을 제외하고는 같은 코드가 반복적으로 들어간다. 그리고, SQLException으로 빨간줄이 그어졌을 때, alt+enter를 입력하면 자동으로 RuntimeException으로 돌리기를 추천해준다. 런타임 예외로 돌려야 특정 기술에 의존하지 않게된다는 것을 잊지 않을것 같다.

728x90