▶ LEVEL 1.
▷ 1. 코드 개선 퀴즈 - @Transactional의 이해
○ 문제의 코드
@PostMapping("/todos")
public ResponseEntity<TodoSaveResponse> saveTodo(
@Auth AuthUser authUser,
@Valid @RequestBody TodoSaveRequest todoSaveRequest
) {
return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest));
}
해당 API("/todos")를 호출할 때, 오류가 발생하게 된다.
○ 에러 로그
jakarta.servlet.ServletException: Request processing failed: org.springframework.orm.jpa.JpaSystemException: could not execute statement [Connection is read-only. Queries leading to data modification are not allowed] [insert into todos (contents,created_at,modified_at,title,user_id,weather) values (?,?,?,?,?,?)]
로그를 살펴봤을 때 알 수 있었던 부분은 "Connection is read-only"부분이다. 즉, Transactional이 "ReadOnly"를 true로 받았기 때문에 나타나는 오류라고 생각했다.
그렇기에는 우선 Transactional(readOnly = true)가 무엇을 의미하는지 알아보기로 했다.
○ Transactional(readOnly = true)란?● 트랜잭션 관리를 위한 어노테이션으로, 해당 트랜잭션을 읽기 전용 모드로 실행하도록 설정하는 기능이다.● 해당 어노테이션을 설정하면 해당 메서드 내에서 데이터 변경(INSERT, UPDATE, DELETE)이 불가능하다.● JPA/Hibernate 최적화를 시킬 수 있다.● 일부 데이터베이스(MySQL 등)에서는 read-only 트랜잭션을 감지하면 해당 Connection을 자동으로 read-only 모드로 설정한다.※ 이 경우 INSERT, UPDATE, DELETE 같은 쓰기 작업을 시도하면 예외 발생한다.
즉, 에러가 나타난 것은 Transactional이 읽기 전용 상태였음에도 INSERT를 시도하려고 했기에 에러가 나타났다고 할 수 있다.
○ 해결 과정
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TodoService {
private final TodoRepository todoRepository;
private final WeatherClient weatherClient;
public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) {
User user = User.fromAuthUser(authUser);
String weather = weatherClient.getTodayWeather();
Todo newTodo = new Todo(
todoSaveRequest.getTitle(),
todoSaveRequest.getContents(),
weather,
user
);
Todo savedTodo = todoRepository.save(newTodo);
return new TodoSaveResponse(
savedTodo.getId(),
savedTodo.getTitle(),
savedTodo.getContents(),
weather,
new UserResponse(user.getId(), user.getEmail())
);
}
public Page<TodoResponse> getTodos(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);
return todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
));
}
public TodoResponse getTodo(long todoId) {
Todo todo = todoRepository.findByIdWithUser(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
User user = todo.getUser();
return new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(user.getId(), user.getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
);
}
}
위의 코드는 TodoService 코드이다.
코드를 살펴보면 TodoService 클래스 내의 모든 매서드를 읽기 전용 상태로 묶은 것을 알 수 있다.
이러면 아래의 get메서드들은 읽기만 수행하기에 예외가 발생하지 않지만 saveTodo의 경우에는 요청 받은 값을 필드에 넣는 INSERT를 하기 때문에 예외가 발생하게 된다.
그렇기에 이 문제를 해결하기 위해서는
@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
private final WeatherClient weatherClient;
@Transactional
public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) {
User user = User.fromAuthUser(authUser);
String weather = weatherClient.getTodayWeather();
Todo newTodo = new Todo(
todoSaveRequest.getTitle(),
todoSaveRequest.getContents(),
weather,
user
);
Todo savedTodo = todoRepository.save(newTodo);
return new TodoSaveResponse(
savedTodo.getId(),
savedTodo.getTitle(),
savedTodo.getContents(),
weather,
new UserResponse(user.getId(), user.getEmail())
);
}
@Transactional(readOnly = true)
public Page<TodoResponse> getTodos(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);
return todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
));
}
@Transactional(readOnly = true)
public TodoResponse getTodo(long todoId) {
Todo todo = todoRepository.findByIdWithUser(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
User user = todo.getUser();
return new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(user.getId(), user.getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
);
}
}
위와 같이 메서드마다 트랜잭션 어노테이션을 상황에 맞게 사용하는 것이 올바르다고 생각한다.
'TIL' 카테고리의 다른 글
| 플러스 주차 개인과제 TIL 3. (0) | 2025.03.12 |
|---|---|
| 플러스 주차 개인과제 TIL 2. (0) | 2025.03.11 |
| Spring 심화주차 개인 과제 TIL (0) | 2025.02.27 |
| 내일배움캠프 TIL 25. (0) | 2025.02.26 |
| 내일배움캠프 TIL 24. (0) | 2025.02.24 |