본문 바로가기
Java | Spring/Spring 입문

[스프링 입문] 6. 스프링 DB접근 기술

by 동기 2022. 4. 4.
반응형

 

이전까지는 Memory에 저장을 했기 때문에, 서버를 재구동하거나 내려가면 데이터가 사라진다.

실무처럼 DB에 저장하고 관리를 해보자. 이를 위해 JDBC를 이용할 것이다.

 

JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다.

JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.

 

JDBC이용 방법은 다양하지만, 지금은 아래 4가지 순서로 알아가 보자.

 

1. 순수 JDBC

2. 스프링 JDBCTemplate

3. JPA (객체를 DB에 쿼리없이 저장 가능)

4. Spring Data JPA (JPA를 편리하게 사용할 수 있도록 지원하는 모듈)

 

DB는 교육용으로 좋은 h2 DB를 이용하였다. (설치과정 생략)

설치 후 application.properties 에 다음과 같이 작성하여 datasoure설정을 해준다.

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

 

1. 순수 JDBC

순수 JDBC를 이용하여 DB 연결을 해 보자.

JdbcMemberRepository 클래스를 작성하였다.

public class JdbcMemberRepository implements MemberRepository{
    /*
    * DB와 연동해서 이용하기위한 MemberRepository 인터페이스의 구현체 클래스
    */

    private final DataSource dataSource;

    //application.properties 에 datasource 세팅을 해 놓았기때문서, 스프링 부트가 데이터소스를 만들어 놓고 주입해 준다.
    public JdbcMemberRepository(DataSource dataSource) {//throws SQLException {
        this.dataSource = dataSource;
//        dataSource.getConnection();//connection을 얻을 수 있다.
    }

    @Override
    public Member save(Member member) {
        //저장을 위한 쿼리
        String sql = "insert into member(name) values(?)"; // ?는 파라미터 바인딩

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

            pstmt.setString(1, member.getName());

            pstmt.executeUpdate();
            rs=pstmt.getGeneratedKeys();

            if(rs.next()){
                member.setId(rs.getLong(1));
            }else{
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }finally {
            close(conn,pstmt,rs);
        }


    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @Override
    public List<Member> findAll() {
        return null;
    }

    private Connection getConnection(){
        return DataSourceUtils.getConnection(dataSource);
    }
    private void close(Connection conn,PreparedStatement pstmt, ResultSet rs){
        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);
            }
        }catch (SQLException e){
            e.printStackTrace();
        }
    }
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn,dataSource);
    }
}

코드가 상당히 길다.

특징으로는 connection을 생성,열고 닫을수 있고, 

PreparedStatement를 이용해 쿼리를 날리고 받아오고,

ResultSet을 통해 결과값을 담아서 확인할 수 있다.

20년전 현업에서 쓰는 코드로 현재는 거의 쓰이지 않으니 전체적으로 느낌만 기억하고 넘어가자.

 

spring framework을 쓸 때는 

 

configuration을 해주자.

지금까지 memory에 저장하고있었으니, DB에 저장을 하기로 했었다.

SpringConfig 클래스에 등록해놨던 memberRepository의 return 객체를 JdbcMemberRepository로 바꿔주기만 하면 된다.

    @Bean
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
        //다른 코드를 손대지 않고 구현체만 바꿨을 뿐인데, 그대로 쓸 수 있다. 데이터를 DB에 저장하므로 서버를 다시 실행해도 데이터가 안전하게 저장된다.
        // 다형성을 활용한 것, 구현체 바꿔끼기 - 스프링은 이것을 편하게 해주게 도와주고 있다.
        // 과거에는, 의존성이 높아 하나 고치면 다른것들도 함께 고쳐야했다.
        // 어쎔블리, 조립하는쪽만 손대면 다른코드는 손대지 않아도 된다.
        //개방 - 폐쇄 원칙( OCP, Open-closed Principle ) 확장에는 열려있고, 수정(변경)에는 닫혀있다.
    }

 

2. 스프링 JDBCTemplate

스프링 JDBCTemplate과 Mabatis 같은 라이브러리는 JDBC API에서 본 반복코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야한다. 

JDBCTemplate은 실무에서도 많이 쓰인다.

 

먼저 JdbcTemplateMemberRepository Class를 생성한다.

public class JdbcTemplateMemberRepository implements MemberRepository{
    @Override
    public Member save(Member member) {
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @Override
    public List<Member> findAll() {
        return null;
    }
}

 

다음으로 생성자를 추가한다.

    private final JdbcTemplate jdbcTemplate;

    @Autowired //생성자가 하나일 경우 Autowired는 생량 가능하다
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

앞서 application.properties 에 DataSource 설정을 해놓았기 때문에, 스프링을 통해 주입이 가능하다.

 

 

rowMapper 메소드 및 findById메소드를 작성해 보자.

private RowMapper<Member> memberRowMapper(){
    return (rs, rowNum) -> {
        Member member = new Member();
        member.setId(rs.getLong("id"));
        member.setName(rs.getString("name"));
        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(); // result 를 stream 으로 바꿔서 findAny
}

JDBC의 findById메소드와 비교했을때, 내용이 엄청 많이 줄어들었다.

 

(JdbcTemplate의 뒷부분이 왜 템플릿이라면, 디자인 패턴 중 템플릿 메소드 패턴이 있고, 그것이 적용되어 코드가 많이 줄어들어 있기 때문이다.)

 

 

다음으로 save 부분을 작성해 보자

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

SimpleJdbcInsert 를 통해, table name 과 keyColums를 통해 insert 쿼리문을 만들수있다.

document를 참조해서 보면 쉽게 작성할 수 있다.

 

findByName 및 findAll도 작성해 보자.

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

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

 

코드를 잘 작성했으니, config에서 repository구현체를 바꿔주고 결과를 확인해 보자.

    @Bean
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
        //다른 코드를 손대지 않고 구현체만 바꿨을 뿐인데, 그대로 쓸 수 있다. 데이터를 DB에 저장하므로 서버를 다시 실행해도 데이터가 안전하게 저장된다.
        // 다형성을 활용한 것, 구현체 바꿔끼기 - 스프링은 이것을 편하게 해주게 도와주고 있다.
        // 과거에는, 의존성이 높아 하나 고치면 다른것들도 함께 고쳐야했다.
        // 어쎔블리, 조립하는쪽만 손대면 다른코드는 손대지 않아도 된다.
        //개방 - 폐쇄 원칙( OCP, Open-closed Principle ) 확장에는 열려있고, 수정(변경)에는 닫혀있다.
    }

 

스프링 통합 테스트시 문제없이 통과된다.

 

3. JPA

JDBC 와 JDBC 템플릿으로 거쳐오면서 , 조금씩 작성해야하는 코드가 줄었지만, 여전히 쿼리는 직접 작성해야한다.

이 JPA를 사용하면 SQL 쿼리도 JPA가 자동으로 처리를 해주기때문에, 개발 생산성을 크게 높일 수 있다.

그것을 넘어 서서 , 이 JPA를 사용하면 , SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환 할 수 있다.

 

3.1 dependency 추가

build.gradle 에 기존 jdbc는 삭제나 주석처리 하고, jpa dependency를 추가해 주자.

	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

3.2 properties에 jpa관련 설정 추가

# jpa sql 을 볼 수 있다.
spring.jpa.show-sql=true
# ddl 관련 설정
spring.jpa.hibernate.ddl-auto=none

 

 

📌ORM

JPA라는 것은 인터페이스 이다. 그 구현체들이 여러개 있다. 우리는 hibernate 를 쓴다.

JPA는 객체와 ORM(Object Relational Mapping)이라는 기술을 이용한다.

 

3.4 @Entity 어노테이션 추가

기존 우리가 작성했었던 Member 객체에 @Entity 어노테이션을 추가해 보자.

이렇게 하면 JPA가 관리하는 엔티티라는 뜻이고, 자바 객체와 DB와 매핑이 된다. 

그리고 이전에 우리가 회원을 생성하면 회원의 id가 자동 증가(auto-increase) 하도록 하였다 (오라클, PostgreSQL 에서는 시퀀스).이러한 방식에는 다양한 방식이 있으며, DB가 알아서 생성해 주는 전략을 Identity 전략 이라고 한다. 

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

    @Column(name="name")
    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;
    }

}

id 에 @Id 를 추가하여 pk임을 알려주고, @GeneratedValue 를 통해 전략을 설정할 수 있다. ( IDENTITY, SEQUENCE, AUTO, TABLE)

또한 각 필드에는 @Column을 통해 해당 테이블의 컬럼명과 일치시킬 수 있다.

column name과 필드의 name은 동일하여 생략 가능하고, 만약 DB의 column name이 user_name 일 경우 @Column 어노테이션에 입력해주면 된다.

 

3.5 JpaRepository 생성

JpaRepository class를 생성하고, EntityManager 를 필드에 선언한다.

EntityManager 란 JPA는 EntityManager로 모두 동작한다.

data jpa 라이브러리를 받았기 때문에, 스프링부트가 자동으로 EntityManager를 생성해준다. 우리는 가져와서 주입 해서 쓰기만 하면 된다.

public class JpaMemberRepository implements MemberRepository{

    private final EntityManager entityManager;

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

    @Override
    public Member save(Member member) {
        entityManager.persist(member);//영속하다. 영구 저장하다
        return member;
    }

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

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = entityManager.createQuery("select m from Member m where m.name = :name",Member.class)
                .setParameter("name",name)
                .getResultList();

        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
    	
    	//List<Member> result = entityManager.createQuery("select m from Member m",Member.class)
        //.getResultList();
        //return result;
        
        //⌃ + t 로 리팩터 옵션을 활성화 하고, inline 입력으로 바꿔줄 수 있다.
        return entityManager.createQuery("select m from Member m",Member.class)
            .getResultList();
    }
}

저장 : entityManager.persist()를 하면 끝이다..

id로 조회 : entityManager.find(Member.class, id);

 

몇몇 메서드에는 JPQL 이라는 객체지향 쿼리를 써야 한다.

이름으로 조회 : entityManager.createQuery("select m from Member m where m.name = :name")

모두 조회 :  entityManager.createQuery("select m from Member m",Member.class)

table을 대상으로 한 쿼리가 아니고, Member 객체를 대상으로 쿼리를 날린다. (SQL로 번역이 되어 DB로 전달된다.)

 

이 JPA기술을 spring이 한번 더 감싸서 제공해주는 기술이 있는데 바로 다음에 배울 스프링 데이터 JPA이다.

JPQL도 사용을 안해도 된다.

 

3.6 @Transactional 작성

JPA를 쓰면서 주의할 점은 Service 계층에 @Transactional 을 써야 하는 것이다.

(데이터를 저장,변경할때는 transaction이 있어야 하고, 그 안에서 실행이 되어야 한다)

Class에 써도 되고, 메서드에 써도 된다.

@Transactional
public class MemberService {
.
.
.

 

3.7 SpringConfig에 구현체 변경

repository를 JpaMemberRepository()로 변경하고, EntityManager를 추가해 준다.

@Configuration
public class SpringConfig {

    //@PersistenceContext 원래 스펙에서는 써야 하지만, 쓰지 않아도 스프링에서 자동으로 주입해준다.
    private EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }

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

    @Bean
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
        //다른 코드를 손대지 않고 구현체만 바꿨을 뿐인데, 그대로 쓸 수 있다. 데이터를 DB에 저장하므로 서버를 다시 실행해도 데이터가 안전하게 저장된다.
        // 다형성을 활용한 것, 구현체 바꿔끼기 - 스프링은 이것을 편하게 해주게 도와주고 있다.
        // 과거에는, 의존성이 높아 하나 고치면 다른것들도 함께 고쳐야했다.
        // 어쎔블리, 조립하는쪽만 손대면 다른코드는 손대지 않아도 된다.
        //개방 - 폐쇄 원칙( OCP, Open-closed Principle ) 확장에는 열려있고, 수정(변경)에는 닫혀있다.
    }
}

 

통합테스트시 정상 작동 한다.

Test 코드에 @Transactional 이 선언되어있다면, 자동으로 rollback 되어 DB에 반영은 되지 않는다.

반영을 해보고 싶다면 반영하고픈 메서드에 @Commit 을 추가해보자.

DB에 반영이 된 것을 알 수 있다.

 

📌JPA는 스프링만큼 공부할게 많기 때문에, 실무에서 잘 사용하려면 깊이있게 공부를 하는것이 좋다.

 

4. 스프링 데이터 JPA

Spring Boot + JPA 만으로도, 코드 길이가 짧아지고, 개발 생산성도 높아진다.

그런데 여기서 스프링 데이터 JPA를 쓰게 되면, 리포지토리 구현 클래스 없이 인터페이스만으로도 개발을 할 수 있게된다.(구현체를 만들어서 등록을 해주기 때문이다.)  또한 기본적인 CRUD 기능도 모두 제공해 준다. 개발자는 핵심 비즈니스 로직을 개발하는데 집중할 수 있게 된다.

 

4.1 스프링 데이터 JPA 회원 리포지토리 작성해보기

인터페이스 에 JpaRepository<T,ID> 를 상속 받으면 된다. 또한 인터페이스는 다중 상속이 되기 때문에, 기존 MemberRepository에 Optional 메소드를 작성해 놨으니, MemberRepository도 상속 받아준다.

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

 findByName(); 을 작성하면 끝이다. 구현할 것이 없다.

 

SpringConfig  역시 더 단순해진다

@Configuration
public class SpringConfig {


    private final MemberRepository memberRepository;

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

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

// MemberRepository를 직접 Bean으로 설정하지 않아도 된다. ( SpringDataJpaRepository에서 jpaRepository
// 를 상속받으면, 스프링이 알아서 Bean으로 등록해 주기 때문이다.)
//    @Bean
//    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
//        return new JpaMemberRepository(entityManager);
//    }
}

 

기존에 직접 Repository @Bean을 등록하는 코드를 없애고, SpringConfig 생성자를 작성하면서 DI를 해주면 된다.

 

📌실무에서는 JPA 와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할수있고, 동적 쿼리도 편리하게 작성 가능하다.

그래도 해결하기 어렵다면, JPA가 제공하는 native query를 사용하거나, jdbcTemplate을 이용하면 된다.

 

 

반응형

댓글