Spring JDBC 과제

오젼·2024년 5월 20일
1

5단계

gradle 의존성 추가

build.gradle dependencies에 spring-boot-stater-jdbc, h2 관련 의존성을 추가한다.

dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    runtimeOnly 'com.h2database:h2'
    ...
}

왜 jdbc는 impelemtation이고 h2는 runtimeOnly?

애플리케이션 코드에 직접 사용되는 것은 impelemtation으로 추가하고
런타임 시점에만 이용되는 데이터베이스 엔진은 runtimeOnly로 추가한다.

Spring을 사용한 애플리케이션에선 애플리케이션 코드에서 직접 데이터베이스에 의존하지 않고 데이터 액세스 계층(예: JdbcTemplate)을 통해 간접적으로 사용하기 때문!

의존성 범위를 구분하면 불필요한 의존성이 포함되지 않아 애플리케이션 크기를 최소화할 수 있다.
또한, 컴파일 시간과 런타임에 필요한 라이브러리를 명확하게 구분할 수 있다.

데이터베이스 설정

application.properties 파일에 아래 내용 추가

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:database
spring.datasource.initialization-mode=always

spring.h2.console.enabled=true

H2 데이터베이스의 웹 콘솔을 활성화
웹 브라우저로 데이터베이스 내역을 볼 수 있는 기능을 활성화 시키는 거다.

spring.h2.console.path=/h2-console

웹 콘솔 경로 지정 http://localhost:스프링부트앱 포트/h2-console 로 접근 가능.

spring.datasource.url=jdbc:h2:mem:database

H2 데이터베이스에 접근하기 위해 사용할 JDBC URL.

jdbc:h2 : mem : database 이렇게 떼어놓고 봐보면

  1. jdbc:h2는 JDBC(Java Database Connectivity)를 통해 H2 데이터베이스에 접근할 때 사용.
  2. mem은 H2 데이터베이스의 유형 중 메모리 데이터베이스에 접근 하겠다는 뜻. (file(파일모드), tcp(tcp 서버 모드), embedded(임베디드 모드) 등이 있음)
  3. database는 데이터베이스 이름. 데이터베이스 만들 때 지정했던 이름을 넣어주면 됨.

spring.datasource.initialization-mode=always

애플리케이션이 시작될 때 데이터베이스의 초기화를 항상 수행하도록 지정.
그리고 schema.sql이 자동으로 실행될 수 있게 한다.

개발 단계니까 애플리케이션 다시 시작할 때마다 초기화가 되는 게 수월.
프로덕션 환경에서는 never이나 embedded를 사용한다고 한다.

+) intellij 데이터베이스 설정

  1. IntelliJ에서 View > Tool Windows > Database
  2. + 버튼을 클릭하고 Data Source > H2
  3. 데이터 소스 이름 지정, User, Password 필드 일단 비워두기
  4. URLjdbc:h2:mem:database 입력
  5. Test Connection 으로 연결 확인
  6. Apply

테이블 스키마 정의

src/main/resources/schema.sql에 스키마 생성.
src/main/resources/ 밑에 sql을 위치시키는 건 스프링부트 애플리케이션의 일반적 관행.
resources 디렉토리가 애초에 리소스 파일(정적 파일, 설정 파일, SQL 파일 등)을 위치시키는 표준 디렉토리.
resources 디렉토리의 파일들은 빌드 과정에서 classpath에 자동으로 포함되어 애플리케이션 실행 시 해당 파일들에 쉽게 접근할 수 있다.

JdbcReservationRepository 클래스 만들기

이전에 ReservationRepository 인터페이스를 만들고,
InMemoryReservationRepository를 구현해서 인메모리 데이터베이스로 사용을 했었다.

이젠 JDBC를 이용한 메모리 데이터베이스를 사용할 거니까
JdbcReservationRepository를 만들어 준다.

JdbcReservationRepository
package roomescape.repository;

@Repository
public class JdbcReservationRepository implements ReservationRepository {

    private final JdbcTemplate jdbcTemplate;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcReservationRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("reservation")
                .usingGeneratedKeyColumns("id");
    }

...

그리고 ApplicationConfig도 바꿔준다.

ApplicationConfig
package roomescape.config;

@Configuration
public class ApplicationConfig {

    @Bean
    public ReservationRepository reservationRepository(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        return new JdbcReservationRepository(jdbcTemplate);
    }
}

의존성 주입 DI

JdbcReservationRepository에서 JdbcTemplate을 인자로 받는다. == 의존성 주입
+) JdbcReservationRepository 클래스는 데이터베이스와 상호작용하기 위해 JdbcTemplate을 사용 해야 한다. JdbcTemplate의존성(Dependency)이 있는 상태. 이 때 의존성 객체를 클래스 내부에서 인스턴스화 하지 않고 인자로 받는 것이 의존성 주입

그럼 의존성 주입을 왜 하냐?

외부에서 주입을 받아야 코드의 결합도를 낮추고, 유닛 테스트를 쉽게 할 수 있기 때문.

위의 코드를 예로 들어 설명해 보면

종속성이 있는 객체를 클래스 내부에서 직접 인스턴스화 하면

ex) 클래스 내부에서 인스턴스화 할 때
public class JdbcReservationRepository implements ReservationRepository {
    private final JdbcTemplate jdbcTemplate;

    // JdbcTemplate을 직접 생성함
    public JdbcReservationRepository() {
        // JdbcTemplate을 생성하고 설정
        this.jdbcTemplate = new JdbcTemplate(/* 설정 정보 */);
    }
    ...
}

JdbcTemplate의 구성이 바뀌면 JdbcReservationRepository에서도 수정을 해야 한다. 결합도가 높음.

이를 낮추기 위해 인자로 받는 것이 나음.
그리고 유닛테스트에서도 Mock 객체를 인자로 전달만 해도 돼서 편하다.

특히 Spring에서는 다양한 방법으로 DI를 지원한다.
스프링 컨테이너에 등록된 빈 객체를 가지고 자동으로 의존성 주입을 해주는데,

지금의 경우 ApplicationConfig에서 reservationRepository 메서드의 @Bean 어노테이션을 보고, 이 메서드가 반환하는 객체를 Spring 컨테이너에 Bean으로 등록한다.

그리고 reservationRepository 메서드에서는 DataSource 객체를 주입받는데, Spring Boot에서 자동으로 구성한 DataSource 객체를 주입받는다.

그리고 JdbcTemplate 인스턴스를 생성할 때, 주입받은 DataSource를 사용한다.

JdbcReservationRepository 인스턴스를 생성할 때, 생성한 JdbcTemplate을 주입한다.

그니까 지금 의존성 주입 관계가 DataSource -> JdbcTemplate -> JdbcReservationRepository

🧌 와 뭐지 어렵다..... 나중에 다시 정리 하기

그럼 왜 config에선 DataSource로 선언하는 게 좋을까?

아하 JdbcTemplate 말고도 JPA, MyBatis 이런 걸로 바꿀 수 있는 거구나 애초에 JdbcTemplate이 DataSource에 의존적으로 구현이 돼있기도 하고.

JPA, MyBatis 나중에 더 찾아보기

6단계

JdbcReservationRepository
package roomescape.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import roomescape.dto.ReservationDTO;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Repository
public class JdbcReservationRepository implements ReservationRepository {

    private final JdbcTemplate jdbcTemplate;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcReservationRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("reservation")
                .usingGeneratedKeyColumns("id");
    }

    @Override
    public List<ReservationDTO> findAll() {
        String sql = "SELECT id, name, date, time FROM reservation";
        return jdbcTemplate.query(sql, this::mapRowToReservation);
    }

    @Override
    public Optional<ReservationDTO> findById(Long id) {
        String sql = "SELECT id, name, date, time FROM reservation WHERE id = ?";
        return jdbcTemplate.query(sql, this::mapRowToReservation, id)
                .stream()
                .findFirst();
    }

    @Override
    public ReservationDTO save(ReservationDTO reservation) {
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", reservation.name());
        parameters.put("date", reservation.date());
        parameters.put("time", reservation.time());

        long id = jdbcInsert.executeAndReturnKey(parameters).longValue();
        return new ReservationDTO(id, reservation.name(), reservation.date(), reservation.time());
    }

    @Override
    public void deleteById(Long id) {
        String sql = "DELETE FROM reservation WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    private ReservationDTO mapRowToReservation(ResultSet rs, int rowNum) throws SQLException {
        return new ReservationDTO(
                rs.getLong("id"),
                rs.getString("name"),
                rs.getString("date"),
                rs.getString("time")
        );
    }
}

5단계에서 SELECT * FROM 하던 거
SELECT id, name, date, time FROM 으로 변경

7단계

음 6단계 그대로

1개의 댓글

comment-user-thumbnail
2024년 5월 20일

와 이해가 잘됩니다... 멋지네요 열심히 글 써주세요~~~

답글 달기