★MySQL이 설치되지 않은 경우: MySQL 설치 및 시작
2021.07.13 - [Back-End/Database] - [MySQL] 설치 및 시작(MacOS 환경, DBeaver)
[중요]
MySQL이 설치되었더라도 외부접속이 허용된 User를 생성해야 한다. 위 글의 5.4.2를 참고하여 외부 접속이 허용된 User를 만들고 오자
환경 설정
1. Gradle 의존 라이브러리 추가: Spring Boot JPA, MySQL
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java'
}
build.gradle 파일에 위와 같이 두 개의 라이브러리를 추가한다.
2. application.properties 설정: DB 접속정보, JPA 관련 설정
# API Call시 JPA가 날리는 SQL 문을 콘솔에 출력
spring.jpa.show-sql=true
# JPA가 테이블을 생성하는 등의 DDL까지 자동으로 하는 기능
spring.jpa.hibernate.ddl-auto=none #테이블이 생성되어 있다고 가정하고 none 설정
# MySQL 사용
spring.jpa.database=mysql
# MySQL 접속정보 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #MySQL 드라이버 사용
spring.datasource.url=jdbc:mysql://localhost:3306/[DB명 입력]?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=[DB User 이름]
spring.datasource.password=[DB User Password]
# MySQL 플랫폼 지정
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
JPA를 사용해 DB(MySQL) 연동 예제
1. Table 생성: user
id(PK), name을 컬럼으로 갖는 테이블을 생성한다
2. Entity 클래스 생성: User
package com.hyppeople.hyppeople.domain;
import javax.persistence.*;
import java.sql.Timestamp;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter @Setter
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
}
- @Entity
JPA가 관리하는 Entity라는 뜻 - @Id
Primary Key라는 뜻 - @GeneratedValue(strategy = GenerationType.IDENTITY)
DB가 자동으로 value를 생성해줌. 이를 Identity 전략이라 함 - @Column(name = "id")
테이블의 컬럼명을 써준다. 이를 이용해 Mapping 함
3. Repository 클래스 생성: JpaUserRepository
Jpa를 이용해 DB를 핸들링하는 Repository 클래스를 생성한다
3.1. UserRepository 인터페이스
package com.hyppeople.hyppeople.repository;
import com.hyppeople.hyppeople.domain.User;
import java.util.List;
import java.util.Optional;
public interface UserRepository {
User save(User user);
Optional<User> findById(Long id); // Optional: find 결과가 없을때 null값을 Optional로 감싸서 return
Optional<User> findByName(String name);
List<User> findAll();
}
3.2. JpaUserRepository: 구현체
package com.hyppeople.hyppeople.repository;
import com.hyppeople.hyppeople.domain.User;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
@Repository
@Qualifier("jpaUserRepos")
public class JpaUserRepository implements UserRepository {
private final EntityManager em;
@Autowired
public JpaUserRepository(EntityManager em) {
this.em = em;
}
@Override
public User save(User user) {
em.persist(user);
return user;
}
@Override
public Optional<User> findById(Long id) {
User user = em.find(User.class, id);
return Optional.ofNullable(user);
}
@Override
public Optional<User> findByName(String name) {
List<User> result = em.createQuery("select m from User as m where m.nickName = :nickname", User.class)
.setParameter("nickname", nickName)
.getResultList();
return result.stream().findAny();
}
@Override
public List<User> findAll() {
return em.createQuery("select m from User as m", User.class)
.getResultList();
}
}
- @Repository
JpaUserRepository를 Bean으로 등록하고, Repository 역할임을 명시한다. - @Qualifier("jpaUserRepos")
의존성 주입시 Bean으로 등록된 MemberRepository의 여러 구현체 중 JpaMemberRepository 객체임을 구분할 수 있도록 명시한다. - EntityManager
JPA는 EntityManager라는 것을 통해 동작한다. build.gradle에서 추가한 'org.springframework.boot:spring-boot-starter-data-jpa'를 통해 Spring Boot가 자동으로 EntityManager를 생성해주고, 생성자를 통해 주입 받는다(@Autowired) - save(User user) 메서드
em.persist(user); // 이 한 줄로 JPA가 Insert 쿼리를 만들어서 테이블에 넣어준다 - findByUserId(Long id) 메서드
User user = em.find(User.class, id); // 이 한 줄로 JPA가 Select 쿼리를 만들어서 실행 후 반환해준다 - findByName(String name) 메서드
em.createQuery()는 JPQL이라는 객체지향 쿼리를 사용하는 것이다. 테이블이 아닌 객체(Member)를 대상으로 쿼리를 하는 것이며 자동으로 sql로 번역해준다.
[참고] JPQL
- Java Persistence Query Language
- 테이블이 아닌 객체(Member)를 대상으로 쿼리를 하는 것이며 자동으로 sql로 번역해줌
- [문법 주의사항]
- 중요1. from절에 들어가는 것은 객체 User다
- 중요2. 테이블명이 아닌 엔티티명을 사용한다. 즉, @Entity 어노테이션이 붙은 User Class를 사용한다.
- 중요3. 별칭은 필수이다. from User as m (as 생략 가능)
- 중요4. 엔티티와 속성은 대소문자를 구분한다. 즉, User, name 등 대소문자를 구분해줘야 한다
- 중요5. JPQL 키워드는 대소문자 구분 안한다
[참고] SpringConfig 클래스를 이용해 JpaUserRespository를 Bean에 등록하기
1. JpaUserRepository에서 Bean 등록 어노테이션(@Repository)과 @Autowired를 제거한다
package com.hyppeople.hyppeople.repository;
import com.hyppeople.hyppeople.domain.User;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
// @Repository
// @Qualifier("jpaUserRepos")
public class JpaUserRepository implements UserRepository {
private final EntityManager em;
// @Autowired
public JpaUserRepository(EntityManager em) {
this.em = em;
}
...
}
SpringConfig에서 해당 생성자를 이용해 의존성을 주입할 것이기 때문에 EntityManager를 받는 생성자는 제거하면 안된다
2. @Configuration 어노테이션을 사용한 SpringConfig.java에서 Bean 등록과 EntityManger를 주입해준다.
package com.hyppeople.hypeople;
import com.hyppeople.hyppeople.repository.JpaUserRepository;
import com.hyppeople.hyppeople.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class SpringConfig {
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
@Bean
@Qualifier("jpaUserRepos")
public UserRepository userRepository() {
return new JpaUserRepository(em);
}
}
4. Service 클래스 생성: UserService
UserRepository를 주입 받아 비즈니스 로직을 수행할 Client인 Service 클래스를 생성한다.
package com.hyppeople.hyppeople.service;
import com.hyppeople.hyppeople.domain.User;
import com.hyppeople.hyppeople.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class UserService {
private UserRepository userRepository;
@Autowired
public UserService(@Qualifier("jpaUserRepos") UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* 회원 가입
*/
public Long join(User user) {
validateDuplicateUser(user); // 중복 회원 검증
userRepository.save(user);
return user.getId();
}
private void validateDuplicateUser(User user) {
userRepository.findByName(user.getName())
.ifPresent(u -> { // ifPresent: 값이 존재한다면
throw new IllegalStateException("이미 존재하는 회원입니다");
});
}
/**
* 전체 회원 조회
*/
public List<User> findUsers() {
return userRepository.findAll();
}
public Optional<User> findOne(Long id) {
return userRepository.findById(id);
}
}
- @Service
UserService를 Bean으로 등록하고, Service 역할임을 명시한다. - @Transactional
DB에 데이터를 저장할 때 @Transactional 어노테이션이 있어야 한다 - 생성자: UserService(@Qualifier("jpaUserRepos") UserRepository userRepository)
this.userRepository로 주입될 의존성이 3.2에서 생성한 JpaUserRepository임을 알려주도록 @Qualifier("jpaUserRepos")를 입력한다. - UserRepository 클래스에서 JPA를 활용해 구현한 메서드로 회원 가입, 회원 조회 기능을 구현하였다.
5. Spring Data JPA로 리팩토링
Spring Data JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다.
5.1. SpringDataUserRepository 생성
package com.hypeople.hypeople.repository;
import com.hyppeople.hyppeople.domain.User;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
@Qualifier("springDataRepos")
public interface SpringDataUserRepository extends JpaRepository<User, Long>, UserRepository {
@Override
Optional<User> findByNickName(String nickName);
}
- extends JpaRepository<User, Long>
User Entity와 Mapping 시켜줌 - JpaRepository를 상속받으면 Spring이 SpringDataUserRepository의 구현체를 자동으로 만들어 Spring Bean에 등록해준다.
따라서 @Component, @Repository가 없어도 문제없다.
5.2. SpringConfig에서 SpringDataUserRepository 구현체 userRepository에 DI
package com.hyppeople.hypeople;
import com.hyppeople.hyppeople.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class SpringConfig {
private final UserRepository userRepository;
@Autowired
public SpringConfig(@Qualifier("springDataRepos") UserRepository userRepository) {
this.userRepository = userRepository;
}
}
SpringDataUserRepository가 JpaRepository를 상속 받았기 때문에 Spring이 구현체를 만들어 Bean으로 등록하였고, SpringConfig를 통해 주입해준다.
5.3. Service 클래스의 의존 객체 수정
@Service
@Transactional
public class UserService {
private UserRepository userRepository;
@Autowired
public UserService(@Qualifier("springDataRepos") UserRepository userRepository) {
this.userRepository = userRepository;
}
...
}
5.1에서 @Qualifier("springDataRepos")와 같이 정의했으므로 UserRepository를 주입받을 UserService 클래스에서도 마찬가지로 @Qualifier를 수정해준다.
5.4. SpringDatJpa 리팩토링 결론
지금까지 Interface만 정의하고 그 외에는 Bean 등록, 의존 객체 변경등만 해주었다. 나머지는 Spring이 구현체를 만들어주기 때문에 실제 쿼리를 작성하지 않아도 사용할 수 있어 매우 편리해졌다.
[의문]
몇가지 테스트를 해본 결과 findByNickName을 call할 때 Spring이 자체적으로 findByNickName 메서드를 찾게 되는데,
먼저 SpringDataMemberRepository 인터페이스에서 찾고, 없다면 JpaRepository, MemberRepository에 있는지 찾는다. 그런데.. findByNickName이 JpaRepository에는 없으므로 디버그를 통해 advised - methodCache에 MemberRepository에서 찾는 것으로 나오는데.. 의문인것은 findByNickName 메서드가 'nickName을 기준으로 find'한다는 것을 어떻게 알 수 있는것인가? findByNickName이 delete를 하는걸로 판단하고 생성될수도 있지 않나?
참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.
'Back-End > JPA' 카테고리의 다른 글
[JPA] EntityManager의 flush와 @Transactional (0) | 2022.04.20 |
---|