초기 세팅


 엘라스틱서치는 일반적으로 대용량 데이터를 처리하기 때문에 클러스터를 구성해서 사용합니다. 따라서, 로컬에서도 클러스터를 구성해서 돌려보기로 하였습니다. 테스트 환경은 아래와 같습니다.



OS

- mac


IDE

- IntelliJ 


Build

- Gradle


Dependency

-implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:6.4.3'


엘라스틱서치

- 클러스터 1개

- node 3개 사용 (port 9200, 9201, 9202)

- monitoring tool cerebro (port 9000)

- cluster-name: javacafe(공통)

- node-name: node1, node2, node3 (개별)




다운로드 링크


엘라스틱서치 6.6.0

https://www.elastic.co/kr/downloads/past-releases/elasticsearch-6-6-0

세레브로 

https://github.com/lmenezes/cerebro/releases



클러스터 구성


 파일을 받았으면 압축을 풀고 elasticsearch-6.6.0을 2개 더 복사하여 클러스터를 만들 준비를 하여 줍니다.


 여기서 주의 사항이 있는데, 인덱스를 만들면 data 파일이 생깁니다. 이렇게 된 경우 yml 파일을 수정하여도 기존 데이터들이 이전 yml 파일을 기준으로 생성되었기 때문에 오류가 생깁니다. 이렇게 된 경우 elasticsearch-6.6.0/data를 밀고 복사하여 줍니다.


 이제 vim으로 해당 파일들을 열어 클러스터를 구성하여 줍니다. 경로는 아래와 같습니다. 주의할 점은 클러스터네임은 공통으로 하여주는 것이고, 노드네임은 다르게 지어주는 것입니다. port는 설정을 따로 하지 않았는데, 일반적으로 마스터노드는 9200번 차순의 노드들은 9201, 9202순으로 매핑되어 집니다.


elasticsearch-6.6.0/config/elasticsearch.yml

cluster.name: javacafe-cluster

node.name: javacafe-node1

node.master: true

node.data: true

elasticsearch-6.6.0-0/config/elasticsearch.yml

cluster.name: javacafe-cluster

node.name: javacafe-node2

node.master: false

node.data: true

elasticsearch-6.6.0-1/config/elasticsearch.yml

cluster.name: javacafe-cluster

node.name: javacafe-node3

node.master: false

node.data: true


 클러스터 구성은 끝이 났습니다. 엘라스틱서치를 실행하여 봅시다. elasticsearch-6.6.0/bin에 있는 elasticsearch 파일을 각자 실행시켜 줍니다. 실행방법은 ./elasticsearch 입니다.


 마찬가지로 cerebro (모니터링 툴)도 실행하여 줍니다. 키바나도 있지만 여기선 cerebro를 사용하여 노드들의 상태를 확인할 것 입니다. 실행방법은 ./cerebro 입니다.




실행을 하게 되면 각 노드들이 올라온 것을 볼 수 있습니다.





 마지막으로, localhost:9000번으로 접속해 봅니다. 마스터노드의 9200번으로 접속하면 아래와 같은 화면이 나올것입니다.




커넥트를 하게 되면 클러스터안에 노드 3대가 떠 있는 것을 볼 수 있을것 입니다.



INDEX API


 이제 클러스터까지 구성해 봤으니, Spring Boot로 API를 실행해 보겠습니다. API 구성은 다음과 같습니다.


  • 인덱스 생성
  • 인덱스 닫기
  • 인덱스 오픈
  • 인덱스 삭제


 여기서 의문이 생길 수 있는데, 인덱스-타입이 일반 RDB의 테이블과 같다고 보면 이해가 쉬울 것 입니다. 개념적인 부분은 구글이나 책을 사서 꼭 공부를 해보길 추천합니다.


 코드보다 엘라스틱서치는 rest를 지원하기 때문에 툴을 이용하여 인덱스를 생성하는 것이 편합니다. cerebro에 있는 rest를 이용하여 movie_rest 인덱스를 생성할 수 있습니다.


movie_rest PUT


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "settings": {
            "number_of_shards"3,
            "number_of_replicas"2
    },
    "mappings": {
        "_doc": {
            "properties": {
                "movieCd": { "type" : "integer" },
                "movieNm": { "type" : "text" },
                "movieNmEn": { "type" : "text" },
                "prdtYear": { "type" : "integer" },
                "openDt": { "type" : "date" },
                "typeNm": { "type" : "keyword" },
                "prdtStatNm": { "type" : "keyword" },
                "nationAlt": { "type" : "keyword" },
                "genreAlt": { "type" : "keyword" },
                "repNationNm": { "type" : "keyword" },
                "repGenreNm": { "type" : "keyword" }
            }
        }
    }
}
cs



 이제 코드로 작성하여 봅니다. 우선 build.gradle에 dependency를 추가하여 줍니다.

implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:6.4.3'


@Autowired
private RestHighLevelClient client;

// Index명
private final String INDEX_NAME = "movie_rest";

// 타입명
private final String TYPE_NAME = "_doc";

@Before
public void connection_생성() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}

@After
public void connection_종료() throws IOException {
client.close();
}


먼저, before/ after로 커넥션을 관리하여 주었습니다.

@Test
public void index_테스트1_생성() throws IOException {

// 매핑정보
XContentBuilder indexBuilder = jsonBuilder()
.startObject()
.startObject(TYPE_NAME)
.startObject("properties")
.startObject("movieCd")
.field("type", "keyword")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNm")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNmEn")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.endObject()
.endObject()
.endObject();

// 매핑 설정
CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);
request.mapping(TYPE_NAME, indexBuilder);

// Alias 설정
String ALIAS_NAME = "moive_auto_alias";
request.alias(new Alias(ALIAS_NAME));

boolean acknowledged = client.indices()
.create(request, RequestOptions.DEFAULT)
.isAcknowledged();

assertThat(acknowledged, is(true));
}

 인덱스 생성 API인데 인덱스 > 타입 > 프로퍼티 순으로 작성후 CREATE를 하였습니다. 테스트를 수행하면 정상작동을 하게 되는데 cerebro를 이용하여 클러스트를 확인해 보겠습니다.



 movie_rest 인덱스가 생성된 것을 볼 수 있습니다. 


추가적으로 실선과 점선으로 생긴 블록들이 생긴 것을 볼 수 있는데 '샤드'와 '레플리카' 라는 것입니다. 이부분이 엘라스틱서치의 중요한 개념인데 엘라스틱서치는 방대한 데이터를 클러스터내에서 분산으로 나누어 요청을 처리합니다. 병렬로 데이터를 처리하기 때문에 빠르게 작업을 하는 것이고 어떠한 이유로 한 노드가 죽게 되어도 '샤드'의 복제본을 다른 노드에서 '레플리카'로 가지고 있기 때문에 모든 데이터를 검색할 수 있습니다.


이 부분은 중요 개념이기에 샤드와 레플리카를 모르시는 분들은 꼭 .. 공부할 것을 권장합니다!!



@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class IndexApiTest {

@Autowired
private RestHighLevelClient client;

// Index명
private final String INDEX_NAME = "movie_rest";

// 타입명
private final String TYPE_NAME = "_doc";

@Before
public void connection_생성() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}

@After
public void connection_종료() throws IOException {
client.close();
}

@Test
public void index_테스트1_생성() throws IOException {

// 매핑정보
XContentBuilder indexBuilder = jsonBuilder()
.startObject()
.startObject(TYPE_NAME)
.startObject("properties")
.startObject("movieCd")
.field("type", "keyword")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNm")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNmEn")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.endObject()
.endObject()
.endObject();

// 매핑 설정
CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);
request.mapping(TYPE_NAME, indexBuilder);

// Alias 설정
String ALIAS_NAME = "moive_auto_alias";
request.alias(new Alias(ALIAS_NAME));

boolean acknowledged = client.indices()
.create(request, RequestOptions.DEFAULT)
.isAcknowledged();

assertThat(acknowledged, is(true));
}

@Test
public void index_테스트2_닫기() throws IOException{

CloseIndexRequest requestClose = new CloseIndexRequest(INDEX_NAME);

boolean acknowledged = client.indices().close(requestClose, RequestOptions.DEFAULT).isAcknowledged();

assertThat(acknowledged, is(true));
}

@Test
public void index_테스트3_오픈() throws IOException{

OpenIndexRequest requestOpen = new OpenIndexRequest(INDEX_NAME);

boolean acknowledged = client.indices().open(requestOpen, RequestOptions.DEFAULT).isAcknowledged();

assertThat(acknowledged, is(true));
}

@Test
public void index_테스트4_삭제() throws IOException {

DeleteIndexRequest request = new DeleteIndexRequest(INDEX_NAME);

boolean acknowledged = client.indices()
.delete(request, RequestOptions.DEFAULT)
.isAcknowledged();

assertThat(acknowledged, is(true));
}
}


추가적으로 닫기, 오픈, 삭제 API가 있는데 코드를 수행해보고 cerebro를 이용해 클러스터의 상태를 확인해 보길 바랍니다.



DOCUMENT API


 이제 인덱스-타입을 생성하였으니 데이터를 쌓아 보겠습니다. 이부분을 RDB와 비교해보면 '인덱스-타입' = '테이블', '다큐먼트' = '행' 이라고 이해하면 됩니다.


@Test
public void index_테스트1_insert() throws IOException {

IndexRequest request = new IndexRequest(INDEX_NAME,TYPE_NAME, ID);

request.source(jsonBuilder()
.startObject()
.field("movieCd", "20173732")
.field("movieNm", "살아남은 아이")
.field("movieNmEn", "Last Child")
.endObject()
);


IndexResponse response = client.index(request, RequestOptions.DEFAULT);
String id = response.getId();
RestStatus status = response.status();

assertThat(id, is("1"));
assertThat(status, is(RestStatus.CREATED));

}


1번 ID에 다음 데이터를 넣어보고 데이터를 확인해 보겠습니다. REST 통신을 지원하니 cerebro에서 실행해 보겠습니다.



인덱스/타입/아이디 HTTP METHOD

movie_rest/_doc/1 GET



 데이터가 정상적으로 들어왔습니다. version값이 보인다 해당 아이디에 crud에서 생성/수정/삭제 를 수행하면 version이 올라가는데 이 부분은 증분색인과 관련이 있습니다.


@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DocumentApiTest {

@Autowired
private RestHighLevelClient client;
// Index명
private final String INDEX_NAME = "movie_rest";
// 타입명
private final String TYPE_NAME = "_doc";
// 문서 키값
private final String ID = "1";


@Before
public void connection_생성() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}

@After
public void connection_종료() throws IOException {
client.close();
}

@Test
public void index_테스트1_insert() throws IOException {

IndexRequest request = new IndexRequest(INDEX_NAME,TYPE_NAME, ID);

request.source(jsonBuilder()
.startObject()
.field("movieCd", "20173732")
.field("movieNm", "살아남은 아이")
.field("movieNmEn", "Last Child")
.endObject()
);


IndexResponse response = client.index(request, RequestOptions.DEFAULT);
String id = response.getId();
RestStatus status = response.status();

assertThat(id, is("1"));
assertThat(status, is(RestStatus.CREATED));

}

@Test
public void index_테스트2_get() throws IOException {

GetRequest request = new GetRequest( INDEX_NAME, TYPE_NAME,ID);

GetResponse response = client.get(request, RequestOptions.DEFAULT);

Map<String, Object> sourceAsMap = response.getSourceAsMap();
String movieCd = (String) sourceAsMap.get("movieCd");
String movieNm = (String) sourceAsMap.get("movieNm");
String movieNmEn = (String) sourceAsMap.get("movieNmEn");

assertThat(movieCd, is("20173732"));
assertThat(movieNm, is("살아남은 아이"));
assertThat(movieNmEn, is("Last Child"));
}

@Test
public void index_테스트3_update() throws IOException {

XContentBuilder builder = jsonBuilder()
.startObject()
.field("createdAt", new Date())
.field("prdtYear", "2018")
.field("typeNm", "장편")
.endObject();

UpdateRequest request = new UpdateRequest(INDEX_NAME, TYPE_NAME, ID).doc(builder);

UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT);
RestStatus status = updateResponse.status();

assertThat(status, is(RestStatus.OK));
}

@Test
public void index_테스트4_delete() throws IOException {

DeleteRequest request = new DeleteRequest(INDEX_NAME, TYPE_NAME, ID);
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);

RestStatus status = deleteResponse.status();
assertThat(status, is(RestStatus.OK));
}
}

 INDEX,DOCUMENT이외에도 검색, 집계에 관한 API가 있는데 이 부분은 추후에 또 올려보도록 하겠습니다.



---

 오류나 오타의 댓글을 남겨 주면 수정하도록 하겠습니다.


참조


[책] 엘라스틱서치 실무 가이드

[링크]  https://github.com/javacafe-project/elastic-book/tree/master/src/main/java/io/javacafe/client/rest


링크에 있는 API를 테스트 케이스로 만들어 기재하였습니다.

블로그 이미지

사용자 yhmane

댓글을 달아 주세요




스프링부트를 공부할 겸 '처음배우는 스프링부트2' 책을 구매하였고, 

이제 완독하였습니다.


이 책의 특징을 나열하자면?

  • 비교적 최신에 나온 spring boot 책이다
  • 컨텐츠가 좋다 (oauth2, security, rest, batch)

총평은

 스프링부트에 대해 관심이 있고 2주 정도의 시간을 갖고 공부를 하기에 유용한 책으로 판단됩니다. 
자세한 설명들은 생략 되어 있는 부분들이 있기에, 1년 이상 스프링 공부를 하셨고,
스프링부트에 입문하려고 할 때 좋은 책으로 생각됩니다~



- 한빛미디어/김영재님 지음

'Books' 카테고리의 다른 글

이것이 Fedora 리눅스다  (0) 2019.11.23
자바 ORM 표준 JPA 프로그래밍  (0) 2019.07.23
처음 배우는 스프링 부트2  (0) 2019.07.07
Effective JAVA 3/E (이펙티브 자바 3판)  (0) 2019.02.07
블로그 이미지

사용자 yhmane

댓글을 달아 주세요


처음배우는 스프링부트2의 

내용을 일부 요약하여 정리한 포스팅입니다.


@SpringBootTest


@SpringBootTest는 통합 테스트를 제공하는 기본적인 스프링 부트 어노테이션. 

실제 애플리케이션과 똑같이 애플리케이션 컨텍스트를 로드. 또한, 설정된 빈을 모두 load하기 때문에 규모가 클수록 느려짐

통합 테스트에 적합한 어노테이션


@RunWith(SpringRunner.class)를 붙여서 SpringJUnit4ClassRunner를 상속 받아 사용


@RunWith(SpringRunner.class)
@SpringBootTest(value = "value=test", classes = {TestApplicationTests.class})
public class TestApplicationTests {

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

@Test
public void contextLoads() {
assertThat(value, is("test"));
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {"property.value=propertyTest"}, classes = {TestApplicationPropertyTests.class})
public class TestApplicationPropertyTests {

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

@Test
public void isEqualPropertyValue() {
assertThat(propertyValue, is("propertyTest"));
}

}

- value : 테스트가 실행되기 전에 적용할 프로퍼티를 주입.

- properties: 테스트가 실행되기 전에 {key=value} 형식으로 프로퍼티를 추가



@WebMvcTest

@WebMvcTest는 웹에서 테스트 하기 힘든 컨트롤러를 테스트하는 데 적합.(테스트하고자 하는 특정 컨트롤러명을 명시해 두어야 함)

웹상에서 응답과 요청에 대한 테스트를 진행.

- MockMvc는 컨트롤러 테스트 시, 모든 의존성을 로드하는 것이 아닌 명시된(BookController) 클래스 관련 빈만 로드하여 가벼운 MVC 테스트를 수행

=> 전체 HTTP 서버를 실행하지 않고 테스트를 할 수 있음


@RunWith(SpringRunner.class)
@WebMvcTest(BookController.class)
public class BookControllerTest {

@Autowired
private MockMvc mvc;

@MockBean
private BookService bookService;

@Test
public void Book_MVC_테스트() throws Exception {
Book book = new Book("Spring Boot Book", LocalDateTime.now());
given(bookService.getBookList()).willReturn(Collections.singletonList(book));

mvc.perform(get("/books"))
.andExpect(status().isOk()) // status? 200
.andExpect(view().name("book")) // return view? book
.andExpect(model().attributeExists("bookList")) // model property exist? bookList
.andExpect(model().attribute("bookList", contains(book))); // bookList contains? book
}
}


@service의 경우, @WebMvcTest의 적용 대상이 아니기 때문에 @MockBean으로 객체를 대체


check

- status ? 200

- viewName ? book

- model property Exist? bookList

- bookList contain ? book


@DataJpaTest


@DataJpaTest는 JPA 관련된 테스트 설정만 로드

기본적으로 인 메모리 데이터베이스를 사용(h2)

테스트가 끝날때마다, 테스트에 사용된 데이터를 롤백하여 줌


@RunWith(SpringRunner.class)
@DataJpaTest
public class BookJpaTest {
private final static String BOOT_TEST_TITLE = "Spring Boot Test Book";

@Autowired
private TestEntityManager testEntityManager;

@Autowired
private BookRepository bookRepository;

@Test
public void Book_저장하기_테스트() {
Book book = Book.builder().title(BOOT_TEST_TITLE).
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book);
assertThat(bookRepository.getOne(book.getIdx()), is(book));
}

@Test
public void BookList_저장하고_검색_테스트() {
Book book1 = Book.builder().title(BOOT_TEST_TITLE).
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book1);
Book book2 = Book.builder().title(BOOT_TEST_TITLE+"2").
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book2);
Book book3 = Book.builder().title(BOOT_TEST_TITLE+"3").
publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book3);

List<Book> bookList = bookRepository.findAll();
assertThat(bookList, hasSize(3));
assertThat(bookList, contains(book1, book2, book3));
}
}


@DataJpaTest 사용할 경우에 TestEntityManager 클래스가 자동으로 빈으로 등록 됨. 

위와 같이 TestEntityManager 주입받아서 사용하면 된다. Jpa EntityManager와는 다르게 메서드가 많이 없지만 테스트하기에는 불편함이 없다.


만약, 인메모리 데이터베이스가 아닌 실메모리 데이터베이스를 사용하고자 한다면 아래와 같이 설정하여 주면 된다.

Replace,NONE으로 설정하면 @ActiveProfiles에서 설정한 프로파일이 적용 됨.


@ActiveProfiles("sample")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)



@RestClientTest


@RestClientTest는 Rest 관련 테스트를 도와주는 어노테이션

Rest 통신의 데이터형으로 사용되는 JSON 형식을 테스트


@RunWith(SpringRunner.class)
@RestClientTest(BookRestService.class)
public class BookRestTest {

@Autowired
private BookRestService bookRestService;

@Autowired
private MockRestServiceServer server;

@Test
public void rest_테스트() {
this.server.expect(requestTo("/rest/test"))
.andRespond(withSuccess(new ClassPathResource("/test.json",
getClass()), MediaType.APPLICATION_JSON));
Book book = this.bookRestService.getRestBook();
assertThat(book.getTitle()).isEqualTo("테스트");
}
}

@RestClinentTest는 대상이 되는 BookRestService를 객체로 주입

MockRestServiceServer는 지정된 경로에 반환되는 예상값을 명시해 줌


@JsonTest


@JsonTest는 JSON의 직렬화와 역직렬화를 수행하는 라이브러리인 Gson과 Jackson API의 테스트를 제공


@RunWith(SpringRunner.class)
@JsonTest
public class BookJsonTest {

@Autowired
private JacksonTester<Book> json;

@Test
public void json_테스트() throws Exception {
Book book = Book.builder()
.title("테스트")
.build();
String content = "{\"title\":\"테스트\"}";

assertThat(this.json.parseObject(content).getTitle()).isEqualTo(book.getTitle());
assertThat(this.json.parseObject(content).getPublishedAt()).isNull();

assertThat(this.json.write(book)).isEqualToJson("/test.json");
assertThat(this.json.write(book)).hasJsonPathStringValue("title");
assertThat(this.json.write(book)).extractingJsonPathStringValue("title")
.isEqualTo("테스트");
}
}

- JacksonTester의 parseObject를 통해 content를 객체로 변환.


[출처]


처음으로배우는 스프링부트2

블로그 이미지

사용자 yhmane

댓글을 달아 주세요


@Controller @RestController의 차이 HTTP Response Body가 생성되는 방식의 차이


@Controller를 사용하는 경우

  1. Client가 URI 요청을 보냄
  2. DispatcherServlet과 Handler Mapping이 요청을 Intercept
  3. Controller에 의해 요청을 처리 하고 DispatcherServlet Model과 View를 적절히 Client에 리턴
@Controller
public class WebController {

@GetMapping("/")
public String main(Model model) {
return "main";
}
}


@ResponseBody를 사용할 경우

Spring3 버전 이후로 출시

자바 객체를 HTTP 요청의 body 내용으로 매핑하는 역할을

 1.2 방식은  @Controller와 같다

 3. 여기에서는 View를 거치지 않고 Controller에서 직접 데이터를 리턴


@Controller
public class WebController {

@PostMapping("/post")
public @ResponseBody Posts hello(@RequestBody Posts post) {
// post.set함수
// post.set함수
// post.set함수
return post;
}
}


[추가]

@RequestBody 어노테이션이란?

HTTP 요청의 body 내용을 자바 객체로 매핑하는 역할을 합니다.

-> 당연히 method 방식으로 Get을 쓰면 원하는 인자값을 받지 못함

        -> Get은 Body데 데이터를 담지 않기 때문에, POST를 사용


추가적으로 @PathVariable, @RequestParam이 있다

자꾸 딴곳으로 새는데 ,,, 이 부분도 같이 보는것이 좋다.


@PathVariable  

URI 템플릿 변수에 접근할 때 사용


@RequestParam  

Http 요청 파라미터를 매핑


@GetMapping("/board/{boardNo}")
public String hello(@RequestParam String name, @RequestParam String birth, @PathVariable String boardNo) {
return "Hello World";
}

들어온 데이터 값을 따로 처리하지는 않았지만, 위와 같은 방식으로 사용된다.

@RequestParam은 String name = request.getParameter("name")과 같다.



@RestController 어노테이션이란?

@RestController = @Controller + @ResponseBody 

Spring 4 버전이후로 출시

@Controller와 @ResponseBody를 @RestController가 가지고 있기 때문에 데이터 중심의 구현

작동 방식은 @ResponseBody와 동일


@RestController
public class WebRestController {

@GetMapping
public String hello() {
return "Hello World";
}
}


@RestController를 자세히 본다면, @Controller와 @ResponseBody 를 확인할 수 있음

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {




[참조]

https://docs.spring.io/spring/docs/current/javadoc-api/




블로그 이미지

사용자 yhmane

댓글을 달아 주세요

JPA Auditing


DB 테이블에 값을 넣거나 수정하는 행위는 '누가'. '언제' 하였는지 기록을 잘 남겨 놓아야 합니다.


행위의 주체자나 시간을 column으로 만들어 놓고 기록을 남겨 놓습니다.


이번 포스팅에선 ;언제'에 해당하는 시간 정보를 자동화 하는 방법을 알아보겠습니다.




Audit


audit의 뜻은 '감사하다. 감시하다' 입니다. 즉, 무엇인가 계속 쳐다보고 쳐다 보고 있는 것인데,


spring data jpa는 시간에 대해서 자동으로 값을 넣어주는 기능을 제공하여 줍니다.



데이터를 insert하거나 update하는 경우 매번 시간 데이터를 입력하여 주어야 하는데,


audit을 이용하면 자동으로 시간을 매핑하여 db에 넣어주게 됩니다.





사용법


build.gradle에 의존성 추가


classpath "io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE" 

ext['hibernate.version'] = '5.2.11.Final' 


LocalDate/LocalDateTime이 DB의 잘 저장되지 않는 이슈로 인해

SpringDataJpa가 사용하는 Hibernate의 버전만 5.2.11로 변경




@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {

@CreatedDate
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime modifiedDate;

}



@MappedSuperclass

JPA Entity 클래스들이 해당 추상클래스를 상속할 경우 createdDate, modifiedDate을 컬럼으로 인식 하도록 하


@EntityListeners(AuditingEntityListener.class)

해당 클래스에 Auditing 기능을 포함 시킴


@CreatedDate

Entity 생성되어 저장될 시간이 자동 저장


@LastModifiedDate

조회한 Entity 값을 변경할 시간이 자동 저장



public class Posts extends TimeEntity {
...
}


클래스를 상속


@EnableJpaAuditing // JPA Auditing 활성화
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}





결과



[참조]


https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.auditing

https://jojoldu.tistory.com/251

블로그 이미지

사용자 yhmane

댓글을 달아 주세요