DEVELOPMENT/Spring

6. 스프링 DB 접근 기술1

Tiny Commit 2025. 5. 25. 09:17

 

 

 

 

 

 

 

1. H2 데이터베이스 설치

https://h2database.com/html/main.html

 

H2 Database Engine

H2 Database Engine Welcome to H2, the Java SQL database. The main features of H2 are: Very fast, open source, JDBC API Embedded and server modes; in-memory databases Browser based Console application Small footprint: around 2.5 MB jar file size     Supp

h2database.com

 

 

실행

 

 

 

  • JDBC URL: 내 로컬에 있는 파일 경로를 말해준다. test에 있는 파일을 말한다. 

 

연결

 

 

 

"jdbc:h2:tcp://localhost/~/test" 여리로 연결하면 애플리케이션이랑 콘솔이랑 같이 접근 할 수 있다. 

  • 파일을 직접 접근하는 것이 아니라 소켓을 통해서 접근해서 여러군데에서 접근할 수 있다. 

 

 

 

 

 

테이블 만들기

drop table if exists member CASCADE;

create table member
(
    id bigint generated by default as identity,
    name varchar(255),
    primary key (id)
);

  • generated by default as identity: null값, 값을 세팅하지 않고 인서트하면 DB가 들어왔을 때 자동으로 이 아이디값을 채워줌.

 

 

 

 

 

 

 

조회

 

 

insert into member(name) values('spring')

 

 

 

 

 

 

 

 

 

 

 

2. 순수 JDBC

// 자바는 DB랑 붙으려면 JDBC 드라이버가 꼭 필요하다. 
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

// DB랑 붙을 때 데이터베이스가 제공하는 클라이언트가 필요한데, 여기서 부른다. 
runtimeOnly 'com.h2database:h2'

 

 

접속정보 넣기 - application.properties

// url 만 넣어주면 spring boot가 알아서 해준다. 
spring.datasource.url=jdbc:h2:tcp://localhost/~/test

// h2 DB에 접근
spring.datasource.driver-class-name=org.h2.Driver

 

 

 

 

jdbc api 개발

  • 회원 저장은 멤버 리포지토리
  • JDBC멤버 리포지토리: 구현을 메모리에, DB랑 연동해서 JDBC로 할거야
  • 인터페이스에서 구현제를 바꾸면서도 기존 코드를 변경하지 않고 바꿀 수 있따. 

 

 

 

 

 

 

 

3.0 MySQL로 DB바꾸기

1. application.yml 파일 만들기

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/introduction
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
        use_sql_comments: true
        jdbc:
          batch_size: 1000

 

 

2. JdbcMemberRepository.java

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import javax.sql.DataSource;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

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

    @Override
    public Member save(Member member) {
        jdbcTemplate.update("insert into member(name) values(?)", member.getName());
        Long id = jdbcTemplate.queryForObject("select max(id) from member", Long.class);
        member.setId(id);
        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 Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }

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

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

 

 

3. SpringConfig.java

package hello.hello_spring;

import hello.hello_spring.repository.JdbcMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

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

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        //return new MemoryMemberRepository(); //인터페이스에는 new 안됨. 구현체를 리턴.
        //return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);//인터페이스에는 new 안됨. 구현체를 리턴.
    }
}

 

 

 

 

 

  • 의존성 주입(Dependency Injection)
    • DataSource 같은 외부 자원을 스프링이 관리하게 해서, 코드는 오직 “무엇을 할지(Repository)”만 선언하고, “어떻게 연결할지(DataSource 설정)”는 설정 파일(application.yml)과 스프링 컨테이너에 맡깁니다.
    • 덕분에 테스트할 땐 메모리용 MemoryMemberRepository로, 운영에선 JdbcMemberRepository로 손쉽게 교체할 수 있어요.
  • 관심사의 분리(Separation of Concerns)
    • MemberService는 비즈니스 로직(회원 가입, 조회)에만 집중하고, DB 저장/조회 로직은 JdbcMemberRepository가 담당합니다.
    • 설정(SpringConfig), 도메인(Member), 서비스, 저장소(Repository)가 각자 역할에만 집중하니 코드가 더 깔끔하고 유지보수가 쉬워집니다.
  • 설정의 외부화 & 유연성
    • DB 커넥션 정보와 JPA 옵션을 application.yml에 둬서, 환경(로컬·테스트·운영)에 따라 별도 수정 없이 프로파일만 바꿔주면 됩니다.
    • 쿼리·매핑 방식(JdbcTemplate, RowMapper)도 필요에 따라 다른 방식(JPA, MyBatis 등)으로 바꾸기 쉽습니다.

 

 

 

 

 

 

 

 

 

3. 스프링 통합 테스트

1. 통합 테스트 - MemberServiceIntergationTest.java

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
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.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

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

 

 

 

 

 

 

 

1. 테스트 환경 설정: @SpringBootTest + @Transactional

  • @SpringBootTest
    • 스프링 컨테이너와 테스트를 함께 실행한다.
    • 애플리케이션의 모든 스프링 빈을 실제로 로드해서 테스트합니다.
    • 서비스ㆍ레포지토리 등 스프링 컨테이너에 등록된 컴포넌트 전체를 통합적으로 검증할 수 있어요.
  • @Transactional
    • 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.  
    • 각 테스트 메서드가 끝나면 수행된 DB 작업을 롤백합니다.
    • 실제 데이터를 더티할 걱정 없이, 반복 실행 가능한 깨끗한 테스트 환경을 보장해 줍니다.

2. 의존성 주입 확인: @Autowired

@Autowired 
MemberService memberService; 

@Autowired 
MemberRepository memberRepository;
  • 스프링 컨테이너가 생성한 빈을 실제로 주입받아 사용합니다.
  • 수동으로 new 하지 않고도, 설정(SpringConfig)에 따라 H2든 MySQL이든 같은 빈을 주입받도록 해 줘야 합니다.

3. 기본 회원 가입 테스트 (회원가입)

// 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());
  • Given–When–Then 패턴: 테스트 가독성·유지보수를 위해 꼭 익혀야 합니다.
  • memberService.join() 호출 후, 실제로 DB에 저장된 엔티티를 memberRepository.findById()로 꺼내와서 비교합니다.
  • 서비스 레이어가 “정상 흐름(회원 저장 → 아이디 반환)”을 제대로 수행하는지 검증.

4. 예외 상황 테스트 (중복_회원_예외)

// When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
    () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
  • 중복 회원 예외가 발생하는지 확인하는 부분
    • 첫 번째 가입은 성공, 두 번째 동일 이름 가입에서 IllegalStateException이 터져야 합니다.
  • assertThrows 사용법:
    • 어떤 예외를 기대하는지,
    • 그리고 예외 메시지까지 일치하는지 검증함으로써, 비즈니스 룰(“이름 중복 금지”)이 정확히 적용됐는지 확인.

5. 통합 테스트에서 학생이 꼭 알아야 할 점

  1. 단위 테스트(Unit Test)와의 차이
    • 단위 테스트는 @SpringBootTest 없이, Mockito 같은 목업으로 서비스 혹은 리포지토리만 검증.
    • 통합 테스트는 실제 스프링 컨테이너와 DB까지 모두 포함해 “실제 구동 환경”을 그대로 재현.
  2. 트랜잭션 롤백
    • 테스트마다 데이터가 남지 않으니, 테스트 순서나 반복 실행에 따른 사이드 이펙트 걱정이 없습니다.
  3. 테스트 설계 원칙
    • Given–When–Then 구조로 역할을 명확히 구분
    • 정상 흐름과 예외 흐름을 각각 검증
    • 의미 있는 이름(예: 중복_회원_예외)으로 테스트 목적을 바로 알 수 있게 할 것
  4. 스프링 빈 주입 검증
    • @Autowired로 주입된 빈이 null 이 아니어야 합니다.
    • 설정(SpringConfig, application.yml)에 문제가 있으면 통합 테스트 단계에서 바로 에러가 나므로, 환경 설정 오류를 빠르게 잡아낼 수 있습니다.

 

 

 


출처 :  김영한, 『스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술』, 인프런 강의.