[Spring Plus] Level 1-1 요구사항 반영

2024. 11. 11. 11:46내일배움캠프/Spring Plus

 'Level 1-1' 의 요구사항을 반영한 내용을 기록한 포스팅이다. 어떠한 생각과 과정을 통해 요구사항을 반영했는지 알 수 있도록 작성해 보았다.

 

0. 요구사항

 현재 'API(POST /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 (?,?,?,?,?,?)]

 

에러를 해결해 정상적으로 API 요청이 수행될 수 있도록 코드를 수정하자.

 

 

1. 문제 원인 파악

 프로젝트를 살펴보니 'org.emaple.export.domain.todo.service.TodoService' 클래스에 아래와 같이 '@Transactional(readOnly = ture)' 가 적용되고 있는 걸 확인 할 수 있었다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TodoService {
    ...
}

 

'springframework' 라이브러리의 '@Transactional' 에 대한 docs 를 살펴보면 아래와 같은 내용을 확인할 수 있다.

springframework 라이브러리 '@Transactional' docs 의 일부

 

'@Transactional' 의 옵션 중 'readOnly()' 에 대한 설명이 되어 있는 부분인데, 해석하면 아래와 같다.

트랜잭션이 실제로 읽기 전용인 경우 true로 설정되어 런타임에 해당 최적화가 가능하도록 설정할 수 있는 boolean 플래그입니다. 기본값은 거짓입니다.

 

즉, 해당 옵션을 'true' 로 설정한다는 것은 해당 트랜잭션을 '읽기 전용' 으로 설정한 것이다. 그래서 '쓰기(= DB 에 데이터 반영)' 를 해야하는 'TodoService.saveTodo()' 의 경우 해당 메서드가 포함된 클래스에 '@Transactional(readOnly = true)' 가 지정되어 있고 해당 메서드는 따로 지정한 것이 없어서 클래스의 설정을 그대로 적용받아 DB 에 새로운 'todo' 를 반영할 수 없다. 그래서 예외가 발생한 것으로 파악했다.

 

 

2. 문제 해결

 당장 떠오른 해결 방식은 3개였다.

  1. TodoService 에 지정된 '@Transactional' 의 'readOnly' 옵션을 'false' 로 수정 또는 옵션 제거
  2. TodoService 에 지정된 '@Transactional' 삭제 후 'saveTodo()' 에 '@Transactional' 지정
  3. TodoService 에 '@Transactional(readOnly = true)' 유지, 'saveTodo()' 에 '@Transactional' 지정

결국 'saveTodo()' 가 '읽기 전용' 이 아닌 '읽기/쓰기' 가 가능해야 하므로 위와 같이 3가지의 해결 방식을 떠올릴 수 있었는데, 고민후 (3) 의 방식으로 해결하기로 하였다. (3) 의 방식을 선택한 이유는 '내가' 현재 프로젝트를 모두(완벽히) 파악하고 있는 것이 아니기 때문, 분명 해당 프로젝트를 작성한 개발자는 나름의 방식을 통해 코드를 작성했을 것이고 '모든 의도' 를 파악하지 못한 나로써는 최대한 현재 상태를 유지하며 문제를 해결해야겠다는 생각이 들었다. 실무 또는 협업 인원과 소통이 가능하다면 소통을 통해 다른 방식을 사용하겠지만 '개인과제' 특성상 주어진 프로젝트를 수정하는데 (3) 의 방법이 제일 괜찮지 않나 라는 생각을 한 것도 있다.

 

그래서 아래와 같이 'TodoService.saveTodo()' 에 '@Transactional' 을 지정해 주었다.

@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' 은 '클래스 메서드, 클래스, 인터페이스 메서드' 순으로 높은 우선순위를 가지고 있기에 (3) 의 방식을 활용할 수 있었다. 이렇게 되면 'TodoService' 클래스에서 'saveTodo()' 를 제외한 나머지 메서드들은 기존과 같이 '읽기 전용' 옵션을 그대로 받으면서 'saveTodo()' 는 '읽기/쓰기' 가 가능해져 DB 에 새로운 일정에 대한 정보를 반영할 수 있다.

 

실제로 Postman 을 통해 API 를 요청해보니 아래와 같이 새로운 일정에 대한 정보가 DB 에 에러 없이 잘 반영된 것을 확인할 수 있었다.

Postman - API(/todos) 요청
요청 수행 후 DB 의 'todos' 테이블 상태