Back-End/JPA

[Spring] JPA를 사용한 MySQL 연동(생성/조회) 예제

유자맛바나나 2021. 7. 19. 02:45

★MySQL이 설치되지 않은 경우: MySQL 설치 및 시작

2021.07.13 - [Back-End/Database] - [MySQL] 설치 및 시작(MacOS 환경, DBeaver)

 

[MySQL] MySQL 설치 및 시작(MacOS 환경, DBeaver)

1. 터미널 실행 후 아래를 입력해 MySQL 설치 brew install mysql 2. 설치 완료 후 설정 We've installed your MySQL database without a root password. To secure it run: mysql_secure_installation → 보안 설..

citronbanana.tistory.com

 

[중요]

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