Spring Boot

1. 테스트 코드 작성

JUnit과 Mockito를 사용하여 유닛 테스트와 통합 테스트 작성

  • MockMvc: Spring MVC 테스트를 위한 클래스입니다. MockMvc를 사용하면 실제 서블릿 컨테이너를 사용하지 않고도 컨트롤러 테스트를 수행할 수 있습니다.

  • Mockito: 목 객체를 생성하고 관리하는 데 사용되는 라이브러리입니다. 여기서 @Mock 어노테이션을 사용하여 목 객체를 생성하고, @InjectMocks를 사용하여 해당 목 객체를 주입합니다.

  • JUnit: Java용 테스트 프레임워크입니다. @Test 어노테이션을 사용하여 테스트 메서드를 정의합니다.

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.time.LocalDateTime;
import java.util.Collections;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class GlucoseControllerTest {

    private MockMvc mockMvc;

    @Mock
    private GlucoseService glucoseService;

    @InjectMocks
    private GlucoseController glucoseController;

    @Test
    void getRecentGlucoseReadings() throws Exception {
        // Mockito 초기화
        MockitoAnnotations.openMocks(this);
        
        // MockMvc 설정
        this.mockMvc = MockMvcBuilders.standaloneSetup(glucoseController).build();

        // Mock 데이터 생성
        Glucose glucose = new Glucose();
        glucose.setId(1L);
        glucose.setSensor("Sensor1");
        glucose.setType("Random");
        glucose.setData("150");
        glucose.setTimestamp(LocalDateTime.now());

        // Mock 동작 정의
        when(glucoseService.getRecentGlucoseReadings()).thenReturn(Collections.singletonList(glucose));

        // MockMvc를 사용한 테스트 요청 및 검증
        mockMvc.perform(get("/api/glucose/recent")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().json("[{'id': 1, 'sensor': 'Sensor1', 'type': 'Random', 'data': '150', 'timestamp': '2023-06-21T12:34:56'}]"));
    }
}

2. 영속성 전이 (Cascade Types)

영속성 전이(Cascade Type)는 엔티티 간의 관계를 설정할 때 부모 엔티티의 상태 변화가 자식 엔티티에 전이되도록 설정하는 것입니다. 이를 통해 부모 엔티티를 저장하거나 삭제할 때 자식 엔티티도 함께 처리할 수 있습니다.

  • CascadeType.PERSIST: 부모 엔티티가 저장될 때 자식 엔티티도 함께 저장됩니다.

  • CascadeType.MERGE: 부모 엔티티가 병합될 때 자식 엔티티도 함께 병합됩니다.

  • CascadeType.REMOVE: 부모 엔티티가 삭제될 때 자식 엔티티도 함께 삭제됩니다.

  • CascadeType.REFRESH: 부모 엔티티가 새로 고침될 때 자식 엔티티도 함께 새로 고침됩니다.

  • CascadeType.DETACH: 부모 엔티티가 분리될 때 자식 엔티티도 함께 분리됩니다.

  • CascadeType.ALL: 모든 작업(PERSIST, MERGE, REMOVE, REFRESH, DETACH)이 자식 엔티티에 전파됩니다.

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.Set;

@Entity
@Getter
@Setter
public class Pet {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;
    private double weight;

    @ManyToOne
    @JoinColumn(name = "species_id")
    private Species species;

    private String photo;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(
            name = "pet_disease",
            joinColumns = @JoinColumn(name = "pet_id"),
            inverseJoinColumns = @JoinColumn(name = "disease_id")
    )
    private Set<Disease> diseases;
}

3. 롬복 어노테이션 추가

롬복(Lombok) 라이브러리는 자바에서 보일러플레이트 코드를 줄이는 데 유용합니다. 롬복 어노테이션을 사용하면 getter, setter, toString, equals, hashCode 등의 메서드를 자동으로 생성할 수 있습니다.

롬복 어노테이션 종류:

  • @Getter: 모든 필드에 대한 getter 메서드를 생성합니다.

  • @Setter: 모든 필드에 대한 setter 메서드를 생성합니다.

  • @ToString: 모든 필드를 포함하는 toString 메서드를 생성합니다.

  • @EqualsAndHashCode: equals 및 hashCode 메서드를 생성합니다.

  • @Data: @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 한 번에 적용합니다.

  • @Builder: 빌더 패턴을 적용합니다.

  • @Slf4j: 로깅 기능을 추가합니다.

4. Redis 캐시 추가

Redis는 인메모리 데이터 구조 저장소로, 고속의 데이터 읽기 및 쓰기 성능을 제공합니다. Redis를 캐시로 사용하여 데이터베이스 부하를 줄이고 애플리케이션 성능을 개선할 수 있습니다. Docker를 통해 Redis를 설정하고 Spring Boot에서 Redis 캐시를 사용합니다.

예시: Redis 설정 및 캐시 적용

# docker-compose.yml
version: '3.8'
services:
  redis:
    image: 'redis:latest'
    ports:
      - '6379:6379'
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class GlucoseService {

    @Autowired
    private GlucoseRepository glucoseRepository;

    @Cacheable(value = "glucoseCache", key = "'glucoseData'")
    @Transactional(readOnly = true)
    public List<Glucose> getRecentGlucoseReadings() {
        LocalDateTime oneDayAgo = LocalDateTime.now().minusDays(1);
        return glucoseRepository.findByTimestampAfter(oneDayAgo);
    }
}

5. 트랜잭션 관리

트랜잭션 관리는 데이터베이스 작업의 일관성을 유지하고 데이터 무결성을 보장하는 데 중요합니다.

Spring의 @Transactional 어노테이션을 사용하여 트랜잭션을 관리할 수 있습니다. 트랜잭션 전파 방식과 격리 수준을 설정하여 데이터 일관성을 유지합니다.

  • Propagation.REQUIRED: 현재 트랜잭션이 존재하면 해당 트랜잭션 내에서 실행되고, 존재하지 않으면 새로운 트랜잭션을 생성합니다.

  • Propagation.REQUIRES_NEW: 항상 새로운 트랜잭션을 생성합니다.

  • Isolation.DEFAULT: 기본 트랜잭션 격리 수준을 사용합니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PetService {

    @Autowired
    private PetRepository petRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public Pet savePet(Pet pet) {
        return petRepository.save(pet);
    }

    @Transactional(readOnly = true)
    public Pet findById(Long id) {
        return petRepository.findById(id).orElse(null);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deleteById(Long id) {
        petRepository.deleteById(id);
    }
}

Last updated