[Spring Boot Tutorial] 2. MySQL + JPA 설정

2019. 9. 9. 17:35Spring/Spring Boot Tutorial

반응형

JPA

Java Persistent API

이전 포스팅 확인하기 : [Spring Boot Tutorial] 1. spring boot 시작하기 + 초기세팅

JPA는 Java ORM기술에 대한 API 표준 명세입니다.
JPA를 구현한 ORM 프레임워크에는 Hibernate, EclipseLink, DataNucleus 등이 있습니다.

ORM은 entity 객체(object)와 database를 매핑하여 SQL 쿼리가 아닌 메서드를 통해 데이터를 조작할 수 있게 합니다.
RDBMS의 데이터 read/write를 object를 이용하여 read/write할 수 있도록 구현한 것으로, DB의 record하나를 읽는 것이 object하나를 읽는 형태로 구현한 것이라고 보면 됩니다.

단, query가 복잡해질 경우 ORM으로 표현하는 것에는 한계가 존재하고, 성능이 raw query에 비해 느려질 수 있습니다.


이런 문제들을 해소해줍니다.

  • table간의 관계를 자바 Object간의 참조관계로 표현하기 어려웠던 부분
  • Mapper 방식으로 처리할 때 이용될 DTO를 대량 만들어야 했던 부분
  • 단순한 입력, 조회 기능의 SQL을 계속해서 만들어야 했던 부분
  • 테이블 컬럼과 자바 객체의 프로퍼티와 매핑하는 기계적인 작업을 많이 해야 했던 부분

※ 단, 복잡한 통계와 같은 연산을 ORM을 통해 나타내는 것에는 한계가 있기 때문에 필요에 따라 JPA와 MyBatis를 함께 사용하는 것이 좋습니다.


그럼, JPA를 이용하여 MySQL DB의 record를 조회 방법을 알아봅시다.


  1. JPA 이론
    1. Entity란?
    2. EntityManager를 이용한 데이터 조작 방법
    3. JpaRepository 상속 인터페이스를 이용한 데이터 조작 방법
    4. 메서드 생성 키워드
  2. JPA와 MySQL을 이용하기 위한 Dependency 설정
  3. MySQL datasource 설정
  4. Entity 클래스 생성
  5. DB access를 위한 Repository 인터페이스 생성하기
  6. 서비스 단위 구성
  7. controller 정의 및 url 접속

1. JPA 이론

1.1. Entity란 ?

Entity는 DB에 저장되어있는 데이터를 메모리상의 자바 객체 instance 형태로 매핑한 것입니다.
Entity는 EntityManager라는 인터페이스를 통해 DB와 동기화 됩니다.

웹 애플리케이션에서 JPA를 이용하여 DB 데이터에 접근하기위해서는 반드시 EntityManager를 이용해야 합니다.

@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
    @Qualifier("dataSource") DataSource primaryDataSource,
    @Qualifier("jpaProperties") JpaProperties jpaProperties) {
  return builder
      .dataSource(primaryDataSource)
      .properties(jpaProperties.getProperties())
      .packages("me.jiniworld.demo.models.entities")
      .persistenceUnit("default")
      .build();
}

EntityManager는 EntityMangerFactory bean에 설정된 dataSource를 이용하여 생성되며, EntityMangerFactory bean에는 유니크한 persistenceUnit 이름을 지어줍니다. (위의 코드에서는 persistenceUnit이름을 default라고 설정했습니다.)

@PersistenceContext(unitName = "default")
private EntityManager entityManager;

서비스 class에서 private 필드로 EntityManager를 설정할 때, @PersistenceContext 애너테이션을 이용하여 persistenceUnit을 설정할 수 있습니다.
unitName을 생략할 경우, @Primary 설정된 EntityManagerFactory 빈을 통해 생성된 EntityManager가 매핑됩니다.

이 EntityManager를 이용하여 PersistenceContext(영속성 컨텍스트)의 Entity를 취득할 수 있고, 생성자를 통해 새로 생성한 Entity를 등록할 수 있습니다.

@Transactional 설정된 서비스 메서드의 종료시, PersistenceContext에 축적되어있던 변경정보가 DB에 반영됩니다.
한마디로, PersistenceContext가 DB의 Cache 역할을 한다고 보면 됩니다.

※ 참고로 PersistenceContext는 트랙잭션마다 준비되는 것으로, 같은 트랜잭션에서만 공유됩니다.


1.2. EntityManager를 이용한 데이터 조작

EntityManager를 이용하여 DB에 데이터를 insert하고 select하는 기본 동작을 알아봅시다.

@PersistenceContext
private EntityManager entityManager;

...
Long userId = 11L, cardId = 1000L, storeId = 2L;
Order order = new Order(userId, cardId, storeId);
entityManager.persist(order);   // 저장

entityManager.find(Order.class, 1L);    // 찾기

persist 메서드를 이용하여 user_id=11, card_id=1000, store_id=2인 레코드를 저장하고 (ln 7)
find 메서드로 PK가 1인 order 레코드를 조회합니다. (ln 9)

PK로 지정된 컬럼 이외의 컬럼을 이용하여 조회를 하고 싶다면 JPQL이나 Creteria Query를 이용하여 조회 쿼리를 실행해야합니다.

String query = "SELECT o FROM order_info o WHERE o.status = :status AND o.type = :type";
String status = "Y", type = "1";
List<Order> orders = entityManager.createQuery(query, Order.class)
  .setParameter("status", status)
  .setParameter("type", type).getResultList();

위의 코드는 JPQL을 이용한 조회 예시 코드입니다.
쿼리 자체가 복잡한 것은 아니나, 위와 같은 간단한 조회쿼리를 작성하기 위해 긴 코드를 작성해야한다는 번거로움이 있습니다.
또, query 내에 오타가 있을 경우, 컴파일시에는 오류를 잡아낼 수 없다는 단점도 있습니다.

이런 번거로움을 해소하기 위해 만들어진 것이 Spring Data JPA 입니다.


1.3. JpaRepository 상속 인터페이스를 이용한 데이터 조작

Spring Data JPA 프로젝트는 DB CRUD의 공통적 기능을 JpaRepository라는 인터페이스로 제공하며,
이 인터페이스를 상속하여 Repository 인터페이스를 만들 경우 기계적으로 생성해야했던 jpql을 대신하여 동적으로 생성해 줍니다.

EntityManager를 이용하여 직접 조회하는 위의 코드를 Spring Data JPA를 이용하여 표현하면 아래와 같습니다.

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
  List<Order> findAllByStatusAndType(String status, String type);
}
@Autowired
private OrderRepository orderRepository;

String status = "Y", type = "1";
List<Order> orders = orderRepository.findAllByStatusAndType(status, type);

JpaRepository를 상속한 인터페이스에서 메서드를 선언할 때에 JpaRepository의 구현 클래스가 자동으로 jpql을 생성하도록 하려면 메서드이름을 규칙에 맞춰 생성해야 합니다.
Spring Data JPA에서 인식할 수 있는 키워드를 이용하여 메서드를 만들어 봅시다.

키워드 규칙은 아래를 참고해주세요.


1.4. 메서드 생성 키워드

  • And
    • findByNameAndType
    • select e from #{#entityName} e where e.name = ?1 and e.type = ?2
  • Or
    • findByNameOrType
    • select e from #{#entityName} e where e.name = ?1 or e.type = ?2
  • Is, Equals
    • findByType
    • findByTypeIs
    • findByTypeEquals
    • select e from #{#entityName} e where e.type = ?1
  • Between
    • findByOpenDateBetween
    • select e from #{#entityName} e where e.openDate between ?1 and ?2
  • LessThan
    • findByAgeLessThan
    • select e from #{#entityName} e where e.age < ?1
  • LessThanEqual
    • findByAgeLessThanEqual
    • select e from #{#entityName} e where e.age <= ?1
  • GreaterThan
    • findByAgeGreaterThan
    • select e from #{#entityName} e where e.age > ?1
  • GreaterThanEqual
    • findByAgeGreaterThanEqual
    • select e from #{#entityName} e where e.age >= ?1
  • After
    • findByOpenDateAfter
    • select e from #{#entityName} e where e.openDate > ?1
  • Before
    • findByOpenDateBefore
    • select e from #{#entityName} e where e.openDate < ?1
  • IsNull, Null
    • findByAge(Is)Null
    • select e from #{#entityName} e where e.age is null
  • IsNotNull, NotNull
    • findByAge(Is)NotNull
    • select e from #{#entityName} e where e.age not null
  • Like
    • findByTypeLike
    • select e from #{#entityName} e where e.type like ?1
  • NotLike
    • findByTypeNotLike
    • select e from #{#entityName} e where e.type not like ?1
  • StartingWith
    • findByTypeStartingWith
    • select e from #{#entityName} e where e.type like %?1
  • EndingWith
    • findByTypeEndingWith
    • select e from #{#entityName} e where e.type like ?1%
  • Containing
    • findByTypeContaining
    • select e from #{#entityName} e where e.type like %?1%
  • OrderBy
    • findByAgeOrderByNameDesc
    • select e from #{#entityName} e where e.age = ?1 order by e.name desc
  • Not
    • findByNameNot
    • select e from #{#entityName} e where e.name <> ?1
  • In
    • findByAgeIn(Collection ages)
    • select e from #{#entityName} e where e.age in ?1
  • NotIn
    • findByAgeNotIn(Collection ages)
    • select e from #{#entityName} e where e.age not in ?1
  • True
    • findByActiveTrue()
    • select e from #{#entityName} e where e.active = true
  • False
    • findByActiveFalse()
    • select e from #{#entityName} e where e.active = false
  • IgnoreCase
    • findByTypeIgnoreCase
    • select e from #{#entityName} e where UPPER(e.type) = UPPER(?1)

2. dependency 추가

JPA와 MySQL을 이용하기 위해 pom.xml에 jpa 스타터폼과 MySQL Driver 의존 라이브러리를 추가합니다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>

20

dependency를 수정했으니 maven 다운로드를 위해 Maven Update를 합니다.


의존 라이브러리를 추가한 후, 그대로 부트앱을 실행할 경우 아래와 같은 에러가 날것입니다.

21

의존 라이브러리만 추가했을 뿐인데 왜 에러가 났을까요.
DataSource url 구성 실패라는 에러메시지가 출력되었습니다.
이 에러메시지는 spring-boot-starter-data-jpa 라이브러리를 추가한 후, db설정을 하지 않아서 발생한 문제입니다.

그럼 MySQL DB datasource 설정을 해봅시다.


3. MySQL datasource 설정

spring boot에서는 프로퍼티의 예약된 key에 dataSource 정보를 설정할 경우 자동으로 datasource 빈을 생성해줍니다.

아래는 datasource 설정을 하지 않은 application.yml 파일입니다.

spring:
  application:
    name: demo
  profiles:
    active: local
server:
  port: 8989

spring boot에서 미리 약속된 datasource와 jpa옵션에 관한 키는 spring.datasource, spring.jpa입니다.

아래는 Spring Boot 2 버전의 default connection pool 인 HikariCP를 이용한 datasource 및 jpa 옵션 설정에 대한 코드입니다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: "jdbc:mysql://localhost:3306/jiniworld?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Seoul"
    username: test
    password: test   
    hikari:
      auto-commit: false
      connection-test-query: SELECT 1
      minimum-idle: 10
      maximum-pool-size: 50
      transaction-isolation: TRANSACTION_READ_UNCOMMITTED
      pool-name: pool-jiniworld
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    properties:
      hibernate:
        format_sql: true
    generate-ddl: false
    open-in-view: false

  • spring.datasource
    • DataSource 자동 구성을 설정하는 곳입니다.
    • driver-class-name
      • JDBC 드라이버 이름
    • url
      • JDBC URL
    • username
      • Database 로그인 username
    • password
      • Database 로그인 password

  • spring.datasource.hikari
    • DB Connection Pool 일종
    • 그밖에 Common DBCP, Tomcat-JDBC 가 있습니다.
    • auto-commit
      • 자동커밋 여부. (default: true)
    • connection-test-query
      • connection 유효성 검사 쿼리
    • minimum-idle
      • pool에 유지할 유휴 connection 최소 개수
    • maximum-pool-size
      • pool에 유지시킬 수 있는 최대 connection 수
    • transaction-isolation
      • 트랜잭션 격리 수준
    • pool-name
      • connection pool 이름

  • spring.jpa
    • database-platform
      • 접속할 database 설정.
    • generate-ddl
      • 앱 시작시 @Entity로 정의한 테이블의 create 문 실행
    • properties.hibernate.format_sql
      • true로 설정할 시 sql 출력문을 보기 좋게 출력해줍니다.
      • false로 설정할 경우 SQL이 한줄로 길게 출력됩니다.
    • open-in-view
      • 템플릿 view 화면의 렌더링이 끝날 때 까지 Lazy fetch 가 가능하도록 해주는 속성
      • default는 true로, 매우 유용한 기능이지만 기능적 면에서 false로 설정해주는 것이 좋습니다.

프로퍼티에 datasource 설정을 완료한 후, 다시 웹 애플리케이션을 실행해봅니다.

22

datasource 설정을 추가한 후 앱을 실행하자 정상적으로 앱이 실행됩니다.
jpa 가 정상적으로 설정되어 persistenceUnitInfo name default도 출력됨을 확인할 수 있었습니다.

참고로, JavaConfig 또는 xml 설정없이 프로퍼티만으로 dataSource 빈을 구성할 경우, persistenceUnit 이름은 default로 설정됩니다.


참고로 multiple datasource 빈을 생성해야할 경우에는, 프로퍼티만으로 생성할 수 없습니다.
이 경우, JavaConfig 또는 xml을 통한 dataSource 빈 생성을 해야합니다.

※ JavaConfig를 이용한 datasource 설정방법은 JavaConfig로 Datasource 설정하기를 참고해주세요.


4. Entity 클래스 생성

Entity는 DB에 영속적으로 저장된 record를 Java 객체로 매핑한 것이라고 했습니다.
각 엔티티 인스턴스를 구분짓기 위해 특별한 구분자가 필수적으로 필요합니다.
때문에, Entity 클래스에는 PK 설정된 @Id 가 반드시 설정되어야 합니다.

@Getter @Setter
@Entity
@Table(name = "user")
@DynamicUpdate
public class User implements Serializable {

	private static final long serialVersionUID = -563329217866858622L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(updatable = false, nullable = false, columnDefinition = "INT(11)")
	private Long id;

	@Column(nullable = false, length = 1, columnDefinition = "CHAR(1) DEFAULT '0'")
	private String type;

	@Column(nullable = false, unique = true, length = 100)
	private String email;

	@Column(nullable = false, length = 50)
	private String name;

	@Column(nullable = false, length = 1, columnDefinition = "CHAR(1) DEFAULT '1'")
	private String sex;

	@Column(nullable = false, length = 6)
	private String birthDate;

	@Column(nullable = false, length = 20)
	private String phoneNumber;

	@Column(nullable = false, length = 150)
	private String password;

	@Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0", length = 1)
	private boolean del;

	@Temporal(TemporalType.TIMESTAMP)
	@Column(updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
	private Date createTimestamp;

	@Temporal(TemporalType.TIMESTAMP)
	@Column(nullable = true, columnDefinition = "TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")
	protected Date updateTimestamp;

	@PrePersist
	protected void onCreate() {
		createTimestamp = Timestamp.valueOf(LocalDateTime.now());
	}

	@PreUpdate
	protected void onUpdate() {
		updateTimestamp = Timestamp.valueOf(LocalDateTime.now());
	}

}
  • @Id

    • PK 키임을 명시합니다. (ln 9)
    • Id 생성 규칙을 IDENTITY로 설정하면 키생성을 database에게 위임합니다.
    • 엔티티 저장시 id를 별도로 설정하지 않으면 MySQL의 AUTO_INCREMENT를 이용하여 자동으로 PK가 생성됩니다.(ln 10)
  • @Column

    • spring.jpa.generate-ddl 를 true로 설정할 경우 @Column에 정의한 행태로 테이블이 생성됩니다.
    • nullable, length, unique 등을 설정할 수 있고 DEFAULT 값 설정과 같은 상세 설정은 columnDefinition으로 설정할 수 있습니다.
    • length는 String 값에만 설정할 수 있는 기능입니다.
  • @PrePersist

    • entity의 값이 persist 영역으로 넘어가기 전에(=DB에 저장되기 전에) 행해져야할 기능을 정의합니다.
  • @PreUpdate

    • entity값이 update 되기 전에 행해져야할 기능을 정의합니다.

Entity 클래스에는 앱 전체에서 공통적으로 이용할 테이블 설정을 추가하면 됩니다.

application.yml 의 spring.jpa.generate-ddl 속성을 true로 설정하면 entity 클래스 명세에 따라 테이블 ddl이 자동으로 실행됩니다.

generate-ddl 속성은 true(웹애플리케이션 시작시 기존 테이블을 모두 제거후 create 실행)와 false(아무동작 안함) 두가지만 설정할 수 있는것에 반해,
hibernate의 ddl 옵션을 이용하면 더 다양한 기능이 가능합니다.

  • spring.jpa.properties.hibernate.hbm2ddl.auto
    • none : 아무 동작 하지 않습니다.
    • create-only : 테이블이 없을 경우 create합니다.
    • drop : 테이블을 drop 합니다.
    • create : 기존에 테이블이 존재할 경우 테이블 drop후 새로 create합니다.
    • create-drop : 앱 실행시 테이블 create하고 앱 종료시 테이블을 drop합니다.
    • validate : 엔티티 설정과 기존 테이블 설정이 다를 경우 에러 발생합니다.
    • update : 테이블, 컬럼정보가 달라졌을 경우 추가됩니다.

아래는 jpa의 ddl 옵션을 이용하여 자동으로 생성된 user 테이블 정보입니다.

CREATE TABLE `user` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `create_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `del` TINYINT(1) NOT NULL DEFAULT '0',
  `email` VARCHAR(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `password` VARCHAR(150) COLLATE utf8mb4_unicode_ci NOT NULL,
  `type` CHAR(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0',
  `sex` CHAR(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1',
  `phone_number` VARCHAR(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `birth_date` VARCHAR(6) COLLATE utf8mb4_unicode_ci DEFAULT NULL
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_ob8kqyqqgmefl0aco34akdtpe` (`email`)
);
insert into `user` (`id`, `create_timestamp`, `del`, `email`, `name`, `password`, `type`, `sex`, `phone_number`, `birth_date`, `update_timestamp`)
values('2','2019-09-09 17:04:32','0','coco','코코','$2a$10$hZV0QkLlu8KE7x0XrGl3zuUYWQE0v7oWtBUMo7/DsgrsQUZgG1rDC','1','1','01012341234','930901','2019-05-08 13:53:57');

조회 테스트틀 위해 레코드 하나를 미리 insert 해둡니다.


5. Repository 인터페이스 생성하기

이제, 이 Entity를 이용하여 DB 데이터를 access 하는 방법을 알아봅시다.

JpaRepository인터페이스를 상속한 인터페이스를 만듭니다.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
  Optional<User> findById(Long id);
  List<User> findAllByNameContains(String name);
}

JpaRepository<T, ID> 에서 T에는 조회하고자 하는 엔티티 클래스를, ID에는 PK타입을 입력합니다.

select * from user where id = ?1;
select * from user where name = %?1%;

위의 메서드는 내부적으로 위와 같은 jpql을 생성합니다.

메서드이름에 대한 규칙에 대해서는 1-4) 메서드 생성 키워드를 확인해주세요.


6. Service 클래스 생성

위에서 작성한 JpaRepository 구현인터페이스를 이용할 서비스클래스를 생성합니다.

서비스 클래스에서는 하나의 서비스 단위로 commit, rollback 되어야할 기능들을 정의하는 곳입니다.

@Service
public class UserService {

  private final UserRepository userRepository;

  @Autowired
  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @Transactional(readOnly = true)
  public Optional<User> findById(Long id) {
    return userRepository.findById(id);
  }
}

조회 기능은 트랜젝션이 일어날 필요가 없기 때문에 메서드에 명시적으로 @Transactional(readOnly = true) 설정을 하여 데이터 변경의 미연을 방지합니다.


7. Controller 정의 및 url 접속

@RestController 애너테이션으로 컨트롤러를 등록합니다.
뷰페이지를 이용하지않고 json과 같은 데이터로 응답받고자 할때 정의합니다.

@RestContoller = @Controller + @ResponseBody


@RequiredArgsConstructor
@RequestMapping(value = "/users", produces = {MediaType.APPLICATION_JSON_VALUE})
@RestController
public class UserController {

	private final UserService userService;

	@GetMapping("/{id}")
	public Map<String, Object> findById(@PathVariable("id") long id) {
		Map<String, Object> response = new HashMap<>();

		Optional<User> oUser = userService.findById(id);
		if(oUser.isPresent()) {
			response.put("result", "SUCCESS");
			response.put("user", oUser.get());
		} else {
			response.put("result", "FAIL");
			response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
		}

		return response;
	}
}

api prefix를 "/users", response 타입을 "application/json"로 설정합니다.(ln 2)

Optional 기능을 이용하여 Entity가 존재할 경우와 존재하지 않을 경우에 대한 분기처리를 합니다.

Entity, Repository, Service, Controller 설정을 모두 마쳤다면 Build 한 후 앱을 실행시킵니다.


23

그리고 user를 조회해 봅니다.
db에 저장된 값이 정상적으로 출력됩니다.

728x90
반응형