[공유] 우분투 설치 시 boot efi 관련 오류 수정 방법

IT 지식/기타지식|2019. 9. 9. 08:25

와이프가 결혼전에 집에서 사용하던 노트북 lenovo 제품이 있어서 이걸 간단하게 사용할 서버로 쓰고 싶었다.

그래서 우분투를 설치하려고 하는데 계속 the 'grub-efi-amd64-signed' package failed to install into /target/. Without the GRUB boot loader, the installed system will not boot. 라는 오류만 발생했다.

구글링을 하면서 efl boot 영역을 설정해주고 별짓 을 다해도 안되었는데

 

오늘 아침에 이 유튜브를 보고 해결했다. 나중을 위해서 공유해논다.

https://www.youtube.com/watch?v=DWlB0_f3GAY

태그 : BIOS, boot, efl, grub, Target, ubuntu

댓글()

Spring boot 모니터링 Actuator 소개 및 설치

web/Spring|2019. 8. 11. 21:38

spring acturator를 통해서 스프링 애플리케이션의 작동여부등을 체크해보자.

 

설정


우선 gradle 라이브러리를 추가한다.

compile 'org.springframework.boot:spring-boot-starter-actuator'

그리고 기존에는 application.properties나 yml에 아래 옵션을 설정해줘야 했지만 기본적으로 설정이 되어있다.

endpoints.health.enabled=true

하지만 이는 Spring boot 2.0에서 다음으로 변경되었다. (https://stackoverflow.com/questions/48900892/how-to-enable-all-endpoints-in-actuator-spring-boot-2-0-0-rc1)

 

How to enable all endpoints in actuator (Spring Boot 2.0.0 RC1)

I moved to Spring Boot 2.0.0 RC1 from 1.5.10 and I am stuck with actuator in the latest version. How can I enable expose and enable all actuator endpoints? The only endpoints that get exposed are:...

stackoverflow.com

각 health, info등으로 적어도 되지만 귀찮으면 asterisk로 모두 포함해도 된다. 대표적인 엔드포인트에 대한 설명은 아래 기재해 놓았다.

management:
  endpoints:
    web:
      exposure:
        include: "*"

endpoint는 다음과 같이 지정해줄수 있다.

management:
  endpoints:
    web:
      base-path: /application

 

기본적인 정보를 보여주는 Endpoint 


Actuator에서 사용할 수 있는 기본적인 엔드포인트는 다음과 같고 더 자세하게 다른 정보를 볼 수 있는 api들도 제공한다.

 

/health

 앱의 대한 건강 정보를 보여준다. 


/info 

전체적인 앱에 대한 정보를 보여준다.


/metrics 

앱에 대한 통계정보를 보여준다. (카운터 등등) 

우선적으로 볼 수 있는 파라미터정보가 나오고 상세히 보고 싶은경우 다음 경로에 추가해서 조회하면 볼 수있다.


/httptrace

앱에 대한 상세 요청정보를 보여준다.

 

Spring Acturator의 데이터는 모두 휘발성으로 데이터를 저장하고 있지는 않다. 이를 저장하는 방법은 따로 있는지는 확인해보지는 않았다. 나주에 기회되면 확인해보고 정리해봐야겠다. 그리고 health endpoint를 내가 원하는대로 커스텀할 수 있다. 이 또한 당장 필요성이 없어 상세하게 알아보지는 않았지만 크게 어렵지는 않은 것 같다. 

모니터링 할 수 있는 Acturator가 생각보다 괜찮은 것 같다. 나중에 실무에서 써볼 수 있으면 써봐야겠다.

 

댓글()

Intellij에서 spring boot multi module 사용시 jsp 못찾는 이슈 해결방법

web/Spring|2019. 4. 10. 23:34

기존에 공부삼아서 개발중이던 wedulpos에 spring batch를 추가해보려고 했다.

그래서 공통으로 mono 프로젝트로 되어있던 wedulpos를 multi module로 수정했다.

 

그랬더니 이상하게 servlet context에서 jsp를 로드하지 못했다.

그래서 계속해서 ServletException not include... jsp 또는 ServletException not jsp found 오류가 발생했다.

 

그래서 엄청난 구글링을 2틀동안했다. 집에서 그리고 약속장소에서 기다리면서 노트북으로 그리고 퇴근하고 오늘..

정말 가지가지한 방법을 다해봤었다. 기본적으로 embed-tomcat의 경우 jasper를 가지고 있지 못해서 별도의 모듈을 추가하고 servlet jspl 추가했고, compileOnly, provieded 별 난리를 다했다 ㅋㅋㅋ

하지만 tiles, url resolver 모두 bean이 등록되어있고 잘 동작하는데 jsp를 못찾는 해결하지 못했다.

최후에 방법으로 검색해본 키워드 intellij에서 정답을 찾았다. 

 

intellij에서 module 안에 웹 모듈을 실행시킬때는 working directory를 해당 모듈로 설정해줘야 한다. 그렇지 않으면 최상의 root로 working directory가 지정되기 때문이다.

https://stackoverflow.com/questions/44794588/intellij-run-configuration-spring-boot-vs-maven-issues

그래서 이 글을 보고 바로 지정해봤다.

 

결과는 성공 ㅋㅋㅋㅋㅋㅋㅋ

 

ㅋㅋㅋㅋ

너무 행복하다.  이 맛에 구글링하고 개발하는거 같다.

 

이제 내일부터는 spring batch를 공부해서 하나하나 정리하고 간단하게 batch를 만들어보자. 휴

git 주소 : https://github.com/weduls/wedulpos_boot

댓글()
  1. 김병관 2019.08.06 09:00 댓글주소  수정/삭제  댓글쓰기

    이 글을 보고 암이 나았습니다.감사합니다.하마터면죽을뻔했네요

  2. 야스오 2019.11.11 13:48 댓글주소  수정/삭제  댓글쓰기

    감사합니다. 덕분에 찾았어요. 이클립스에서 인텔리J로 넘어가기 힘드네요 ~

heroku 에서 spring boot jar파일 deploy시 Web process failed to bind to $PORT within 90 seconds of launch 에러 처리

web/Spring|2019. 3. 31. 17:43

heroku에 코드를 올리지 않고 바로 jar 파일을 deploy하기 위해서 heroku cli를 이용하여 올리는데 자꾸 Web process failed to bind to $PORT within 90 seconds of launch가 발생했다.

이유를 몰라서 계속 알아보던 중 heroku에서 spring boot를 실행시키기 위해서는 Procfile을 작성하고 port를 지정해줘야 한다.

우선 application.yml 설정

server:
    port: ${port:8080}

 

Procfile 설정

- Procfile은 확장자 없이 만들어야한다. 

- 포트는 8080이나 원하는 걸로 지정해 주고 profile까지 작성해주고 나머지 depoly를 위한 내요을 작성한다.

web: java -Dspring.server.port=8080 -Dspring.profiles.active=production $JAVA_OPTS -jar wedulpos-0.0.1-SNAPSHOT.war

 

Deploy 실행

heroku deploy:jar wedulpos-0.0.1-SNAPSHOT.war --app wedulpos

 

로그를 확인해보면 정상적으로 실행되는 걸 확인할 수 있땅. 2시간을 삽질했네 짱나겡

wedul$ heroku logs --tail --app wedulpos

 

댓글()

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

web/Spring|2019. 2. 9. 14:34

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
@Builder
@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

댓글()
  1. Favicon of https://benny.website/ BlogIcon 베니 2019.11.18 15:42 댓글주소  수정/삭제  댓글쓰기

    안녕하세요~
    올려주신 글 통해 ElasticSearch 연결 중에 깃허브에는 올라와 있는 소스가 블로그에는 없는 것 같아서요~

    dto - WedulPlay 클래스에 대한 @Builder 가 빠져있는 것 같습니다~!

    좋은 정보 글 올려주셔서 감사합니다~

  2. hoonick 2020.02.19 13:38 댓글주소  수정/삭제  댓글쓰기

    안녕하세요~
    github source clone해서
    whenValidParameter_thenSuccessSave() test코드를 동작시켰는데,

    NoNodeAvailableException[None of the configured nodes are available: [{#transport#-1}{lbyXeyaCQCKh64ZYC6Hexw}{localhost}{127.0.0.1:9200}]]

    위의 Exception을 계속 잡네요... 혹시 개발 하시면서 보신 Exception인가요?

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

web/Spring|2018. 12. 25. 00:45

바로 직전 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

댓글()
  1. 이정욱 2019.08.12 18:53 댓글주소  수정/삭제  댓글쓰기

    정말 깔끔하고 좋은 코드 감사합니다.

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

web/JPA|2018. 11. 4. 23:53

스프링 프레임워크에서 제공하는 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 프로그래밍


댓글()
  1. ParkNoIl 2020.02.25 16:10 댓글주소  수정/삭제  댓글쓰기

    안녕하세요 선배님? 잘지내시나요?

    다름이 아니라 회사 내에서 응용프로그램(.exe파일)을 개발 해야하는데,

    JPA에 대해 공부도 할겸 적용을 해볼까하는데, 응용프로그램에 JPA를 적용 할수 있나요?

    JPA 관련 인강 및 구글링에서는 대부분 웹과 연동 하여 사용한 내용만 있어서요..

    답변 부탁드립니다. 감사합니다.

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

web/JPA|2018. 10. 11. 20:43

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


댓글()