
assertThat(findMember.getMemberId()).isEqualTo(2);
assertThat(findMember.getUsername()).isEqualTo("member");
assertThat(findMember.getPassword()).isEqualTo("member");
assertThat(findMember.getUsername()).isNull();
assertThat(findMember.getPassword()).isNull();
assertThat 문으로 값을 비교하여 테스트할 수 있다. log로 값을 확인하는 것과 달리, 기대값과 다른 값이 담길 때는 테스트가 실패한다.Member admin = genMember("admin", "' or '' = '");
String sql = genSelectQuery(admin);
위와 같이 비밀번호에 ' or '' = '를 넣었을 경우, 문자열이 그대로 아래의 SQL문에 들어가게 된다.
select m.member_id, m.username, m.password
from member as m
where m.username = '%s' AND m.password = '%s'
그렇게 되면, 다음과 같은 SQL문이 만들어진다.
SELECT *
FROM member
WHERE m.username = 'admin' AND m.password = '' or '' = '';
OR 연산자 때문에 비밀번호 조건이 true가 된다. 즉, 올바른 비밀번호를 입력하지 않았지만, SQL 쿼리를 일반 입력 또는 양식 필드에 삽입하여 값을 얻어오도록 한 것이다.
assertThat(findMember.getUsername()).isEqualTo("admin");
assertThat(findMember.getPassword()).isEqualTo("admin");
쿼리를 수행한 ResultSet의 정보들을 findMember에 저장하고 비교하는 테스트를 진행했을 때, 실제로 테스트가 성공한다.
⚠️ SQL Injection 공격이 발생하지 않도록 SQL문을 사전에 컴파일하고 실행 시 변수 값을 바인딩하여 실행하는 방식을 사용해야 한다. -> PreparedStatement
Member unsafeAttempt = genMember("admin", "' or '' = '");
String sql = "SELECT m.member_id, m.username, m.password "
+ "FROM member as m "
+ "WHERE m.username = ? AND m.password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, unsafeAttempt.getUsername());
pstmt.setString(2, unsafeAttempt.getPassword());
rs = pstmt.executeQuery();
SQLi 문제를 방지하기 위하여 쿼리문에 필요한 조건값들에 ?을 입력하여 SQL문을 만든다.
이전과 같이 문제가 발생할 수 있는 비밀번호를 가진 Member을 만들어두었다.
.setString(), .setInt() 등의 메서드를 사용하여 ?에 들어가야 할 것들을 넣어준다.
PreparedStatement는 Statement와 달리 이미 위에서 넣어줬기 때문에 .executeQuery()할 때 인자를 넣어주지 않아도 된다.
assertThat(findMember.getUsername()).isNull();
assertThat(findMember.getPassword()).isNull();
해당 테스트를 진행했을 때 테스트가 성공하는 것을 알 수 있다.
DriverManagerDataSourceDriverManagerDataSource dataSource = new DriverManagerDataSource(
MysqlDbConnectionConstant.URL,
MysqlDbConnectionConstant.USERNAME,
MysqlDbConnectionConstant.PASSWORD
);
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();
DriverManagerDataSource는 Spring의 간단한 DataSource 구현체인데, 매번 getConnection()을 호출할 때마다 새 커넥션을 생성한다.
즉, 커넥션 풀 없고, 매번 DB에 접속하고, 끊고를 반복하는 것이다.
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl(MysqlDbConnectionConstant.URL);
hikariDataSource.setUsername(MysqlDbConnectionConstant.USERNAME);
hikariDataSource.setPassword(MysqlDbConnectionConstant.PASSWORD);
hikariDataSource.setMaximumPoolSize(5);
Connection conn1 = hikariDataSource.getConnection();
Connection conn2 = hikariDataSource.getConnection();
Connection conn3 = hikariDataSource.getConnection();
실제 커넥션 풀 구현체인 HikariCP (Hikari Connection Pool)를 사용하여 구현한 것이다. 이것도 DataSource의 구현체이다.
setMaximumPoolSize(5)와 같이 설정을 통해 최대 커넥션 수 제한 할 수 있다.정확히 언제 Connection을 만드는 걸까?
누군가가 .getConnection()을 호출하면, 내부적으로 Connection이 있는지 확인하고, 없으면 새로 만든다. 해당 Connection은 풀에 넣고 재사용한다. 이렇게 하면서 최대 maximumPoolSize까지만 커넥션을 생성한다.
미리 몇 개 만들어 놓을 순 없을까?
minimumIdle : 놀고 있는(Idle) Connection을 몇 개까지 유지할지 이걸 설정하면 미리 커넥션을 만들어 놓기도 한다.
hikariDataSource.setMinimumIdle(3); // 최소 3개는 미리 만들어놓음
@RequiredArgsConstructor
public class SimpleJdbcCrudRepository implements SimpleCrudRepository {
private final DataSource dataSource;
private Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
private void closeConnection(Connection connection, Statement statement, ResultSet resultSet) {
JdbcUtils.closeConnection(connection);
JdbcUtils.closeStatement(statement);
JdbcUtils.closeResultSet(resultSet);
}
DataSource는 넣어주는 사람이 알아서 넣어주는 걸로 구현을 했다.
DataSource는 Connection 연결 객체를 꺼내오는 역할을 한다.closeConnection 함수에서 JdbcUtils의 메서드로 Connection, Statement, ResultSet를 닫아주었다.
만약, 커넥션 풀을 사용하고 있다면, 닫는 게 아니라 반납해주고 (active -> idle), 그게 아니라면 close()해준다.
@Override
public Member save(Member member) throws SQLException{
String sql = "insert into member (username, password) 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.getUsername());
pstmt.setString(2, member.getPassword());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if ( rs.next() ) {
int idx = rs.getInt(1);
member.setMemberId(idx);
}
return member;
} catch (SQLException e) {
throw e;
} finally {
closeConnection(conn, pstmt, rs);
}
}
insert SQL문에서도 preparedStatement를 사용하기 위해 ?을 사용하였다. conn.preparedStatement의 인자로 전달된 Statement.RETURN_GENERATED_KEYS)는 DB에서 자동 생성된 키(주로 AUTO_INCREMENT) 값을 돌려달라는 의미이다.pstmt.GeneratedKeys를 사용하면 ResultSet이 반환되고, 여기에 방금 insert한 row의 키가 담겨있다. getString("member_id")를 사용하면 안된다. 어떤 식으로 반환되는지 알지 못하기 때문에 index로 접근해야 한다.@Override
public Optional<Member> findById(Integer id) throws SQLException{
String sql = "select * from member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, id);
rs = pstmt.executeQuery();
if( rs.next() ) {
Member findMember = new Member(rs.getInt("member_id"), rs.getString("username"), rs.getString("password"));
return Optional.of(findMember);
} else {
return Optional.empty();
}
} catch (SQLException e) {
throw e;
} finally {
closeConnection(conn, pstmt, rs);
}
}
null일 수도 있는 결과를 명확하게 표현하기 위해 반환형이 Optional<Member>이다.ifPresent 등을 이용하여 확인하여 사용할 수 있다.Optional.of(findMember) 혹은 Optional.empty()로 반환해준다.@BeforeEach
void init() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(ConnectionUtil.MysqlDbConnectionConstant.URL);
dataSource.setUsername(ConnectionUtil.MysqlDbConnectionConstant.USERNAME);
dataSource.setPassword(ConnectionUtil.MysqlDbConnectionConstant.PASSWORD);
repository = new SimpleJdbcCrudRepository(dataSource);
}
@BeforeEach 를 사용하면 각 테스트 이전에 실행된다.@AfterEach를 사용하면 각 테스트 이후에 실행된다.이전 DataSource로만 추상화해놓은 부분에 실질적인 구현체를 넣어줘야 하기 때문에 구현한 설정 메서드이다.
커넥션 풀을 사용하고 싶기 때문에 HikariDataSource를 사용하여 구현하였다. 연결 정보를 위와 같이 넣어줘야 한다.
@Test
@DisplayName("read 성공 test")
void read_test_ok() throws Exception {
int availableIdx = 1;
Optional<Member> memberOptional = repository.findById(availableIdx);
boolean result = memberOptional.isPresent();
assertThat(result).isTrue();
Member findMember = memberOptional.get();
assertThat(findMember).isNotNull();
assertThat(findMember.getMemberId()).isEqualTo(availableIdx);
log.info("findMember = {}", findMember);
}
findById의 반환값이 Optional<Member>이었기 때문에 isPresent()를 통해 Optional 안에 값이 있는지 없는지 확인해준다.assertThat(result).isTrue()를 통해 값을 확인하는 방법이 있다. assertThat(result).isEqualTo(true)랑 동일하다.repository.findById(...)의 결과는 Optional<Member>memberOptional.get()을 사용해야 한다.오늘은 시간이 정말 빨리 간 것 같다. 그리고 오늘은 수업을 꽤 잘 따라간 것 같아서 뿌듯하다. (물론 마지막에 잠이 좀 왔지만...크흠) 확실히 O/X를 체크하니까 수업 템포를 잘 맞춰갈 수 있는 것 같아 좋은 것 같다. 그리고 강사님이 계속 제발 솔직하게 해라!! 라고 여러번 얘기해주시니까 다들 조금이라도 이해 안되면 질문을 날린다. 나도 편하게.. 질문을 해본다 하핫.. DataSource 추상화했다는 부분 이해가 안됐었는데 강사님이 엄청 자세하게 다른 예시까지 써가면서 알려주시니까 정말 이해가 잘 됐다 !
날이 따듯해져서 그런가.. 왤케 요새 조금씩이라도 잠이 오는 거지? 컨디션 조절하려고 잠을 평소보다 더 자는데 거참 어이없군. 커피를 마셔야 하나.. 흠..
요새 컨디션이 너무 안 좋아서 슬프다 흑 컨디션 안 좋으니까 너무 불편하다 !!! 빨리 씩씩 튼튼 모드로 돌아가고 싶다!! 아자아자 힘을 내자!!