[01.31] 내일배움캠프[Spring] TIL-62

박상훈·2023년 1월 31일
0

내일배움캠프[TIL]

목록 보기
62/72

[01.31] 내일배움캠프[Spring] TIL-62

1. JPA가 되기 까지..

JDBC

  • java앱과 DB를 연결하기 위한 기술
  • JDBC Driver는 여러타입의 DB와 연결할 수 있는 기능을 제공한다.

JDBC RunTime

  • Connection(연결) 을 생성하여 쿼리를 요청할 수 있는 상태를 만들어주고
  • Statement(상태) 를 생성하여 쿼리를 요청하게 해주고
  • ResultSet(결과셋) 을 생성해 쿼리 결과를 받아올 수 있게 해준다.
  • 사용후에는 반드시!! close()를 호출하여 자원 해제를 시켜줘야한다.

JDBC 실습

 @Test
    @DisplayName("테이블 생성테스트")
    void jdbcTest() throws SQLException {
        // given

        // docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_USER=sanghoon -e POSTGRES_DB=messenger --name postgres_boot -d postgres

        // docker exec -i -t postgres_boot bash
        // su - postgres
        // psql --username sanghoon --dbname messenger
        // \list (데이터 베이스 조회)
        // \dt (테이블 조회)

        // IntelliJ Database 에서도 조회
        String url = "jdbc:postgresql://localhost:5432/messenger";
        String username = "sanghoon";
        String password = "pass";

        // when
        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            try {
                String creatSql = "CREATE TABLE ACCOUNT (id SERIAL PRIMARY KEY, username varchar(255), password varchar(255))";
                try (PreparedStatement statement = connection.prepareStatement(creatSql)) {
                    statement.execute();
                    //원래는 Connection, PreParedStatement둘다 close()를 해야하는게 맞는데, try구문안에 넣어줌으로써, 자동으로 닫아줌
                }
            } catch (SQLException e) {
                if (e.getMessage().equals("ERROR: relation \"account\" already exists")) {
                    System.out.println("ACCOUNT 테이블이 이미 존재합니다.");
                } else {
                    throw new RuntimeException();
                }
            }
        }

    }
 @Test
    @DisplayName("JDBC 삽입/조회 실습")
    void jdbcInsertSelectTest() throws SQLException {
        // given
        String url = "jdbc:postgresql://localhost:5432/messenger";
        String username = "sanghoon";
        String password = "pass";

        // when
        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            System.out.println("Connection created: " + connection);

            String insertSql = "INSERT INTO ACCOUNT (id, username, password) VALUES ((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), 'user1', 'pass1')";
            try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
                statement.execute();
            }

            // then
            String selectSql = "SELECT * FROM ACCOUNT";
            try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
                var rs = statement.executeQuery();
                while (rs.next()) {
                    System.out.printf("%d, %s, %s", rs.getInt("id"), rs.getString("username"),
                            rs.getString("password"));
                }
            }
        }
    }
 @Test
    @DisplayName("JDBC DAO 삽입/조회 실습")
    void jdbcDAOInsertSelectTest() throws SQLException {
        // given
        AccountDAO accountDAO = new AccountDAO();

        // when
        var id =accountDAO.insertAccount(new AccountVO("new user", "new password"));

        // then
        var account = accountDAO.selectAccount(id);
        assert account.getUsername().equals("new user");
    }

JDBC DAO

public class AccountDAO {

    //JDBC관련 변수

    private Connection conn = null;

    private PreparedStatement stmt = null;

    private ResultSet rs = null;

    private String url = "jdbc:postgresql://localhost:5432/messenger";
    private String username = "sanghoon";
    private String password = "pass";

    // SQL쿼리
    private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD)"
            +"VALUES((SELECT coalesce(MAX(ID), 0) +1 FROM ACCOUNT A), ?, ?)";

    private final String ACCOUNT_SELECT = "SELECT * FROM account WHERE ID = ?";

    //CRUD 기능 메소드

    public Integer insertAccount(AccountVO accountVO) throws SQLException {
        var id = -1;
        try{

            String [] returnId = {"id"};
            conn = DriverManager.getConnection(url,username,password);
            stmt = conn.prepareStatement(ACCOUNT_INSERT,returnId);
            stmt.setString(1,accountVO.getUsername());
            stmt.setString(2,accountVO.getPassword());
            stmt.executeUpdate(); // 따고 들어가보면 id가 아닌 count를 리턴

            try(ResultSet rs = stmt.getGeneratedKeys()){
                if(rs.next()){
                    id = rs.getInt(1);
                }
            }

        }catch (SQLException sqlException){
            sqlException.printStackTrace();
        }

        return id;
    }

    public AccountVO selectAccount(Integer id) throws SQLException{

        AccountVO accountVO = null;

        try{
            conn = DriverManager.getConnection(url,username,password);
            stmt = conn.prepareStatement(ACCOUNT_SELECT);
            stmt.setInt(1,id);
            var rs = stmt.executeQuery(); // 따고 들어가보면 id가 아닌 count를 리턴

            if(rs.next()){
                accountVO = new AccountVO();
                accountVO.setId(rs.getInt("ID"));
                accountVO.setUsername(rs.getString("USERNAME"));
                accountVO.setPassword(rs.getString("PASSWORD"));

            }

        }catch (SQLException sqlException){
            sqlException.printStackTrace();
        }

        return accountVO;

    }
}
  • 여기서 Connection객체 ~ PreparedStatement ~ ResultSet를 중복으로 쓰고 있기 때문에 따로 DAO파일로 빼고 작업한 것

JDBC Problem

  • SQL 쿼리 요청시 중복 코드 발생
  • DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
  • Connection, Statement 등.. 자원 관리를 따로 해줘야함
    • 안해주면 메모리 꽉차서 서버가 죽음

👍 QueryMapper의 등장 : JDBCTemplate , Mybatis
👍 ORM : JPA, Hibernate

JDBC Template

  • 쿼리 수행 결과와 객채 필드 매핑
  • RowMapper 로 응답필드 매핑코드 재사용
  • Connection, Statement, ResultSet 반복적 처리 대신 해줌
    😵‍💫 But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함

JDBC Template실습

  • Dependency
// 4. SpringBoot 의존성 추가
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'

    // 5. JDBC Template 등.. Spring 의존성을 받기위한 의존성
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
  • TestCode
 @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    @DisplayName("SQL Mapper - JDBC Template 실습")
    void sqlMapper_JDBCTemplateTest() throws SQLException {
        // given
        var accountTemplateDAO = new AccountTemplateDAO(jdbcTemplate);

        // when
        var id = accountTemplateDAO.insertAccount(new AccountVO("new user2", "new password2"));

        // then
        var account = accountTemplateDAO.selectAccount(id);
        assert account.getUsername().equals("new user2");
    }
  • DAO
public class AccountTemplateDAO {

    private final JdbcTemplate jdbcTemplate;

    private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD)"
            +"VALUES((SELECT coalesce(MAX(ID), 0) +1 FROM ACCOUNT A), ?, ?)";

    private final String ACCOUNT_SELECT = "SELECT * FROM account WHERE ID = ?";
    public AccountTemplateDAO(JdbcTemplate jdbcTemplate) {

        this.jdbcTemplate = jdbcTemplate;

    }


    //CRUD 기능 메소드

    public Integer insertAccount(AccountVO accountVO) throws SQLException {
        KeyHolder keyHolder = new GeneratedKeyHolder();

        //DB연결정보 넣어서 connection객체만듬 -> 그 연결 객체에 SQL쿼리문 넣어서 PrepaerStatement객체만듬, 거기에 필요한 저장값,파라미터 등의 값 set!

        jdbcTemplate.update(con->{
            var ps = con.prepareStatement(ACCOUNT_INSERT,new String[]{"id"});
            ps.setString(1,accountVO.getUsername());
            ps.setString(2,accountVO.getPassword());
            return ps;
        },keyHolder);

        return (Integer) keyHolder.getKey();
    }

    public AccountVO selectAccount(Integer id) throws SQLException{

        return jdbcTemplate.queryForObject(ACCOUNT_SELECT, new AccountRowMapper(),id);

    }
}
  • RowMapper
package jpa.pricehoon.jdbc.template;

import jpa.pricehoon.jdbc.vo.AccountVO;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;


public class AccountRowMapper implements RowMapper<AccountVO> {

    @Override
    public AccountVO mapRow(ResultSet rs, int rowNum) throws SQLException {


        var accountVO = new AccountVO();


        accountVO = new AccountVO();
        accountVO.setId(rs.getInt("ID"));
        accountVO.setUsername(rs.getString("USERNAME"));
        accountVO.setPassword(rs.getString("PASSWORD"));


        return accountVO;
    }
}

Mybatis

  • 반복적인 JDBC 프로그래밍을 단순화
  • SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL 을 분리!
  • 😵‍💫 But, 결국 SQL을 직접 작성하는것은 피곤하다…(DB 기능에 종속적) 😫
  • 😵‍💫 But, 테이블마다 비슷한 CRUD 반복, DB타입 및 테이블에 종속적이다.

Mybatis실습

  • MybatisTestCode
@SpringBootTest
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(DBConfiguration.class)


public class MyBatisTest {

    // Mapper 클래스를 받으려면 mapper.xml 빌드 해야하고, 그러려면 main 으로 옮겨서 해야함...
    @Autowired
    AccountMapper accountMapper;// xml 로 쿼리관리

    @Autowired
    AccountMapperV2 accountMapperV2;// annotation 으로 쿼리관리

    @Test
    @DisplayName("SQL Mapper - MyBatis 실습(XML)")
    void sqlMapper_MyBatisTest() {
        // given

        // when
        accountMapper.insertAccount(new AccountMyBatisVO("new user3", "new password3"));
        var account = accountMapper.selectAccount(1);

        // then
        assert !account.getUsername().isEmpty();
    }

    @Test
    @DisplayName("SQL Mapper - MyBatis V2 실습(Annotation")
    void sqlMapper_MyBatisV2Test() {
        // given

        // when
        accountMapperV2.insertAccount(new AccountMyBatisVO("new user4", "new password4"));
        var account = accountMapperV2.selectAccount(1);

        // then
        assert !account.getUsername().isEmpty();
    }
}
  • DBConfigureation
@Configuration
@MapperScan(basePackages = "jpa.pricehoon.mapper.*")
@EnableTransactionManagement
public class DBConfiguration {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        // mapping xml 파일 위치를 bean mapper location 에 등록
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources("classpath:mappings/*.xml"));
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}
  • MapperInterface 1
@Mapper
public interface AccountMapper {

    AccountMyBatisVO selectAccount(@Param("id") int id);

    void insertAccount(AccountMyBatisVO vo);
}
  • MapperInterface 2
@Mapper
public interface AccountMapper {

    AccountMyBatisVO selectAccount(@Param("id") int id);

    void insertAccount(AccountMyBatisVO vo);
}

영속성 컨텍스트(1차캐시)

  • 영속성?
    - 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
    - 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
    -그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.

Raw JPA관점

persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)

예제 코드

Item item = new Item();		// 1
item.setItemNm("테스트 상품");	

EntityManager em = entityManagerFactory.createEntityManager();	// 2
EntityTransaction transaction = em.getTransaction();		// 3
	
transaction.begin();		
em.persist(item);		// 4-1
em.flush(item).     // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit();		// 5

em.close();			// 6

1️⃣  영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣  엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣  데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣  영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣  트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣  엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환

쓰기지연

  • 쓰기 지연이 발생하는 시점
    • flush() 동작이 발생하기 전까지 최적화한다.
    • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
  • 쓰기 지연 효과
    • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
    • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
    • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
    • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.

💁‍♂️ 키 생성전략이 generationType.IDENTITY 로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.

  • why? 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.

Raw JPA로 개발 맛보기

  • 연관관계 구상

  • 알아간 것
    1) 간단한 것이지만 yml파일은 depth로 파이썬 처럼 인식한다.. 이것 땜에 2시간 고생함
    2) 영속성 컨텍스트를 활용하여 쿼리문 없이 Select 함
   @Test
    void insertSelectGroupTest() {
        // given
        var newChannel = Channel.builder().name("new-group2").build();


        // when
        var savedChannel = channelRepository.insertChannel(newChannel);

        // then
        var foundUser = channelRepository.selectChannel(savedChannel.getId());//영속성 컨텍스트에서 조회하기 때문에 Select 쿼리 안나감
        assert foundUser.getId().equals(savedChannel.getId());
    }
profile
기록하는 습관

0개의 댓글