TIL

플러스 주차 개인과제 TIL 1.

phonebee 2025. 3. 10. 20:21

▶ 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