Spring Projcect/날씨 일기 프로젝트

Chapter 03. DB에 작업하기

계란💕 2022. 8. 30. 21:21

3.1 ORM 개념 이해하기

 

 Persistence Framework란?

  • persistance: 영속성 
    •  데이터의 영속성: 휴대폰을 새로 구매해도 기존에 있던 데이터를 끌어올 수 있는 것처럼 데이터가 없어지지 않는 특성을 말한다.
  • Persistance Framework는 DB와의 연동되는 시스템을 빠르게 개발하고 안정적인 구동을 보장해주는 프레임워크를 말한다.
    • 장점: 재사용, 유지 보수에 용이하다. 코드가 직관적이다. 
    • 종류
      • SQL Mapper: SQL을 개발자가 직접 작성한다. 매핑: 쿼리 수행 결과 <=> 객체
        • 단점: DB 종류 변경 시에 쿼리 수정해야한다. 비슷한 쿼리를 반복적으로 작성해야한다.
      • ORM(Object Relation Mapping): Object와 DB 테이블을 매핑한다. Java 함수를 사용하면 자동으로 SQL이 만들어진다. 매핑: DB 테이블 <=> 객체
        • 단점: 복잡한 쿼리를 자바 메서서만으로 해결하는 것이 불편하다.

 

 

 

3.2 JPA vs JDBC

 

 JPA(Java Persistence API) vs JDBC(Java Database Connectivity)

  • SQL Mapper라는 방식의 Persistence Framework를 자바로 쓰면 => JDBC
  • ORM 방식의  Persistence Framework를 자바로 쓰면 => JPA

 

JDBC(Java Database Connectivity)

출처 - 제로베이스

  • JDBC는 SQL Mapper 중 하나이다.
  • JDBC는 자바에서 DB를 사용할 수 있는 최소한의 API라고 볼 수 있다.
  • 쿼리는 개발자가 알아서 짜야한다.

 

 

JPA(Java Persistence API)

출처 - 제로베이스

  • JPA는 자바용 공식 ORM이다. ORM 기능을 쓰기 위한 인터페이스를 모아둔 것이다.
  • JPA는 자바 객체와 DB의 테이블을 연결해주면 프로그램이 스스로 쿼리를 짠다.

 

 

 

3.3 MySQL 연동 작업

 

MySQL 다운로드 및 설치 (윈도우)

 

 

  • 맨 위의 개발자 모드 선택

 

 

 

 

 

3.4 JDBC방식으로 데이터 저장하기

 

  - 먼저 MySQL에서 데이터베이스 project를 추가한다.

 

 

 JDBC로 데이터 저장하기

 

  • build.gradle 파일에 jdbc, mysql 관련된 라이브러리를 추가한다.
<hide/>
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	runtimeOnly 'mysql:mysql-connector-java'
}

 

  • application.properties 파일에 DB 접속 정보 저장하기 - sql에서 저장한 비밀번호를 입력한다.
<hide/>
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/project?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = "비밀번호"

 

  • MySQL에 memo 테이블 만들기
    • auto_increment: 아이디 값을 정하지 않은 경우, 자동으로 1씩 늘려서 ID를 지정한다.
<hide/>
create table memo(
    id INT NOT NULL primary key auto_increment,
    text varchar(50) NOT NULL
);

 

  • Memo라는 클래스를 만든다.
    • 데이터베이스와 연결 시키기 위해서 테이블과 같은 필드 두 개를 만든다.
<hide/>
package zerobase.weather.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Memo {

    private int id;
    private String text;

}

 

  • JdbcMemoRepository: 보통 데이터베이스와 연동할 클래스들을 'repository'라고 한다.
    • JdbcTemplate를 이용해서 MySQL과 연동하자
    • @Autowired를 붙이면 properties 파일에서 DataSource를 자동으로 가져온다.
    • 저장(update), 조회(query)
  • JDBC
<hide/>
package zerobase.weather.repository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import zerobase.weather.domain.Memo;
import javax.sql.DataSource;
import java.util.List;
import java.util.Optional;
@Repository
public class JdbcMemoRepository {
    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcMemoRepository(DataSource dataSource) {   // properties에 저장한 정보들이 담긴다.
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // 데이터베이스에 저장
    public Memo save(Memo memo){
        String sql = " INSERT INTO memo values(?, ?); ";    // 데이터베이스에 메모 클래스를 넣어준다.
        jdbcTemplate.update(sql, memo.getId(), memo.getText());
        return memo;
    }

    // 모든 메모를 조회한다.
    private List<Memo> findAll(){
        String sql = " SELECT * FROM memo; ";
        return jdbcTemplate.query(sql, memoRowMapper());   // sql을 입력해서 나온 모든 객체가 반환된다. 데이터를 memoRowMapper()로 반환한다.
    }

    private Optional<Memo> findById(int id){
        String sql = " SELECT * " +
                     " FROM memo " +
                     " WHERE id = ?;";
        return  jdbcTemplate.query(sql, memoRowMapper(), id).stream().findFirst();
    }
    
    // 데이터베이스에서 조회
    private RowMapper<Memo> memoRowMapper(){    // 데이터베이스에서 가져온 rs를 스프링 부트에 메모 클래스 형태로 반환한다.

        return  (rs, rowNum) -> new Memo(       // rs: resultSet
                rs.getInt("id"),
                rs.getString("text")
        );
    }
}

 

  • 새로운 테스트를 만든다.
    • @Test
    • @Transactional: 데이터베이스를 테스트할 때 자주 쓰인다. 데이터베이스 테스트할 때는 내용이 변경되면 안된다. 이를 막기 위한 어노테이션
    • Java에서 jdbc를 사용할 때는 Connection 객체가 필요했던 것처럼, Spring에서는 Datasource를 필요로한다.
<hide/>
package zerobase.weather.repository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import zerobase.weather.domain.Memo;
import javax.sql.DataSource;
import java.util.List;
import java.util.Optional;
@Repository
public class JdbcMemoRepository {
    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcMemoRepository(DataSource dataSource) {   // properties에 저장한 정보들이 담긴다.
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // 데이터베이스에 저장
    public Memo save(Memo memo){
        String sql = " INSERT INTO memo values(?, ?) ; ";    // 데이터베이스에 메모 클래스를 넣어준다.
        jdbcTemplate.update(sql, memo.getId(), memo.getText());
        return memo;
    }

    // 모든 메모를 조회한다.
    private List<Memo> findAll(){
        String sql = " SELECT * FROM memo ;";
        return jdbcTemplate.query(sql, memoRowMapper());   // sql을 입력해서 나온 모든 객체가 반환된다. 데이터를 memoRowMapper()로 반환한다.
    }

    private Optional<Memo> findById(int id){
        String sql = " SELECT * " +
                     " FROM memo " +
                     " WHERE id = ?  ; ";

        return  jdbcTemplate.query(sql, memoRowMapper(), id).stream().findFirst();
    }

    // 데이터베이스에서 조회
    private RowMapper<Memo> memoRowMapper(){    // 데이터베이스에서 가져온 rs를 스프링 부트에 메모 클래스 형태로 반환한다.

        return  (rs, rowNum) -> new Memo(       // rs: resultSet
                rs.getInt("id"),
                rs.getString("text")
        );
    }
}

 

============================ 오류 =============================

  - 오류: project라는 데이터베이스가 없다고 나온다.

  - 원인: MySQL에서 "project"라는 데이터베이스를 만들지 않았다. 만들고나서 memo 테이블까지 만드니까 테스트 성공

 

 

 

3.5 JPA 방식으로 데이터 저장하기

 

JPA와 ORM 개념 - 면접 단골 질문

  • ORM: DB테이블과 Java 객체를 매핑한다. SQL 자동 생성  ex) JPA
  • SQL Mapper: 단순히 DB와 Java를 연결한다. SQL을 명시해줘야한다.  ex) JDBC

 

  • Dependency 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

  • application.properties 추가
    • spring.jpa.show-sql = true: 내부적으로 생성된 쿼리를 자동으로 콘솔에 출력할 것인가? (show-sql)
    • mysql의 데이터베이스를 쓰겠다고 선언
spring.jpa.show-sql = true
spring.jpa.database = mysql

 

  • memo클래스에 @Entity 붙인다.
    • 클래스를 엔티티화 한다.
    •  JPA 사용할 때는 DB 테이블과 매핑할 클래스에 @Entity를 꼭 붙인다.
    • primary key를 붙여줘야 한다. @Id => 오로지 식별을 위한 용도로 사용한다.
      • Id가 없을 때는 자동으로 증가하게끔 설정 가능하다. 
      • GenerationType.IDENTITY: MySQL이 만들어준 key값이 있으면 그걸 가져온다.
    • 테이블 하나에 여러 엔티티를 매핑할 수 있다. 이럴 때는 name을 명시한다.
      • @Table(name="Memo")
      • @Entity(name="Memo")

 

  • JpaMemoRepository
    • Jpa은 자바의 표준 ORM 명세이다. 따라서, 자바에서 ORM 개념을  활용할 때 사용할 메서드들은 JpaRepo에 모두 정의되어 있다. <Memo, Integer> 뒤의  Integer 자리는 앞서 @Id로 지정한 키의 타입을 의미한다.

 

  • JPA를 활용하면 아래와 같이 코드가 아주 간결해진다.
<hide/>
package zerobase.weather.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zerobase.weather.domain.Memo;
@Repository
public interface JpaMemoRepository extends JpaRepository<Memo, Integer> {
    
}

 

  • JPA test
    • 위에 test insertMemoTest()는 통과한다.
    • 아래 findById()는 ?
<hide/>
package zerobase.weather;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import zerobase.weather.domain.Memo;
import zerobase.weather.repository.JpaMemoRepository;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class JpaMemoRepositoryTest {

    @Autowired
    JpaMemoRepository jpaMemoRepository;

    @Test
    void insertMemoTest(){
        //given
        Memo newMemo = new Memo(10, "this is new Memo");
        // when
        jpaMemoRepository.save(newMemo);
        // then
        List<Memo> memoList = jpaMemoRepository.findAll();
        assertTrue(memoList.size() > 0);
    }
    
    @Test
    void findById(){

        Memo newMemo = new Memo(11, "jpa");

        jpaMemoRepository.save(newMemo);

        Optional<Memo> result = jpaMemoRepository.findById(11);
        assertEquals(result.get().getText(), "jpa");
    }
}

    

  • 아래와 같이 실패한다.
    • newMemo라는 객체에 필드 id의 값에 11을 지정해줬지만 틀리다고 나온다. (NoSuchElementException)
    • 왜냐하면 데이터베이스에서 key 값이 자동 생성(auto-increment)되었기 때문이다. mysql에서 만들어진 Id 값은 11이 아닌 다른 값일 수 있는 것이다.

 

  • 따라서 아래와 같이 코드를 바꿔주면 테스트 성공한다.
<hide/>
@Test
void findById(){

    Memo newMemo = new Memo(11, "jpa");
    Memo memo = jpaMemoRepository.save(newMemo);
    Optional<Memo> result = jpaMemoRepository.findById(memo.getId());
    assertEquals(result.get().getText(), "jpa");
}

 

 

 출처 - 제로베이스 백엔드 스쿨 (김조현 강사님)

https://zero-base.co.kr/

 

제로베이스 - 누구나 취업하는 가장 합리적인 취업 스쿨

코딩 부트 캠프 개발자, 데이터 사이언티스트, 마케터, PM, 디자이너 등 제대로 공부하고 확실하게 취업하세요. 당신의 삶의 전환점이 될 제로베이스 스쿨입니다.

zero-base.co.kr