# 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 { List findByDeviceIdOrderByRecordedTimeDesc(String deviceId); Optional 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 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 { Optional findByDeviceId(String deviceId); List findByStatus(Device.DeviceStatus status); List 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 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 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 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> getDeviceHistory( @PathVariable String deviceId, @RequestParam(defaultValue = "7") int days) { log.info("Getting {} days history for device: {}", days, deviceId); List history = sensorDataService.getDeviceHistory(deviceId, days); return ResponseEntity.ok(history); } @DeleteMapping("/cache") public ResponseEntity clearCache() { log.info("Clearing sensor data cache"); sensorDataService.clearCache(); return ResponseEntity.ok().build(); } } ``` --- ## 🎨 웹 인터페이스 ### 1. Thymeleaf 템플릿 #### **dashboard.html** ```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 = `
디바이스 ID: ${data.deviceId}
온도: ${data.temperature}°C
습도: ${data.humidity}%
위도: ${data.latitude}
경도: ${data.longitude}
수신 시간: ${new Date(data.receivedTime).toLocaleString()}
`; } 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 = ` ${devices.map(device => ` `).join('')}
디바이스 ID 이름 상태 마지막 연결
${device.deviceId} ${device.name || '-'} ${device.status} ${device.lastSeen ? new Date(device.lastSeen).toLocaleString() : '-'}
`; 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 | --- **계속...**