Commit 003942b3 authored by Sensor MVP Team's avatar Sensor MVP Team
Browse files

document add

parent b33d4b9a
This diff is collapsed.
# Java Spring(전자정부프레임워크) 센서 시스템 개발 가이드 - Part 1
## 📋 프로젝트 개요
**프로젝트명**: Java Spring 기반 센서 데이터 수집 및 관리 시스템
**기술 스택**: Java 11, Spring Boot 2.7.x, 전자정부프레임워크 4.0.x
**아키텍처**: 계층형 아키텍처 + REST API
**데이터베이스**: PostgreSQL, Redis
**상태**: 개발 가이드 작성 완료
---
## 🏗️ 시스템 아키텍처
### 전체 시스템 구성도
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 센서 디바이스 │────│ Java 브리지 │────│ Spring Boot │
│ (RSNet SDK) │ │ (RSNet SDK) │ │ (REST API) │
│ │ │ (포트: 8020) │ │ (포트: 8080) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 웹 대시보드 │◄───│ PostgreSQL │◄───│ Redis 캐시 │
│ (Thymeleaf) │ │ (데이터 저장) │ │ (실시간 캐시) │
│ (포트: 8080) │ │ (포트: 5432) │ │ (포트: 6379) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 서비스 구성
| 서비스 | 기술 스택 | 포트 | 역할 | 상태 |
|--------|-----------|------|------|------|
| **sensor-spring-app** | Spring Boot + 전자정부프레임워크 | 8080 | 메인 웹 애플리케이션 | 🔄 개발 중 |
| **sensor-bridge** | Java + RSNet SDK | 8020 | 센서 데이터 브리지 | ✅ 기존 유지 |
| **postgres** | PostgreSQL 15 | 5432 | 메인 데이터베이스 | ✅ 기존 유지 |
| **redis** | Redis 7 | 6379 | 실시간 캐시 | ✅ 기존 유지 |
---
## 📁 프로젝트 구조
### 새로운 Spring 애플리케이션 구조
```
sensor-spring-app/
├── 📁 src/
│ ├── 📁 main/
│ │ ├── 📁 java/
│ │ │ └── 📁 com/sensor/spring/
│ │ │ ├── 📁 config/ # 설정 클래스
│ │ │ ├── 📁 controller/ # REST 컨트롤러
│ │ │ ├── 📁 service/ # 비즈니스 로직
│ │ │ ├── 📁 repository/ # 데이터 접근 계층
│ │ │ ├── 📁 entity/ # JPA 엔티티
│ │ │ ├── 📁 dto/ # 데이터 전송 객체
│ │ │ ├── 📁 exception/ # 예외 처리
│ │ │ ├── 📁 util/ # 유틸리티
│ │ │ └── 📄 SensorSpringApplication.java
│ │ ├── 📁 resources/
│ │ │ ├── 📁 static/ # 정적 리소스
│ │ │ ├── 📁 templates/ # Thymeleaf 템플릿
│ │ │ ├── 📄 application.yml # Spring 설정
│ │ │ ├── 📄 application-dev.yml # 개발 환경 설정
│ │ │ └── 📄 application-prod.yml # 운영 환경 설정
│ │ └── 📁 webapp/ # 전자정부프레임워크 설정
│ └── 📁 test/ # 테스트 코드
├── 📄 pom.xml # Maven 프로젝트 설정
├── 📄 Dockerfile # Docker 이미지 빌드
└── 📄 README.md # 프로젝트 문서
```
---
## 🚀 개발 환경 설정
### 1. 필수 요구사항
- **Java**: JDK 11 이상
- **Maven**: 3.6.x 이상
- **IDE**: IntelliJ IDEA, Eclipse, VS Code
- **데이터베이스**: PostgreSQL 15, Redis 7
- **기존 시스템**: sensor-bridge, postgres, redis
### 2. Maven 프로젝트 생성
#### **pom.xml 설정**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.sensor</groupId>
<artifactId>sensor-spring-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Sensor Spring Application</name>
<description>Spring Boot based sensor data management system</description>
<properties>
<java.version>11</java.version>
<egovframework.rte.version>4.0.0</egovframework.rte.version>
<spring-boot.version>2.7.18</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 전자정부프레임워크 -->
<dependency>
<groupId>org.egovframe.rte</groupId>
<artifactId>org.egovframe.rte.ptl.mvc</artifactId>
<version>${egovframework.rte.version}</version>
</dependency>
<dependency>
<groupId>org.egovframe.rte</groupId>
<artifactId>org.egovframe.rte.psl.dataaccess</artifactId>
<version>${egovframework.rte.version}</version>
</dependency>
<dependency>
<groupId>org.egovframe.rte</groupId>
<artifactId>org.egovframe.rte.fdl.idgnr</artifactId>
<version>${egovframework.rte.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
### 3. Spring Boot 메인 클래스
#### **SensorSpringApplication.java**
```java
package com.sensor.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableJpaAuditing
@EnableCaching
@EnableAsync
@EnableScheduling
public class SensorSpringApplication {
public static void main(String[] args) {
SpringApplication.run(SensorSpringApplication.class, args);
}
}
```
---
## ⚙️ 설정 파일
### 1. application.yml (메인 설정)
```yaml
spring:
profiles:
active: dev
application:
name: sensor-spring-app
datasource:
url: jdbc:postgresql://localhost:5432/sensor_db
username: postgres
password: password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
redis:
host: localhost
port: 6379
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
mode: HTML
server:
port: 8080
servlet:
context-path: /
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
logging:
level:
com.sensor.spring: DEBUG
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
```
### 2. application-dev.yml (개발 환경)
```yaml
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
h2:
console:
enabled: true
logging:
level:
com.sensor.spring: DEBUG
org.springframework.web: DEBUG
```
### 3. application-prod.yml (운영 환경)
```yaml
spring:
jpa:
show-sql: false
hibernate:
ddl-auto: validate
logging:
level:
com.sensor.spring: INFO
org.springframework.web: WARN
```
---
## 🗄️ 데이터 모델
### 1. JPA 엔티티
#### **SensorReading.java**
```java
package com.sensor.spring.entity;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Entity
@Table(name = "sensor_readings")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorReading {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(name = "device_id", nullable = false)
private String deviceId;
@Column(name = "node_id")
private Integer nodeId;
@Column(name = "temperature", precision = 5, scale = 2)
private Double temperature;
@Column(name = "humidity", precision = 5, scale = 2)
private Double humidity;
@Column(name = "longitude", precision = 10, scale = 6)
private Double longitude;
@Column(name = "latitude", precision = 10, scale = 6)
private Double latitude;
@Column(name = "recorded_time")
private LocalDateTime recordedTime;
@Column(name = "received_time")
private LocalDateTime receivedTime;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
if (receivedTime == null) {
receivedTime = LocalDateTime.now();
}
}
}
```
#### **Device.java**
```java
package com.sensor.spring.entity;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Entity
@Table(name = "devices")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Device {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(name = "device_id", unique = true, nullable = false)
private String deviceId;
@Column(name = "name")
private String name;
@Column(name = "description")
private String description;
@Enumerated(EnumType.STRING)
@Column(name = "status")
private DeviceStatus status;
@Column(name = "last_seen")
private LocalDateTime lastSeen;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
public enum DeviceStatus {
ACTIVE, INACTIVE, ERROR, MAINTENANCE
}
}
```
---
**계속...**
# Java Spring(전자정부프레임워크) 센서 시스템 개발 가이드 - Part 2
## 🔧 계층별 구현
### 1. DTO 클래스
#### **SensorDataRequest.java**
```java
package com.sensor.spring.dto;
import lombok.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorDataRequest {
@NotNull
private String deviceId;
private Integer nodeId;
private Double temperature;
private Double humidity;
private Double longitude;
private Double latitude;
private LocalDateTime recordedTime;
}
```
#### **SensorDataResponse.java**
```java
package com.sensor.spring.dto;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorDataResponse {
private Long id;
private String deviceId;
private Integer nodeId;
private Double temperature;
private Double humidity;
private Double longitude;
private Double latitude;
private LocalDateTime recordedTime;
private LocalDateTime receivedTime;
private LocalDateTime createdAt;
}
```
### 2. Repository 계층
#### **SensorReadingRepository.java**
```java
package com.sensor.spring.repository;
import com.sensor.spring.entity.SensorReading;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface SensorReadingRepository extends JpaRepository<SensorReading, Long> {
List<SensorReading> findByDeviceIdOrderByRecordedTimeDesc(String deviceId);
Optional<SensorReading> findFirstByDeviceIdOrderByRecordedTimeDesc(String deviceId);
@Query("SELECT sr FROM SensorReading sr WHERE sr.deviceId = :deviceId " +
"AND sr.recordedTime BETWEEN :startTime AND :endTime " +
"ORDER BY sr.recordedTime DESC")
List<SensorReading> findByDeviceIdAndTimeRange(
@Param("deviceId") String deviceId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime
);
@Query("SELECT COUNT(sr) FROM SensorReading sr WHERE sr.deviceId = :deviceId " +
"AND sr.recordedTime >= :since")
Long countByDeviceIdSince(@Param("deviceId") String deviceId,
@Param("since") LocalDateTime since);
}
```
#### **DeviceRepository.java**
```java
package com.sensor.spring.repository;
import com.sensor.spring.entity.Device;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface DeviceRepository extends JpaRepository<Device, Long> {
Optional<Device> findByDeviceId(String deviceId);
List<Device> findByStatus(Device.DeviceStatus status);
List<Device> findByLastSeenBefore(java.time.LocalDateTime time);
}
```
### 3. Service 계층
#### **SensorDataService.java**
```java
package com.sensor.spring.service;
import com.sensor.spring.dto.SensorDataRequest;
import com.sensor.spring.dto.SensorDataResponse;
import com.sensor.spring.entity.SensorReading;
import com.sensor.spring.repository.SensorReadingRepository;
import com.sensor.spring.repository.DeviceRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class SensorDataService {
private final SensorReadingRepository sensorReadingRepository;
private final DeviceRepository deviceRepository;
private final RedisCacheService redisCacheService;
@Transactional
public SensorDataResponse saveSensorData(SensorDataRequest request) {
log.info("Saving sensor data for device: {}", request.getDeviceId());
// 디바이스 상태 업데이트
updateDeviceLastSeen(request.getDeviceId());
// 센서 데이터 저장
SensorReading reading = SensorReading.builder()
.deviceId(request.getDeviceId())
.nodeId(request.getNodeId())
.temperature(request.getTemperature())
.humidity(request.getHumidity())
.longitude(request.getLongitude())
.latitude(request.getLatitude())
.recordedTime(request.getRecordedTime())
.receivedTime(LocalDateTime.now())
.build();
SensorReading saved = sensorReadingRepository.save(reading);
// Redis 캐시 업데이트
redisCacheService.updateLatestData(request.getDeviceId(), saved);
return convertToResponse(saved);
}
@Cacheable(value = "latestData", key = "#deviceId")
public SensorDataResponse getLatestData(String deviceId) {
log.info("Getting latest data for device: {}", deviceId);
return sensorReadingRepository
.findFirstByDeviceIdOrderByRecordedTimeDesc(deviceId)
.map(this::convertToResponse)
.orElse(null);
}
@Cacheable(value = "deviceHistory", key = "#deviceId + '_' + #days")
public List<SensorDataResponse> getDeviceHistory(String deviceId, int days) {
log.info("Getting {} days history for device: {}", days, deviceId);
LocalDateTime since = LocalDateTime.now().minusDays(days);
return sensorReadingRepository
.findByDeviceIdAndTimeRange(deviceId, since, LocalDateTime.now())
.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
@CacheEvict(value = {"latestData", "deviceHistory"}, allEntries = true)
public void clearCache() {
log.info("Clearing all sensor data cache");
}
private void updateDeviceLastSeen(String deviceId) {
deviceRepository.findByDeviceId(deviceId)
.ifPresent(device -> {
device.setLastSeen(LocalDateTime.now());
device.setStatus(Device.DeviceStatus.ACTIVE);
deviceRepository.save(device);
});
}
private SensorDataResponse convertToResponse(SensorReading reading) {
return SensorDataResponse.builder()
.id(reading.getId())
.deviceId(reading.getDeviceId())
.nodeId(reading.getNodeId())
.temperature(reading.getTemperature())
.humidity(reading.getHumidity())
.longitude(reading.getLongitude())
.latitude(reading.getLatitude())
.recordedTime(reading.getRecordedTime())
.receivedTime(reading.getReceivedTime())
.createdAt(reading.getCreatedAt())
.build();
}
}
```
### 4. Controller 계층
#### **SensorDataController.java**
```java
package com.sensor.spring.controller;
import com.sensor.spring.dto.SensorDataRequest;
import com.sensor.spring.dto.SensorDataResponse;
import com.sensor.spring.service.SensorDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/sensor-data")
@RequiredArgsConstructor
@Slf4j
@Validated
public class SensorDataController {
private final SensorDataService sensorDataService;
@PostMapping
public ResponseEntity<SensorDataResponse> saveSensorData(
@Valid @RequestBody SensorDataRequest request) {
log.info("Received sensor data request: {}", request);
try {
SensorDataResponse response = sensorDataService.saveSensorData(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (Exception e) {
log.error("Error saving sensor data: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/devices/{deviceId}/latest")
public ResponseEntity<SensorDataResponse> getLatestData(
@PathVariable String deviceId) {
log.info("Getting latest data for device: {}", deviceId);
SensorDataResponse response = sensorDataService.getLatestData(deviceId);
if (response != null) {
return ResponseEntity.ok(response);
} else {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/devices/{deviceId}/history")
public ResponseEntity<List<SensorDataResponse>> getDeviceHistory(
@PathVariable String deviceId,
@RequestParam(defaultValue = "7") int days) {
log.info("Getting {} days history for device: {}", days, deviceId);
List<SensorDataResponse> history = sensorDataService.getDeviceHistory(deviceId, days);
return ResponseEntity.ok(history);
}
@DeleteMapping("/cache")
public ResponseEntity<Void> clearCache() {
log.info("Clearing sensor data cache");
sensorDataService.clearCache();
return ResponseEntity.ok().build();
}
}
```
---
## 🎨 웹 인터페이스
### 1. Thymeleaf 템플릿
#### **dashboard.html**
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>센서 데이터 대시보드</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">센서 모니터링 시스템</a>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>실시간 센서 데이터</h5>
</div>
<div class="card-body">
<div id="latestData">
<p>데이터를 불러오는 중...</p>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>온도 변화 그래프</h5>
</div>
<div class="card-body">
<canvas id="temperatureChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5>디바이스 목록</h5>
</div>
<div class="card-body">
<div id="deviceList">
<p>디바이스 목록을 불러오는 중...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
<script th:src="@{/js/dashboard.js}"></script>
</body>
</html>
```
### 2. JavaScript 파일
#### **dashboard.js**
```javascript
// 센서 데이터 대시보드 JavaScript
class SensorDashboard {
constructor() {
this.temperatureChart = null;
this.init();
}
init() {
this.initTemperatureChart();
this.loadLatestData();
this.loadDeviceList();
this.startRealTimeUpdates();
}
initTemperatureChart() {
const ctx = document.getElementById('temperatureChart').getContext('2d');
this.temperatureChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '온도 (°C)',
data: [],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: false
}
}
}
});
}
async loadLatestData() {
try {
const response = await fetch('/api/sensor-data/devices/15328737/latest');
if (response.ok) {
const data = await response.json();
this.displayLatestData(data);
}
} catch (error) {
console.error('Error loading latest data:', error);
}
}
displayLatestData(data) {
const container = document.getElementById('latestData');
container.innerHTML = `
<div class="row">
<div class="col-6">
<strong>디바이스 ID:</strong> ${data.deviceId}<br>
<strong>온도:</strong> ${data.temperature}°C<br>
<strong>습도:</strong> ${data.humidity}%
</div>
<div class="col-6">
<strong>위도:</strong> ${data.latitude}<br>
<strong>경도:</strong> ${data.longitude}<br>
<strong>수신 시간:</strong> ${new Date(data.receivedTime).toLocaleString()}
</div>
</div>
`;
}
async loadDeviceList() {
try {
const response = await fetch('/api/devices');
if (response.ok) {
const devices = await response.json();
this.displayDeviceList(devices);
}
} catch (error) {
console.error('Error loading device list:', error);
}
}
displayDeviceList(devices) {
const container = document.getElementById('deviceList');
const table = `
<table class="table table-striped">
<thead>
<tr>
<th>디바이스 ID</th>
<th>이름</th>
<th>상태</th>
<th>마지막 연결</th>
</tr>
</thead>
<tbody>
${devices.map(device => `
<tr>
<td>${device.deviceId}</td>
<td>${device.name || '-'}</td>
<td><span class="badge bg-${this.getStatusColor(device.status)}">${device.status}</span></td>
<td>${device.lastSeen ? new Date(device.lastSeen).toLocaleString() : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = table;
}
getStatusColor(status) {
switch (status) {
case 'ACTIVE': return 'success';
case 'INACTIVE': return 'secondary';
case 'ERROR': return 'danger';
case 'MAINTENANCE': return 'warning';
default: return 'secondary';
}
}
startRealTimeUpdates() {
setInterval(() => {
this.loadLatestData();
}, 30000); // 30초마다 업데이트
}
}
// 페이지 로드 시 대시보드 초기화
document.addEventListener('DOMContentLoaded', () => {
new SensorDashboard();
});
```
---
## 🔄 기존 시스템 연동
### 1. Docker Compose 수정
#### **docker-compose.yml 업데이트**
```yaml
version: '3.8'
services:
# 기존 서비스들
postgres:
image: postgres:15
environment:
POSTGRES_DB: sensor_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
sensor-bridge:
build: ./sensor-bridge
ports:
- "8020:8020"
environment:
GO_SERVER_URL: http://sensor-spring-app:8080
depends_on:
- postgres
- redis
# 새로운 Spring 애플리케이션
sensor-spring-app:
build: ./sensor-spring-app
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/sensor_db
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: password
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
depends_on:
- postgres
- redis
volumes:
- ./sensor-spring-app/logs:/app/logs
volumes:
postgres_data:
redis_data:
```
### 2. 기존 Go 서버와의 차이점
| 구분 | Go 서버 | Spring Boot |
|------|---------|-------------|
| **언어** | Go | Java 11 |
| **프레임워크** | Gin | Spring Boot + 전자정부프레임워크 |
| **데이터 접근** | 직접 SQL | JPA + Hibernate |
| **캐싱** | Redis 클라이언트 | Spring Data Redis |
| **템플릿** | 없음 | Thymeleaf |
| **보안** | 기본 | Spring Security (추가 가능) |
| **모니터링** | 기본 | Spring Actuator + Micrometer |
---
**계속...**
# Java Spring(전자정부프레임워크) 센서 시스템 개발 가이드 - Part 3
## 🚀 배포 및 실행
### 1. 개발 환경 실행
```bash
# 프로젝트 디렉토리로 이동
cd sensor-spring-app
# Maven 의존성 설치
mvn clean install
# Spring Boot 애플리케이션 실행
mvn spring-boot:run
```
### 2. Docker 빌드 및 실행
```bash
# 전체 시스템 실행
docker-compose up -d
# 특정 서비스만 실행
docker-compose up -d sensor-spring-app
# 로그 확인
docker-compose logs -f sensor-spring-app
```
### 3. 운영 환경 배포
```bash
# JAR 파일 빌드
mvn clean package -DskipTests
# JAR 파일 실행
java -jar target/sensor-spring-app-1.0.0.jar \
--spring.profiles.active=prod \
--server.port=8080
```
---
## 📊 모니터링 및 로깅
### 1. Spring Actuator 엔드포인트
- **Health Check**: `/actuator/health`
- **Info**: `/actuator/info`
- **Metrics**: `/actuator/metrics`
- **Prometheus**: `/actuator/prometheus`
### 2. 로깅 설정
```yaml
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/sensor-spring-app.log
level:
com.sensor.spring: INFO
org.springframework.web: INFO
org.hibernate.SQL: DEBUG
```
---
## 🧪 테스트
### 1. 단위 테스트
```java
@SpringBootTest
class SensorDataServiceTest {
@Autowired
private SensorDataService sensorDataService;
@Test
void testSaveSensorData() {
SensorDataRequest request = SensorDataRequest.builder()
.deviceId("TEST_DEVICE")
.temperature(25.5)
.humidity(60.0)
.build();
SensorDataResponse response = sensorDataService.saveSensorData(request);
assertNotNull(response);
assertEquals("TEST_DEVICE", response.getDeviceId());
assertEquals(25.5, response.getTemperature());
}
}
```
### 2. 통합 테스트
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class SensorDataControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testSaveSensorDataEndpoint() {
SensorDataRequest request = new SensorDataRequest();
request.setDeviceId("TEST_DEVICE");
request.setTemperature(25.5);
ResponseEntity<SensorDataResponse> response = restTemplate.postForEntity(
"/api/sensor-data", request, SensorDataResponse.class);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertNotNull(response.getBody());
}
}
```
---
## 🔒 보안 설정
### 1. Spring Security 설정
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll()
.and()
.csrf().disable()
.httpBasic();
}
}
```
### 2. CORS 설정
```java
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
```
---
## 📈 성능 최적화
### 1. 캐싱 전략
- **Redis 캐싱**: 최신 데이터, 디바이스 목록
- **JPA 2nd Level Cache**: 엔티티 캐싱
- **HTTP 응답 캐싱**: 정적 리소스
### 2. 데이터베이스 최적화
- **인덱스**: device_id, recorded_time
- **파티셔닝**: 날짜별 테이블 파티셔닝
- **Connection Pool**: HikariCP 설정
### 3. 비동기 처리
- **@Async**: 센서 데이터 저장
- **@Scheduled**: 정기적인 데이터 정리
- **WebSocket**: 실시간 데이터 스트리밍
---
## 🐛 문제 해결
### 1. 일반적인 문제
#### **포트 충돌**
```bash
# 사용 중인 포트 확인
netstat -tulpn | grep :8080
# Spring Boot 애플리케이션 중지
pkill -f "sensor-spring-app"
```
#### **데이터베이스 연결 실패**
```bash
# PostgreSQL 상태 확인
docker-compose ps postgres
# 연결 테스트
docker exec -it sensor-postgres psql -U postgres -d sensor_db
```
#### **메모리 부족**
```bash
# JVM 메모리 설정
java -Xmx2g -Xms1g -jar target/sensor-spring-app-1.0.0.jar
```
### 2. 로그 분석
```bash
# 애플리케이션 로그 확인
tail -f logs/sensor-spring-app.log
# 에러 로그 필터링
grep "ERROR" logs/sensor-spring-app.log
# 특정 디바이스 로그
grep "deviceId=15328737" logs/sensor-spring-app.log
```
---
## 📝 향후 개선 계획
### 1. 단기 개선 (1-2주)
- [ ] Spring Security 인증/인가 시스템 구축
- [ ] API 문서화 (Swagger/OpenAPI)
- [ ] 단위 테스트 커버리지 향상
### 2. 중기 개선 (1-2개월)
- [ ] 실시간 알림 시스템 (WebSocket)
- [ ] 데이터 검증 및 이상치 탐지
- [ ] 성능 모니터링 대시보드
### 3. 장기 개선 (3-6개월)
- [ ] 머신러닝 기반 예측 시스템
- [ ] 마이크로서비스 아키텍처 전환
- [ ] 클라우드 네이티브 배포
---
## 📚 참고 자료
### 1. 공식 문서
- [Spring Boot Reference](https://docs.spring.io/spring-boot/docs/current/reference/html/)
- [전자정부프레임워크 가이드](https://www.egovframe.go.kr/home/sub.do?menuNo=9)
- [Spring Data JPA](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)
### 2. 관련 라이브러리
- [Thymeleaf](https://www.thymeleaf.org/documentation.html)
- [Chart.js](https://www.chartjs.org/docs/latest/)
- [Bootstrap](https://getbootstrap.com/docs/)
---
## ✅ 체크리스트
### **개발 환경 설정**
- [x] Java 11 설치
- [x] Maven 3.6.x 설치
- [x] IDE 설정
- [x] 프로젝트 구조 생성
### **핵심 기능 구현**
- [x] JPA 엔티티 정의
- [x] Repository 계층 구현
- [x] Service 계층 구현
- [x] Controller 계층 구현
- [x] 웹 인터페이스 구현
### **설정 및 배포**
- [x] application.yml 설정
- [x] Docker 설정
- [x] 데이터베이스 연동
- [x] Redis 캐싱 설정
### **테스트 및 모니터링**
- [x] 단위 테스트 작성
- [x] 통합 테스트 작성
- [x] 로깅 설정
- [x] Actuator 설정
---
## 🔄 기존 시스템과의 연동 시나리오
### 1. 점진적 마이그레이션
```
1단계: Spring Boot 애플리케이션 추가
├── 기존 Go 서버 유지 (포트: 8080)
├── 새로운 Spring Boot 추가 (포트: 8081)
└── 로드 밸런서로 트래픽 분산
2단계: 기능별 전환
├── 센서 데이터 수집: Spring Boot로 전환
├── 데이터 조회: Spring Boot로 전환
└── 웹 대시보드: Spring Boot로 전환
3단계: 완전 전환
├── Go 서버 중단
├── Spring Boot를 메인 서비스로 설정
└── 포트를 8080으로 변경
```
### 2. 데이터 마이그레이션
```sql
-- 기존 테이블 구조 유지
-- 새로운 인덱스 추가
CREATE INDEX idx_sensor_readings_device_time
ON sensor_readings(device_id, recorded_time);
-- 파티셔닝 테이블 생성 (선택사항)
CREATE TABLE sensor_readings_2024 PARTITION OF sensor_readings
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
```
### 3. API 호환성 유지
```java
// 기존 Go 서버와 동일한 API 엔드포인트 제공
@RestController
@RequestMapping("/api")
public class LegacyApiController {
@PostMapping("/sensor-data")
public ResponseEntity<SensorDataResponse> legacySensorData(
@RequestBody LegacySensorDataRequest request) {
// 기존 형식의 요청을 새로운 형식으로 변환
SensorDataRequest newRequest = convertLegacyRequest(request);
return sensorDataService.saveSensorData(newRequest);
}
}
```
---
## 🎯 개발 우선순위
### **Phase 1: 기본 구조 (1-2주)**
1. Spring Boot 프로젝트 생성
2. 기본 엔티티 및 Repository 구현
3. 간단한 REST API 구현
4. 데이터베이스 연결 테스트
### **Phase 2: 핵심 기능 (2-3주)**
1. 센서 데이터 수집 API 완성
2. 데이터 조회 및 히스토리 API 구현
3. Redis 캐싱 시스템 구축
4. 기본 웹 대시보드 구현
### **Phase 3: 고도화 (3-4주)**
1. 전자정부프레임워크 통합
2. 보안 시스템 구축
3. 모니터링 및 로깅 시스템
4. 성능 최적화
### **Phase 4: 테스트 및 배포 (1-2주)**
1. 단위/통합 테스트 작성
2. Docker 컨테이너화
3. 운영 환경 배포
4. 기존 시스템과의 연동 테스트
---
## 💡 개발 팁
### 1. Spring Boot 개발 팁
- **프로파일 활용**: dev, test, prod 환경별 설정 분리
- **자동 설정**: Spring Boot의 자동 설정 기능 최대한 활용
- **의존성 관리**: Maven BOM을 통한 버전 관리
### 2. JPA 사용 팁
- **N+1 문제 방지**: fetch join, @EntityGraph 활용
- **벌크 연산**: JpaRepository.saveAll() 활용
- **트랜잭션 관리**: @Transactional 범위 최소화
### 3. 성능 최적화 팁
- **캐싱 전략**: Redis + JPA 2nd Level Cache 조합
- **비동기 처리**: @Async + CompletableFuture 활용
- **데이터베이스 최적화**: 인덱스, 파티셔닝, 커넥션 풀
---
## 🚨 주의사항
### 1. 보안 관련
- **입력 검증**: 모든 사용자 입력에 대한 검증 필수
- **SQL 인젝션**: JPA 사용으로 자동 방지
- **XSS 방지**: Thymeleaf의 기본 이스케이프 활용
### 2. 성능 관련
- **메모리 사용량**: JVM 힙 메모리 모니터링 필수
- **데이터베이스 연결**: 커넥션 풀 크기 적절히 설정
- **캐시 무효화**: 데이터 변경 시 캐시 무효화 전략 수립
### 3. 운영 관련
- **로그 관리**: 로그 파일 크기 및 보관 기간 설정
- **모니터링**: Spring Actuator + Prometheus 연동
- **백업**: 데이터베이스 정기 백업 및 복구 테스트
---
## 📞 지원 및 문의
### **개발 지원**
- **기술 문서**: 프로젝트 내 README.md 참조
- **API 문서**: Swagger UI (/swagger-ui.html)
- **모니터링**: Spring Actuator 엔드포인트 활용
### **문제 해결**
- **로그 분석**: 애플리케이션 로그 및 시스템 로그 확인
- **성능 분석**: JVM 메트릭 및 데이터베이스 쿼리 분석
- **네트워크 분석**: 포트 및 연결 상태 확인
---
**작성일**: 2024년 12월
**작성자**: AI Assistant
**버전**: 1.0.0
---
*이 가이드는 Java Spring(전자정부프레임워크)으로 센서 정보를 수집하고 관리하는 웹프로그램을 개발하기 위한 것입니다.
추가 질문이나 명확화가 필요한 부분이 있으면 언제든 연락주세요.*
# Java Spring(전자정부프레임워크) 센서 시스템 개발 가이드 - 전체 목차
## 📚 가이드 구성
현재 프로젝트를 바탕으로 Java Spring(전자정부프레임워크)으로 센서 정보를 수집하고 관리하는 웹프로그램을 개발하기 위한 완전한 가이드입니다.
---
## 📖 가이드 목차
### **Part 1: 프로젝트 개요 및 기본 설정**
- [Java_Spring_센서_시스템_개발_가이드_1.md](./Java_Spring_센서_시스템_개발_가이드_1.md)
- 프로젝트 개요 및 시스템 아키텍처
- 프로젝트 구조 및 개발 환경 설정
- Maven 프로젝트 설정 (pom.xml)
- Spring Boot 메인 클래스
- 설정 파일 (application.yml)
- 데이터 모델 (JPA 엔티티)
### **Part 2: 핵심 구현 및 웹 인터페이스**
- [Java_Spring_센서_시스템_개발_가이드_2.md](./Java_Spring_센서_시스템_개발_가이드_2.md)
- DTO 클래스 및 Repository 계층
- Service 계층 및 Controller 계층
- 웹 인터페이스 (Thymeleaf + JavaScript)
- 기존 시스템 연동 방법
- Docker Compose 설정
### **Part 3: 배포, 테스트 및 운영**
- [Java_Spring_센서_시스템_개발_가이드_3.md](./Java_Spring_센서_시스템_개발_가이드_3.md)
- 배포 및 실행 방법
- 모니터링 및 로깅
- 테스트 코드 작성
- 보안 설정 및 성능 최적화
- 문제 해결 가이드
- 향후 개선 계획
---
## 🎯 주요 특징
### **기술 스택**
- **백엔드**: Java 11 + Spring Boot 2.7.x + 전자정부프레임워크 4.0.x
- **데이터베이스**: PostgreSQL + Redis
- **웹 프론트엔드**: Thymeleaf + Bootstrap + Chart.js
- **빌드 도구**: Maven
- **컨테이너**: Docker + Docker Compose
### **아키텍처**
- **계층형 아키텍처**: Controller → Service → Repository → Entity
- **REST API**: 센서 데이터 수집 및 조회
- **캐싱 전략**: Redis + JPA 2nd Level Cache
- **모니터링**: Spring Actuator + Micrometer
### **기존 시스템과의 차이점**
| 구분 | 기존 Go 서버 | 새로운 Spring Boot |
|------|-------------|-------------------|
| **언어** | Go | Java 11 |
| **프레임워크** | Gin | Spring Boot + 전자정부프레임워크 |
| **데이터 접근** | 직접 SQL | JPA + Hibernate |
| **웹 인터페이스** | 없음 | Thymeleaf 템플릿 |
| **보안** | 기본 | Spring Security |
| **모니터링** | 기본 | Spring Actuator |
---
## 🚀 빠른 시작
### **1. 개발 환경 준비**
```bash
# Java 11 설치 확인
java -version
# Maven 설치 확인
mvn -version
# 프로젝트 클론
git clone <repository-url>
cd docker_sensor_server
```
### **2. Spring 애플리케이션 생성**
```bash
# 새로운 Spring 프로젝트 디렉토리 생성
mkdir sensor-spring-app
cd sensor-spring-app
# 가이드의 pom.xml 복사 및 Maven 의존성 설치
mvn clean install
```
### **3. 기존 시스템과 함께 실행**
```bash
# 전체 시스템 실행 (기존 + 새로운 Spring 앱)
docker-compose up -d
# Spring 애플리케이션만 실행
docker-compose up -d sensor-spring-app
```
---
## 📋 구현 체크리스트
### **Phase 1: 기본 구조 (1-2주)**
- [ ] Spring Boot 프로젝트 생성
- [ ] 기본 엔티티 및 Repository 구현
- [ ] 간단한 REST API 구현
- [ ] 데이터베이스 연결 테스트
### **Phase 2: 핵심 기능 (2-3주)**
- [ ] 센서 데이터 수집 API 완성
- [ ] 데이터 조회 및 히스토리 API 구현
- [ ] Redis 캐싱 시스템 구축
- [ ] 기본 웹 대시보드 구현
### **Phase 3: 고도화 (3-4주)**
- [ ] 전자정부프레임워크 통합
- [ ] 보안 시스템 구축
- [ ] 모니터링 및 로깅 시스템
- [ ] 성능 최적화
### **Phase 4: 테스트 및 배포 (1-2주)**
- [ ] 단위/통합 테스트 작성
- [ ] Docker 컨테이너화
- [ ] 운영 환경 배포
- [ ] 기존 시스템과의 연동 테스트
---
## 🔗 관련 파일
### **기존 시스템 파일**
- `sensor-bridge/` - Java 센서 브리지 (RSNet SDK)
- `docker-compose.yml` - 전체 시스템 설정
- `README.md` - 기존 프로젝트 문서
- `SENSOR_MVP_인수인계서.md` - 상세 인수인계서
### **새로운 Spring 애플리케이션**
- `sensor-spring-app/` - Spring Boot 애플리케이션 (가이드에 따라 생성)
- `Java_Spring_센서_시스템_개발_가이드_*.md` - 개발 가이드 문서
---
## 💡 개발 팁
### **Spring Boot 개발**
- 프로파일을 활용한 환경별 설정 분리
- Spring Boot의 자동 설정 기능 최대한 활용
- Maven BOM을 통한 의존성 버전 관리
### **JPA 사용**
- N+1 문제 방지를 위한 fetch join, @EntityGraph 활용
- 벌크 연산을 위한 JpaRepository.saveAll() 활용
- @Transactional 범위 최소화
### **성능 최적화**
- Redis + JPA 2nd Level Cache 조합
- @Async + CompletableFuture를 활용한 비동기 처리
- 데이터베이스 인덱스, 파티셔닝, 커넥션 풀 최적화
---
## 🚨 주의사항
### **보안**
- 모든 사용자 입력에 대한 검증 필수
- JPA 사용으로 SQL 인젝션 자동 방지
- Thymeleaf의 기본 이스케이프 활용
### **성능**
- JVM 힙 메모리 모니터링 필수
- 데이터베이스 커넥션 풀 크기 적절히 설정
- 데이터 변경 시 캐시 무효화 전략 수립
### **운영**
- 로그 파일 크기 및 보관 기간 설정
- Spring Actuator + Prometheus 연동
- 데이터베이스 정기 백업 및 복구 테스트
---
## 📞 지원 및 문의
### **개발 지원**
- **기술 문서**: 각 가이드 파일 참조
- **API 문서**: Swagger UI (/swagger-ui.html)
- **모니터링**: Spring Actuator 엔드포인트 활용
### **문제 해결**
- **로그 분석**: 애플리케이션 로그 및 시스템 로그 확인
- **성능 분석**: JVM 메트릭 및 데이터베이스 쿼리 분석
- **네트워크 분석**: 포트 및 연결 상태 확인
---
## 📝 변경 이력
| 날짜 | 버전 | 변경 내용 | 작성자 |
|------|------|-----------|--------|
| 2024-12-XX | 1.0.0 | 초기 가이드 작성 완료 | AI Assistant |
---
**작성일**: 2024년 12월
**작성자**: AI Assistant
**버전**: 1.0.0
---
*이 가이드는 현재 구현된 프로젝트를 바탕으로 Java Spring(전자정부프레임워크)으로 센서 정보를 수집하고 관리하는 웹프로그램을 개발하기 위한 완전한 가이드입니다.
각 Part를 순서대로 참조하여 단계별로 개발을 진행하시기 바랍니다.*
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment