데이터 일관성(Data Consistency)은 시스템 내에서 데이터가 정확하고 신뢰할 수 있는 상태로 유지되는 것을 의미합니다. 특히 백엔드 개발에서는 여러 컴포넌트나 사용자 간에 데이터가 일관되게 유지되는 것이 매우 중요합니다.
ACID 원칙
Atomicity(원자성) : 트랜잭션 내의 모든 작업이 모두 성공하거나 모두 실패해야 합니다.
Consistency(일관성) : 트랜잭션이 시작되기 전과 후에 데이터베이스의 일관된 상태가 유지되어야 합니다.
Isolation(격리성) : 동시에 실행되는 트랜잭션들이 서로 간섭하지 않아야 합니다.
Durability(지속성) : 트랜잭션이 성공적으로 완료된 후에는 그 결과가 영구적으로 저장되어야 합니다.
Spring에서의 트랜잭션 관리 : Spring에서는 @Transactional 어노테이션을 사용하여 트랜잭션을 쉽게 관리할 수 있습니다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUser(User user) {
userRepository.save(user);
// 다른 데이터베이스 작업
// 모든 작업이 성공해야 커밋되고, 하나라도 실패하면 롤백됩니다.
}
}
여러 사용자가 동시에 데이터를 수정하려고 할 때 데이터 일관성이 깨질 수 있습니다. 이를 방지하기 위해 동시성 제어가 필요합니다.
@Entity
public class Product {
@Id
private Long id;
@Version
private Integer version;
private String name;
// 기타 필드
}
낙관적 락을 사용하면 업데이트 시점에 버전 번호를 체크하여 다른 트랜잭션에서 수정된 경우 예외를 발생시킵니다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findProductForUpdate(@Param("id") Long id);
데이터가 저장되기 전에 유효성을 검증하여 잘못된 데이터가 저장되지 않도록 합니다.
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserDTO userDTO, BindingResult result) {
if (result.hasErrors()) {
// 에러 처리
}
// 사용자 생성 로직
}
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) NOT NULL,
-- 기타 필드
);
애플리케이션에서 캐시를 사용할 경우, 캐시된 데이터와 데이터베이스의 데이터가 일치하도록 관리해야 합니다.
@Service
public class ProductService {
@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productRepository.save(product);
}
}
데이터베이스 스키마와 애플리케이션의 데이터 모델을 일관되게 설계하여 데이터 무결성의 유지합니다. ORM(Object-Relational Mapping) 프레임워크인 Hibernate를 사용하여 엔티티를 설계할 때도 주의가 필요합니다.
@Entity
public class Order {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
// 기타 필드
}
마이크로서비스 아키텍처 등 분산 시스템에서는 데이터 일관성을 유지하기가 더 복잡합니다. 이 경우 다음과 같은 방법을 사용할 수 있습니다.
신입 Java/Spring 백엔드 개발자로서 데이터 일관성을 이해하고 실습하는 것은 매우 중요합니다. 이를 위해 다음과 같은 실습 프로젝트와 연습 문제를 추천드립니다. 각 실습은 데이터 일관성의 다양한 측면을 다루며, 실제 프로젝트에 적용할 수 있는 기술들을 익히는 데 도움이 될 것입니다.
프로젝트 예시: Todo List 애플리케이션
실습 목표:
실습 내용:
Todo
엔티티를 정의하고 데이터베이스에 매핑@Entity
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String title;
private boolean completed;
// getters and setters
}
public interface TodoRepository extends JpaRepository<Todo, Long> {}
@Service
public class TodoService {
@Autowired
private TodoRepository todoRepository;
@Transactional
public Todo createTodo(Todo todo) {
return todoRepository.save(todo);
}
// 다른 CRUD 메서드도 @Transactional 사용
}
@RestController
@RequestMapping("/api/todos")
public class TodoController {
@Autowired
private TodoService todoService;
@PostMapping
public ResponseEntity<Todo> createTodo(@Valid @RequestBody Todo todo) {
Todo created = todoService.createTodo(todo);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// 다른 CRUD 엔드포인트도 구현
}
프로젝트 예시: 은행 계좌 관리 시스템
실습 목표:
실습 내용:
엔티티 설계: Account
엔티티에 버전 필드 추가
@Entity
public class Account {
@Id
private Long id;
private String owner;
private BigDecimal balance;
@Version
private Integer version;
// getters and setters
}
낙관적 락 적용:
@Transactional
public void transferOptimistic(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
}
비관적 락 적용:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findAccountForUpdate(@Param("id") Long id);
@Transactional
public void transferPessimistic(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findAccountForUpdate(fromId);
Account to = accountRepository.findAccountForUpdate(toId);
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
}
프로젝트 예시: 사용자 등록 시스템
실습 목표:
실습 내용:
public class UserDTO {
@NotBlank
@Size(min = 3, max = 50)
private String username;
@Email
@NotBlank
private String email;
@NotBlank
private String password;
// getters and setters
}
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserDTO userDTO, BindingResult result) {
if (result.hasErrors()) {
// 에러 처리 로직
return ResponseEntity.badRequest().body(result.getAllErrors());
}
// 사용자 생성 로직
User user = new User();
user.setUsername(userDTO.getUsername());
user.setEmail(userDTO.getEmail());
user.setPassword(userDTO.getPassword());
userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) NOT NULL,
password VARCHAR(100) NOT NULL
);
프로젝트 예시: 상품 관리 시스템
실습 목표:
실습 내용:
Spring Cache 설정: application.properties
에 캐시 설정 추가
spring.cache.type=simple
서비스 레이어에서 캐시 적용:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productRepository.save(product);
}
@CacheEvict(value = "products", allEntries = true)
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
}
캐시 테스트: 상품 조회, 수정, 삭제 시 캐시가 올바르게 동작하는지 확인
프로젝트 예시: 주문 처리 시스템
실습 목표:
실습 내용:
Order
, OrderItem
, Product
엔티티 정의@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository;
@Transactional
public void placeOrder(OrderDTO orderDTO) {
Order order = new Order();
// 주문 정보 설정
orderRepository.save(order);
for (OrderItemDTO itemDTO : orderDTO.getItems()) {
Product product = productRepository.findById(itemDTO.getProductId())
.orElseThrow(() -> new RuntimeException("Product not found"));
if (product.getStock() < itemDTO.getQuantity()) {
throw new RuntimeException("Insufficient stock for product " + product.getName());
}
product.setStock(product.getStock() - itemDTO.getQuantity());
productRepository.save(product);
OrderItem orderItem = new OrderItem();
// 주문 항목 설정
orderItem.setOrder(order);
orderItem.setProduct(product);
orderItem.setQuantity(itemDTO.getQuantity());
// 주문 항목 저장
}
}
}
프로젝트 예시: 블로그 게시물 관리 시스템
실습 목표:
실습 내용:
게시물 엔티티와 레포지토리 작성
캐시 적용 및 무효화 로직 구현:
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
@Cacheable(value = "posts", key = "#id")
public Post getPost(Long id) {
return postRepository.findById(id).orElse(null);
}
@Transactional
@CachePut(value = "posts", key = "#post.id")
public Post updatePost(Post post) {
return postRepository.save(post);
}
@Transactional
@CacheEvict(value = "posts", key = "#id")
public void deletePost(Long id) {
postRepository.deleteById(id);
}
}
동시 업데이트 시 캐시와 데이터베이스의 일관성 확인
프로젝트 예시: 간단한 마이크로서비스 아키텍처
실습 목표:
실습 내용:
위의 실습 프로젝트들은 데이터 일관성을 유지하는 다양한 방법들을 실제로 구현해보는 데 도움이 됩니다. 각 프로젝트를 통해 트랜잭션 관리, 동시성 제어, 데이터 유효성 검증, 캐시 관리 등 중요한 개념들을 깊이 있게 이해할 수 있습니다. 실습을 진행하면서 발생하는 문제들을 해결해 나가며 경험을 쌓는 것이 중요합니다. 또한, 코드 리뷰를 통해 다른 개발자들의 피드백을 받는 것도 큰 도움이 될 것입니다.
취업 준비 과정에서 이러한 실습을 통해 실무 경험을 쌓고, 포트폴리오에 포함시키면 면접 시 좋은 인상을 줄 수 있습니다. 꾸준히 실습을 이어가며 Java와 Spring 프레임워크에 대한 이해를 높이시길 바랍니다. 화이팅입니다!