Study/Spring Boot

6. 데이터 파고들기

희투 2023. 12. 14. 21:46

• 스프링 데이터 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장은 여기까지 ..

 

어노테이션들만 알아두면 될듯 ..!!!