본문 바로가기

Programming/Spring

[Spring-boot] Facebook Timeline 프로젝트 : MySQL, JPA 개발 환경 구성하기

반응형

1. Spring boot 2.1.6/ Gradle project에서 프로젝트의 디렉토리 구조

directory structure of Timeline project

디렉토리 내의 패키지, 파일 명은 개발 방향에 따라 많이 바뀌게 되겠지만, 전반적인 프로젝트의 구조는 다음과 같이 구성할 예정이다. 설정 값을 넣어줄 때에 있어서 application.properties 보다 application.yml이 확장성을 고려하면 더 편리하다는 의견이 많았기 때문에 이 파일도 변경해주었다. 

비스니스 로직은 service 패키지에 들어갈 예정이며 컨트롤러는 web 패키지에 들어갈 것이다. 이번 프로젝트를 하며 스프링 부트의 구조를 좀 더 확실히 알고자하는 취지도 있었기 때문에 패키지를 역할별로 엄격히 나눌 예정이다.

 

 

2. MySQL, JPA 연동하기 

/build.gradle

 

plugins {
    id 'org.springframework.boot' version '2.1.6.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.webapp'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-web-services'
    implementation 'org.springframework.session:spring-session-jdbc'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.3.1'
}

 

gradle 프로젝트는 build.gradle 파일 dependencies에 다음과 같은 속성들을 추가해준다. 버전은 2019년 7월 11일 기준 가장 최신 버전을 채택했다. 

HikariCP는 Spring Boot 2.x부터 기본 커넥션 풀 라이브러리로 채택되었을 정도로 현존 최고 성능을 자랑한다고 한다. MySQL을 사용하고자 하기 떄문에 다음과 같은 라이브러리들을 추가해주었다. 

 

 

src/main/resource/application.yml

 

spring:
  datasource:
    localdb:
      jdbc-url: jdbc:mysql://localhost:3306/user?useSSL=false&useUnicode=yes&characterEncoding=utf-8&serverTimezone=UTC
      username: *****oject
      password: ****
      driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    properties:
      hibernate:
        format_sql: true

  session:
    store-type: jdbc
    jdbc:
      initialize-schema: always

logging:
  level:
    org:
      hibernate:
        SQL: DEBUG
        type:
          descriptor:
            sql:
              BasicBinder: TRACE
              

  • 원래는 spring.datasource에 적절히 커넥션 풀 정보를 명시하면 Spring Boot가 자동으로 인식하여 Datasource 빈을 생성해준다. 하지만 한 프로젝트 내에서 2개 이상의 Datasource 빈 설정을 유연히 할 수는 없다. 확장이 불가능한 구조라는 의미이다. 따라서 위와 같이 localdb 라는 이름으로 식별자를 주었다. 
  • logging.level.org.hibernate.SQLDEBUG로 설정하면 로그에 JPA가 실행하는 쿼리문을 출력해주는 역할을 한다. logging.level.org.hibernate.type.descriptor.sql.BasicBinder TRACE 값이면 쿼리문의 파라미터까지 출력해준다. 
  • spring.jpa.properties.hibernate.format_sqlTRUE로 설정해주면 쿼리문을 가독성 좋게 정렬하여 출력해준다.
  • spring.session.store-type 값을 jdbc로 설정해주면 스프링 세션이 저장될 때 jdbc 타입으로 저장된다. 이는 스프링부트 2.x.x 버전 사용자들이 반드시 설정해주어야 하는 값이다. 
  • 열심히 구글링을 하다보니, 사실 datasource를 위한 계정 정보도 암호화한 뒤 오픈소스화 하여야한다는 사실을 알게 되었다. 5-6 line은 서치 후 바뀔 예정이다. 

 

 

JPAConfig 작성하기

src/main/java/com.webapp.timeline/config/JpaConfig.class

 

package com.webapp.timeline.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.*;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class JpaConfig {

    @Primary
    @Bean(name = "dataSource")
    @ConfigurationProperties("spring.datasource.localdb")
    public DataSource dataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("dataSource") DataSource dataSource) {

        return builder.dataSource(dataSource).packages("com.webapp.timeline").build();
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {

        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);

        return transactionManager;
    }
}

 

생성된 DataSource 빈을 이용하여 EntityManagerFactory 빈과 PlatformTransactionManager 빈을 생성하여 트랜잭션 기반의 JPA 개발 환경을 구축하고자 한다.

EntityManagerFactory 빈 : 스프링이 직접 제공하는 컨테이너 관리 EntityManager를 위한 EntityManagerFactory를 만들어준다. 이 방법을 이용하면 JavaEE 서버에 배치하지 않아도 컨테이너에서 동작하는 JPA의 기능을 활용할 수 있을 뿐 아니라, 스프링이 제공하는 일관성있는 데이터 액세스 기술의 접근 방법을 채택할 수 있으며 스프링의 JPA 확장 기능도 활용할 수 있다. Hibernate 기반의 JPA를 사용하기 위해 Datasource와 Hibernate Property, Entity가 위치한 패키지를 지정한다. 

 

 

VO 작성하기 

src/main/java/com.webapp.timeline/domain/Masterkeys.class

 

package com.webapp.timeline.domain;

import javax.persistence.*;

@Entity
@Table(name = "masterkeys")
public class Masterkeys {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int masterId;

    private String userId;

    public Masterkeys(int masterId, String userId) {
        this.masterId = masterId;
        this.userId = userId;
    }

    public Masterkeys() {}

    public void setMasterId(int masterId) {
        this.masterId = masterId;
    }

    public int getMasterId() {
        return masterId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }

    @Override
    public String toString() {
        return "Masterkeys {" +
                "masterId=" + masterId +
                " , userId='" + userId + '\'' + '}';
    }
}

 

DB에서 테이블을 생성할 때에는 masterId에 Auto Increment를 주었기 때문에 처음에는 @GeneratedValue(strategy = GenerationType.AUTO)로 설정하였으나, 에러가 났다. 확인해보니, 아직은 GenerationType.AUTO가 MySQL과 부합하지 않은 부분이 있기 때문에 사용하지 않는 것이 좋다고 한다. 그래서 일단은 IDENTITY로 주었고, 테스트한 결과물로 확인할 수 있겠지만, setter를 통해 다른 값을 넣어주어도 알아서 Auto Increment가 적용되어 1부터 차례로 들어간다. 하지만 아예 masterId 값을 insert하는 수고가 없어도 알아서 increment된 값이 들어가도록 하고 싶다면, JPARepository를 상속한 클래스 내에서 쿼리문을 auto_increment를 적용시키는 방향으로 오버라이드하거나 추가구현 해야 할 것이다.

 

Spring boot - JPA에서 기본키를 매핑하는 방법은 2가지가 있다. 

  • 직접 할당 : 기본키를 어플리케이션에서 직접 할당해주는 방법
  • 자동 생성 : 데이터베이스가 자동으로 할당해주는 방법(ex: Oracle의 sequence, MySQL의 auto_increment)

데이터베이스 벤더마다 sequence, auto_increment 등 기본키를 자동 생성하는 방법은 서로 다른데, Spring Data JPA는 이를 해결하기 위해 4가지 자동생성 방법을 제공한다.

 

기본 키 자동 생성 방법

  • IDENTITY : 기본 키 생성을 데이터베이스에 의존적으로 위임하는 방법. 
  • SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본키를 할당하는 방법. (데이터베이스에 의존적) -> @SequenceGenerator를 사용하여 시퀀스 생성기를 등록하고, 실제 데이터베이스에 생성될 시퀀스 이름을 지정해주어야 한다.
  • TABLE : 키 생성 테이블을 사용하는 방법 -> 키 생성 저용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만드는 방법이다. 데이터베이스 벤더에 상관없이 모든 데이터베이스에 사용이 가능하다.
  • AUTO : 데이터베이스 벤더에 의존하지 않는 방법. 데이터베이스에 따라서 IDENTITY, SEQUENCE, TABLE 방법 중 하나를 자동으로 선택해주는 방법이다. 따라서, 데이터베이스를 변경하여도 코드를 수정할 필요가 없다.

나는 Lombok 라이브러리의 @Data 어노테이션을 사용할 지 포기할 지를 두고 꽤 고민했었다. 하지만 일단 Lombok을 사용하게 된다면 협업하는 사람들에게 반강제적으로 Lombok 라이브러리를 다운받도록 하게 된다는 점이 조금 거부감이 있었고, @Data가 모든 필드에 대해 setter/getter를 만들어주기 때문에 hidden 필드가 어디서나 노출되고 읽기 전용 필드가 언제든 변경가능하게 된다는 점이 조금 크게 다가왔다. 따라서 나는 이 프로젝트에서 @Data 어노테이션을 사용하지 않으려 한다. 

 

 

MasterkeyRepository 작성하기

src/main/java/com.webapp.timeline/repository/MasterkeyRepository

 

package com.webapp.timeline.repository;

import com.webapp.timeline.domain.Masterkeys;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface MasterkeyRepository extends JpaRepository<Masterkeys, Integer> {

}

 

일단 지금은 테스트를 목적으로 하기 때문에, JpaRepository 인터페이스에서 제공하는 메소드만 사용해보자. 타입은 <엔티티 클래스 타입, 엔티티의 @Id 필드의 타입> 으로 준다.

 

 

Main메소드 작성하기

src/main/java/com.webapp.timeline/TimelineApplication.java

 

package com.webapp.timeline;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;


@EntityScan(basePackages = {"com.webapp.timeline.domain"})
@EnableJpaRepositories(basePackages = {"com.webapp.timeline.repository"})
@SpringBootApplication
public class TimelineApplication {

    public static void main(String[] args) {
        try {
            SpringApplication.run(TimelineApplication.class, args);
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }

}

 

@EntityScan : 이 어노테이션으로 엔티티 클래스를 스캔할 곳을 명시적으로 지정한다. main 어플리케이션 패키지 내에 엔티티 클래스가 없는 경우 이 어노테이션을 사용해서 패키지 밖에 존재하는 엔티티를 지정할 수도 있다.

@EnableJpaRepositories : 이 어노테이션은 JpaRepository에 대한 설정정보를 자동으로 로딩하고 이 정보를 토대로 Repository 빈을 등록하는 역할을 한다. 

 

 

Test Code 작성하기

src/test/java/com.webapp.timeline/TimelineApplicationTests.java

 

package com.webapp.timeline;

import com.webapp.timeline.repository.MasterkeyRepository;
import com.webapp.timeline.domain.Masterkeys;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;

import javax.transaction.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TimelineApplicationTests {

    @Autowired
    private MasterkeyRepository userRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void createKeys() {
        Masterkeys masterkeys = new Masterkeys();
        masterkeys.setMasterId(6);
        masterkeys.setUserId("test용222");
        userRepository.save(masterkeys);
        userRepository.flush();
    }

}

 

@Rollback(false)는 해당 테스트 메소드를 롤백되지 않도록 하는 어노테이션이다. 위 메소드를 실행시키면 다음과 같이 테스트 성공을 알리는 화면이 뜰 것이다.

 

이것으로 JPA 연동은 성공했다~~ ^^

 

 

반응형