[Spring] 스프링 DB 접근 기술

kdkdhoho·2022년 2월 15일
0

Spring

목록 보기
4/26

인프런 - 김영한 님의 스프링 입문을 보고 공부한 것을 정리한 글입니다.

이전까지 멤버컨트롤러, 멤버서비스, 멤버리포지토리를 만들고 테스트하고, 자바빈 및 의존관계 주입 후 웹 MVC 개발하여 회원가입과 목록을 웹브라우저를 통해 눈을 볼 수 있었다. 하지만 현재 가상의 시나리오로 데이터 저장소가 정해지지 않았다. 때문에 멤버리포지토리를 메모리에 저장하는 방식으로 구현했는데, 이는 서버를 재가동 시 저장된 데이터가 모두 날라간다. 이러한 문제를 방지하기 위해 이제 DB에 저장하는 것을 해보자.

H2 데이터베이스 설치

설치 후 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)
);

여기서 bigintlong과 같음.
BY DEFAULT AS IDENTITY는 NULL값이 들어가도 자동으로 수를 채워주는 키워드

데이터 삽입

INSERT INTO member(id) VALUE('spring1');
INSERT INTO member(id) VALUE('spring2');

순수 JDBC

가장 고대의 방식이다.

자바는 DB와 붙으려면 JDBC 드라이버가 꼭 있어야 함!

옛날 방식이고 전부 코드를 작성해야함.

여기서 중요한 것: 스프링 프레임워크를 쓰는 이유 == MemberRepository 인터페이스를 확장해서 새로운 JdbcMemeberRepository 클래스를 만들고 @Configuration만 수정함. 이렇게 객체지향성이 좋음. 다형성 활용을 아주 편리하게 지원해줌

개방-폐쇄 원칙(OCP, Open-CLosed Principle): 확장에는 열려있고, 수정에는 닫혀있다.

스프링의 DI(Dependecies Injection)을 사용하면 "기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경" 할 수 있다.

DataSource는 DB Connection을 획득할 때 사용하는 객체. 스프링이 DB Connection을 바탕으로 DataSource를 생성하고 스프링 빈으로 알아서 만들어줌.


중요: 기존에는 Repository가 메모리였음. 이제는 DB와 연동하여 데이터를 DB에 저장. 꺼내올 때에도 DB에서 꺼내옴.
결국 서버는 클라이언트에서 요청을 받아 DB에 접근해서 데이터를 가져오고 그 데이터를 이용한 로직을 통해 클라이언트에게 제공하는 식
Service는 클라이언트에서 얻은 요청을 서버 & DB로 요청하는 클래스?
Repository는 그 요청을 기반으로 DB에 접근하는 역할
DB는 데이터 저장소

스프링 통합 테스트

@SpringBootTest
@Transactonial

@SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional: TEST용 DB에 테스트를 거친 후 남아있는 데이터를 모두 날리기 위해 트랜젝션 ROLLBACK을 자동으로 걸어주는 어노테이션 => 다음 테스트에 영향을 주지 않음

단위테스트: 스프링을 안띄우고 코드를 검증하는 테스트하는 것 => 시간이 좀 더 짧게 걸림. 훨씬 더 좋은 테스트일 확률이 높다. 스프링 없이 테스트할 수 있는 능력을 길러야 함.
통합테스트: 스프링, DB, 연동해서 하는 테스트 => 시간이 좀 더 오래 걸림

스프링 JdbcTemplate

디자인패턴 중에 메소드템플릿이 있는데 그게 많이 들어가있음.

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

실무에서도 많이 씀

@AutoWired
private final JdbcTemplate jdbcTemplate;

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

이때 생성자가 1개면 Autowired 생략 가능

return new RowMapper<Member>() {
	@Override
	public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
		Member member = new Member();
		member.setId(rs.getLong("id"));
		member.setName(rs.getString("name"));
		return member;
	}
};

얘를 람다로 변경 가능

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

얘를 이용해서

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

이렇게 2줄로 가능. 이전 Jdbc와 비교했을 때 엄청나게 간단.

@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;
}

JdbcTemplate 기능 중 하나. 테이블명과 PK를 이용해서 Insert문 자동 생성

자세한 사용법은 JdbcTemplate 문서 참고 !

이렇게 JdbcTemplateMemberRepository를 만들고 SpringConfig에서 MemberRepository를 JdbcTemplateMemberRepository와 연동하면 끝.

이제 간단히 이미 만들어 놓은 통합테스트코드를 통해 테스트 할 수 있다.

여기서 중복회원검증 메소드에서 assertThrows 부분이 에러가 뜬다.
에러 메세지는, 예외가 터져야 하는데 아무것도 안터져서 그렇다는데, 코드 상으로는 문제가 없다.
member1, 2 모두 같은 "spring"으로 설정하였는데도 자꾸 터진다.

하나씩 생각해보았다. 중복회원예외에서 중복걸려 예외가 발생해야하는데 안났으면, 중복이 안걸린건데, 그러면 Service에서 join할 때 잘못걸린건가? 싶어서 Service가서 코드보는데 수정한 것이 아무것도 없고 수정한거라곤 Repository 구현체를 변경했으니 JdbcTemplateRepository에서 문제가 있을 것이라 판단하고 코드를 다시 보니, 코드를 모두 작성하지 않았었다.

이렇게 테스트코드를 통해 간단하게 문제를 파악할 수 있다.

김영한 님도 강조하시길 실제 실무에서도 60~70%는 테스트코드를 작성하는 데에 투자하고, 나머지는 프로덕션 코드를 짜는 데에 투자한다고 말씀하셨다.

JPA

기존의 반복코드는 물론, 기본적인 SQL까지도 JPA가 직접 만들어 실행해준다. 이로써 생산성 증가

또, JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 전환가능

JPA를 사용하려면 build.gradle에서 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 추가.
얘는 JPA, JBDC 모두 포함

그리고 application.properties에

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

추가한다.
spring.jpa.show-sql=true: JPA가 날리는 sql을 볼 수 있음
spring.jpa.hibernate.ddl-auto=none: JPA를 사용하면 회원 객체를 보고 알아서 테이블을 만드는 데, 우리는 현재 만들어 놓은 테이블을 사용할 것이기에 none으로 끈다.
none이 아닌 create로 하면 만들어 줌.

JPA는 interface임.
따라서 hibernate나 여러 구현 기술들이 있는데, JPA인터페이스에 hibernate만 주로 쓴다고 생각.

JPA는 ORM(Object-Relational Database-Mapping) 기술.

이제 Member클래스에 @Entity 를 추가한다. 이제 Member클래스는 JPA가 관리하는 Entity라고 표현한다.
그리고 @id, @GeneratedValue(strategy = GenerationType.IDENTITY) 를 적어주어 id에 PK를 매핑해준다.

여기서 strategy = GenerationType.IDENTITY는 DB에 값을 넣어주면 아이디가 자동으로 지정이 되는 방식을 의미한다.

만약 DB 테이블 칼럼명이 username이면 name에 @Column(name="username") 하면 매칭이 됨.

이제 JpaMemberRepository를 만든다.

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepositiry implements MemberRepository {

    private final EntityManager em;

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

JPA는 위에 있는 EntityManager라는 것으로 모든걸 동작한다.
EntityManager는 data-jpa 라이브러리를 받았을 경우, Spring Boot가 알아서 DB Connection 정보랑 설정해놓은 정보 등을 모두 합쳐서 만들어준다. 얘는 내부적으로 DataSource를 들고 있어서 DB랑 통신하고 이런걸 모두 안에서 처리.
그렇기에 EntityManager를 인젝션해주기만 하면 됨.

save는 em.persist()

id조회는 pk이니까 find() 사용
이름 조회랑 전체 조회는, JPQL이라는 객체지향쿼리문을 만든다.
em.createQuery("select m from Member m where m.name = :name", Member.class).setParameter("name", name).getResultList();
em.createQuery("select m from Member m", Member.class).getResultList();

여기서 쿼리문처럼 생긴 것이 JPQL.
기본 SQL은 테이블 대상으로 날리는데, JPQL은 객체(Entity)를 대상으로 날린다.
그러면 JPQL이 SQL로 번역이 된다.

위에 적힌 JPQL문을 보면 객체(Entity)자체를 select한다.

저장, 조회, 업데이트는 쿼리문 짤 필요 x. 다 자동으로 됨.
하지만, findByName이나 findAll같이 PK기반이 아닌 나머지는 쿼리문 작성 필요

Ctrl + Alt + N : 인라인 화

주의: JPA를 쓰려면 항상 트랜젝션이 있어야 함. => Service에 @Transactional

Repository 클래스 만들고, Service에 @Transactional 걸어주면 이제 최종적으로 Bean을 수정해주자!

@Configuration
public class SpringConfig {
	@PersistenceContext
    EntityManager em;
    
    public SpringConfig(EntityManager em) {
    	this.em = em;
    }
    
    @Bean
    public MemberRepository memberRepository() {
    	return new JpaMemberRepository(em);
    }
}

그리고 테스트하는데 또 오류가 걸렸다. 알고보니 H2 버전 차이때문에 생긴 오류인데 때문에 다시 지우고 설치 후 실행했는데도 안돼서 db파일을 새로 만들어보았다. 결국 해결했다 !

스프링 데이터 JPA

스프링 부트랑 JPA만 사용해도 생산성 정말 증가, 코드도 확 줄음.
여기에 스프링 데이터 JPA를 사용하면, 인터페이스만으로도 개발을 완료할 수 있다.
그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공 => 위에 findByName, findByAll에서 쿼리문 작성 필요x

실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA필수

스프링 데이터 JPA는 반드시 JPA를 먼저 학습 후에 공부하기 !!

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    @Override
    Optional<Member> findByName(String name);
}

JpaRepository를 상속하는 인터페이스는 구현체를 자동으로 만들어줌. 그래서 스프링빈에 자동으로 등록.
그래서 그냥 가져다 쓰기만 하면 됨.

이러고 Config에

@Autowired // 생성자가 하나인 경우 생략 가능
private final MemberRepository memberRepository;

public SpringConfig(MemberRepository memberRepository) {
	this.memberRepository = memberRepository;
}

하면, 스프링 데이터 JPA가 만들어놓은 구현체가 등록이 된다.

여기서 드는 의문이, 단순히 인터페이스이고 JpaRepository를 상속받았음에도 자바빈이 등록되고, 그 안에 있던 save(), findById() 등의 메소드들은 구현을 하지 않았음에도 불구하고 테스트를 진행하면 모두 정상 실행된다는 것이다.

이 의문에 대한 해답은, JpaRepository를 타고 들어가면 알 수 있다.
JpaRepository는 PagingAndSortinRepository, QueryByExampleExecutor의 인터페이스를 상속하고,
여기서 PagingAndSortinRepository은 CrudRepository 인터페이스를 상속하고,
CrudRepository 안에 기존에 구현해놨던 save(), findById()같은 메소드들이 기본제공되기 때문이다.

하지만 이렇게 기본 제공되는 메소드들을 제외하고, 나머지의 경우는 어떡할까?
가령 비즈니스 별로 사용하는 변수 명이나 사용하고자는 도메인 등이 다른데, 어떻게 스프링 데이터 JPA를 사용할까?
바로, 메소드명으로 개발을 끝낼 수 있다.
findByName, findByNameAndId..

이것이 바로 스프링 데이터 JPA의 리플렉션 기술로 풀어서 알아서 해결해주는 것이다.

참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용.
Querydsl 사용하면 쿼리도 자바코드로 안전하게 작성 가능, 동적 쿼리도 편리하게 작성 가능.
이 조합으로도 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리(개발자가 직접 쿼리를 짜는 것)를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.

profile
newBlog == https://kdkdhoho.github.io

0개의 댓글