스프링 입문6 (코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술)(feat.김영한 강사님)

권영태·2023년 5월 2일
0

스프링

목록 보기
6/18

Project : Gradle-Groovy Project
JDK : JAVA 17
Spring Boot Version : 3.0.6
Dependencies : Spring Web, Thymeleaf, H2database
IDEA : IntelliJ IDEA

이번 시간에는 스프링 DB 접근 기술에 대해 알아본다.

  • H2 데이터베이스는 1.4.200 버전을 설치해주세요

테이블 생성

  • sql/ddl.sql 파일 생성
drop table if exists member CASCADE;
create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

순수 JDBC

  • build.gradle 파일에 JDBC, H2 관련 라이브러리 추가

    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    runtimeOnly 'com.h2database:h2'

  • 스프링 부트 데이터베이스 연결 설정 추가

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

public class JdbcMemberRepository implements MemberRepository {
 private final DataSource dataSource; // DB에 붙기 위한 데이터 소스
 
 public JdbcMemberRepository(DataSource dataSource) {
 	this.dataSource = dataSource;
 }
 
 @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); 
        // RETURN_GENRATED_KEYS : KEYS(1,2,3등)을 얻을 수 있음
 		pstmt.setString(1, member.getName());
        /* parameterIndex -> 변수 sql values와 매핑되어,
           Member에서 가져온 Name을 slq안 name에 넣음 */
 		pstmt.executeUpdate();   // db에 실제 query가 날라감
 		rs = pstmt.getGeneratedKeys();
 		if (rs.next()) {	// 값이 있으면
 			member.setId(rs.getLong(1));
 		} 
        else {
 		throw new SQLException("id 조회 실패");
 		}
 		return member;
 	} catch (Exception e) {
 		throw new IllegalStateException(e);
	} finally {
 		close(conn, pstmt, rs);
 	}
 }
    
 @Override
 public Optional<Member> findById(Long id) {
 	String sql = "select * from member where id = ?";
 	Connection conn = null;
 	PreparedStatement pstmt = null;
 	ResultSet rs = null;
 	try {
 		 conn = getConnection();
         pstmt = conn.prepareStatement(sql);
         pstmt.setLong(1, id);
         rs = pstmt.executeQuery();
         if(rs.next()) {
             Member member = new Member();
             member.setId(rs.getLong("id"));
             member.setName(rs.getString("name"));
             return Optional.of(member);
         } 
         else {
         	return Optional.empty();
         }
    } catch (Exception e) {
         throw new IllegalStateException(e);
    } finally {
         close(conn, pstmt, rs);
    }
 }
 
 @Override
 public List<Member> findAll() {
 	String sql = "select * from member";
 	Connection conn = null;
 	PreparedStatement pstmt = null;
 	ResultSet rs = null;
 	try {
 		 conn = getConnection();
         pstmt = conn.prepareStatement(sql);
         rs = pstmt.executeQuery();
         List<Member> members = new ArrayList<>();
         while(rs.next()) {
         Member member = new Member();
         member.setId(rs.getLong("id"));
         member.setName(rs.getString("name"));
         members.add(member);
 		 }
 		return members;
 	} catch (Exception e) {
 		throw new IllegalStateException(e);
 	} finally {
 		close(conn, pstmt, rs);
 	}
 }
 
 @Override
 public Optional<Member> findByName(String name) {
   String sql = "select * from member where name = ?";
   Connection conn = null;
   PreparedStatement pstmt = null;
   ResultSet rs = null;
   try {
       conn = getConnection();
       pstmt = conn.prepareStatement(sql);
       pstmt.setString(1, name);
       rs = pstmt.executeQuery();
       if(rs.next()) {
         Member member = new Member();
         member.setId(rs.getLong("id"));
         member.setName(rs.getString("name"));
         return Optional.of(member);
       }
 	 	return Optional.empty();
 	} catch (Exception e) {
 		throw new IllegalStateException(e);
 	} finally {
 		close(conn, pstmt, rs);
 	}
 }
 
 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);
 }
}

스프링 설정 변경

@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();
	return new JdbcMemberRepository(dataSource);	// DI
 }
}

인터페이스 구현체를 확장해 Configuration만 수정

🛠️ 스프링 통합 테스트

@SpringBootTest
@Transactional 
/* testcase에 달면 test 실행 시 트랜젝션을 먼저 실행하고, 
db에 data를 넣고 test 실행 한 뒤 rollback 해줌 -> 반영X */
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("이미 존재하는 회원입니다.");
 }
}

🌈 스프링 JDBCTemplate

  • 스프링 JDBCTemplate는 JDBS API에서 본 반복 코드 대부분을 제거해준다. 하지만 SQL문은 직접 작성 필요!

스프링 JdbcTemplate 회원 리포지토리

public class JdbcTemplateMemberRepository implements MemberRepository {
 private final JdbcTemplate jdbcTemplate;
 
 public JdbcTemplateMemberRepository(DataSource dataSource) {
 	jdbcTemplate = new JdbcTemplate(dataSource);
 }
 
 @Override
 public Member save(Member member) {
 	SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id") // query문 대신 직접 접근
 	Map<String, Object> parameters = new HashMap<>();
 	parameters.put("name", member.getName());
 	Number key = jdbcInsert.executeAndReturnKey(new
	MapSqlParameterSource(parameters));
 	member.setId(key.longValue());
 	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 List<Member> findAll() {
 	return jdbcTemplate.query("select * from member", memberRowMapper());
 }
 
 @Override
 public Optional<Member> findByName(String name) {
 	List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
 	return result.stream().findAny();
 }
 
 private RowMapper<Member> memberRowMapper() {
 	return (rs, rowNum) -> {
 	Member member = new Member();
 	member.setId(rs.getLong("id"));
 	member.setName(rs.getString("name"));
 	return member;
 	};
 }
}

JdbcTemplate을 사용하도록 스프링 설정 변경

@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();
  		// return new JdbcMemberRepository(dataSource);
   	return new JdbcTemplateMemberRepository(dataSource);
   }
}

📗 JPA

  • JAP는 기존 반복 코드는 물론, 기본적인 SQL도 JPA가 직접 만들어준다.
    간단하게 ORM을 사용하기 위한 인터페이스며 구현체로 Hibernate가 있다.
  • ORM은 객체와 DataBase Table을 Mapping한다.

build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가

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

'org.springframework.boot:spring-boot-starter-data-jpa'는 내부 JDBC관련 라이브러리 모두 포함하여 ~~-starter-jdbc는 삭제해도 된다.

스프링 부트에 JPA 설정 추가

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • show-sql은 JPA가 생성하는 SQL 출력
  • ddl-auto는 JPA가 테이블을 자동 생성하는 기능. none를 사용하면 끄기 가능

JPA 엔티티 매핑

@Entity		// JPA가 관리
public class Member {
 
 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 /* Long id와 같이 DB가 자동 생성해주는 것을 Identity라고 함*/
 private Long id;
 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;
 }
}

JPA 회원 리포지토리

public class JpaMemberRepository implements MemberRepository {
 
 private final EntityManager em;
 
 public JpaMemberRepository(EntityManager em) {
 	this.em = em;
 }
 
 public Member save(Member member) {
 	em.persist(member);
 	return member;
 }
 
 public Optional<Member> findById(Long id) {
	 Member member = em.find(Member.class, id);       // 조회 type, pk값
 	return Optional.ofNullable(member);
 }
 
 public List<Member> findAll() {
 	return em.createQuery("select m from Member m", Member.class)   // JPQL query 언어, 객체(정확히는 entity)를 대상으로 query로 날림
    		.getResultList();
 }
 
 public Optional<Member> findByName(String name) {
 List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
 		.setParameter("name", name)
 		.getResultList();
        
 return result.stream().findAny();
 }
}

서비스 계층에 트랜잭션 추가

@Transactional
public class MemberService {}

메서드 실행 시 트랜잭션 시작하고 정상 종료되면 커밋, 예외 발생 시 롤백

JPA를 사용하도록 스프링 설정 변경

@Configuration
public class SpringConfig {
 
 private final DataSource dataSource;
 private final EntityManager em;
 
 public SpringConfig(DataSource dataSource, EntityManager em) {
 	this.dataSource = dataSource;
 	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);
 }
}

💡스프링 데이터 JPA

스프링 데이터 JPA 회원 리포지토리

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

스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경

@Configuration
public class SpringConfig {
 private final MemberRepository memberRepository;
 public SpringConfig(MemberRepository memberRepository) {
 	this.memberRepository = memberRepository;
 }
 
 @Bean
 public MemberService memberService() {
 	return new MemberService(memberRepository);
 }
}

스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해준다.

  • 스프링 데이터 JAP를 이용하면 인터페이스르 통한 안정적인 CURD를 할 수 있고, 메서드 이름만으로 조회가 가능하다.
profile
GitHub : https://github.com/dudxo

0개의 댓글