🎈 실습
Repository에 메서드명 변경 -> Service에 수정
1. memberCreate -> save로 변경. return 타입 Member
2. findById -> return 타입 Optional<Member>
3. 리스트 조회 -> findAll
[ MemberRepository.java ]
인터페이스에서 메서드명, 반환타입 변경
public interface MemberRepository {
public List<Member> findAll();
public Member save(Member member);
public Optional<Member> findById(int id);
}
[ MemoryMemberRepository.java ]
Optional.of()와 Optional.empty() 활용
@Repository
public class MemoryMemberRepository implements MemberRepository{
private final List<Member> memberDB;
public MemoryMemberRepository(){
memberDB = new ArrayList<>();
}
@Override
public List<Member> findAll(){
return memberDB;
}
@Override
public Member save(Member member){
memberDB.add(member);
return member; //지정된 format 때문에 바꾸는 것
}
@Override
public Optional<Member> findById(int id) {
for(Member member : memberDB){
if(member.getId() == id){
return Optional.of(member);
}
}
return Optional.empty();
}
}
[ MemberService.java ]
findAll()로 메서드명 변경, findAll() 내부 memberRepository.findAll()로 변경
findById() 메서드 Optional과 throw, orElseThrow를 사용해 처리
public MemberResponseDto findById(int id) throws NoSuchElementException { // Optional, 예외처리 디테일 챙기기
//Member 객체를 MemberResponseDto로 변환
//생성자 초기화보다는 유연성이 좋다.
// Member member = memberRepository.findById(id).orElseThrow(()-> new NoSuchElementException());
// Optional로 객체가 반환되지 않으면(비어있으면) 예외 터져서 아래 코드로 내려가지 않음.
//개발자 간 NoSuchElementException이 발생하면 어떻게 처리할지 다루려고 적는 것이지,
//에러만 적는다고 페이지 상으로 404 에러가 나가지는 않음! ResponseEntity로 잘 처리하자!
Member member = memberRepository.findById(id).orElseThrow(NoSuchElementException::new);
MemberResponseDto memberResponseDto = new MemberResponseDto();
memberResponseDto.setId(member.getId());
memberResponseDto.setName(member.getName());
memberResponseDto.setEmail(member.getEmail());
memberResponseDto.setPassword(member.getPassword());
memberResponseDto.setCreate_time(member.getCreate_time());
return memberResponseDto;
}
[ templates/404-error-page.html ]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Error Page</title>
</head>
<body>
<h1>잘못된 요청입니다.</h1>
</body>
</html>
DB 연결하기
1. DB 생성
basic 스키마 생성 -> member 데이터베이스 생성
(1) workbench 사용
(2) CREATE TABEL문 사용
use basic;
CREATE TABLE `basic`.`member` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`));
2. DB와 싱크 맞추면서 구조 조정하기
[ MemberService.java ]
id와 create_time은 DB에서 자동으로 생성되어 객체에 값이 세팅되기 때문에
id, create_time 관련 세팅 코드들은 Repository.java로 이동
public void createMember(MemberRequestDto memberRequestDto){
Member member = new Member(memberRequestDto.getName(), memberRequestDto.getEmail(), memberRequestDto.getPassword());
memberRepository.save(member);
}
[ MemoryMemberRepository.java ]
@Override
public Member save(Member member){
++total_id;
LocalDateTime now = LocalDateTime.now();
member.setId(total_id);
member.setCreate_time(now);
memberDB.add(member);
return member;
}
3. DB 설정정보 세팅하기
[ application.yml ]
spring:
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:3306/basic
username: root
password: 1234
[ build.gradle ]
//DB 연결을 위한 의존성 : jdbc, mariadb 클라이언트
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.mariadb.jdbc:mariadb-java-client'
3-(1). JDBC 연결
DDL 수행 -> Raw 쿼리 수행 -> 객체 직접 조립
✅ 특징
- Connection, PreparedStatement, ResultSet을 사용해 SQL문을 직접 작성 및 처리해주어야 한다
- SQL문에 들어가야 할 값들은 ?로 표시한다
- 쿼리문이 " " 안에 쌓여 있어서 오타가 났어도 컴파일 타임에 발견되지 않는 점이 단점이다
- ResultSet에 담긴 데이터를 일일이 객체에 담아줘야 한다
[ JdbcMemberRepository.java ]
save() 구현
@Override
public Member save(Member member) {
try{
Connection connection = dataSource.getConnection();
String sql = "insert into member(name, email, password) values(?,?,?)";
// preparedStatement : ? 빠진 SQL
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// ?, ?, ? 값들을 세팅
preparedStatement.setString(1, member.getName());
preparedStatement.setString(2, member.getEmail());
preparedStatement.setString(3, member.getPassword());
preparedStatement.executeUpdate();
}catch (SQLException e){
e.printStackTrace();
}
return member;
}
[ MemberService.java ]
구현체 변경 MemoryMemberRepository -> JdbcMemberRepository
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(JdbcMemberRepository jdbcMemberRepository) {
this.memberRepository = jdbcMemberRepository;
}
회원등록 테스팅. 회원등록 누르면 에러창 뜨는 게 맞음.
DB에 정상적으로 값 들어갔는지 확인
[ JdbcMemberRepository.java ]
findAll(), findById() 구현
@Override
public List<Member> findAll() {
List<Member> members = new ArrayList<>();
try{
Connection connection = dataSource.getConnection();
String sql = "select * from member";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String email = resultSet.getString("email");
String password = resultSet.getString("password");
LocalDateTime now = resultSet.getTimestamp("create_time").toLocalDateTime();
Member member = new Member(name, email, password);
member.setId(id);
member.setCreate_time(now);
members.add(member);
}
}catch (SQLException e){
e.printStackTrace();
}
return members;
}
@Override
public Optional<Member> findById(int inputId) {
Member member = null;
try{
Connection connection = dataSource.getConnection();
String sql = "select * from member where id = ?";
// preparedStatement : ? 빠진 SQL
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// ?값을 세팅
preparedStatement.setInt(1, inputId);
// 결과 가져오기
ResultSet resultSet = preparedStatement.executeQuery();
resultSet.next();
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String email = resultSet.getString("email");
String password = resultSet.getString("password");
LocalDateTime now = resultSet.getTimestamp("create_time").toLocalDateTime();
member = new Member(name, email, password);
member.setId(id);
member.setCreate_time(now);
}catch (SQLException e){
e.printStackTrace();
}
return Optional.ofNullable(member);
}
3-(2). SpringDataJpa 연결
DDL 수행 X -> Raw 쿼리 X -> 객체 조립 X
✅ 특징
- SpringDataJpa 기술의 근간에는 JDBC 기술이 들어가 있다
- Hibernate는 JPA의 구현체이기 때문에. JPA == Hibernate 의미가 동일하다
- jpa 및 hibernate 설정을 추가해줘야 한다
- @Column으로 변경한 객체의 변수 이름과, 테이블 컬럼명이 일치하지 않아도 JPA가 인지해서 잘 넣어준다
- @Entity를 통해 mariaDB 테이블과 컬럼을 자동 생성한다
- DML 쿼리 생성 자동화 : 메서드 명으로 약속. ex) findAll, findById, ..
- 리플렉션 기술을 통해 런타임에 Setter를 이용해 객체를 초기화한다. 몇 개의 속성을 이용해 만든 생성자가 따로 존재한다면, @NoArgsConstructor을 통해 매개변수 없는 기본 생성자를 생성해줘야 한다 !
- return 이후 객체 변환까지 자동화되어 있다
[JPA 포스팅 추가 예정]
JPA : 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스
[ORM 포스팅 추가 예정]
orm : 쿼리없이 프로그램으로 DB 컨트롤, 객체지향
ORM == JPA 의미가 통한
[ build.gradle ]
//spring data jpa 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
[ application.yml ]
jpa 및 hibernate 설정. 실무에서는 절대 create 사용하지 말 것 !!!!
spring:
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:3306/basic
username: root
password: 1234
jpa:
databases: mysql
# innoDB 엔진 사용 설정
database-platform: org.hibernate.dialect.MariaDBDialect
# 스키마는 사전 생성 필요
# 테이블과 컬럼은 자동 생성해줌
generate-ddl: true
hibernate:
#create 옵션은 매번 table을 drop 후 생성
#update 옵션은 변경 사항만 alter를 적용
#validate는 실제 DB만의 차이만을 check
#실무에서는 update 또는 validate 사용
ddl-auto: create
#jpa가 자동으로 쿼리를 실행할 때, 쿼리가 console창에 보여지도록
show-sql: true
ddl-auto : create이기 때문에, Spring 서버를 실행할 때마다 새로운 테이블이 생성되어 회원등록 전에는 데이터가 없다.
[ build.gradle ]
jdbc 설정 정보 "삭제"
jpa 설정에 jdbc가 포함되어 있음.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
[ MemberService.java ]
구현체 변경
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(SpringDataJpaMemberRepository springDataJpaMemberRepository) {
this.memberRepository = springDataJpaMemberRepository;
}
[ Member.java ]
@Entity를 통해 mariaDB 테이블과 컬럼을 자동 생성⭐
JPA는 리플렉션 기술을 통해 런타임에 Member 객체를 하나 생성해서,
모든 속성에 대해 .set으로 값을 넣어주게 되는데 매개변수 3개인 생성자만 덜렁 있으면 이 과정이 불가능하다.
@NoArgsConstructor을 통해 매개변수 없는 기본 생성자를 생성해줘야 한다 !
@Entity : 테이블 자동생성하는 JPA 기술(스프링 부트 기술XX, MyBatis 기술XXXX)
@Id : pk 설정
@Column : DB에서 사용할 컬럼명지정.
== @Column(name="created_time")과 private LocalDateTime create_time을 같은 것으로 인지
@GeneratedValue(strategy = GenerationType.IDENTITY) : Auto Increment
@Column 자체가 JPA 기술이기 때문에 MyBatis에서는 @Column인지하지 못해 변수명과 DB컬럼명이 다르면 문제가 발생한다.
@Getter
//Entity 어노테이션을 통해 mariaDB 테이블 및 컬럼을 자동 생성
//Class 명은 테이블명, 변수명은 컬럼명
@Entity
@NoArgsConstructor //기본 생성자 말고 다른 생성자가 있을 때 추가해줘야 함.
//// JPA가 모든 속성에 setter로 런타임에 값을 넣어줘야 하기 때문이다.
public class Member {
@Setter
@Id //pk 설정
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id; //Repository에서 set하기 위해 어쩔 수 없이 붙임 (DB와 싱크를 맞추기 위해 필요함)
// String은ㅇ DB의 varchar로 자동 변환
private String name;
@Column(nullable = false, length = 50)
private String email;
private String password;
@Setter
@Column(name = "created_time") //name 옵션을 통해 DB의 컬럼명 별도 지정 가능
//DB의 컬럼명은 created_time, 스프링에서 Member 객체 조립할 때 create_time으로 알아서 넣어줌
private LocalDateTime create_time;
public Member(String name, String email, String password){
this.name = name;
this.email = email;
this.password = password;
this.create_time = LocalDateTime.now();
}
}
➡️ 추후 SpringDataJpa 사용 시에는 @CreationTimestamp 어노테이션을 사용해 DB와의 싱크를 맞춰준다.
@Column(name = "created_time") //name 옵션을 통해 DB의 컬럼명 별도 지정 가능
@CreationTimestamp
private LocalDateTime create_time;
@UpdateTimestamp
private LocalDateTime updated_time;
public Member(String name, String email, String password){
this.name = name;
this.email = email;
this.password = password;
//this.create_time = LocalDateTime.now();
}
[ SpringDataJpaMemberRepository.java ] ⭐ ⭐ ⭐
SpringDataJpaMemberRepository는 인터페이스는 MemberRepository, JpaRepository를 모두 상속하고 있다.
MemberRepository에도 findAll, JpaRepository에도 findAll이 있어, 같은 이름의 메서드 중 어떤 것을 호출할지의 이슈가 생긴다.
구현체를 @Repository, extends JpaRepository를 쓰면 SpringDataJpa 기술을 통해 주입되므로, 별도의 구현체가 필요없다.
<Entity 타입명, PK 타입명>만 명시해주면 인터페이스를 가져다가 별도의 구현없이 구현체를 사용할 수 있다.
@Repository
//Spring Data Jpa의 기본 기능을 쓰기 위해서는 JpaRepository를 상속해야함
//상속 시 Entity명과 해당 Entitiy의 PK 타입을 명시
//<Class명, PK타입>
//구현 클래스와 스펙은 SimpleJpaRepository 클래스에 있고, 실질적인 구동 상황에서 hibernate 구현체에 동작을 위임⭐
public interface SpringDataJpaMemberRepository extends MemberRepository, JpaRepository<Member, Integer> { //인터페이스 간의 관계는 extends.
}
[ MemberService.java ]
기존에는 인터페이스를 코드에 넣어주면서 공동된 함수만 사용하도록 기능을 제한했는데,
JpaRepository안에 있는 다양한 메서드들을 사용하기 위해 구현체에 의존하도록 바꾸면서 기능 제한을 풀어준다.
private final MemberRepository memberRepository;
public class MemberService {
private final SpringDataJpaMemberRepository springDataJpaMemberRepository;
@Autowired
public MemberService(SpringDataJpaMemberRepository springDataJpaMemberRepository) {
this.springDataJpaMemberRepository = springDataJpaMemberRepository;
}
Spring 코드를 돌리면 자동으로 테이블이 생성된다.
콘솔 창에 Hibernate가 동작하고 있는 것을 확인할 수 있다.
3-(3). MyBatis 연결
DDL 수행 -> Raw 쿼리 수행 -> 객체 조립 X
✅ 특징
- 일일이 조립해줘야 하기 때문에, 객체의 변수와 테이블 컬럼명이 일치해야 한다.
- @Mapper 어노테이션과 MemberMapper.xml을 사용한다.
Mapper 파일 확인 > MybatisMemberRepo랑 싱킹되어 있네? > Mapper 파일 가지고 구현체 생성해서 MemberService에서 해당 메소드들이 동작하게 함.
[ build.gradle ]
//mybatis 관련 의존성 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
[ MybatisMemberRepository.java ]
@Mapper //mybatis 레파지토리로 쓰겠다는 어노테이션
@Repository
public interface MybatisMemberRepository extends MemberRepository{
//본문에 MybatisRepository에서 사용할 메서드 명세를 정의해야 하나,
//MemberRepository에서 상속받고 있으므로 생략 가능.
//실질적인 쿼리 등 구현은 resources/mapper/MemberMapper.xml파일에서 수행
}
💡MybatisMemberRepository와 MemberMapper.xml연결
MybatisMemberRepository - @Mapper키워드, MemberMapper.xml - mapper namespace 키워드로 상호 연결
[ MemberMapper.xml ]
<?xml version="1.0" encoding="UTF-8" ?>
<!-- mapper DTD 선언 -->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.encore.basic.repository.MybatisMemberRepository">
<select id="findAll" resultType="com.encore.basic.domain.Member">
select * from member;
</select>
<select id="findById" resultType="com.encore.basic.domain.Member" parameterType="Integer">
select * from member where id=#{id};
</select>
<insert id="save">
insert into member(name, email, password, created_time) values(#{name}, #{email}, #{password}, #{create_time});
</insert>
</mapper>
[ MemberService.java ]
Repo연결 변동사항 적용
private final MemberRepository memberRepository;
@Autowired
public MemberService(MybatisMemberRepository mybatisMemberRepository) {
this.memberRepository = mybatisMemberRepository;
}
요렇게까지 바꾸고 나면 회원가입 시 WhiteLabel 에러 발생한다.
[ MemberMapper.xml ]
insert문 -> select문으로 변경해서 resultType으로 Member 받아줘야 DB에 잘 들어간다.
<select id="save" resultType="com.encore.basic.domain.Member">
insert into member(created_time, name, email, password) values(#{create_time}, #{name}, #{email}, #{password});
</select>
3-(4). 순수 Jpa 연결
✅ 특징
- JPA의 쿼리 문법인 JPQL 사용 ex) select m from Member m
- 장점 : DB에 따라 문법이 달라지지 않는 객체지향 언어, 컴파일 타임에서 check⭐!!
- 단점 : DB 고유의 기능과 성능을 극대화하기는 어려움.
- SpringDataJpa에 비해 지원하는 자동화 메서드가 적다.
[ JpaMemberRepository.java ]
@Repository
public class JpaMemberRepository implements MemberRepository{
//EntityManager는 JPA의 핵심클래스(객체)
//Entity의 생명주기를 관리. 데이터베이스와의 모든 상호작용을 책임.
//엔티티를 대상으로 CRUD하는 기능을 제공
private final EntityManager entityManager;
public JpaMemberRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Member save(Member member) {
//persist : 전달된 엔티티(member)가 EntityManager의 관리 상태가 되도록 만들어주고,
// 트랜잭션이 커밋될 때 데이터베이스에 저장. insert
// EntityManager는 매개변수로 넘어온 member의 id값이 이미 존재하면 update, 아니면 insert
entityManager.persist(member);
return member;
}
@Override
public Optional<Member> findById(int id) {
//find메서드는 pk를 매개변수로 준다.
Member member = entityManager.find(Member.class, id); //Member 테이블 가서 id 값 가지는 member 찾음
//그 외 컬럼으로 조회할 때는 select m from Member m where m.name = :name => 아래 findByName() 메서드
return Optional.ofNullable(member);
}
@Override
public void delete(Member member) {
//remove 메서드 사용
//update의 경우 merge 메서드 사용.
}
//pk아닌 컬럼으로 조회
public List<Member> findByName(String name){
List<Member> members = entityManager.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name).getResultList();
return members;
}
@Override
//따로 제공하는 문법이 없어 직접 만들어야 함.
public List<Member> findAll() {
//jpql : jpa의 쿼리문법
//장점 : DB에 따라 문법이 달라지지 않는 객체지향 언어, 컴파일 타임에서 check⭐!!
//단점 : DB 고유의 기능과 성능을 극대화하기는 어려움.
List<Member> members = entityManager.createQuery("select m from Member m", Member.class).getResultList();
return members;
}
}
⭐ ⭐ ⭐ ⭐ Transactional ⭐ ⭐ ⭐ ⭐
순수 JPA는 persist()에서 commit을 보장하지 않는다. => commit 필요 ==> @Transactional 처리 필요
SpringDataJpa는 @Transactional 어노테이션 없어도 commit 보장
💡 SpringDataJpa에서 Transactional 어노테이션이 필요한 상황
→ 전형적인 트랜잭션 작동이 필요한 상황 ex) save(); 예외; save();
→ 중간에 예외 발생했을 때 rollback 처리 하기위해 필요
따라서 MemberService.java 클래스 단에 @Transactional 어노테이션을 붙여,
아래 비즈니스 로직에서 예외가 발생했을 때 이전 상태로 돌아가거나 성공했을 때 DB에 저장해야 한다.
==> 원자성 보장 !
<순수 Jpa에서 commit 용도로 @Transactional이 사용될 경우 테스트>
@Transactional이 있는 경우
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(JpaMemberRepository jpaMemberRepository) {
this.memberRepository = jpaMemberRepository;
}
@Transactional //⭐⭐
public void createMember(MemberRequestDto memberRequestDto) throws IllegalArgumentException {
Member member = new Member(memberRequestDto.getName(), memberRequestDto.getEmail(), memberRequestDto.getPassword());
memberRepository.save(member);
}
}
커밋 완료 ⭐
@Transactional이 없는 경우
커밋 실패 ⭐
<SpringDataJpa에서 롤백 용도로 @Transactional이 사용될 경우 테스트>
@Transactional이 있는 경우
@Service
@Transactional
public class MemberService {
...
public void createMember(MemberRequestDto memberRequestDto){
//Transaction 테스트
Member member = new Member(memberRequestDto.getName(), memberRequestDto.getEmail(), memberRequestDto.getPassword());
memberRepository.save(member);
if(member.getName().equals("kim")){
throw new IllegalArgumentException();
}
}
}
에러가 나서 데이터가 저장되지 않아야 하는 상황 => 데이터 삽입 X, 롤백 성공⭐
@Transactional이 없는 경우
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(SpringDataJpaMemberRepository springDataJpaMemberRepository) {
this.memberRepository = springDataJpaMemberRepository;
}
// 사용자의 입력 값이 담긴 DTO를 통해, 실제 시스템에서 사용되는 정보를 조합해 Member 객체로 변환 후 저장
public void createMember(MemberRequestDto memberRequestDto){
//Transaction 테스트
Member member = new Member(memberRequestDto.getName(), memberRequestDto.getEmail(), memberRequestDto.getPassword());
memberRepository.save(member);
if(member.getName().equals("kim")){
throw new IllegalArgumentException();
}
}
}
@Transactional을 붙이지 않는다면 에러가 발생하면 들어가지 말아야 할 값들이 DB에 저장된다 !
에러가 나서 데이터가 저장되지 않아야 하는 상황 => 데이터 삽입 O, 롤백 실패⭐
'Back-End 공부 > Spring' 카테고리의 다른 글
스프링 HTML 화면없이 PostMan으로 테스트하기 (0) | 2024.01.18 |
---|---|
스프링 MVC - 기본 CRUD 만들기 프로젝트 (0) | 2024.01.17 |
스프링 용어 쉽게 정리하기 (빈, 싱글톤, DI, 제어의 역전, IoC 컨테이너) (0) | 2024.01.16 |
Controller, Service, Repository에 Dto 객체 추가해 실습하기 (1) | 2024.01.15 |
application.properties와 application.yml 환경 설정 변경 (1) | 2024.01.15 |