본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2025 1학기 스프링부트 스터디] 최비성 #6주차

반응형


섹션 7. 스프링 DB 접근 기술

H2 데이터베이스 설치

 

java에서 DB에 연결할 때 필요한 라이브러리가 JDBC인데, 20년 전으로 돌아가서 순수한 JDBC로 어떻게 개발하는 지를 경험시켜줄 것이다.

 

순수한 JDBC 다음으로는 스프링이 중복을 다 제거해준 JDBC 템플릿, 그리고 더욱 더 혁신적인 기술로 JPA라는 기술이 있다.

 

 

암튼 일단 먼저 DB를 설치해주어야 하는데, H2라는 가볍고 편리한 DB를 사용할 것이다. 공식 사이트에서 다운 받고 설치하면 아래 사진처럼 H2 Console이 뜬다(윈도우 갓갓).

H2 Console 딸깍

 

아무것도 건들지 말고 그냥 '연결' 딸깍

 

사용자 폴더 들어가서 test.mv.db 파일 확인

 

중요: 이후부터는 JDBC URL을 다르게 접근해야 한다. 이렇게 파일로 접근하게 되면 웹 콘솔과 애플리케이션이 동시에 할 때 같이 접근이 안될 수가 있다고 한다. 오류 나고 파일 충돌나고 그런다고 한다. 그래서 아래 처럼 한다.

이러면 소켓으로 접근하게 된다.

 

그리고 연결해서 테이블 만들어주면 된다.

 

Ctr+Enter == SQL문 실행

 

 

그리고 테이블 관리를 위해 프로젝트 루트에 sql/ddl.sql 파일을 생성한다.

 

 

 

 

 


순수 JDBC

고대의 방식으로 insert와 select해서 DB에 넣고 빼는 작업 해보기

 

순수 Jdbc
환경 설정
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

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



스프링 부트 데이터베이스 연결 설정 추가 (`resources/application.properties`)

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

 

 

 

MemoryMemberRepository 대신에 JdbcMemberRepository를 생성하는데, 모두의 정신건강을 위해 코드는 복붙 진행됐다. 그러니 핵심만 설명한다.

  private final DataSource dataSource;

  public JdbcMemberRepository(DataSource dataSource) {
    this.dataSource = dataSource;
  }

이렇게 디비소스 만들어주고

conn = getConnection();

이렇게 하면 연결 소켓이 만들어지며

close(conn, pstmt, rs);

DB 연결 소켓 안 닫아주면 리소스 엄청 먹어서 대장애가 일어난다고 한다. 

 

private Connection getConnection() {
    return DataSourceUtils.getConnection(dataSource);
  }

추가로, String framework를 통해서 데이터베이스 커넥션을 쓸 때 유틸즈를 써서 커넥션을 하고 닫아주어야 한다고 한다. 그렇지 않으면 트렌젝션이 걸릴 수 있다 한다.

 

스트링 빈을 설정할 때는 아래와 같이 config를 작성하면 된다.

...

import wink.spring_boot_study.repository.JdbcMemberRepository;
import javax.sql.DataSource;

@Configuration
public class SpringConfig {
  private final DataSource dataSource;

  public SpringConfig(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  ...

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

 

- 개방-폐쇄 원칙(OCP, Open-Closed Principle) : 확장에는 열려있고, 수정, 변경에는 닫혀있다.
- 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클
래스를 변경할 수 있다.
- 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.

 

 

 


스프링 통합 테스트

이번엔 테스트를 스프링이랑 막 엮어서 DB의 실제 데이터를 테스트하는 과정을 진행해볼 것이다.

package wink.spring_boot_study.repository;

import wink.spring_boot_study.domain.Member;
import wink.spring_boot_study.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 static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

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

여기서 핵심은 '@Transactional' 애노테이션이다.

트렌젝션 없이 진행을 하면 테스트로 넣은 데이터가 그대로 DB에 반영이 되어 버리는데, 트렌젝션 애노테이션을 적용해주면 테스트를 한 데이터가 롤백되어 없던 일로 만들 수 있다. 오토 커밋 모드를 꺼버리는 것이다.

 

참고로 이 @Transactional이 테스트 코드가 아닌 서비스 코드에 붙으면 롤백되지 않고 정상적으로 커밋이 수행된다.

 

- @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.
- @Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

 

 

 

이번에는 이렇게 통합 테스트를 진행하였는데, 실행 시간이 매우 오래 걸리므로 가능하면 단위 테스트로 만드는 것이 좋은 테스트일 확률이 높다고 한다.

 

 




스프링 JdbcTemplate

- 순수 Jdbc와 동일한 환경설정을 하면 된다.
- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.

 

디자인 패턴 중에 템플릿 메서드 패턴이라는 것이 있는데, 그걸로 중복 제거를 많이 했기 때문에 JDBC Template이라고 불리는 것이다.

 

package wink.spring_boot_study.repository;

import wink.spring_boot_study.domain.Member;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository {
  private final JdbcTemplate jdbcTemplate;

  public JdbcTemplateMemberRepository(DataSource dataSource) {
    jdbcTemplate = new JdbcTemplate(dataSource);
  }

  @Override
  public Member save(Member member) {
    SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
    Map<String, Object> parameters = new HashMap<>();
    parameters.put("name", member.getName());
    Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
    member.setId(key.longValue());
    return member;
  }

@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}

  @Override
  public List<Member> findAll() {
    return jdbcTemplate.query("select * from member", memberRowMapper());
  }

@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}

  private RowMapper<Member> memberRowMapper() {
    return (rs, rowNum) -> {
      Member member = new Member();
      member.setId(rs.getLong("id"));
      member.setName(rs.getString("name"));
      return member;
    };
  }
}

 

코드가 겁나 줄어들었다.

 

DB 커넥션, Exception, 등 많은 부분이 중복 제거되었지만 SQL은 직접 작성해주어야 한다.

 

 

 


JPA

 

 

- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
- JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.

 

간단하게 말하자면 JAP라는 것은 인터페이스다. 인터페이스만 제공되는 것이고 구현체로 하이버네이트, 이클립스 링크 등등 구현 기술들이 여러개의 벤더들로 있는 것이다. 그 중에서 우리는 JPA 인터페이스에 하이버네이트만 거의 쓴다고 보면 된다.

 

그러니까 JPA라는 것은 그 자바 진영의 표준 인터페이스고, 구현은 여러 업체들이 하는 거라고 보면 된다.

 

일단 JPA는 객체 ORM이라는 기술이다.

 

...

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

- `show-sql` : JPA가 생성하는 SQL을 출력한다.
- `ddl-auto` : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 `none` 를 사용하면 해당 기능을 끈다.
- `create` 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다.

 

 

domain/Member.java

package wink.spring_boot_study.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;

  public Long getId() {
    return id;
  }

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

  public String getName() {
    return name;
  }

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

엔티티가 관리하는 엔티티라고 표현을 하기 위해 @Entity라고 적어준다.

주의 : 관리하는 회사가 바뀌면서 import 이름이 바뀌었다고 한다. javax로 시작하는 게 아니라 jakarta로 시작해야 한다. https://www.samsungsds.com/kr/insights/java_jakarta.html

 

package wink.spring_boot_study.repository;

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

import jakarta.persistence.EntityManager;
import wink.spring_boot_study.domain.Member;

public class JpaMemberRepository implements MemberRepository {
  private final EntityManager em;

  public JpaMemberRepository(EntityManager em) {
    this.em = em;
  }

  public Member save(Member member) {
    em.persist(member);
    return member;
  }

  public Optional<Member> findById(Long id) {
    Member member = em.find(Member.class, id);
    return Optional.ofNullable(member);
  }

  public List<Member> findAll() {
    return em.createQuery("select m from Member m", Member.class)
        .getResultList();
  }

  public Optional<Member> findByName(String name) {
    List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
        .setParameter("name", name)
        .getResultList();
    return result.stream().findAny();
  }
}

JPA는 엔티티 매니저라는 걸로 모든 게 동작을 한다. 프로퍼티 세팅하던거, 데이터베이스 커넥션 정보랑 다 자동으로 짬뽕을 해서 내부적으로 처 해주기 때문에 우리는 이 만들어진 거를 그냥 인젝션 받아서 쓰면 된다.

 

엔티티 매니저를 줄여서 em이라고 한다.

 

그리고 SQL 쿼리 대신 객체를 대상으로 쿼리를 날리는 JPQL이라는 것을 짜야한다. 이게 SQL로 변역된다고 한다.

 

또, JPA를 쓸 때의 주의점을 항상 트랜잭션이라는 게 있어야 한다는 것이다. 조인 들어올 때 모든 데이터 병견이 다 트랜잭션 안에서 실행이 되야 한다.

서비스에 트렌젝션 추가하고

 

Config 설정 바꾼뒤

 

테스트를 실행하면 잘 insert 되었다는 것을 알 수 있다.

 

 

 


스프링 데이터 JPA

요약 : 스프링 데이터 JPA까지 쓰면 코드를 더욱 확확 줄일 수 있게 된다.

 

실무에서는 기본적으로 Spring Boot JPA, JPA, Sprint Boot 이렇게 기본으로 깔고 들어간다고 한다.

 

 

package wink.spring_boot_study.repository;

import wink.spring_boot_study.domain.Member;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;


public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

  @Override
  Optional<Member> findByName(String name);
  // Spring Data JPA는 findByName 메서드를 자동으로 구현해줌
}

스프링 데이터 JAP가 JAP 레포지토리를 받고 있으면 구현체를 자동으로 만들어주고, 스프링 빈을 자동으로 등록해준다.

 

JpaRepository안에 웬만한 건 다 들어있다. 공통으로 쓸만한 메서드는 이미 다 있기 때문에 만들어 줄 필요가 없다.

 

Optional<Member> findByNameAndId(String name, String i);

설령 비즈니스 로직이 특이해서 특별한 메소드를 만들어주어야 한다면, 위처럼 메소드의 네이밍 규칙, 파라미터, 반환 타입만으로 개발을 끝내줄 수도 있다.

 

구현 클래스를 만들어 줄 필요없이 인터페이스 만으로 개발이 끝나는 것이다.

 

 

반응형