Spring MVC 기본 구조와 CRUD 구현 예제

Jayson·2025년 5월 28일
0
post-thumbnail

서론

Spring MVC는 자바 진영의 대표적인 웹 프레임워크인 스프링(Spring)에서 웹 애플리케이션을 만들기 위한 MVC 아키텍처를 제공하는 모듈입니다. MVC란 Model-View-Controller의 약자로, 애플리케이션을 세 가지 역할로 나누어 구조화하는 디자인 패턴입니다. Spring MVC를 사용하면 웹 요청 처리 로직을 컨트롤러(Controller), 비즈니스 로직을 서비스(Service), 데이터 접근을 레포지토리(Repository)와 같이 레이어별로 구분하여 깔끔하고 유지보수가 쉬운 코드를 작성할 수 있습니다. 또한 대부분의 웹 애플리케이션에서는 데이터를 생성, 조회, 수정, 삭제하는 CRUD(Create, Read, Update, Delete) 기능이 필수적입니다. 본 글에서는 Spring MVC의 기본 구조와 동작 원리를 살펴보고, 간단한 예제를 통해 CRUD 기능을 구현하는 방법을 소개하겠습니다.

Spring MVC 개요

MVC란 무엇인가?

MVC(Model-View-Controller)는 소프트웨어를 모델(Model), 뷰(View), 컨트롤러(Controller) 세 가지 역할로 나누는 아키텍처 패턴입니다.

  • Model(모델): 애플리케이션의 데이터와 그 데이터를 처리하는 비즈니스 로직을 담당합니다. 예를 들어 데이터베이스와 연동하여 사용자가 입력하거나 조회할 데이터를 다루는 부분입니다. 도메인 객체(엔티티)나 데이터 처리 코드를 포함합니다.

  • View(뷰): 최종 사용자에게 보여지는 UI 화면을 담당합니다. HTML, JSP, 타임리프(Thymeleaf) 같은 템플릿 엔진을 통해 모델의 데이터를 시각적으로 표현하는 역할입니다.

  • Controller(컨트롤러): 사용자의 요청을 받아 필요한 처리를 결정하고, 그 결과를 모델과 함께 뷰에 전달하는 중간 제어자 역할을 합니다. 예를 들어 웹 브라우저에서 특정 URL 요청이 들어오면, 해당 요청을 처리할 비즈니스 로직을 호출하고, 처리 결과를 모델에 담아 적절한 뷰를 선택하여 반환합니다.

MVC 패턴을 사용하면 화면(View)과 데이터 처리(Model)를 분리하고, Controller가 둘을 연결해 줌으로써 각 구성 요소에 관심사 분리(Separation of Concerns)를 달성할 수 있습니다. 이렇게 역할을 분리하면 코드를 이해하기 쉽고 유지보수가 용이하며, 기능 변경이나 확장이 필요할 때 다른 부분에 미치는 영향이 적어집니다.

Spring MVC의 구조

Spring MVC는 위의 MVC 패턴을 효과적으로 적용하기 위한 프레임워크 구현체입니다. 일반적인 Spring 애플리케이션에서는 다음과 같은 레이어드 아키텍처로 구성합니다:

  • Controller (컨트롤러): 웹 계층을 담당합니다. 클라이언트(브라우저)의 HTTP 요청을 받아 어떤 서비스를 실행할지 결정하고, 그 결과를 응답으로 반환합니다. 주로 @Controller나 @RestController 어노테이션을 붙여 사용합니다. 컨트롤러 메서드에서는 웹 요청 정보를 검사하거나 Service를 호출하고, 최종적으로 뷰(View)를 반환하거나 JSON 형태의 데이터를 직접 반환합니다.

  • Service (서비스): 서비스 계층, 또는 비즈니스 계층이라고 부릅니다. 컨트롤러로부터 호출되어 실제 비즈니스 로직을 수행합니다. 예를 들어, 사용자가 돈을 송금하는 요청을 했다면, 컨트롤러는 해당 요청을 서비스로 전달하고 서비스는 “송금”에 대한 도메인 로직(잔액 확인, 이체 처리 등)을 실행합니다. Service는 필요에 따라 데이터베이스 작업을 위해 Repository를 호출합니다.

  • Repository (레포지토리): 저장소 계층으로, 데이터 접근을 담당합니다. 보통 데이터베이스와 직접 연동되어 CRUD(Create, Read, Update, Delete) 작업을 수행합니다. Spring에서는 @Repository 어노테이션을 사용하며, JDBC나 JPA를 이용해 DB와 통신합니다. DAO(Data Access Object)라고 부르기도 합니다. Spring Data JPA 같은 라이브러리를 활용하면 별도의 구현 코드 없이 인터페이스 정의만으로도 기본적인 CRUD 메서드를 사용할 수 있습니다 (예: CrudRepository나 JpaRepository 상속).

이 밖에 Model에 해당하는 부분으로, 애플리케이션에서 사용되는 엔티티(entity)나 DTO(Data Transfer Object) 클래스들이 있습니다. 예를 들어 사용자 정보, 게시글 정보 등을 담는 순수 자바 객체를 정의해 사용하고, 이는 Controller-View 사이에서 데이터를 전달하거나 Service-Repository 계층에서 DB와 데이터를 주고받는 데 활용됩니다.

요청이 들어왔을 때의 동작 흐름

Spring MVC에서는 DispatcherServlet이라는 특별한 서블릿이 프론트 컨트롤러(Front Controller)로 동작하여 모든 요청을 중앙에서 가로채 처리합니다. 전체 흐름은 다음과 같습니다:

Spring MVC 요청 처리 흐름:

1.사용자가 브라우저 등 클라이언트에서 요청을 보내면(예: URL 접속이나 폼 전송) 해당 요청이 우선 DispatcherServlet에 전달됩니다.

  1. DispatcherServlet은 HandlerMapping을 통해 요청 URL에 매핑된 적절한 컨트롤러를 찾은 뒤, 그 컨트롤러의 메서드를 호출합니다.

  2. 컨트롤러는 요청을 처리하기 위해 서비스 계층을 호출하고, 서비스는 필요한 비즈니스 로직을 수행한 후 Repository를 통해 데이터베이스를 조회하거나 변경합니다.

  3. 이러한 처리가 모두 끝나면, 컨트롤러는 결과 데이터를 모델(Model) 객체에 담거나 API 응답용 객체로 만들고 반환값을 전달합니다. 만약 뷰 이름(String)과 모델을 반환했다면 DispatcherServlet은 ViewResolver를 통해 해당 이름에 해당하는 뷰를 찾아 모델 데이터를 넘겨주고, 뷰를 렌더링하여 최종 HTML 응답을 생성합니다. API의 경우라면 컨트롤러가 반환한 객체를 적절한 포맷(JSON/XML)으로 직렬화하여 응답 본문에 담습니다.

  4. 마지막으로 DispatcherServlet이 최종 결과를 클라이언트에게 응답합니다.

정리하면, 클라이언트의 모든 요청은 DispatcherServlet(프론트 컨트롤러)을 거쳐 해당 컨트롤러 → 서비스 → 레포지토리로 전달되고, 작업 완료 후 다시 레포지토리 → 서비스 → 컨트롤러 → DispatcherServlet 경로를 통해 응답이 만들어집니다. Spring MVC가 이처럼 프론트 컨트롤러 패턴을 사용함으로써, 공통 로직을 한 곳에서 처리하고 각 컨트롤러는 자신의 작업에만 집중할 수 있도록 해줍니다. 개발자는 각 역할에 해당하는 코드만 구현하면 나머지 흐름은 프레임워크가 관리해주므로 생산성이 높아집니다.

CRUD란?

CRUD는 Create(생성), Read(읽기/조회), Update(갱신/수정), Delete(삭제)의 약자로, 소프트웨어에서 데이터를 다루는 기본적인 네 가지 동작을 의미합니다. 예를 들어 게시판 애플리케이션을 생각해보면, 새로운 게시글을 작성하는 것이 Create, 게시글 목록이나 상세 내용을 보는 것이 Read, 게시글 내용을 편집하는 것이 Update, 게시글을 삭제하는 것이 Delete에 해당합니다. 대부분의 데이터 중심 애플리케이션은 이 CRUD 사이클을 통해 사용자가 정보를 생성하고 관리할 수 있게 합니다.

일반적으로 CRUD 각각의 동작은 다음과 같은 HTTP 메서드와 매핑됩니다:

  • Create (생성) – HTTP POST 메서드를 사용하여 서버에 새로운 자원을 만듭니다. (예시: POST /boards 요청으로 새로운 게시글 등록)

  • Read (조회) – HTTP GET 메서드를 사용하여 서버로부터 자원(데이터)을 조회합니다. (예시: GET /boards로 전체 게시글 목록 조회, GET /boards/1로 ID가 1인 게시글 상세 조회)

  • Update (수정) – HTTP PUT 메서드를 사용하여 기존 자원을 수정합니다. (예시: PUT /boards/1 요청으로 ID=1 게시글 내용 수정) ※ 부분 수정은 PATCH 메서드를 사용하기도 합니다.

  • Delete (삭제) – HTTP DELETE 메서드를 사용하여 자원을 삭제합니다. (예시: DELETE /boards/1로 ID=1 게시글 삭제)

위와 같이 CRUD 동작을 HTTP 메서드와 URL로 표현하는 방식을 RESTful한 스타일이라고 합니다. RESTful API에서는 자원(Resource)을 URL로 나타내고, 해당 자원에 대한 행위는 HTTP 메서드로 구분합니다. 이렇게 하면 API 설계가 일관되고 이해하기 쉬워지기 때문에, Spring MVC/Boot로 웹 서비스를 개발할 때도 자연스럽게 이러한 RESTful 매핑을 따르게 됩니다.

Spring MVC에서의 CRUD 구현 예시

이제 Spring MVC를 사용하여 간단한 CRUD 기능을 어떻게 구현하는지 예시를 통해 알아보겠습니다. 예제로는 게시판의 게시글(Board) 엔티티를 다루는 CRUD를 만들어보겠습니다. 우선 게시글 정보를 표현하는 간단한 모델(엔티티)을 정의한다고 가정합니다. (예: Board 클래스에 id, title, content 필드가 있고, 생성자와 getter/setter가 있는 구조입니다.)

또한 데이터 저장소로 BoardRepository를 준비합니다. 이는 인터페이스로서 JPA를 사용한다면 JpaRepository<Board, Long>를 상속받아 구현할 수 있고, 메모리 상의 리스트로 간단히 대체할 수도 있습니다. Repository에는 save(…), findById(…), findAll(), deleteById(…) 등의 CRUD 메서드가 있어 게시글 데이터를 저장하고 조회하는 데 사용됩니다. 실제 애플리케이션에서는 이 Repository가 데이터베이스와 연결되어 동작합니다.

마지막으로 BoardController를 만들어 각 CRUD 기능을 위한 HTTP 요청 처리 메서드를 작성합니다. 컨트롤러는 @RestController로 표시하여 RESTful JSON 응답을 보내도록 하고, 경로(prefix)는 @RequestMapping("/boards")로 지정하겠습니다. 그러면 이 컨트롤러의 각 메서드는 /boards 경로 이하에서 동작하게 됩니다. 서비스 계층(BoardService)도 존재한다고 가정하고, 컨트롤러는 필요한 경우 서비스에 작업을 위임합니다 (초심자를 위해 여기서는 서비스 단의 상세 구현을 생략하고 컨트롤러에서 바로 Repository를 호출해도 무방합니다). 아래 코드는 게시글(Board)에 대한 Create, Read, Update, Delete를 처리하는 컨트롤러 메서드 예시입니다:

@RestController
@RequestMapping("/boards")
public class BoardController {

    // (보통 서비스에 주입하지만, 예시로 바로 Repository 주입)
    @Autowired 
    private BoardRepository boardRepository; 

    // 1) Create - 새 게시글 등록
    @PostMapping
    public ResponseEntity<Board> createBoard(@RequestBody Board board) {
        Board saved = boardRepository.save(board);               // 새 게시글을 저장 (DB에 INSERT)
        return new ResponseEntity<>(saved, HttpStatus.CREATED);  // 201 Created 응답 반환
    }

    // 2) Read - 전체 게시글 목록 조회
    @GetMapping
    public List<Board> getAllBoards() {
        return boardRepository.findAll();  // 모든 게시글 조회하여 List로 반환 (기본적으로 200 OK)
    }

    // 3) Read - 특정 ID 게시글 조회
    @GetMapping("/{id}")
    public ResponseEntity<Board> getBoardById(@PathVariable Long id) {
        Optional<Board> boardOpt = boardRepository.findById(id);   // ID로 게시글 조회
        if (boardOpt.isPresent()) {
            return ResponseEntity.ok(boardOpt.get());              // 존재하면 200 OK와 게시글 반환
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); // 없으면 404 Not Found
        }
    }

    // 4) Update - 게시글 수정
    @PutMapping("/{id}")
    public ResponseEntity<Board> updateBoard(@PathVariable Long id, @RequestBody Board boardData) {
        Optional<Board> boardOpt = boardRepository.findById(id);
        if (!boardOpt.isPresent()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); // 수정 대상 없으면 404
        }
        Board board = boardOpt.get();
        board.setTitle(boardData.getTitle());       // 제목 수정
        board.setContent(boardData.getContent());   // 내용 수정
        Board updated = boardRepository.save(board);  // 변경된 내용 저장 (DB에 UPDATE)
        return ResponseEntity.ok(updated);           // 200 OK와 수정된 게시글 반환
    }

    // 5) Delete - 게시글 삭제
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBoard(@PathVariable Long id) {
        if (boardRepository.existsById(id)) {
            boardRepository.deleteById(id);                      // ID에 해당하는 게시글 삭제 (DB에서 DELETE)
            return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); // 204 No Content 반환
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();  // 없으면 404 반환
        }
    }
}

위 컨트롤러 코드에서 각 메서드는 Spring MVC에 의해 URL 경로와 HTTP 메서드로 매핑됩니다. 예를 들어 createBoard() 메서드는 @PostMapping("/boards")이므로 POST 요청이 /boards로 들어올 때 호출되며, 요청 본문의 JSON 데이터를 @RequestBody로 Board 객체에 매핑받아 새 게시글을 생성합니다. 성공 시에는 HttpStatus.CREATED(201 상태 코드)와 함께 생성된 Board 객체를 응답합니다. 마찬가지로 getAllBoards()는 GET /boards 요청에 응답하여 전체 목록 (List 형태의 JSON 배열)을 반환하고, getBoardById()는 GET /boards/{id} 요청에 대해 하나의 게시글을 조회합니다. 여기서는 Optional을 사용하여 해당 ID의 게시글이 존재하는지 확인하고, 존재하면 200 OK와 데이터를, 없으면 404 Not Found 상태를 반환했습니다.

updateBoard() 메서드는 PUT /boards/{id} 요청을 처리하여, 경로로 받은 ID의 게시글을 요청 본문의 데이터로 업데이트합니다. 존재하지 않는 게시글 ID가 주어지면 404를 반환하고, 존재하면 제목과 내용을 수정한 뒤 save를 호출하여 변경사항을 저장합니다. (JPA의 save는 ID가 있으면 UPDATE로 동작합니다.) 마지막으로 deleteBoard()는 DELETE /boards/{id} 요청에 대해 실행되며, 주어진 ID의 게시글이 있으면 deleteById로 삭제하고 204 No Content 상태를 응답합니다. 없는 경우 404를 반환합니다.

💡 참고: 위 코드에서는 이해를 돕기 위해 Controller에서 바로 boardRepository를 사용했습니다. 실제로는 BoardService 클래스에서 @Transactional 어노테이션과 함께 비즈니스 로직을 처리하고 Repository를 호출하도록 설계하는 것이 좋습니다. 예를 들어 boardService.delete(id) 내부에서 존재 여부를 체크하고 삭제하도록 구현할 수 있습니다. 그래도 큰 흐름은 동일하며, 컨트롤러는 서비스/레포지토리를 통해 CRUD 작업을 수행하고, 그 결과를 HTTP 응답으로 매핑하는 역할을 합니다.

결론 및 참고자료

Spring MVC를 통해 웹 애플리케이션의 기본 뼈대를 구성하는 방법과 CRUD 기능 구현 패턴을 살펴보았습니다. 정리하자면, Spring MVC에서는 MVC 아키텍처에 따라 역할별 클래스를 작성하고, DispatcherServlet이 중앙 조율자가 되어 컨트롤러, 서비스, 레포지토리를 차례로 호출함으로써 요청을 처리합니다. 그 위에 CRUD와 같은 기능을 RESTful하게 구현함으로써 일관성 있고 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다. 처음에는 다소 복잡해 보일 수 있지만, 작은 예제부터 직접 CRUD 기능을 만들어보면서 Spring MVC의 동작 원리를 체험해 보는 것을 권장합니다.

🔗 References

profile
Small Big Cycle

0개의 댓글