반응형

코드 작성

로깅(Logging)은 Commons Logging, Log4j, Slf4j, Logback 등 다양한 사용할 수 있다.

Main.java

package sample.springboot;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@EnableConfigurationProperties
public class Main {

    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            logger.error("error log");
            logger.warn("warn log");
            logger.info("info log");
            logger.debug("debug log");
            logger.trace("trace log");
        }
    }
}

실행 결과

2015-04-29 17:31:25.023 ERROR 8872 --- [           main] sample.springboot.Main                   : error log
2015-04-29 17:31:25.023  WARN 8872 --- [           main] sample.springboot.Main                   : warn log
2015-04-29 17:31:25.023  INFO 8872 --- [           main] sample.springboot.Main                   : info log
  • 기본적으로 INFO 레벨 이상만 출력된다.
  • 형식은 날짜 오류 레베 PID --- [스레드 명] 로거(logger) 이름 : 로그 메시지.

파일 출력

기본적으로 표준 출력만 로그가 출력되지 않지만 로그 파일을 지정하면 파일도 출력되게 된다.
로그 파일은 10MB 씩 회전된다.

파일 이름 지정

$ java -jar build/libs/spring-boot-sample.jar --logging.file=sample.log

$ dir /b *.log
sample.log
  • logging.file에서 출력 파일의 이름을 지정할 수 있다.
  • 편의상 명령 라인 인수로 지정하고 있지만, 속성 파일 등에서도 지정할 수 있다.
  • 파일의 대상 디렉토리가 존재하지 않으면 자동으로 만들어 진다.

폴더 지정

$ java -jar build/libs/spring-boot-sample.jar --logging.path=logs

$ dir /b logs
spring.log
  • logging.path에 로그 파일의 대상을 지정할 수 있다.
  • 로그 파일의 이름은 spring.log가 된다.
  • 디렉토리가 존재하지 않으면 자동으로 만들어 진다.

로거(logger)마다 로그 레벨을 지정

$java -jar build/libs/spring-boot-sample.jar --logging.level.sample.springboot.Main=TRACE

2015-04-29 18:14:17.969 ERROR 8288 --- [           main] sample.springboot.Main                   : error log
2015-04-29 18:14:17.970  WARN 8288 --- [           main] sample.springboot.Main                   : warn log
2015-04-29 18:14:17.970  INFO 8288 --- [           main] sample.springboot.Main                   : info log
2015-04-29 18:14:17.970 DEBUG 8288 --- [           main] sample.springboot.Main                   : debug log
2015-04-29 18:14:17.970 TRACE 8288 --- [           main] sample.springboot.Main                   : trace log
  • logging.level.[로거]=[로그 수준]에 로거에 대한 로그 수준을 지정할 수있다.
  • 로거 이름이 FQCN이되도록하고 있다면, --logging.level.sample.springboot=DEBUG과 같이 패키지 단위로 지정 가능하다.


반응형
반응형

Gmail을 사용하여 이메일을 보내 본다.

앱 비밀번호 생성

2 단계 인증을 사용하는 경우는 먼저 앱 비밀번호를 생성한다.

앱 비밀번호 생성

의존 jar를 얻는다.

Java Mail과 JavaBeans Activation Framework를 다운로드 해 온다.

의존 관계 추가

build.gradle

dependencies {
    compile 'org.springframework.boot:spring-boot-starter'
+   compile 'org.springframework.boot:spring-boot-starter-mail'
+   compile fileTree(dir: 'libs', include: '*.jar')
}

폴더 구성

|-build.gradle
|-libs/
|  |-activation.jar
|  `-javax.mail.jar
`-src/

코드 작성

application.properties

spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=<Gmail のアドレス>
spring.mail.password=<비밀번호※>
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

※ 2 단계 인증을 사용하는 경우는 "응용 프로그램 비밀번호"를, 그렇지 않은 경우는 보통의 로그인 비밀번호를 설정한다.

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            ctx.getBean(Main.class).sendMail();
        }
    }

    @Autowired
    private MailSender sender;

    public void sendMail() {
        SimpleMailMessage msg = new SimpleMailMessage();

        msg.setFrom("test@mail.com");
        msg.setTo("대상 이메일 주소");
        msg.setSubject("Send mail from Spring Boot");
        msg.setText("Spring Boot에서 메일을 보낼 수 있다.");

        this.sender.send(msg);
    }
}


반응형

'Spring Boot' 카테고리의 다른 글

[Spring Boot] 엔드 포인트(Endpoints)  (0) 2017.10.29
[Spring Boot] 로깅(Logging)  (0) 2017.10.29
[Spring Boot] 메일 송신  (0) 2017.10.29
[Spring Boot] 외부 설정 이용  (0) 2017.10.29
[Spring Boot] 데이터베이스 접근  (0) 2017.10.29
[Spring Boot] 배포  (0) 2017.10.29
반응형

외부 설정(Externalized Configuration) 이용에 대해 설명한다.


프로퍼티 파일(properties)

기본

폴더 구성

|-build.gradle
`-src/main/
  |-java/sample/springboot/
  |  `-Main.java
  `-resources/
    `-application.properties

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${sample.value}")
    private String value;

    public void hello() {
        System.out.println("sample.value = " + this.value);
    }
}

실행 결과

sample.value = Hello Properties File!!
  • 클래스 경로 아래에 application.properties라는 속성 파일을 배치한다.
  • 그러면 Spring Boot가 자동으로 해당 파일을 읽어 준다.
  • 속성 파일의 값은 @Value 애노테이션을 사용하여 Bean에 주입 할 수있다.
  • ${프로퍼티 이름} 형식으로 취득하고자 하는 값을 지정한다.

파일을 두는 위치

속성 파일의 두는 곳은 몇 가지 읽기의 우선 순위가 존재한다.

  1. 시작할 때 --spring.config.location에서 지정한 파일.
  2. 현재 디렉토리 바로 아래의 config 디렉토리에 있는 파일.
  3. 현재 디렉토리에있는 파일.
  4. 클래스 경로 바로 아래의 config 패키지에 있는 파일.
  5. 클래스 경로 바로 아래에 있는 파일.

숫자가 낮을 수록 우선 순위가 높다.
우선 순위가 낮은 설정은 상위의 설정을 덮어 쓴다.

폴더 구성(jar내부)

|-application.properties
|-config/
|  `-application.properties
`-sample/springboot/
   `-Main.class

폴더 구성(실행할 때)

|-application.properties
|-other.properties
|-config/
| `-application.properties
`-build/libs/
  `-spring-boot-sample.jar

other.properties

value5=other

application.properties(현재 디렉토리의 config 디렉토리 아래)

value4=currentdir/config
value5=currentdir/config

application.properties(현재 디렉토리 아래)

value3=currentdir/
value4=currentdir/
value5=currentdir/

application.properties(클래스 경로의 config 패키지 아래)

value2=classpath/config
value3=classpath/config
value4=classpath/config
value5=classpath/config

application.properties(클래스 경로 바로 아래)

value1=classpath/
value2=classpath/
value3=classpath/
value4=classpath/
value5=classpath/

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${value1}") private String value1;
    @Value("${value2}") private String value2;
    @Value("${value3}") private String value3;
    @Value("${value4}") private String value4;
    @Value("${value5}") private String value5;

    public void hello() {
        System.out.println("value1=" + value1);
        System.out.println("value2=" + value2);
        System.out.println("value3=" + value3);
        System.out.println("value4=" + value4);
        System.out.println("value5=" + value5);
    }
}

실행

$ java -jar build/libs/spring-boot-sample.jar --spring.config.location=other.properties

value1=classpath/
value2=classpath/config
value3=currentdir/
value4=currentdir/config
value5=other

우선 순위에 맞게 설정을 덮어 쓰고 있다.

프로파일을 지정

폴더 구성

|-application.properties
|-application-develop.properties
`-build/libs/
   `-spring-boot-sample.jar

application.properties

value=release module

application-develop.properties

value=develop module

실행

$ java -jar build/libs/spring-boot-sample.jar

value=release module

$ java -jar build/libs/spring-boot-sample.jar --spring.profiles.active=develop

value=develop module
  • 프로퍼티 파일을 application-[프로파일 이름].properties 형식으로 작성한다.
  • 커멘트 라인 인수로 spring.profiles.active에 사용하려는 프로파일 이름을 지정한다.
    • 커멘드 라인 인수 이외에도 시스템 속성이나 OS의 환경 변수도 지정 가능.
  • 그러면 지정된 프로파일에 해당하는 속성 파일이 읽혀진다.



같은 접두사를 가지는 속성을 Bean에 매핑

코드 작성

application.properties

person.firstName=Sato
person.last-name=Taro
person.age=18

Person.java

package sample.springboot;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix="person")
public class Person {

    private String firstName;
    private String lastName;
    private int age;

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void hello() {
        System.out.println(firstName + " " + lastName + " : " + age);
    }
}

Main.java

package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@EnableConfigurationProperties
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Person person = ctx.getBean(Person.class);
            person.hello();
        }
    }
}

실행 결과

Sato Taro : 18
  • @ConfigurationProperties을 사용하면 특정 접두사와 속성들을 Bean에 매핑 할 수 있다.
    • Bean은 setter 메소드가 필요하다.
    • 필드의 이름은 낙타 대문자(camelcase) 이외에도 하이픈(-) 구분과 언더스코어(_) 경우에도 매핑을해 준다.
  • 이 구조를 활성화하려면 @EnableConfigurationProperties 어노테이션을 추가를 해야 한다.
    • 엄밀히 말하면, @Configuration 어노테이션이 부여 된 클래스에 추가한다.



Yaml 사용

Yaml 사용

설정 파일을 application.yaml으로 하면, Yaml이 사용 가능하다.

application.yaml

aaa:
    bbb:
        ccc: Hoge
        ddd: Fuga
    eee:
        fff: Piyo

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${aaa.bbb.ccc}") private String ccc;
    @Value("${aaa.bbb.ddd}") private String ddd;
    @Value("${aaa.eee.fff}") private String fff;

    public void hello() {
        System.out.println("ccc=" + ccc);
        System.out.println("ddd=" + ddd);
        System.out.println("fff=" + fff);
    }
}

실행 결과

ccc=Hoge
ddd=Fuga
fff=Piyo

목록 매핑

application.yaml

myconf:
    list:
        - hoge
        - fuga
        - piyo

MyConf.java

package sample.springboot;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix="myconf")
public class MyConfig {

    private List<String> list;

    public List<String> getList() {
        return list;
    }

    public void setList(List<String> list) {
        this.list = list;
    }
}
Main.java
package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@EnableConfigurationProperties
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyConfig conf = ctx.getBean(MyConfig.class);
            System.out.println(conf.getList());
        }
    }
}

실행 결과

[hoge, fuga, piyo]
  • Bean에 매핑을 이용하면 List에 매핑도 가능하게 된다.


속성 파일 이외의 설정 값을 전달

속성 파일 이외의 설정 값을 전달

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${value}") private String value;

    public void hello() {
        System.out.println("value=" + value);
    }
}

커멘트 라인 인수

$java -jar build/libs/spring-boot-sample.jar --value=commandline

value=commandline
  • --[속성 이름]=[값]에서 커멘트 라인 인수에서 설정 값을 전달한다.

Java 시스템 속성

$java -Dvalue=systemproperty -jar build/libs/spring-boot-sample.jar

value=systemproperty
  • --D [속성 이름]=[값]에서 시스템 속성에서 설정 값을 전달한다.

OS 환경 변수

$ set value=osenvironment

$ java -jar build/libs/spring-boot-sample.jar

value=osenvironment

※ OS는 Windows이다.

기본 속성

Main.java

package sample.springboot;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("value", "default property");

        SpringApplication app = new SpringApplication(Main.class);
        app.setDefaultProperties(properties);

        try (ConfigurableApplicationContext ctx = app.run(args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${value}") private String value;

    public void hello() {
        System.out.println("value=" + value);
    }
}

실행결과

value=default property

SpringApplication#setDefaultProperties(Map<String, Object>) で、デフォルトの設定を指定できる。

우선 순위

속성 파일의 경우와 마찬가지로, 설정 값의 전달 방법에는 우선 순위가 있으며 우선 순위가 높은 방법이 낮은 방법으로 지정된 설정 값을 덮어 쓰기한다.

우선 순위는 다음과 같이 되어있다.

  1. 커멘드 라인 인수
  2. JNDI의 java : comp / env로부터 취득한 속성
  3. 시스템 속성
  4. OS 환경 변수
  5. jar 외부의 프로필 지정된 속성 파일
  6. jar 안에 있는 프로필 지정된 속성 파일
  7. jar 밖에 있는 속성 파일
  8. jar 안에 있는 속성 파일
  9. @PropertySource에서 지정된 속성 파일
  10. 기본 속성

숫자가 작은 쪽이 우선 순위가 높다.



반응형

'Spring Boot' 카테고리의 다른 글

[Spring Boot] 로깅(Logging)  (0) 2017.10.29
[Spring Boot] 메일 송신  (0) 2017.10.29
[Spring Boot] 외부 설정 이용  (0) 2017.10.29
[Spring Boot] 데이터베이스 접근  (0) 2017.10.29
[Spring Boot] 배포  (0) 2017.10.29
[Spring Boot] Thymeleaf 엔진 사용  (0) 2017.10.29
반응형

데이터베이스 접근에 대해서 설명한다.


Hello World

코드 작성

build.gradle

dependencies {
    compile 'org.hsqldb:hsqldb'
    compile 'org.springframework.boot:spring-boot-starter-jdbc'
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private JdbcTemplate jdbc;

    public void method() {
        this.jdbc.execute("CREATE TABLE TEST_TABLE (ID INTEGER NOT NULL IDENTITY, VALUE VARCHAR(256))");

        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "hoge");
        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "fuga");
        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "piyo");

        List<Map<String, Object>> list = this.jdbc.queryForList("SELECT * FROM TEST_TABLE");
        list.forEach(System.out::println);
    }
}

실행 결과

{ID=0, VALUE=hoge}
{ID=1, VALUE=fuga}
{ID=2, VALUE=piyo}
  • 의존관계에 spring-boot-starter-jdbc과 사용하는 DB (org.hsqldb:hsqldb)를 추가한다.
  • 그러면 지정한 DB를 메모리 상에 올려서 사용할 수 있게 된다.
  • 메모리에 저장되기에 JVM이 정지하면 데이터가 손실된다.
  • HSQLDB 외에 H2 및 Derby를 동일하게 내장해서 이용이 가능하다.

데이터를 파일에 저장하기

application.properties

spring.datasource.url=jdbc:hsqldb:file:./db/testdb;shutdown=true
  • 프로퍼티 파일에 spring.datasource.url을 정의하여 JDBC 연결 가능한 URL을 지정할 수있다.
  • HSQLDB의 경우 URL에서 데이터를 파일에 저장할지 여부를 지정하는 것이기에 위와 같이 설정하면 데이터를 파일에 저장이 된다.



MySQL 테이블

로컬 MySQL을 이용한다.

test_table

idvalue
1hoge
2fuga
3piyo

코드 작성

build.gradle

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-jdbc'
    compile 'mysql:mysql-connector-java:5.1.35'
}

application.properties

spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=test
spring.datasource.password=test
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

src/main/java/sample/springboot/Main.java

package sample.springboot;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private JdbcTemplate jdbc;

    public void method() {
        List<Map<String, Object>> list = this.jdbc.queryForList("SELECT * FROM TEST_TABLE");
        list.forEach(System.out::println);
    }
}

실행 결과

{id=1, value=hoge}
{id=2, value=fuga}
{id=3, value=piyo}
  • application.properties 연결 설정을 추가하여, 외부 DB에 접속할 수있다.



JPA 이용

기본

build.gradle

dependencies {
-   compile 'org.springframework.boot:spring-boot-starter-jdbc'
+   compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.hsqldb:hsqldb'
}

application.properties

spring.datasource.url=jdbc:hsqldb:file:./db/testdb;shutdown=true
spring.jpa.hibernate.ddl-auto=update

src/main/java/sample/springboot/jpa/MyEntity.java

package sample.springboot.jpa;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class MyEntity {

    @Id @GeneratedValue
    private Long id;
    private String value;

    public MyEntity(String value) {
        this.value = value;
    }

    private MyEntity() {}

    @Override
    public String toString() {
        return "MyEntity [id=" + id + ", value=" + value + "]";
    }
}

src/main/java/sample/springboot/jpa/MyEntityRepository.java

package sample.springboot.jpa;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
}

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.MyEntity;
import sample.springboot.jpa.MyEntityRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private MyEntityRepository repository;

    public void method() {
        this.repository.save(new MyEntity("test"));

        this.repository.findAll().forEach(System.out::println);
    }
}

실행 결과

$ gradle bootRun
MyEntity [id=1, value=test]

$ gradle bootRun
MyEntity [id=1, value=test]
MyEntity [id=2, value=test]

$ gradle bootRun
MyEntity [id=1, value=test]
MyEntity [id=2, value=test]
MyEntity [id=3, value=test]
  • JPA를 사용하는 경우에는 org.springframework.boot:spring-boot-starter-data-jpa을 의존관계에 추가한다.
  • JPA 구현은 Hibernate가 이용된다.
  • 기본으로 테이블이 매번 다시 만들게 되므로 spring.jpa.hibernate.ddl-auto=update를 설정하고있다.
  • JpaRepository를 상속한 인터페이스를 정의하면 Spring이 데이터 접근을 해준다.

메소드 이름에서 쿼리 자동 생성

데이터베이스

hoge

idnumberstringvalue
11onehoge
21twofuga
31threepiyo
42fourhoge
52fivefoga
63sixpiyo
73sevenhoge

엔티티

src/main/java/sample/springboot/jpa/Hoge.java

package sample.springboot.jpa;

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Hoge {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private int number;
    private String string;
    @Embedded
    private Fuga fuga;

    @Override
    public String toString() {
        return "Hoge [id=" + id + ", number=" + number + ", string=" + string + ", fuga=" + fuga + "]";
    }
}

src/main/java/sample/springboot/jpa/Fuga.java

package sample.springboot.jpa;

import javax.persistence.Embeddable;

@Embeddable
public class Fuga {

    private String value;

    @Override
    public String toString() {
        return "Fuga [value=" + value + "]";
    }
}

저장소 인터페이스

src/main/java/sample/springboot/jpa/HogeRepository.java

package sample.springboot.jpa;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

public interface HogeRepository extends JpaRepository<Hoge, Long> {

    List<Hoge> findByNumber(int number);

    List<Hoge> findByNumberOrderByIdDesc(int number);

    List<Hoge> findByStringLike(String string);

    List<Hoge> findByNumberLessThan(int number);

    List<Hoge> findByStringIgnoreCase(String string);

    List<Hoge> findByFugaValue(String string);

    long countByStringLike(String string);

    List<Hoge> findByNumberAndStringLike(int number, String string);

    List<Hoge> findByNumberOrString(int number, String string);
}

기동확인

src/main/java/sample/springboot/jpa/Main.java

package sample.springboot;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.Hoge;
import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        print("findByNumber",              repository.findByNumber(1));
        print("findByNumberAndStringLike", repository.findByNumberAndStringLike(1, "%e"));
        print("findByNumberOrString",      repository.findByNumberOrString(2, "seven"));
        print("findByNumberOrderByIdDesc", repository.findByNumberOrderByIdDesc(2));
        print("findByStringLike",          repository.findByStringLike("t%"));
        print("findByNumberLessThan",      repository.findByNumberLessThan(3));
        print("findByStringIgnoreCase",    repository.findByStringIgnoreCase("FIVE"));
        print("findByFugaValue",           repository.findByFugaValue("hoge"));
        print("countByStringLike",         repository.countByStringLike("%o%"));
    }

    private void print(String methodName, List<Hoge> list) {
        System.out.println("<<" + methodName + ">>");
        list.forEach(System.out::println);
        System.out.println();
    }

    private void print(String methodName, long number) {
        System.out.println("<<" + methodName + ">>");
        System.out.println(number);
        System.out.println();
    }
}

실행 결과

<<findByNumber>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberOrderByIdDesc>>
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]

<<findByStringLike>>
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberLessThan>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]

<<findByStringIgnoreCase>>
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]

<<findByFugaValue>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=7, number=3, string=seven, fuga=Fuga [value=hoge]]

<<countByStringLike>>
3

<<findByNumberAndStringLike>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberOrString>>
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]
Hoge [id=7, number=3, string=seven, fuga=Fuga [value=hoge]]

hoge

idnumberstringvalue
11onehoge
21twofuga
31threepiyo
42fourhoge
52fivefoga
63sixpiyo
73sevenhoge
  • Repository를 상속한 인터페이스에 find~ 같은 메소드를 정의하면 Spring이 해석해서 쿼리를 자동으로 생성해 준다.
  • 기본은 findBy[조건으로하는 속성의 이름]으로 정의한다.
  • And와 Or로 연결이 가능하다.
  • OrderBy[속성 이름][Asc|Desc]으로 정렬을 할 수 있다.
  • Like를 붙이면 문자열 포함 검색 수 있다.
  • LessThan, GreaterThan, Between 등도 사용할 수 있다.
  • IgnoreCase를 붙이면 대소 문자 구분없이 비교할 수 있다.
  • count~ 하면 검색 결과의 엔티티 수를 얻을 수 있다.
  • 내장 가능 클래스의 속성을 조건으로하는 경우는 findBy[조합 가능한 클래스] [내장 가능 클래스의 속성]와 연결한다.

JPQL을 사용

src/main/java/sample/springboot/jpa/HogeRepository.java

package sample.springboot.jpa;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface HogeRepository extends JpaRepository<Hoge, Long> {

    @Query("SELECT h FROM Hoge h WHERE (h.id % 2) = 0")
    List<Hoge> findEvenIdEntities();
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        this.repository.findEvenIdEntities().forEach(System.out::println);
    }
}

실행 결과

Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=6, number=3, string=six, fuga=Fuga [value=piyo]]

hoge

idnumberstringvalue
11onehoge
21twofuga
31threepiyo
42fourhoge
52fivefoga
63sixpiyo
73sevenhoge

@Query 어노테이션을 메소드에 부여하여 JPQL을 지정할 수 있다. JPQL은 @Query의 value로 설정한다.

EntityManager을 이용

src/main/java/sample/springboot/Main.java

package sample.springboot;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.Hoge;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private EntityManager em;

    public void method() {
        TypedQuery<Hoge> query = this.em.createQuery("SELECT h FROM Hoge h WHERE h.id=:id", Hoge.class);
        query.setParameter("id", 3L);

        Hoge hoge = query.getSingleResult();

        System.out.println(hoge);
    }
}

실행 결과

Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

@Autowired를 사용하여 일반적으로 EntityManager를 인젝션(주입)할 수 있다.





선언적 트랜잭션 사용

코드 작성

src/main/java/sample/springboot/jpa/MyService.java

package sample.springboot.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MyService {

    @Autowired
    private HogeRepository repository;

    public void save(String value) {
        Hoge hoge = new Hoge(value);
        this.repository.save(hoge);
    }

    public void saveAndThrowRuntimeException(String value) {
        this.save(value);
        throw new RuntimeException("test");
    }

    @Transactional
    public void saveAndThrowRuntimeExceptionWithTransactional(String value) {
        this.saveAndThrowRuntimeException(value);
    }

    @Transactional
    public void saveAndThrowExceptionWithTransactional(String value) throws Exception {
        this.save(value);
        throw new Exception("test");
    }

    public void show() {
        this.repository.findAll().forEach(System.out::println);
    }
}

src/main/java/sample/springboot/jpa/Main.java

package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.MyService;

@SpringBootApplication
public class Main {

    public static void main(String[] args) throws Exception {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyService s = ctx.getBean(MyService.class);

            s.save("normal");

            try {
                s.saveAndThrowRuntimeException("runtime exception without @Transactional");
            } catch (Exception e) {}

            try {
                s.saveAndThrowRuntimeExceptionWithTransactional("runtime exception with @Transactional");
            } catch (Exception e) {}

            try {
                s.saveAndThrowExceptionWithTransactional("exception with @Transactional");
            } catch (Exception e) {}

            s.show();
        }
    }
}

실행 결과

Hoge [id=1, value=normal]
Hoge [id=2, value=runtime exception without @Transactional]
Hoge [id=4, value=exception with @Transactional]
  • @Transactional 어노테이션을 메소드에 부여하면, 그 메소드 앞뒤가 트랜잭션 경계가 된다.
  • 트랜잭션 경계 안에서 RuntimeException 및 그 서브 클래스가 throw 되면 트랜잭션은 롤백된다.
  • @Transactional 어노테이션이 부여되어 있지 않으면, Exception 및 그 서브 클래스가 throw 되면 경우 롤백되지 않는다.
  • Exception이 발생된 경우도 롤백되었으면 하는 경우는 @Transactional (rollbackFor = Exception.class)과 같이 설정한다.



Flyway으로 마이그레이션

코드 작성

build.gradle

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.hsqldb:hsqldb'
+   compile 'org.flywaydb:flyway-core'
}

application.properties

spring.jpa.hibernate.ddl-auto=none

src/main/resources/db/migration/V1__create_database.sql

CREATE TABLE HOGE (
    ID INTEGER NOT NULL IDENTITY,
    VALUE VARCHAR(256)
);

INSERT INTO HOGE (VALUE) VALUES ('HOGE');
INSERT INTO HOGE (VALUE) VALUES ('FUGA');
INSERT INTO HOGE (VALUE) VALUES ('PIYO');

Main.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) throws Exception {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        this.repository.findAll().forEach(System.out::println);
    }
}

실행 결과

Hoge [id=0, value=HOGE]
Hoge [id=1, value=FUGA]
Hoge [id=2, value=PIYO]
  • Flyway을 의존관계에 추가하면 서버 시작시에 마이그레이션을 실행하게 된다.
  • JPA를 사용하는 경우에는 JPA가 DB를 자동 생성하지 않도록하지 않으면 안되기 때문에, spring.jpa.hibernate.ddl-auto=none을 지정한다.



복수 데이터 소스 사용

기본

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class SecondaryDataSourceConfiguration {

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

src/main/java/sample/springboot/MySecondary.java

package sample.springboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.annotation.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface MySecondary {
}
MyDatabaseAccess.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

    public void insertPrimary(String value) {
        this.primary.update(INSERT_SQL, value);
    }

    public void insertSecondary(String value) {
        this.secondary.update(INSERT_SQL, value);
    }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }
}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary!!");
            db.insertSecondary("secondary!!");

            db.showRecords();
        }
    }
}

동작 확인

콘솔 출력

Primary >>>>
{VALUE=primary!!}

Secondary >>>>
{VALUE=secondary!!}

설명

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • @Bean을 사용하여 DataSource의 빈을 정의하고 있다 (createPrimaryDataSource()).
  • 만든 DataSource를 인수받게 되고, 또한 JdbcTemplate의 빈을 정의하고 있다 (createPrimaryJdbcTemplate()).
  • DataSource를 여러 정의 할 때 하나의 정의를 @Primary에서 주석한다.
    • @Primary은 기본적으로 주입되는 bean을 명시하는 어노테이션이다.
    • 빈의 후보가 복수 존재하는 상태에서 한정자를 지정하지 않으면 @Primary에서 어노테이션된 빈이 주입된다.
  • DataSource 인스턴스는 DataSourceBuilder를 사용하여 작성할 수 있다.

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • 두 번째 DataSource의 정의는 자작 한정자를 부여하고 있다.

src/main/java/sample/springboot/MyDatabaseAccess.java

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;
  • 삽입할 때 @Autowired 뿐이라면 @Primary에서 어노테이션한 쪽의 빈이 자작 한정자에서 어노테이션하면 해당 빈이 인젝션(주입)된다.
  • 나머지는 대체로 지금까지한대로 하면 데이터베이스 액세스가 가능하다.

선언적인 트랜잭션

여러 DataSource를 정의한 경우, 그대로라면 @Primary 아닌 데이터 소스에 대한 선언적인 트랜잭션을 사용할 수 없다.

@Primary이 아닌 데이터 소스로 선언적인 트랜잭션을 사용하는 경우는 다음과 같이 구현한다.

코드 작성

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean @Primary
+   public PlatformTransactionManager createTransactionManager(DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class SecondaryDataSourceConfiguration {

+   public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}

src/main/java/sample/springboot/MyDatabaseAccess.java

package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
+ import org.springframework.transaction.annotation.Transactional;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

-   public void insertPrimary(String value) {
-       this.primary.update(INSERT_SQL, value);
-   }
-   
-   public void insertSecondary(String value) {
-       this.secondary.update(INSERT_SQL, value);
-   }

+   @Transactional
+   public void insertPrimary(String value, boolean rollback) {
+       this.primary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }
+   
+   @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public void insertSecondary(String value, boolean rollback) {
+       this.secondary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }

}

src/main/java/sample/springboot/Main.java

package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary commit!!", false);
            db.insertSecondary("secondary commit!!", false);

            try {
                db.insertPrimary("primary rollback!!", true);
            } catch (Exception e) {}

            try {
                db.insertSecondary("secondary rollback!!", true);
            } catch (Exception e) {}

            db.showRecords();
        }
    }
}

기동확인

콘솔 출력

Primary >>>>
{VALUE=primary commit!!}

Secondary >>>>
{VALUE=secondary commit!!}

설명

src/main/java/sample/springboot/PrimaryDataSourceConfiguration.java

    @Bean @Primary
    public PlatformTransactionManager createTransactionManager(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }

src/main/java/sample/springboot/SecondaryDataSourceConfiguration.java

    public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    ...

    @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
  • 여러 데이터 소스를 정의한 후에 선언적 트랜잭션을 사용하는 경우PlatformTransactionManager의 bean을 정의한다.
  • @Primary 쪽은 @Primary에서 어노테이션만으로 괜찮지만, 그렇지 않은 쪽은 bean 이름을 지정 둔다.

src/main/java/sample/springboot/MyDatabaseAccess.java

    @Transactional
    public void insertPrimary(String value, boolean rollback) {
        this.primary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
    @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public void insertSecondary(String value, boolean rollback) {
        this.secondary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
  • @Primary의 DataSource를 사용하는 경우에는 @Transactional 어노테이션이 부여함으로써 선언적 트랜잭션이 사용할 수있다.
  • @Primary가 아닌 DataSource를 사용하는 경우는 @Transactional의 value에 PlatformTransactionManager의 bean 이름을 지정해야 한다.

참고


반응형

'Spring Boot' 카테고리의 다른 글

[Spring Boot] 메일 송신  (0) 2017.10.29
[Spring Boot] 외부 설정 이용  (0) 2017.10.29
[Spring Boot] 데이터베이스 접근  (0) 2017.10.29
[Spring Boot] 배포  (0) 2017.10.29
[Spring Boot] Thymeleaf 엔진 사용  (0) 2017.10.29
[Spring Boot] WebJars 이용하기  (0) 2017.10.29
반응형

build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
+       classpath 'org.springframework:springloaded:1.2.1.RELEASE'
    }
}

apply plugin: 'java'
apply plugin: 'spring-boot'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
}

jar.baseName = 'spring-boot-sample'

buildscript 의존관계에 org.springframework:springloaded:1.2.1.RELEASE을 추가한다. 그리고, 일반적으로 gradle bootRun실행하면 배포가 활성화된다.

템플릿 엔진에 Thymeleaf를 사용하는 경우, 캐시 기능을 해제하는 것이 좋다.

application.properties

spring.thymeleaf.cache=false

Thymeleaf 이외의 템플릿 엔진을 사용하는 경우는 이 페이지를 참조하길 바란다.

IntelliJ IDEA을 사용하는 경우

build.gradle에 아래 내용을 추가한다.

build.gradle

apply plugin: 'idea'

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

기본이라면 IlleliJ가 컴파일 결과를 출력할 대상이 Gradle의 대상과 다르기 때문에 파일 모니터링이 잘되지 않아 관련된 부분을 변경을 해야 한다.

참고:80. Hot swapping

반응형

+ Recent posts