6. 데이터 파고들기
• 스프링 데이터 Spring Data
: 기본적인 데이터 저장의 특수한 속성을 유지하면서 데이터에 액세스하는 친숙하고 일관된 스프링 기반 프로그래밍 모델을 제공하는 것
https://spring.io/projects/spring-data
Spring Data
Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store. It makes it easy to use data access technologies, relational and non-rela
spring.io
6.1 엔티티 정의
스프링 데이터는 다양한 복잡성(추상성) 수준에서 스프링 부트 애플리케이션이 사용할 수 있는 다양한 메커니즘과 데이터 엑세스 옵션을 제공
1단계) 적용 가능한 데이터를 처리하는 데 사용할 도메인 클래스를 정의
2단계) 데이터 사용 범위, 클라이언트가 사용하는 외부 API, DB 종류를 고려하여 DB, 추상화 수준 정하기
• 도메인 클래스: 그 연관성과 중요성이 다른 데이터와 독립적인 기본 도메인 엔티티
→ 도메인 클래스는 다른 엔티티와 연결되지 않은 때에도 단독으로 존재하고 그 자체로 의미있다는 뜻
보통 이런 작업을 할 때 템플릿, repository 중 하나 사용
*DDD Domain-Driven Design 도메인 주도 설계
*롬복(Lombok): 자바 클래스를 만들때 자주 사용되는 getter/setter나 toString 등의 코드를 어노테이션으로 대체해서 선언하고 java 코드를 컴파일 할 때 그에 맞는 코드를 생성해주는 것이다.
6.2 템플릿 지원
스프링데이터는 다양한 데이터 타입에 Operations 타입의 인터페이스(예: MongoOperations, RedisOperations, CassandraOperations)를 정의함
• Template 클래스에 Operations 인터페이스가 구현됨
• 템플릿은 일종의 SPI(Service Provider Interface 서비스 제공 인터페이스)
- 일반적으로는 매번 반복되는 단계를 거쳐야 함
→ 일반적인 패턴의 데이터 액세스에서는 repository가 더 좋은 옵션이 됨
- repository가 템플릿을 기반으로 하기 때문에 추상화를 높이더라도 잃을게 없음 (?????)
6.3 저장소 지원
• Repository 인터페이스
- Repository 인터페이스로부터 그 외 모든 유형의 스프링데이터 repository 인터페이스가 파생됨
- findAll(), findById(), count(), delete(), deleteAll() 등과 같은 유용한 상위 수준 함수를 지정함
6.5 레디스로 템플릿 기반 서비스 생성하기
• Redis
: 일반적으로 서비스 내 인스턴스 간에 상태를 공유, 캐싱과 서비스 간 메시지를 중개하기 위해 인메모리 repository로 사용하는 데이터베이스
6.5.1 프로젝트 초기화하기
6.5.2 레디스 서비스 개발하기
도메인 클래스 정의하기
package com.thehecklers.sburredis;
import java.time.Instant;
import org.springframework.data.annotation.Id;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown=true)
public class Aircraft {
@Id
private Long id;
private String callsign, squawk, reg, flightno, route, type, category;
private int altitude, heading, speed;
@JsonProperty("vert_rate")
private int vertRate;
@JsonProperty("selected_altitude")
private int selectedAltitude;
private double lat, lon, barometer;
@JsonProperty("polar_distance")
private double polarDistance;
@JsonProperty("polar_bearing")
private double polarBearing;
@JsonProperty("is_adsb")
private boolean isADSB;
@JsonProperty("is_on_ground")
private boolean isOnGround;
@JsonProperty("last_seen_time")
private Instant lastSeenTime;
@JsonProperty("pos_update_time")
private Instant posUpdateTime;
@JsonProperty("bds40_seen_time")
private Instant bds40SeenTime;
public String getLastSeenTime() {
return lastSeenTime.toString();
}
public void setLastSeenTime(String lastSeenTime) {
if(null != lastSeenTime) {
this.lastSeenTime = Instant.parse(lastSeenTime);
} else {
this.lastSeenTime = Instant.ofEpochSecond(0);
}
}
public String getPosUpdateTime() {
return posUpdateTime.toString();
}
public void setPosUpdateTime(String posUpdateTime) {
if(null != posUpdateTime) {
this.posUpdateTime = Instant.parse(posUpdateTime);
} else {
this.posUpdateTime = Instant.ofEpochSecond(0);
}
}
public String getBds40SeenTime() {
return bds40SeenTime.toString();
}
public void setBds40SeenTime(String bds40SeenTime) {
if(null != bds40SeenTime) {
this.bds40SeenTime = Instant.parse(bds40SeenTime);
} else {
this.bds40SeenTime = Instant.ofEpochSecond(0);
}
}
}
- @Data:: 롬복에서 게터, 세터, equals(), hashCode(), toString() 메서드를 생성해 데이터 클래스를 만듦
- @NoArgsConstructor:: 롬복에 매개변수가 없는 생성자를 만들도록 지시
- @AllArgsConstructor:: 롬복에 각 멤버 변수의 매개변수가 있는 생성자를 만들도록 지시하고 모든 멤버 변수에 인수를 제공
- @JsonIgnoreProperties(ignoreUnknown = true):: JSON응답필드 중에서 클래스에 상응하는 멤버 변수가 없는 경우, Jackson 역직렬화 메커니즘이 이를 무시하도록 함
- @Id:: 어노테이션이 달린 멤버 변수가 DB 항목/레코드의 고유식별자를 가지도록 지정
- @JsonProperty(" "):: 한 멤버 변수를 다른 이름이 붙은 JSON 필드와 연결
(+) Instant 클래스 참고 https://sujl95.tistory.com/85
템플릿 지원 추가하기
• RedisTemplate 클래스 - RedisAccessor 클래스를 상속해 RedisOperations 인터페이스를 구현함
- RedisOperations → 레디스가 상호작용하는데 필요한 기능을 지정해줌
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.thehecklers.sburredis.Aircraft;
@SpringBootApplication
public class SburRedisApplication {
@Bean
public RedisOperations<String, Aircraft>
redisOperations(RedisConnectionFactory factory){
Jackson2JsonRedisSerializer<Aircraft> serializer = new Jackson2JsonRedisSerializer<Aircraft>(Aircraft.class);
RedisTemplate<String, Aircraft> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setDefaultSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
return template;
}
public static void main(String[] args) {
SpringApplication.run(SburRedisApplication.class, args);
}
}
SburRedisApplication.java
package com.thehecklers.sburredis;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@EnableScheduling
@Component
public class PlaneFinderPoller {
private WebClient client =
WebClient.create("http://localhost:7634/aircraft"); //PlaneFinder 서비스에 의해 노풀되는 객체의 엔드포인트를 가리키도록
private final RedisConnectionFactory connectionFactory;
private final RedisOperations<String, Aircraft> redisOperations;
//Redis ConntctionFactory - 레디스 의존성을 추가했으므로 스프링 부트의 '자동설정'에 의해 제공됨
PlaneFinderPoller(RedisConnectionFactory connectionFactory,
RedisOperations<String, Aircraft> redisOperations){
this.connectionFactory = connectionFactory;
this.redisOperations = redisOperations;
}
@Scheduled(fixedRate=1000)
private void pollPlanes() {
connectionFactory.getConnection().serverCommands().flushDb(); //flushDb(): 존재하는 모든 키를 지움
client.get()
.retrieve()
.bodyToFlux(Aircraft.class)
.filter(plane -> !plane.getReg().isEmpty())
.toStream()
.forEach(ac ->
redisOperations.opsForValue().set(ac.getReg(), ac));
redisOperations.opsForValue()
.getOperations()
.keys("*") //모든 키 조회
.forEach(ac ->
System.out.println(redisOperations.opsForValue().get(ac))); //각각의 키로 해당 항공기 값을 조회한 다음 출력
}
}
PlaneFinderPoller.java
6.6 템플릿에서 repository로 변환하기
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.thehecklers.sburredis.Aircraft;
@SpringBootApplication
public class SburRedisApplication {
/*
* @Bean public RedisOperations<String, Aircraft>
* redisOperations(RedisConnectionFactory factory){
* Jackson2JsonRedisSerializer<Aircraft> serializer = new
* Jackson2JsonRedisSerializer<Aircraft>(Aircraft.class);
*
* RedisTemplate<String, Aircraft> template = new RedisTemplate<>();
* template.setConnectionFactory(factory);
* template.setDefaultSerializer(serializer); template.setKeySerializer(new
* StringRedisSerializer());
*
* return template; }
*/
public static void main(String[] args) {
SpringApplication.run(SburRedisApplication.class, args);
}
}
SburRedisApplication.java
package com.thehecklers.sburredis;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@EnableScheduling
@Component
public class PlaneFinderPoller {
private WebClient client =
WebClient.create("http://localhost:7634/aircraft"); //PlaneFinder 서비스에 의해 노풀되는 객체의 엔드포인트를 가리키도록
private final RedisConnectionFactory connectionFactory;
// private final RedisOperations<String, Aircraft> redisOperations; 1) 멤버변수 제거, 아래로 교체
private final AircraftRepository repository;
//Redis ConntctionFactory - 레디스 의존성을 추가했으므로 스프링 부트의 '자동설정'에 의해 제공됨
/*
* PlaneFinderPoller(RedisConnectionFactory connectionFactory,
* RedisOperations<String, Aircraft> redisOperations){ this.connectionFactory =
* connectionFactory; this.redisOperations = redisOperations; }
*/
PlaneFinderPoller(RedisConnectionFactory connectionFactory, AircraftRepository repository){
this.connectionFactory = connectionFactory;
this.repository = repository;
}
@Scheduled(fixedRate=1000)
private void pollPlanes() {
connectionFactory.getConnection().serverCommands().flushDb(); //flushDb(): 존재하는 모든 키를 지움
client.get()
.retrieve()
.bodyToFlux(Aircraft.class)
.filter(plane -> !plane.getReg().isEmpty())
.toStream()
.forEach(repository::save);
/*.forEach(ac ->
redisOperations.opsForValue().set(ac.getReg(), ac));*/
/*
* redisOperations.opsForValue() .getOperations() .keys("*") //모든 키 조회
* .forEach(ac -> System.out.println(redisOperations.opsForValue().get(ac)));
* //각각의 키로 해당 항공기 값을 조회한 다음 출력
*/
repository.findAll().forEach(System.out::println);
}
}
PlaneFinderPoller.java
package com.thehecklers.sburredis;
import org.springframework.data.repository.CrudRepository;
public interface AircraftRepository extends CrudRepository<Aircraft, Long> {
}
AircraftRepository.java
레디스에 값 추가하는 방법..?
책 불친절..

6.7 JPA로 repository 기반 서비스 만들기
package com.example7;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Aircraft {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String callsign, squawk, reg, flightno, route, type, category;
private int altitude, heading, speed;
@JsonProperty("vert_rate")
private int verRate;
@JsonProperty("selected_altitude")
private int selectedAltitude;
private double lat, lon, barometer;
@JsonProperty("polar_distance")
private double polarDistance;
@JsonProperty("polar_bearing")
private double polarBearing;
@JsonProperty("is_adsb")
private boolean isADSB;
@JsonProperty("is_on_ground")
private boolean isOnGround;
@JsonProperty("last_seen_time")
private Instant lastSeenTime;
@JsonProperty("pos_update_time")
private Instant posUpdateTime;
@JsonProperty("bds40_seen_time")
private Instant bds40SeenTime;
}
package com.example7;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@EnableScheduling
@Component
@RequiredArgsConstructor
public class PlaneFinderPoller {
@NonNull //final이면 굳이 안붙여도 될듯..
private final AircraftRepository repository;
private WebClient client = WebClient.create("http://localhost:7634/aircraft");
@Scheduled(fixedRate = 1000)
private void pollPlanes(){
repository.deleteAll();
client.get()
.retrieve()
.bodyToFlux(Aircraft.class)
.filter(plane -> !plane.getReg().isEmpty())
.toStream()
.forEach(repository::save);
repository.findAll().forEach(System.out::println);
}
}
PlaneFinderPoller.java
- @GeneratedValue:: 식별자(Identifier)가 기본 데이터베이스 엔진에 의해 생성될 것을 알려줌
- @EnableScheduling:: 스프링 스케줄러를 사용하기 위해서 Spring Boot 실행 파일에 선언해야 함
- @Scheduled:: 스케줄러 설정을 적용할 메소드에 사용
- @RequiredArgsConstructor:: 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대해 생성자를 생성해줌
(+) 스프링 스케줄러 참고.. https://king-ja.tistory.com/81

6장 공부하면서 느낀점 ..
이 책은 spring boot + α 의 지식이 있어야 .. 이해가 된다..
JPA 부분이 있길래 열심히 보려했는데
내용 보면 .. JPA 공부를 따로 하는게 나을것 같아서 (따라해서 도움이 안될것 같음..)
6장은 여기까지 ..
어노테이션들만 알아두면 될듯 ..!!!