데브코스 26일차 TIL

Heesu Song·2025년 4월 8일

데브코스 - 백엔드

목록 보기
28/32
post-thumbnail

이번주에 배운 내용(엔티티 매니저, 커넥션 풀 등)들이 생각보다 너무 어려워서 머리가 어질어질하다.
심지어 오늘 병원가느라 오후 수업도 못들어서 그 핑계로 모르는척 외면하고 싶을정도ㅎㅎ…
모르거나 이해가 안가는건 아닌데 머리속에서 정리할 시간이 부족한것 같다.
나는 개념을 하나 배우면 생각이 빨리빨리 돌아가지 않아서 개념을 배운뒤 바로 실습에 적용하는게
쉽지않다.. 이럴땐 어떻게 해야될까😢 연습밖에 답이 없나


JDBC 실습


Spring에서 직접 쿼리문 작성

@Test
    @DisplayName("INSERT INTO TEST")
    void insert_into_test() throws Exception {
        //쿼리입력

        String query = "INSERT INTO member (username, password) VALUES ('%s', '%s')".formatted("user1", "user1");

        Connection con = ConnectionUtil.getConnection();
        Statement stmt = con.createStatement();

        //파괴적인 행위 -> 값을 삭제, 삽입 등
        //비파괴적인 행위  -> 값 조회
        int resultRows = stmt.executeUpdate(query);

        log.info("Insert result: {}", resultRows);

        //닫는 순서 중요
        stmt.close();
        con.close();

    }

메서드로 변환

@Test
    @DisplayName("INSERT INTO TEST")
    void insert_into_test() throws Exception {
        //쿼리입력

        Member admin = genMember("admin", "admin");
        Member member = genMember("admin", "admin");

        String sql1 = genInsertQuery(admin);
        String sql2 = genInsertQuery(member);

        Connection con = ConnectionUtil.getConnection();
        Statement stmt = con.createStatement();

        //파괴적인 행위 -> 값을 삭제, 삽입 등
        //비파괴적인 행위  -> 값 조회
        int resultRows = stmt.executeUpdate(sql1);
        log.info("Insert result: {}", resultRows);

        resultRows = stmt.executeUpdate(sql2);
        log.info("Insert result: {}", resultRows);

        //닫는 순서 중요
        stmt.close();
        con.close();

    }

    private static String genInsertQuery(Member admin) {
        return "INSERT INTO member (username, password) VALUES ('%s', '%s')".formatted(admin.getUsername(), admin.getPassword());
    }

    private static Member genMember(String username, String password) {
        return new Member(0, username, password);
    }
  • @BeforeEach , @AfterEach 를 통해 테스트 전 후에 반복되는 작업을 분리해서 실행시킬 수 있음
Connection con = ConnectionUtil.getConnection();
    Statement stmt;

    @BeforeEach
    void init(){
        con = ConnectionUtil.getConnection();
    }

    @AfterEach
    void close(){
        //에러에 대한 핸들링이 필요
        if(stmt != null){
            try {
                stmt.close();
            } catch (SQLException e) {
                log.error(e.getMessage());
            }
        }
        if(con != null){
            try {
                con.close();
            } catch (SQLException e) {
                log.error(e.getMessage());
            }
        }
    }

테스트는 서로 독립적으로 이루어져야 좋은 테스트

로그인 테스트

  • ResultSet을 통해 값을 자료구조로 가져오고, 안에 커서를 둬서 행별로 읽어올 수 있도록함
Member user1 = genMember("member", "member");
String sql = genSelectQuery(user1);
stmt = con.createStatement();
//rs도 닫아줘야함
rs = stmt.executeQuery(sql);

/*String findUsername = "";
String findPassword = "";*/
Member findMember = new Member();

//커서의 처음은 컬럼명을 가리키고 있기 때문에
//다음줄이 있는지 확인해야함
if(rs.next()){
    findMember.setUsername(rs.getString("username"));
    findMember.setPassword(rs.getString("password"));
}
log.info("Select result: {}", findMember.getUsername());
log.info("Select result: {}", findMember.getPassword());

테스트 검증

  • assertThat 으로 검증 가능

❗️비밀번호를 틀리게 입력해도 로그인이 정상적으로 수행됨
password = ’’ or ‘‘ = ‘‘ 모두 참이기 때문에 공격이 가능함
SQL Injection 공격

@Test
    @DisplayName("Statement Test, SQL Injection")
    void statement_test() throws Exception {
        Member admin = genMember("admin", "");
        String query = genSelectQuery(admin);
        stmt = con.createStatement();
        rs = stmt.executeQuery(query);

        Member findMember = new Member();
        if(rs.next()){
            findMember.setMemberId(rs.getInt("member_id"));
            findMember.setUsername(rs.getString("username"));
            findMember.setPassword(rs.getString("password"));
        }
        log.info("Find Member: {}", findMember);

    }
  • 해결 방법

PreparedStatement

풀링 기법 사용

매번 connection을 생성해야되는 문제가 있음

  • 커넥션을 따오는 방법
@Test
    @DisplayName("Pool")
    void test_1() throws Exception{
        //접속정보를 풀한테 넘겨줌 -> 대신 커넥션을 따와준다.
        DriverManagerDataSource dataSource = new DriverManagerDataSource(
                ConnectionUtil.MysqlDbConnectionConstant.URL,
                ConnectionUtil.MysqlDbConnectionConstant.USERNAME,
                ConnectionUtil.MysqlDbConnectionConstant.PASSWORD
        );

        Connection conn1 = dataSource.getConnection();
        Connection conn2 = dataSource.getConnection();
        
        log.info("conn1 = {}", conn1);
        log.info("conn2 = {}", conn2);
        conn1.close();
        conn2.close();
        

    }
  • Spring에서 만든 커넥션풀을 이용

logback.xml

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

정보를 보기 위해선 디버그를 확인해야함

<appender> → 설정안에 기능을 묶어서 설정을 추가해줌

<encoder>→ 어떤 로그를 출력해줄지 결정

%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n

[%thread] → 로그를 발생시킨 쓰레드 이름

%-5level → 로그의 레벨을 5글자로 출력

%msg → 진짜 로그에 들어있는 메세지

클래스 주요 필드, 연결 유틸

 private final DataSource dataSource;

    private Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    private void closeConnection(Connection connection, Statement statement, ResultSet resultSet) {
        JdbcUtils.closeResultSet(resultSet);
        JdbcUtils.closeStatement(statement);
        JdbcUtils.closeConnection(connection);
    }
  • DataSource: DB 커넥션을 얻기 위한 객체. Spring에서는 보통 커넥션 풀(HikariCP)를 설정해 주입
  • JdbcUtils는 Spring에서 제공하는 JDBC 자원 정리 유틸, null 체크 및 예외처리가 자동으로 되어 있어 편리하다.

❓HikariCP란


  • HikariCP는 자바에서 사용되는 가장 빠르고 가벼운 커넥션 풀
  • Spring Boot 기본 내장 커넥션 풀로도 사용됨 (spring-boot-starter-jdbc or spring-boot-starter-data-jpa 사용 시 자동 설정)

HikariCP의 특징

  • 다른 커넥션풀보다 속도가 빠름
  • 코드가 단순하고 불필요한 기능 제거
  • 멀티스레드 환경에 최적화
  • 평균 대기 시간이 짧음

SpringBoot에서 설정 방법

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: user
    password: pass
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 30000
      connection-timeout: 20000
      pool-name: HikariPool

내부 동작 원리

  1. 애플리케이션 시작 시 미리 커넥션들을 생성해 Pool에 저장.
  2. dataSource.getConnection() 호출 → 풀에서 유휴 커넥션 반환.
  3. 쿼리 작업 완료 후 connection.close() 호출 → 실제 종료가 아니라 “풀에 반납”.
  4. 커넥션 상태 확인 및 유효성 체크도 백그라운드에서 자동 수행.
profile
Abong_log

0개의 댓글