'boot'에 해당되는 글 18건

web/Spring

Spring Boot에서 6.4 Elasticsearch 연결 및 간단 CRUD

Elasticsearch를 Spring Boot에서 작업을 하는 간단한 정리를 해보자.


1. Library 추가

Elasticsearch를 사용하기 위해서는 spring-data-elasticsearch 라이브러리가 추가되어야 한다. 

gradle에 추가해보자.

1
2
3
4
5
6
7
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    compileOnly "org.projectlombok:lombok:1.16.16"
}
 
cs


spring-data-elasticsearch 버전별로 호환되는 elasticsearch가 상이하니 참고

spring data elasticsearchelasticsearch
3.2.x6.5.0
3.1.x6.2.2
3.0.x5.5.0
2.1.x2.4.0
2.0.x2.2.0
1.3.x1.5.2


2. Configuration

Elasticsearch에 접속하기 위한 Configuration을 정의해준다.

Elasticsearch  접속을 위해서는 host, port, cluster name이 필요하다. cluster name을 알아야 하는데 docker에 설치 한 경우 여기서 확인하면 된다.

우선 docker exec -it elastic bash로 콘솔에 접속한 후에 elasticsearch.yml에 적혀있는 cluster name을 확인한다.

그리고 application.properties에 설정 내용을 적어준다.

1
2
3
4
elasticsearch.host=127.0.0.1
elasticsearch.port=9300
elasticsearch.cluster_name=docker-cluster
spring.main.allow-bean-definition-overriding=true
cs

그리고 EnableElasticsearchRepositories 애노테이션을 설정한 Configuration 클래스를 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.elasticsearch.study.configuration;
 
import org.springframework.beans.factory.annotation.Value;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
 
import java.net.InetAddress;
 
/**
 * Elasticsearch Configuration
 *
 * @author wedul
 * @since 2019-02-09
 **/
@EnableElasticsearchRepositories
@Configuration
public class ElasticConfiguration {
 
  @Value("${elasticsearch.host}")
  private String host;
 
  @Value("${elasticsearch.port}")
  private int port;
 
  @Value("${elasticsearch.cluster_name")
  private String clusterName;
 
  @Bean
  public Client client() throws Exception {
    Settings settings = Settings.builder().put("cluster.name", clusterName).build();
 
    TransportClient client = new PreBuiltTransportClient(settings);
    client.addTransportAddress(new TransportAddress(InetAddress.getByName(host), port));
    return client;
  }
 
  @Bean
  public ElasticsearchOperations elasticsearchTemplate() throws Exception {
    return new ElasticsearchTemplate(client());
  }
 
}
 
cs


3. DTO 생성

Elasticsearch에서 Document 내용을 담을 DTO를 만들어주고 @Document 애노테이션을 달고 index name과 type을 정의해준다.

@Id 어노테이션이 붙은 필드는 각 Doucument에 붙어있는 _id 값이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.elasticsearch.study.dto;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
 
/**
 * studyFor
 *
 * @author wedul
 * @since 2019-02-09
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Document(indexName = "wedul_play", type = "story")
public class WedulPlay {
 
  @Id
  private String id;
  private String title;
  private String user;
  private long startAt;
  private long endAt;
 
}
 
cs


4. Repository

JPA를 사용하면 익숙할 패턴으로 Elasticsearch에서도 ElasticsearchRepository가 존재한다. 사용방법은 JPA와 동일하게 저장할 때는 save, 조회할 때는 find(), findByUser()등으로 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.elasticsearch.study.repository;
 
import com.elasticsearch.study.dto.WedulPlay;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
 
/**
 * study
 *
 * @author wedul
 * @since 2019-02-09
 **/
@Repository("wedulPlayRepository")
public interface WedulPlayRepository extends ElasticsearchRepository<WedulPlay, String> {
 
  WedulPlay findByUser(String user);
  
}
 
cs


5. Service

지금 테스트 하는 부분에서는 크게 비즈니스 로직에 들어갈 소스가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.elasticsearch.study.service;
 
import com.elasticsearch.study.dto.WedulPlay;
import com.elasticsearch.study.repository.WedulPlayRepository;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Service;
 
import java.util.List;
 
/**
 * study
 *
 * @author wedul
 * @since 2019-02-09
 **/
@AllArgsConstructor
@NoArgsConstructor
@Service
public class WedulPlayService {
 
  private WedulPlayRepository wedulPlayRepository;
 
  public void save(WedulPlay play) {
    wedulPlayRepository.save(play);
  }
 
  public List<WedulPlay> findAll() {
    return Lists.newArrayList(wedulPlayRepository.findAll());
  }
 
  public WedulPlay findByUser(String user) {
    return wedulPlayRepository.findByUser(user);
  }
 
}
 
cs


6. Test 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.elasticsearch.study.wedulplay;
 
import com.elasticsearch.study.dto.WedulPlay;
import com.elasticsearch.study.repository.WedulPlayRepository;
import com.elasticsearch.study.service.WedulPlayService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import java.util.List;
 
import org.hamcrest.core.IsNull;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
 
/**
 * wedul play document 조회
 *
 * @author wedul
 * @since 2019-02-09
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
public class WedulPlayTest {
 
  WedulPlayService wedulPlayService;
 
  @Autowired
  @Qualifier("wedulPlayRepository")
  WedulPlayRepository wedulPlayRepository;
 
  @Before
  public void setup() {
    wedulPlayService = new WedulPlayService(wedulPlayRepository);
  }
 
  @Test
  public void whenValidParameter_thenSuccessFind() {
    List<WedulPlay> list = wedulPlayService.findAll();
 
    assertNotNull(list);
  }
 
  @Test
  public void whenValidParameter_thenSuccessSave() {
    Exception ex = null;
 
    try {
      wedulPlayService.save(WedulPlay.builder().title("안녕 이건 테스트야").user("위들").startAt(1242421424).endAt(23214124).build());
    } catch (Exception exception) {
      ex = exception;
    }
 
    assertTrue(null == ex);
  }
 
  @Test
  public void whenValidParameter_thenSuccessFindByUser() {
    Exception ex = null;
 
    try {
      WedulPlay play = wedulPlayService.findByUser("위들");
 
      assertThat(play, is(IsNull.notNullValue()));
    } catch (Exception exception) {
      ex = exception;
    }
 
    assertTrue(null == ex);
  }
 
 
}
 
cs


설정이 간단하다. 

나중에 이용해 먹어야지


자세한 소스코드는 여기 참조

https://github.com/weduls/spring_elastic

web/Spring

생성한 Custom validation으로 에러메시지 출력하기

바로 직전 https://wedul.tistory.com/562?category=595982 에서 Custom validation을 만들어서 입력된 값에 validation을 체크하는 방법을 알아봤다.

그럼 이 validation체크를 통해서 front에 상황에 맞는 에러를 보내줄 수 있도록 조치를 취해보자.

우선 @valid 처리를 했었던 컨트롤러에서 에러 메시지를 수집해야한다. 


1. Controller

Spring에서 Validation 작업을 진행할 시 validation에 문제가 발생하면 에러 내용을 묶어서 BindingResult로 처리할 수 있도록 제공해준다. 이를 사용하기 위해서 parameter로 BindingResult값을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 회원가입
 *
 * @param reqDto
 * @return
 * @throws Exception
 */
@RequestMapping("/join")
public ResponseEntity<?> join(@Valid UserDto reqDto, BindingResult bindingResult) throws Exception {
    // check constraint rules
    this.checkConstraintRule(bindingResult);
 
    return ResponseEntity.ok(userService.insertUser(reqDto));
}
cs


2. 에러 처리

BindingResult에 validation을 체크하고 발생한 에러들에 대한 내용을 하나씩 뽑아서 국제화 메시지로 변경해주고 \n으로 데이터를 묶어서 view에 전달할 수 있도록 데이터를 바꿔 준다. 공통적으로 사용하것이기 때문에 공통 Controller 클래스를 하나 만들어서 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.wedul.common.controller;
 
import com.wedul.common.error.BadRequestException;
import com.wedul.common.util.MessageBundleUtil;
import lombok.AllArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.validation.BindingResult;
 
import java.util.stream.Collectors;
 
/**
 * wedulpos
 *
 * @author wedul
 * @since 2018-12-24
 **/
@AllArgsConstructor
public class BaseController {
 
  private final MessageBundleUtil messageBundleUtil;
 
  protected void checkConstraintRule(BindingResult bindingResult) throws BadRequestException {
    String msg = null;
    if (bindingResult.hasErrors()) {
       msg = bindingResult.getFieldErrors()
              .stream()
              .map(error -> messageBundleUtil.getMessage(error.getDefaultMessage()))
              .collect(Collectors.joining("\n"));
    }
 
    if(StringUtils.isNotBlank(msg)) {
      throw new BadRequestException(msg);
    }
 
    return;
  }
 
}
 
cs


3. 에러 핸들링

나는 에러를 에러 코드와 메시지로 전달해주는 방식을 좋아한다. 사실 다른 정보를 다 전달해줘봐야 프론트에서 처리하기도 어렵고 나머지는 로그로써 확인하는게 더 편하다. 그래서 전달하는 값을 정제하기 위해서 @ControllerAdvice를 통해 출력되는 에러를 재정의 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.wedul.common.config;
 
import com.wedul.common.enums.EnumErrorType;
import com.wedul.common.error.BadRequestException;
import com.wedul.common.error.ForbiddenException;
import com.wedul.common.error.NotFoundException;
import com.wedul.common.error.InternalServerException;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
 
/**
 * 에러 유형을 나타내는 Config
 *
 * @author wedul
 * @Date 2017. 07. 09
 */
@ControllerAdvice
public class ExceptionConfig {
 
  @Data
  @Builder
  private static class ErrorResponse {
    private int errCode;
    private String msg;
  }
 
  @ExceptionHandler({Exception.class})
  @ResponseBody
  public ErrorResponse errorHandler(Exception ex) {
    if(ex instanceof BadRequestException) {
      return this.getError(400, ex);
    } else if(ex instanceof ForbiddenException) {
      return this.getError(403, ex);
    } else if(ex instanceof NotFoundException) {
      return this.getError(404, ex);
    } else if(ex instanceof InternalServerException) {
      return this.getError(500, ex);
    } else {
      return ErrorResponse.builder().errCode(500).msg(ex.getMessage()).build();
    }
  }
 
  /**
   * 기본 에러 내용 출력
   *
   * @param errorCode
   * @param ex
   * @return
   */
  private ErrorResponse getError(int errorCode, Exception ex) {
    String message = ex.getMessage();
    if(StringUtils.isBlank(message)) {
      message = EnumErrorType.getErrorMsg(errorCode);
    }
 
    return ErrorResponse.builder().errCode(errorCode).msg(message).build();
  }
 
  /**
   * Error code 만들기
   *
   * @return String
   * @date 2017. 7. 9.
   * @author wedul
   */
  private String makeErrorCode(Exception ex) {
    StackTraceElement[] ste = ex.getStackTrace();
    StringBuffer sb = new StringBuffer();
    StackTraceElement[] arrayOfStackTraceElement1;
    int j = (arrayOfStackTraceElement1 = ste).length;
    for (int i = 0; i < j; i++) {
      StackTraceElement el = arrayOfStackTraceElement1[i];
      String className = el.getClassName();
      if (className.startsWith("com.wedul.wedulpos")) {
        sb.append(className.substring(className.lastIndexOf("."+ 1).toUpperCase()).append("[");
        sb.append(el.getLineNumber()).append("]");
        break;
      }
    }
    if (StringUtils.isBlank(sb.toString())) {
      return ex.getStackTrace()[0].getClassName();
    }
    return sb.toString();
  }
}
 
cs


4. 테스트 진행

1) @NotBlank, @Email 확인


2) Custom Validation인 password check



소스코드 : https://github.com/weduls/wedulpos_boot

참고 : https://meetup.toast.com/posts/147

web/JPA

스프링 부트에서 사용하는 JPA 기능 정리

스프링 프레임워크에서 제공하는 JPA는 별도의 구현 클래스 없이 인터페이스만을 사용할 수 있도록 제공한다. 제공되는 인터페이스 JpaRepository는 실행시점에 자동으로 인터페이스 내용을 연결하는 엔티티에 맞게 자동으로 구현해준다. 만약 스프링 JPA 인터페이스에서 제공하지 않는 기능을 사용하고 싶을 때는 메서드명을 특정한 규칙대로 만들어서 사용하면 인터페이스가 알아서 그 이름에 맞는 JPQL을 만들어서 실행해준다.


스프링 JPA 인터페이스는 Mysql같은 RDBMS 뿐만 아니라 Mongodb, Redis와 같은 NoSQL에도 동일한 인터페이스를 사용해서 기능을 사용할 수 있도록 제공해준다. 공통으로 사용할 수 있기에 아주 편리하다.


우선 스프링 부트에 JPA를 사용하기 위해서 Gradle에 라이브러리를 넣자.

1
compile ('org.springframework.boot:spring-boot-starter-data-jpa')
cs

 

기본적인 구조

JpaRepository를 상속받아 구현하고자 하는 인터페이스를 만들고 제네릭에 구현하려는 엔티티와 엔티티의 식별자 타입을 지정하여 인터페이스를 선언한다.

1
2
3
public interface StudentRepository extends JpaRepository<Student, Long> {
}
 
cs


주요 메서드 몇개만 정리해보자.

save() : 저장하거나 업데이트한다.

delete(entity) : em.remove() 호출하여 엔티티를 제거한다.

findOne(ID) : em.find() 호출하여 엔티티를 찾는다.

findAll() : 엔티티 모두를 조회한다. 


JpaRepository 인터페이스로 부족한 기능을 구현한 PagingAndSortingRepository CrudRepository 사용해서 보완할 있다.


JPA에서 메서드 사용하는 방법

JPA에서 엔티티에 맞는 메서드를 사용하는 방법은 크게 3가지이다.

- 메서드 이름으로 쿼리 생성

- 메서드 이름으로 JPA NamedQuery 호출

- @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의


1. 메서드 이름으로 쿼리 생성

메서드 이름으로 쿼리를 생성할 있는데 정식 Document 이용하면 자세히 나와있다

https://docs.spring.io/spring-data/jpa/docs/2.1.2.RELEASE/reference/html/#jpa.query-methods


2. 메서드 이름으로 JPA NamedQuery 호출

JPA NamedQuery 쿼리에 이름을 부여해서 사용하는 방법은 다음과 같이 엔티티에 선언해주면 된다.

1
2
3
4
5
@NamedQuery(
  name="student.findByName",
  query="select s from student s where s.name = :name")
public class Student {

}
cs


위와 같이 선언하고 실제 사용할 때는 entityManager에 아래와 같이 createQuery를 사용해서 쿼리를 호출하면 된다.

1
2
3
4
5
6
7
@PersistenceContext
private EntityManager entityManager;
 
public List<Student> findByUser(String name) {
  List<Student> students = entityManager.createQuery("student.findByName", Student.class).setParameter("name""wedul").getResultList();
}
 
cs


위와 같이 EntityManager를 사용할 수 있지만 스프링 JPA를 사용하여 간단하게 메소드 이름만으로 호출이 가능하다. 이렇게 호출하면 레포지토리에서 Student.쿼리메소드 형태로 쿼리를 찾는다. 만약 실행할 쿼리가 없으면 메서드 이름으로 쿼리를 자동으로 바꿔 동작한다.

1
2
3
4
5
public interface StudentRepository extends JpaRepository<Student, Long> {
  
  List<Student> findByUserName(@Param("name") String name);
  
}
cs


3. @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의

2번에서는 @Entity 클래스에서 정의한 쿼리를 레포지토리에서 메소드 형태로 접근하여 사용하였다.  이번에는 레포지토리에서 직접적으로 쿼리를 만들어서 조회하는 방식을 확인해보자.


@Query("select s from Student s where s.name = ?1")

Student findByName(String name);


인터페이스에 정의하는 메소드에 @Query 어노테이션을 붙혀서 정의하고 사용하면 된다. 바인딩 값은 1부터 시작한다. 스프링 데이터 JPA에서는 ?1 ?2 같은 위치기반 파라미터와 :name 같은 이름 기반 방식을 모두 사용가능하다.


페이징과 정렬

스프링 데이터 JPA에서 쿼리 메서드에 페이징과 정렬 기능을 사용할 있다. 파라미터로 Pageable 인터페이스를 사용할 경우에는 Page 또는 List 반환 받을 있다.

1
2
3
4
public interface StudentRepository extends JpaRepository<Student, Long> {
 
  Page<Student> findByNameStartingWith(String name, Pageable pageable);
}
cs


실제 사용할 때는 Pageable 인터페이스이기 때문에 구현체인 PageRequest 객체를 사용해서 사용한다.

// 파라미터 순서대로 페이지 번호, 사이즈, 정렬 기준 등으로 사용한다.

1
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Sort.Direction.DESC, "name"));
cs


반환되는 값인 Page에서 제공하는 다양한 메소드를 사용해서 편하게 페이징과 소트 기능을 사용할 있다. 

컨트롤러에서 사용자 요청에게 전달되는 페이징 정보를 받기 위해서는 다음과 같이 Pageable 인터페이스를 받으면 되고 받을 속성값은 page, size, sort 사용해서 받는다. (/student?page=0&size=20&sort=name,desc&sort=address.city)


1
2
3
4
5
6
7
@GetMapping("/student")
public String list(Pageable pageable, Model model) {
  Page<Student> page = studentRepository.findByNameStartingWith("dbsafer", pageable);
 
  return "dfdfdf";
}
 
cs


참고 싸이트

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html


출처. : 자바 ORM 표준 JPA 프로그래밍


web/JPA

JPA 영속성 컨테이너에서 엔티티 사용하기

JPA에서 사용하는 영속성 컨테이너에서 사용되는 엔티티에 대해 정리해보자.


엔티티 생명주기

영속성 컨테이너(Persistent Context)에서 존재하는 엔티티의 생명주기를 정리해보자. 
엔티티는 4가지 상태가 존재한다.

이름 

특징 

 비영속

 영속성 컨텍스트와 전혀 관계가 없는 상태 (엔티티 객체가 생성만 되고 컨텍스트와 아무런 연관이 없는경우)

 영속

 영속성 컨텍스트에 저장된 상태 (엔티티 매니저를 통해서 영속성 컨텍스트에 저장되고 영속성 컨텍스트가 관리한다.)

 준영속

 영속성 컨텍스트에 저장되었다가 분리된 상태 (영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다. close(), clear(), detach() 메소드를 사용하면 준영속 상태가 된다.

 삭제

 삭제된 상태 (remove() 메소드를 통해 영속성 컨텍스트와 데이터베이스에서 엔티티를 제거한다.)


엔티티 조회

- 영속성 컨텍스트 매니저는 내부에 캐시를 가지고 있는데 이를 1차캐시라고 한다. 영속 상태에 들어간 엔티티는 1차 캐시에 먼저 저장되고 키는 엔티티의 @Id가 사용되고 값은 엔티티이다. entityManager.persist(member); 를  하게 되면  영속성  컨텍스트에  엔티티가 1 캐시에 들어가게 된다.

- find(Class<T> entityClass, Object primaryKey)를 통해 데이터를 조회하는데 파라미터에서  첫번째는  엔티티 클래스 번째  파라미터는  키의  값이다.

1
2
3
4
5
6
7
8
9
Member member = new Member();
member.setId("member1");
member.setAge(30);
 
// 1차 캐시에 저장
em.persist(member);
 
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, "member");
cs


- 데이터 조회  1 캐시 내부에 데이터가 존재하면 데이터베이스를 뒤지지 않고 1차캐시에서 데이터를 가지고온다. 만약 1차 캐시에 데이터가 없으면 데이터를 DB에서 조회한 후 반환하면서 1차캐시에 삽입한다. 다음부터는 1차 캐시에서 동일한 데이터가 있으면 그곳에서 가져온다.

- 영속성 컨텍스트에서 꺼낸 같은 키의 데이터는 항상 같다. (meber1 == member1)


엔티티 삽입

Persistence Context에 엔티티가 들어가고 1차 캐시에 저장되고 데이터베이스에 값이 저장되기 위해서내부에 INSERT SQL을 내부 쿼리 저장소에 저장해두고 커밋되는 순간에 데이터 베이스에서 처리한다. 이 과정을  쓰기지연(transactional write-behind)이라 한다.

1
2
3
4
5
6
Member member = new Member();
member.setId("member1");
member.setAge(30);
 
// 1차 캐시에 저장 (이 부분 까지는 데이터가 저장되지 않는다.)
em.persist(member);
cs


엔티티 수정
기본적으로 SQL 쿼리로 업데이트 진행 시 수정되는 필드가 많으면 그만큼 쿼리가 많아지고 복잡해진다.

1
2
3
4
5
6
7
8
9
10
11
update
    member
set
    name = 'cjung',
    AGE = 22,
    ADDRESS = 'Seoul'
    ..
    ..
    ...
WHERE
    id = 'weduld'
cs

JPA로 기존 데이터를 수정하려고 할 때는 단순하게 데이터를 조회하고 데이터를 변경한 뒤에 commit만 하면된다. 추가적으로 update() 명령이 필요가 없다. 왜냐하면 데이터베이스가 엔티티의 변경사항을 자동으로 감지하는 dirty checking을 하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
 * springboottest
 *
 * @author wedul
 * @since 03/10/2018
 **/
@Service
@Slf4j
public class MemberServiceImpl implements MemberService {
 
  @PersistenceContext
  private EntityManager entityManager;
 
  @Override
  @Transactional
  public void jpaService() {
 
    try {
      login(entityManager);
    } catch (Exception ex) {
      log.error(ex.getMessage());
    }
  }
 
  private void login(EntityManager em) {
    String id = "wedul";
        
    // 사용자 찾기
    Member findMember = em.find(Member.class, id);
 
    member.setUserName("weduls");
    member.setAge(2);
 
    // 수정
    findMember.setAge(28);
  }
}
cs

위에 작업이 종료되면 UserName과 Age만 수정될 것 같지만 전체 모든 필드가 업데이트 된다. 모든 필드가 업데이트 되어서 데이터 전송량이 문제가 될 수 있지만 장점도 있다. 데이터베이스는 동일한 쿼리에 대해서는 파싱된 쿼리를 재사용하기 때문에 바인딩된 데이터만 바뀐 쿼리가 실행되기 때문에 성능적 효과가 있을 수 있다.

만약 수정된 부분만 업데이트 하고 싶으면 @DynamicUpdate 어노테이션을 붙히면 된다. Insert에서도 null이 아닌 필드만 삽입 하고 싶은 경우에는 @DynamicInsert를 사용할 수 있다.


엔티티 삭제

엔티티를 삭제하려면 업데이트와 동일하게 데이터를 조회하고 remove 명령어를 사용하여 엔티티를 삭제할 수 있다. 입력과 마찬가지로 바로 삭제하는 것이 아니라 삭제 쿼리를 모으는 저장소에 보관했다가 커밋을 하게 되면 수행된다.



Flush

플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영한다. 영속성 컨텍스트의 내용을 데이터베이스에만 반영할 뿐 컨텍스트에서 지우는 것은 아니다. 플러시가 호출되는 방법은 세가지가 있다.
1. flush() 메서드를 직접 호출해서 강재로 flush한다.
2. 트랜잭션이 커밋될때 자동으로 flush()가 호출된다.
3. JPQL 쿼리 실행시 JPA와 다르게 flush가 바로 호출된다.


옵션

- FlushModeType.AUTO : 커밋과 쿼리를 실행할 때 기본 옵션
- FlushModeType.COMMIT : 커밋할 경우에만 플러시


상태

연속성 상태를 준영속 상태로 변경하는 방법 (준영속 상태는 연속성 컨텍스트로 부터 엔티티가 분리된 상태로 더이상 관리 하지 않는 것을 의미한다.)

em.detach(entity) : 특정 엔티티만 준영속 상태로 전환 
-> 1차 캐시와 쓰기 지연 SQL 저장소에서 해당 엔티티가 관리하는 모든 정보 제거

em.clear() : 영속성 컨텍스트를 완전히 초기화
em.close() : 영속성 컨텍스트를 종료
=> 개발자가 준영속성 상태로 만들일이 거의 없다.

다시 준영속 또는 비용속 상태에서 영속 상태로 바꾸기 위해서 merge 메스드를 이용하면 된다. merge는 준영속 엔티티를 새롭게 병합된 영속 엔티티로 반환해준다. merge를 호출할때 넘긴 파라미터에 엔티티의 식별자의 값으로 찾는 엔티티가 없으면 데이터베이스에서 조회하고 만약 없으면 새로운 엔티티를 생성해서 병합한다.

1
2
3
4
@PersistenceContext
private EntityManager entityManager;
 
Member member = em.merge(member);
cs


web/Spring

Spring Boot Cross Domain 처리

웹 브라우저에서 같은 도메인을 사용하는 서비스에 대한 요청은 정상적으로 처리가 되나 도메인이 같지 않은 서비스에 요청을 할경우에 오류가 발생한다.

이는 동일 출처 정책(Same Origin Policy) 라는 정책을 두어 다른 도메인의 서버에 요청하는 것을 보안 문제로 간주하고 이를 차단하는 것 때문에 발생한다.

Spring Boot에서 간단한 방식으로 해결할 수 있다.



1. 요청에 @CrossOrigin 애노테이션 붙히기

1
2
3
4
5
@GetMapping("/greet")
@CrossOrigin
public Greet greeting() {
    return new Greet("dbsafer");
}
cs


2. 특정 도메인에서 오는 요청만 받고 싶을 경우.

-> 특정 도메인에서 오는 요청만 받고 싶은경우에는 도메인 주소를 기입해주면 된다.

1
2
3
4
5
@GetMapping("/greet")
@CrossOrigin("http://www.naver.com")
public Greet greeting() {
    return new Greet("dbsafer");
}
cs


3. 서비스 전체에 대한 요청 허용

-> 하나하나 모든 요청에 마다 @CroossOrigin 애노테이션을 부여하는건 정말 의미없는 짓이다. 그래서 모든 부분에 대한 요청을 처리하기 위해서는 WebMvcConfigurer 빈을 사용하고 addCorsMappings(CorsRegistry registry) 매소드를 재정의하면 애플리케이션 전체에 걸쳐 효력을 미치는 CORS를 적용할 수 있다.

해당 설정에는 다음 내용이 포함되어 있다.

이름
내용
Access-Control-Allow-Orgin
요청을 보내는 도메인 주소 (모든 도메인 대상인 경우 *)
Access-Control-Allow-Methods
요청 메소드 (Default : GET, POST, HEAD)
Access-Control-Max-Age
클라이언트에서 pre-flight의 요청 결과를 저장할 시간 지정. 해당 시간 동안은 pre-flight를 다시 요청하지 않는다. 
Access-Control-Allow-Headers
요청을 허용하는 헤더

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.wedul.microservice.rabbit.dto.config;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
/**
 * microservice
 *
 * @author wedul
 * @since 2018. 8. 24.
 **/
@Configuration
public class ServletContextConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/")
                .allowedOrigins("http://www.naver.com")
                .allowedMethods(HttpMethod.GET.name())
                .allowCredentials(false)
                .allowedHeaders("Content-Type""x-xsrf-token")
                .maxAge(200);
    }
}
 
cs


 [ 1 ]  [ 2 ]  [ 3 ]  [ 4 ] 

푸터바

알림

이 블로그는 구글에서 제공한 크롬에 최적화 되어있고, 네이버에서 제공한 나눔글꼴이 적용되어 있습니다.

카운터

  • Today : 36
  • Yesterday : 651
  • Total : 55,511