CODE SQUAD/FeedBack 정리

[Review] 스프링 카페 3단계 - DB에 저장하기 (2022/03/11)

샤아이인 2022. 3. 11.

 

[Shine] 스프링 카페 3단계 - DB에 저장하기 by zbqmgldjfh · Pull Request #46 · codesquad-members-2022/java-spring-ca

안녕하세요 Shine 입니다! 먼저 리뷰를 남겨주시는 리뷰어님께 감사한 마음을 전합니다. Step03 ToDo-List user form 검증 로직 작성하기 (BeanValidation 사용하지 말것!) article form 검증 로직 작성하기 (BeanVal

github.com

1. 질문

질문 1

디렉터리 구조의 개편이 필요한것 같습니다. Repository가 너무 복잡해지는 것 같은데 어떻게 디렉토리 구조를 개편해야 할까요?

질문 2

저장소 테스트 방식?
3단계 과정에서 SQL을 통해 repository에서 DB에 저장을 하는데, 이 Repository를 테스트 하고 싶어 @jdbcTest 같은 에노테이션을 사용하려 하는데, 이 에노테이션을 사용하면 해당 태스트 내에서 repositpry를 @Autowired 받아 사용할수가 없습니다.
@jdbcTest 같은 에노테이션은 Scan을 안해준다 하더라구요 ㅠ,ㅠ 어떻게 해결해야 할까요?

 

질문 3

실제 물리적 저상소에 저장해야 하는가?
Repository를 테스트 할때, 실제 Db상에 테스트를 해야 할까요? 아니면 메모리 DB를 사용해야 할까요?

 

답변

 

2. 코드 리뷰

1. PrepareStatement를 사용하는 이유

아마 "pstmt를 왜 사용하느냐?" 란 질문이실꺼라 생각합니다! 다행이 호눅스가 수업을 해줬기에 복습겸 이유를 적어보면

  1. 캐싱을 통한 성능 개선
    Statement를 사용하면 매번 쿼리를 수행할 때마다 (쿼리 문장 분석 -> 컴파일 ->실행) 의 과정을 매번 수행하는데 비하여,
    PreparedStatement는 처음 한 번만 세 단계를 거친 후 캐시에 담아 재사용을 합니다!
    만약 동일한 쿼리를 반복적으로 수행한다면 PreparedStatment가 DB에 훨씬 적은 부하를 주며, 성능도 좋다고 합니다.
  2. SQL injection 방지
    보안쪽에서 SQL injection 방식을 통해 SQL문을 강제로 실행하여 데이터를 탈취해 가는 것을 방지해 줍니다!

 

2. NamedParameter 사용하기

음 순수 JDBC에는 namedParameter가 없는것으로 알고 있습니다.
따라서 JDBCtemplate를 사용하여 namedParameter를 사용하도록 변경해 보겠습니다!

 

3. 적합한 이름

원래 이 메서드가 처음 만들어 질때만 해도 User를 id(Long)로 찾아오고 있었다.

하지만 변경사항이 생기면서 userId(String)를 통해 찾아오도록 변경했는데, 파라미터를 변경하면서 메서드의 이름을 바꿀 생각을 하지 못했다.

따라서 변경하게 되었다.

 

4. Optional의 사용 이유 알기

사실 Optional을 왜 사용하는지는 다행이도 알고 있었다!!

NullPointException 방지 차원에서 사용한다고 알고 있습니다.

예를 들어 기존의 null 에 도트연산(.)을 진행하면 NullPointException 이 발생하는데 비하여,
Optional을 사용하면 우선 객체로 null을 wrapper 했기 때문에 NullPointException이 발생하지 않습니다!

 

5. 중복 코드 제거하기

위와 같은 중복 코드의 문제가 있었다.

따라서 Class에 Static 메서드를 하나 만들어 사용해보기로 하였다.

다음과 같이 static method로 분리하여 사용하였다.

public class DbCleaner {

    public static void close(Connection conn, PreparedStatement pstmt, ResultSet rs, DataSource dataSource) {
        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, dataSource);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private static void close(Connection conn, DataSource dataSource) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

 

6. @JdbcTest 사용하기

다음과 같이 @jdbcTest를 작성할 수 있었다.

생각해보면 원래 @SpringBootTest 를 사용한 이유는 @jdbcTest를 사용하면 @Component로 지정된 repository를 불러올수가 없었기 때문이다.

따라서 빈을 찾기 위해 @SpringBootTest 를 사용했다. 하지만 @jdbcTest 는 DataSource를 지원해준다는 사실을 알게 되었고, 메모리 상에서 테스트 하는 다음과 같은 코드를 작성하게 되었다.

@JdbcTest
@Sql("classpath:/schema.sql")
public class JdbcUserRepositoryTest {

    private final DbUserRepository repository;

    @Autowired
    public JdbcUserRepositoryTest(DataSource dataSource) {
        this.repository = new DbUserRepository(dataSource);
    }

    User user;

    @BeforeEach
    public void setUp() {
        user = new User("Shine", "1234", "Shine", "shine@naver.com");

    }

    @Test
    public void saveUserTest() {
        // when
        Long saveId = repository.save(user);
        Optional<User> findUser = repository.findByUserId(user.getUserId());

        // then
        then(findUser).hasValueSatisfying(user -> {
                    then(user.getUserId()).isEqualTo("Shine");
                    then(user.getPassword()).isEqualTo("1234");
                    then(user.getName()).isEqualTo("Shine");
                    then(user.getEmail()).isEqualTo("shine@naver.com");
                });
    }

    @Test
    public void findByUserIdTest() {
        // given
        repository.save(user);

        // when
        User findUser = repository.findByUserId(user.getUserId()).get();

        // then
        then(findUser).isEqualTo(user);
    }

    @Test
    public void findEmptyTest() {
        // when
        Optional<User> findUser = repository.findByUserId(user.getUserId());

        // then
        then(findUser).isEqualTo(Optional.empty());
    }

    @Test
    public void findUsersTest() {
        // give
        User user2 = new User("Shine2", "5678", "Shine2", "shine2@naver.com");
        repository.save(user);
        repository.save(user2);

        // when
        List<User> users = repository.findAll();

        // then
        then(users).containsExactly(user, user2);
    }
}

 

 

7. 단순 예외 출력보다는, 예외 던지기

아마 "try-with-resources" 방식을 사용하라는 의도로 생각됩니다!

finally를 사용하는 경우 여러 문제점이 있더군요 찾아본 바로는...

  1. finally 구문 자체에서 예외가 발생할수도 있다. -> finally 블럭 안에 try-catch를 추가할수는 있다.
  2. finally에서 예외가 터지면 기존 try문의 예외를 덮어버리는 문제가 생길 수 있다.
  3. 자원이 둘 이상이면 코드가 복잡해 진다.

댓글