HyunJun 기술 블로그

스프링 프레임워크 JDBC, Connection Pool, Datasource 본문

Spring Framework

스프링 프레임워크 JDBC, Connection Pool, Datasource

공부 좋아 2023. 5. 25. 14:27
728x90
반응형

1. JDBC(Java Database Connectivity)란?


Java에서 사용하는 DB Connection 및 SQL문 수행을 위한 API입니다.

 

  • 관계형 데이터베이스에서 쿼리를 직접 사용하는 것처럼, 자바에서 데이터베이스에 접근하여, 데이터를 검색, 삽입, 수정, 삭제할 수 있게 해줍니다.
  • myBatis, Hibernate 등 여러 가지 관련된 기술들이 있지만, 해당 기술들도 DB에 접근하기 위해서는 모두 내부적으로 JDBC를 사용하고 있습니다.
  • 일반적으로 Java 개발자 입장에서 Application과 Database를 연동할 때 가장 기초가 되는 기술이라고 할 수 있습니다.

2. Connection (Connection Pool)

2-1. Connection

Database Connection이란 하나의 애플리케이션에서 데이터베이스와 연결하여 데이터베이스를 조작하기 위한 데이터베이스와의 연결 상태를 뜻합니다. 그렇다면 DB를 조작하기 위해서는 Connection을 맺어야 하겠네요?

 

 

JDBC를 사용하여 DB와 Connection을 맺기 위해서는, 아래와 같은 정보들이 필요합니다.
  • Database Driver
  • Database 연결 정보 (URL, Username, Password, ..)

 

그런데.. 이 커넥션이라는 작업은 비용이 많이 든다고 알려져 있습니다.

 

커넥션은 왜 비용이 많이 드는 작업일까요?

아래의 문서들을 참고하였습니다.

Database Connections Are Performance Expensive1

Database Connections Are Performance Expensive2

 

실제로 커넥션 과정에 있어서, 드라이버와 데이터베이스 서버 간의 많은 네트워크 왕복이 일어납니다. 예를 들어, 드라이버가 Oracle 또는 Sybase에 연결할 때 해당 연결은 아래와 같은 작업들을 수행하기 위해 7~10번의 네트워크 왕복이 필요할 수 있습니다.

  • 사용자의 자격 증명을 확인합니다.
  • 필요한 경우 데이터베이스 드라이버가 기대하는 것과 데이터베이스에서 사용할 수 있는 것 사이에서 코드 페이지 설정을 협상합니다.
  • 데이터베이스 버전 정보를 가지고 옵니다.
  • 통신에 사용할 최적의 데이터베이스 프로토콜 패킷 크기를 설정합니다.
  • 세션 설정을 지정합니다.

 

데이터란 매우 중요한 자산이고, 보안이 중요합니다. 이러한 데이터를 데이터베이스에 담기 때문에, 데이터베이스에 접근 및 SQL 쿼리를 사용하기 위해 커넥션을 맺으려면, 중간중간에 많은 절차들을 걸쳐야 한다고 생각하셔도 될 것 같습니다.

 

각각의 Database 회사들은 자신들의 DBMS를 사용할 수 있게 하도록 JDBC Driver를 제공합니다.

JDBC Driver는 자바 애플리케이션의 요청을 각 DBMS가 이해할 수 있는 프로토콜로 변환해 줍니다. 이 덕분에 자바 개발자는 JDBC Interface를 사용하여 DBMS 종류에 상관없이 일관된 방식으로 데이터베이스를 사용할 수 있습니다.

출처: https://hudi.blog/java-db-connection-pooling/

2-2. Connection Pool

그렇다면 DB를 사용해야 할 때마다 커넥션을 맺고 연결하는 것일까요?

정답은? 커넥션 과정은 비용이 많이 들기 때문에, 이러한 문제점을 해결하기 위해 Connection Pool이 존재합니다.

 

Connection Pool은 일정량의 Connection 객체를 미리 만들어서 pool에 저장해 둡니다. 그리고 후에, DB 요청이 오면 Connection 객체를 빌려주고, 해당 객체의 사용이 완료되면 다시 반납 받아서 pool에 저장을 하는 기술입니다.

 

SpringBoot 2.0 이전에는 tomcat-jdbc라는 커넥션 풀을 사용을 하였지만,

SpringBoot 2.0 이후부터 HikariCP를 기본으로 채택하고 있습니다.

 

 

2-3. Hikari CP

히카리 벤치마크 페이지를 확인하면 월등히 성능이 좋다는 것을 알 수 있습니다.

 

HikariCP 동작 방식

 

 

3. Datasource

3-1. Datasource란?

지금까지 글을 보았을 때, 매번 Connection을 생성하는 것은 오버헤드가 크다는 것을 알 수 있었고, 이러한 문제점 때문에 Connection Pool을 사용한다고 했습니다.

 

이때, DB 연결 정보를 저장하고, Connection을 생성하고, Connection Pool에 등록하고 관리하는 역할을 하는 것이 바로 DataSoruce입니다. 간단하게 DataSource는 DB Connection을 관리하는 인터페이스라고 생각하면 됩니다. getConnection() 등의 메서드를 지원합니다.

 

3-2. Spring Boot Auto Configuration

Datasource는 application.yml(혹은 properties)에 입력하면 스프링 부트의 AutoConfiguration 기능으로 자동으로 주입이 되게 됩니다.

 

application.yml

spring:
  profiles:
    include: db # datasource(url, username, password)
  datasource:
    hikari:
      maximum-pool-size: 10 # 최대 pool size (default 10)
      connection-timeout: 5000 # timeout
      connection-init-sql: SELECT 1
      validation-timeout: 2000
      minimum-idle: 10 # 연결 풀에서 HikariCP가 유지 관리하는 최소 유휴 연결 수
      idle-timeout: 600000 # 연결을위한 최대 유휴 시간
      max-lifetime: 1800000 # 닫힌 후 pool에 있는 connection의 최대 수명(ms)
      auto-commit: true # auto commit 여부 (default true)

 

 

application-db.yml

spring:
  datasource:
    url: jdbc:mysql://url.ap-northeast-2.rds.amazonaws.com:3306/spring-jdbc
    username: 계정
    password: 비밀번호

4. JDBC 실행 과정

다시 돌아와서 결론적으로 JDBC는 아래와 같은 실행 순서를 가집니다.

  1. JDBC 드라이버 Load
  2. Connection 객체 생성 (Connection Pool에서 꺼내옴)
  3. Statement(정적 쿼리) 객체 생성 (동적 쿼리 = PreparedStatement)
  4. Query 실행
  5. Result 객체로부터 데이터 추출(쿼리 실행 결과 사용)
  6. Result 객체 Close
  7. Startment 객체 Close
  8. Connection 객체 Close

 

 

5. 구현하기

5-1. DB 스키마

이번 구현에서는 기존의 yml(혹은 properties)에 Datasource를 입력하여 AutoConfiguration을 이용 자동으로 주입하는 방식이 아닌, 사용하려는 데이터베이스가 2개 이상인 경우를 가정하여, Datasource를 2개 사용하여, 2개의 데이터베이스에 입력을 해보도록 하겠습니다.

 

같은 DB 서버에 데이터베이스만 다르게 생성했으며, 추후에 다른 서버를 이용 시 url, username, password, driver 등만 변경하면 됩니다.

DBeaver로 RDS 데이터베이스 2개 생성

 

 

MySQL 문법으로 spring-jdbc1, spring-jdbc2 데이터베이스에 멤버 테이블을 각각 만들어 볼게요.

drop table if exists member CASCADE;
create table member
(
 id bigint NOT NULL AUTO_INCREMENT,
 name varchar(255),
 PRIMARY KEY (id)
);

 

5-2. 클래스 구조

TestCode로 검증할 예정이므로, 컨트롤러는 만들지 않았습니다.

클래스 구조

application.yml

spring:
  profiles:
    include: db # datasource(url, username, password)

application-db.yml

datasource1:
  url: jdbc:mysql://url.ap-northeast-2.rds.amazonaws.com:3306/spring-jdbc1
  username: 계정
  password: 비밀번호

datasource2:
  url: jdbc:mysql://url.ap-northeast-2.rds.amazonaws.com:3306/spring-jdbc2
  username: 계정
  password: 비밀번호

build.gradle

    /*web*/
    implementation 'org.springframework.boot:spring-boot-starter-web'
    /*jdbc*/
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    /*lombok*/
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    /*mysql*/
    runtimeOnly 'com.mysql:mysql-connector-j'
    /*test*/
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

5-3. 도메인 구현

Member

package com.example.springjdbc.domain;

public class Member {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }
    public String getName() {
        return name;
    }

    public void setId(Long id) {
        this.id = id;
    }
    public void setName(String name) {
        this.name = name;
    }
}

 

MemberRepository

package com.example.springjdbc.repository;

import com.example.springjdbc.domain.Member;

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

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

 

보통 Repository에서 JPA를 사용하는 경우 MemberRepository에서 JPARepository...를 상속받고 메서드(쿼리)들을 사용하게 됩니다.

 

하지만 JDBC만으로 구현하려면 직접 구현해 주어야 합니다.

그렇다면 MemberRepository를 구현한 구현체가 있어야겠죠?

 

JdbcMemberRepository

package com.example.springjdbc.repository;

import com.example.springjdbc.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 {

    /*DB Info(URL, ID, PW), Connection Pool 등에 필요한 datasource*/
    private final DataSource dataSource;

    /*SpringConfig에서 설정된 datasource를 자동으로 주입받는다.*/
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /*커넥션을 꺼내오는 메서드*/
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }

    /*커넥션을 종료하는 메서드*/
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }

    @Override/*회원가입 메서드*/
    public Member save(Member member) {
        /*sql문 입력*/
        String sql = "insert into member(name) values(?)";
        Connection conn = null;
        /*SQL문을 담아 전송하기 위한 객체*/
        PreparedStatement pstmt = null;
        /*결과를 받을 ResultSet*/
        ResultSet rs = null;
        try {
            /*커넥션을 꺼내와 담는다.*/
            conn = getConnection();
            /*커넥션에서 prepareStatement를 꺼내 sql을 담는다.
            * Statement.RETURN_GENERATED_KEYS는 DB에서 AUTO_INCREMENT로 자동 생성된 key(id)를 가지고 온다.*/
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            /*values(?)에 바인딩할 내용을 담는다. 첫 물음표이므로 인덱스는 1*/
            pstmt.setString(1, member.getName());
            /*쿼리가 DB로 전송됨*/
            pstmt.executeUpdate();
            /*ResultSet에 key(id)를 담는다.*/
            rs = pstmt.getGeneratedKeys();
            /*ResultSet에 값이 있으면*/
            if (rs.next()) {
                /*멤버의 아이디를 받아온 값으로 설정한다.*/
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }

            /*맴버 객체를 리턴한다.*/
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            /*사용된 자원은 꼭 close 해주어야 한다.*/
            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 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();
        }
    }

}

이때, 자원의 반납(close) 순서는 ResultSet -> PrepareStatement-> Connection 순서이어야 합니다.

 

 

 

 

DataSourceUtils

DataSourceUtils는 데이터베이스 연결에 관련된 유틸리티 클래스입니다. 주로 스프링 프레임워크에서 제공하는 트랜잭션 관리를 위해 사용됩니다. 데이터베이스 연결을 획득하고 해제하는 메서드를 제공하여 데이터베이스 트랜잭션의 일관성과 안전성을 보장합니다.

 

  • 데이터베이스 커넥션 획득: getConnection() 메서드를 사용하여 DataSource로부터 데이터베이스 연결을 획득합니다. 이 메서드는 스프링의 트랜잭션 관리자와 함께 사용될 때, 현재 활성화된 트랜잭션과 관련된 데이터베이스 커넥션을 가져옵니다. 트랜잭션 경계 내에서 데이터베이스 연결을 획득하는 것은 트랜잭션의 일관성을 유지하는 데 중요합니다.
  • 트랜잭션 경계에서 데이터베이스 Connection 해제: releaseConnection() 메서드를 사용하여 데이터베이스 연결을 해제합니다. 이 메서드는 트랜잭션이 완료되거나 롤백 될 때 데이터베이스 연결을 해제하는 역할을 합니다. 데이터베이스 연결을 명시적으로 해제하지 않으면 리소스 누수가 발생할 수 있으므로 DataSourceUtils의 releaseConnection() 메서드를 사용하여 안전하게 연결을 해제할 수 있습니다.
  • 트랜잭션 상태 관리: getConnection() 메서드를 사용하여 데이터베이스 연결을 획득할 때 DataSourceUtils는 내부적으로 연결을 스레드 로컬(thread-local) 변수에 저장합니다. 이를 통해 트랜잭션 경계를 벗어나는 동안에도 올바른 데이터베이스 연결을 사용할 수 있습니다. 또한 releaseConnection() 메서드를 호출하여 연결을 해제할 때는 해당 스레드 로컬 변수에서 연결을 제거합니다.

 

또한 커넥션 풀에서 커넥션을 가지고 오는 것이기 때문에, 매번 새로운 커넥션을 맺는 비용과 시간을 절약하고, 효율적인 데이터베이스 연결 관리를 할 수 있습니다.

 

 

 

MemberService

package com.example.springjdbc.service;

import com.example.springjdbc.domain.Member;
import com.example.springjdbc.repository.MemberRepository;

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

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

 

 

 

5-4. Datasource 설정

package com.example.springjdbc.config;

import com.example.springjdbc.repository.JdbcMemberRepository;
import com.example.springjdbc.repository.MemberRepository;
import com.example.springjdbc.service.MemberService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


import javax.sql.DataSource;

@Configuration
public class SpringConfig {

    DataSource dataSource;
                                /*사용할 datasource bean 이름 설정*/
    public SpringConfig(@Qualifier("mySqlTableOneDataSource") DataSource dataSource) {
        this.dataSource = dataSource;
    }

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

    @Bean
    public MemberRepository memberRepository() {
        return new JdbcMemberRepository(dataSource);
    }


}

 

 

 

 

OneDataSourceConfig

package com.example.springjdbc.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class OneDataSourceConfig {

    @Value("${datasource1.url}")
    String urlOne;

    @Value("${datasource1.username}")
    String usernameOne;

    @Value("${datasource1.password}")
    String passwordOne;

    @Bean
    public HikariDataSource mySqlTableOneDataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setPoolName("hyunjun1");
        hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
        hikariConfig.setJdbcUrl(urlOne);
        hikariConfig.setUsername(usernameOne);
        hikariConfig.setPassword(passwordOne);
        hikariConfig.setMaximumPoolSize(10);
        hikariConfig.setConnectionTestQuery("SELECT 1");
        hikariConfig.addDataSourceProperty("cachePrepStmts", true);
        hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
        hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        return new HikariDataSource(hikariConfig);
    }

    @Bean
    public PlatformTransactionManager oneTransactionManager() {
        final DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(mySqlTableOneDataSource());
        return transactionManager;
    }
}

 

 

TwoDataSourceConfig

package com.example.springjdbc.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class TwoDataSourceConfig {
    
    @Value("${datasource2.url}")
    String urlTwo;

    @Value("${datasource2.username}")
    String usernameTwo;

    @Value("${datasource2.password}")
    String passwordTwo;
    
    @Bean
    public HikariDataSource mySqlTableTwoDataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setPoolName("hyunjun2");
        hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
        hikariConfig.setJdbcUrl(urlTwo);
        hikariConfig.setUsername(usernameTwo);
        hikariConfig.setPassword(passwordTwo);
        hikariConfig.setMaximumPoolSize(10);
        hikariConfig.setConnectionTestQuery("SELECT 1");
        hikariConfig.addDataSourceProperty("cachePrepStmts", true);
        hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
        hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        return new HikariDataSource(hikariConfig);
    }

    @Bean
    public PlatformTransactionManager twoTransactionManager() {
        final DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(mySqlTableTwoDataSource());
        return transactionManager;
    }
}

 

 

또한 Database를 2개 이상 사용하는 경우, 트랜잭션은 아래와 같은 2가지의 경우로 사용할 수 있습니다.

1. 두 가지의 DB가 각각 독립적인 트랜잭션을 나누어 사용해야 하는 경우.

2. 두 가지의 DB를 하나의 트랜잭션으로 묶어 처리해야 하는 경우.

 

이 글은 JDBC의 기초와, 다수 Datasource를 사용하는 것에 초점을 맞추고 있으므로, 1번의 형태만 구현해 보려고 합니다.

 

 

각각의 구현체에서 어떠한 트랜잭션 구현체를 사용하는지에 대한 사진입니다.

출처: 김영한님의 스프링 강좌

5-5. Test Code

package com.example.springjdbc;

import com.example.springjdbc.domain.Member;
import com.example.springjdbc.repository.MemberRepository;
import com.example.springjdbc.service.MemberService;
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 java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional("oneTransactionManager")
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("이미 존재하는 회원입니다.");
    }

    @Test
    public void 모든_회원_조회() throws Exception {

        // Given
        List<Member> memberList = new ArrayList<>();
        Member member1 = new Member();
        member1.setName("java");
        Member member2 = new Member();
        member2.setName("python");
        Member member3 = new Member();
        member3.setName("javascript");

        memberList.add(member1);
        memberList.add(member2);
        memberList.add(member3);

        // When
        memberService.join(member1);
        memberService.join(member2);
        memberService.join(member3);

        // Then
        List<Member> findMemberList = memberService.findMembers();

        for (int i = 0; i < findMemberList.size(); i++) {
            assertEquals(findMemberList.get(i).getName(),
                    memberList.get(i).getName());
        }
        
    }
}

JDBC와 Timeout 이해하기

728x90
반응형
Comments