
SpringBoot에서 3-Tier Architecture는 유지보수성과 확장성을 높이기 위한 표준 설계 방식이다. 각 계층은 “자기 책임만” 수행하며, 계층 간 데이터 이동은 Entity가 아닌 DTO를 통해 이루어진다. 이번 정리는 TodoList 예제를 기준으로, 주의할 부분까지 수정한 최종 구조다.
Client → Controller → Service → Repository → DB → Repository
→ Service (Entity → DTO 변환) → Controller (ResponseEntity 생성) → Client
이 구조를 유지해야 코드가 커져도 유지보수가 가능해진다.
Request DTO와 Response DTO는 목적이 다르므로 분리하는 것이 정석이다.
com.todo.todolist
├─ controller
├─ service
├─ repository
├─ entity
└─ dto
주의: 필드명은 소문자로 통일한다. (JSON 매핑 안정성)
public record TodoRequest(String content) {
}
public record TodoResponse(Long id, String content, boolean isCompleted) {
}
주의할 부분 수정:
@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
private final UserRepository userRepository;
@Transactional
public TodoResponse createTodo(Long userId, TodoRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
Todo todo = new Todo(request.content(), user);
Todo savedTodo = todoRepository.save(todo);
return toResponse(savedTodo);
}
@Transactional(readOnly = true)
public List<TodoResponse> getTodoList() {
return todoRepository.findAll().stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
@Transactional
public TodoResponse completeToggle(Long todoId) {
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new RuntimeException("할 일을 찾을 수 없습니다."));
todo.setCompleted(!todo.isCompleted());
return toResponse(todo);
}
@Transactional
public void deleteTodo(Long todoId) {
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new RuntimeException("할 일을 찾을 수 없습니다."));
todoRepository.delete(todo);
}
private TodoResponse toResponse(Todo todo) {
return new TodoResponse(
todo.getId(),
todo.getContent(),
todo.isCompleted()
);
}
}
Controller는 로직을 직접 처리하지 않는다. Service를 호출하고 HTTP 응답만 구성한다.
@RestController
@RequestMapping("/api/todos")
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@PostMapping("/{userId}")
public ResponseEntity<TodoResponse> addTodo(
@PathVariable Long userId,
@RequestBody TodoRequest request) {
TodoResponse response = todoService.createTodo(userId, request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping
public ResponseEntity<List<TodoResponse>> getAllTodos() {
return ResponseEntity.ok(todoService.getTodoList());
}
@PutMapping("/{todoId}")
public ResponseEntity<TodoResponse> completeTodo(@PathVariable Long todoId) {
return ResponseEntity.ok(todoService.completeToggle(todoId));
}
@DeleteMapping("/{todoId}")
public ResponseEntity<Void> deleteTodo(@PathVariable Long todoId) {
todoService.deleteTodo(todoId);
return ResponseEntity.noContent().build();
}
}
React(Frontend)는 fetch/axios로 /api/todos를 호출한다. JSON 요청이 Controller로 들어오고, Service에서 Entity를 만들고 DB에 저장한다. 저장 결과는 DTO로 변환되어 JSON으로 다시 React로 전달된다.
즉 이 구조는 SPA(React) + REST API 기반 설계의 기본 골격이다.
이 구조를 제대로 이해하면 이후 JWT, Role-based Security, 예외 처리, Validation 확장까지 자연스럽게 연결된다. 이제 이 설계를 기반으로 기능을 확장해 나가면 된다.