# Java Spring(전자정부프레임워크) 센서 시스템 개발 가이드 ## 📋 프로젝트 개요 **프로젝트명**: 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 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.18 com.sensor sensor-spring-app 1.0.0 jar Sensor Spring Application Spring Boot based sensor data management system 11 4.0.0 2.7.18 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-actuator org.egovframe.rte org.egovframe.rte.ptl.mvc ${egovframework.rte.version} org.egovframe.rte org.egovframe.rte.psl.dataaccess ${egovframework.rte.version} org.egovframe.rte org.egovframe.rte.fdl.idgnr ${egovframework.rte.version} org.postgresql postgresql runtime org.flywaydb flyway-core com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-jsr310 org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-test test org.testcontainers postgresql test org.springframework.boot spring-boot-maven-plugin org.flywaydb flyway-maven-plugin ``` ### 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 } } ``` ### 2. 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; } ``` --- ## 🔧 계층별 구현 ### 1. 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); } ``` ### 2. 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(); } } ``` ### 3. 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 | --- ## 🚀 배포 및 실행 ### 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 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 설정 --- **작성일**: 2024년 12월 **작성자**: AI Assistant **버전**: 1.0.0 --- *이 가이드는 Java Spring(전자정부프레임워크)으로 센서 정보를 수집하고 관리하는 웹프로그램을 개발하기 위한 것입니다. 추가 질문이나 명확화가 필요한 부분이 있으면 언제든 연락주세요.*