Spring boot MVC RESTful CRUD
@RequestMapping
@RequestMapping은 Spring MVC의 어노테이션이며, 특정 URL 요청을 어떤 컨트롤러의 메서드가 처리할지 연결(mapping)해주는 역할을 합니다.
예를 들어 @RequestMapping(value="sendMessage")
으로 되어있다면 이 메서드가 /sendMessage라는 경로(path)로 들어오는 요청을 처리하겠다는 것을 의미합니다.
예를 들어, 웹 애플리케이션이 http://localhost:8080에서 실행 중이라면, 사용자가 http://localhost:8080/sendMessage로 접속할 때 이 어노테이션이 붙은 메서드가 실행됩니다.
사용 위치:
- 컨트롤러 클래스(class) 레벨이나 메서드(method) 레벨에 붙일 수 있습니다.
- 클래스 레벨에 붙이면 하위 모든 메서드의 기본 URL 경로가 됩니다.
HTTP 메서드 (GET, POST 등):
- @RequestMapping만 사용하면 기본적으로 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)의 요청을 다 받습니다.
- 만약 특정 메서드(예: POST)만 받도록 하고 싶다면 method = RequestMethod.POST 속성을 추가하거나, 더 명확하게 @PostMapping("sendMessage")을 사용하는 것이 좋습니다.
@ResponseBody
@ResponseBody는 메서드의 반환(return) 값을 HTTP 응답 본문(Response Body)에 직접 써서 클라이언트에게 전송하겠다는 것을 의미합니다.
주요 역할:
- 이 어노테이션이 없으면, Spring은 메서드가 반환하는 문자열(String)을 뷰(View) 템플릿 파일의 이름 (예: sendMessage.html이나 sendMessage.jsp)으로 해석하고 해당 뷰를 렌더링하려고 시도합니다.
- @ResponseBody를 사용하면, Spring은 뷰를 찾는 대신 메서드가 반환한 데이터 자체를 응답으로 보냅니다.
데이터 변환:
- 반환 타입이 String이면: 문자열 그대로를 text/plain 또는 text/html 형태로 보냅니다.
- 반환 타입이 자바 객체(Object, DTO, Map, List 등)이면 Spring Boot는 내장된 Jackson 라이브러리를 사용해 해당 객체를 JSON 형식의 문자열로 자동 변환하여 응답 본문에 담아 보냅니다. 이는 REST API를 만들 때 매우 유용합니다.
예시
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; // (참고)
import org.springframework.web.bind.annotation.ResponseBody;
@Controller // 이 클래스가 Spring의 컨트롤러임을 나타냅니다.
public class MessageController {
/**
* "/sendMessage" 경로로 들어오는 모든 HTTP 요청을 처리합니다.
* 이 메서드가 반환하는 "메시지 전송 성공!" 문자열은
* 뷰 이름이 아니라 HTTP 응답 본문(body)에 직접 담겨 전송됩니다.
*/
@RequestMapping(value = "sendMessage") // 1. /sendMessage URL과 이 메서드를 매핑
@ResponseBody // 2. 메서드의 반환 값을 뷰가 아닌 데이터 자체로 응답
public String sendMessage() {
// (여기에서 실제 메시지 전송 로직이 수행될 수 있습니다.)
// 뷰 템플릿(sendMessage.html)을 찾는 대신,
// 이 "메시지 전송 성공!" 문자열이 클라이언트에게 바로 응답됩니다.
return "메시지 전송 성공!";
}
/**
* 만약 객체를 반환하면 JSON으로 자동 변환됩니다.
*/
@RequestMapping(value = "messageDetails")
@ResponseBody
public MessageDto getMessageDetails() {
// MessageDto는 단순한 자바 객체(DTO)라고 가정
return new MessageDto("user123", "안녕하세요!");
}
}
// (참고용) MessageDto 클래스 예시
class MessageDto {
private String sender;
private String content;
// 생성자, Getter, Setter 등 ...
public MessageDto(String sender, String content) {
this.sender = sender;
this.content = content;
}
// (Gatters)
public String getSender() { return sender; }
public String getContent() { return content; }
}
- 사용자가 http://.../sendMessage로 요청을 보냅니다.
- @RequestMapping(value = "sendMessage") 때문에 sendMessage() 메서드가 실행됩니다.
- 메서드는 "메시지 전송 성공!"이라는 문자열을 반환합니다.
- @ResponseBody 때문에 Spring은 이 문자열을 뷰 이름으로 해석하지 않고, HTTP 응답 본문에 그대로 담아 사용자에게 보냅니다.
- 사용자의 브라우저나 클라이언트는 "메시지 전송 성공!"이라는 텍스트 데이터를 받게 됩니다.
@RestController
만약 클래스 내의 모든 메서드가 뷰 템플릿이 아닌 데이터를 반환(즉, 모든 메서드에 @ResponseBody를 붙여야) 한다면, 매번 @ResponseBody를 붙이는 것은 번거롭습니다.
이때는 클래스 레벨에 @Controller 대신 @RestController를 사용하면 됩니다.
@RestController는 @Controller와 @ResponseBody를 합쳐놓은 어노테이션입니다.
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController // @Controller + @ResponseBody
public class MessageRestController {
// @RestController를 사용했기 때문에
// @ResponseBody를 따로 붙이지 않아도
// 반환 값이 자동으로 응답 본문에 담겨 전송됩니다.
@RequestMapping(value = "sendMessage")
public String sendMessage() {
return "메시지 전송 성공! (RestController 사용)";
}
@RequestMapping(value = "messageDetails")
public MessageDto getMessageDetails() {
// 이 객체도 자동으로 JSON으로 변환되어 응답됩니다.
return new MessageDto("user123", "안녕하세요!");
}
}
RESTful CRUD API
Spring Boot, Spring MVC, MyBatis, 그리고 PostgreSQL을 사용하여 RESTful CRUD API를 구현해보겠습니다.
회원 정보(Member)를 관리하는 간단한 API를 만든다고 가정해 보겠습니다.
프로젝트 파일들의 구조
src/main/
├── java/com/example/demo/
│ ├── controller/
│ │ └── MemberController.java // (7. REST 컨트롤러)
│ ├── dto/
│ │ └── Member.java // (4. 데이터 모델 DTO)
│ ├── mapper/
│ │ └── MemberMapper.java // (5. MyBatis 매퍼 인터페이스)
│ ├── service/
│ │ ├── MemberService.java // (6. 서비스 인터페이스)
│ │ └── MemberServiceImpl.java // (6. 서비스 구현체)
│ └── DemoApplication.java // (프로젝트 시작점)
│
└── resources/
├── mappers/
│ └── MemberMapper.xml // (5. MyBatis SQL 쿼리)
└── application.properties // (2. 프로젝트 설정)
1. 의존성 설정 (pom.xml)
Spring Boot 프로젝트를 생성할 때 다음 의존성들이 필요합니다.
- Spring Web: Spring MVC와 REST 컨트롤러를 사용하기 위해 필요합니다.
- MyBatis Spring Boot Starter: Spring Boot와 MyBatis를 쉽게 연동해 줍니다.
- PostgreSQL Driver: PostgreSQL 데이터베이스에 연결하기 위한 JDBC 드라이버입니다.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version> </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 설정 파일 (application.properties)
src/main/resources/application.properties에 PostgreSQL 연결 정보와 MyBatis 설정 정보를 추가합니다.
# 1. PostgreSQL 데이터베이스 연결 설정
# (사용자 환경에 맞게 URL, username, password를 수정하세요)
spring.datasource.url=jdbc:postgresql://localhost:5432/your_database_name
spring.datasource.username=your_postgres_username
spring.datasource.password=your_postgres_password
spring.datasource.driver-class-name=org.postgresql.Driver
# 2. MyBatis 설정
# DTO 클래스들의 기본 패키지 경로를 지정합니다 (SQL 결과 매핑 시 사용).
mybatis.type-aliases-package=com.example.demo.dto
# SQL 쿼리가 담긴 XML 파일들의 위치를 지정합니다.
mybatis.mapper-locations=classpath:mappers/*.xml
3. 데이터베이스 테이블 생성 (PostgreSQL SQL)
CREATE TABLE members (
id SERIAL PRIMARY KEY, -- 고유 ID (자동 증가)
username VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
-- (테스트용 초기 데이터)
INSERT INTO members (username, email) VALUES ('user', 'user@example.com');
4. 데이터 모델 (DTO)
데이터베이스 테이블의 데이터를 자바 객체로 주고받기 위한 DTO(Data Transfer Object) 클래스입니다.
src/main/java/com/example/demo/dto/Member.java
package com.example.demo.dto;
// Lombok을 사용하면 @Data 어노테이션 하나로 Getter, Setter, 생성자 등을 대체할 수 있습니다.
// 여기서는 Lombok 없이 명시적으로 작성했습니다.
public class Member {
private int id;
private String username;
private String email;
// 기본 생성자
public Member() {
}
// 모든 필드를 받는 생성자
public Member(int id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
// --- Getter and Setter ---
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
'}';
}
}
5. MyBatis 매퍼 (Interface + XML)
MyBatis는 Mapper 인터페이스와 XML 파일이 한 쌍으로 동작하여 SQL을 실행합니다.
src/main/java/com/example/demo/mapper/MemberMapper.java (인터페이스)
- 실행할 SQL 메서드들을 정의합니다.
- @Mapper 어노테이션을 붙여 Spring이 MyBatis 매퍼로 인식하도록 합니다.
package com.example.demo.mapper;
import com.example.demo.dto.Member;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper // 이 인터페이스가 MyBatis 매퍼임을 선언
public interface MemberMapper {
// (C)reate: 회원 추가
void insertMember(Member member);
// (R)ead: ID로 회원 조회
Member findMemberById(int id);
// (R)ead: 모든 회원 조회
List<Member> findAllMembers();
// (U)pdate: 회원 정보 수정
void updateMember(Member member);
// (D)elete: 회원 삭제
void deleteMember(int id);
}
src/main/resources/mappers/MemberMapper.xml (SQL 쿼리)
- 실제 실행될 SQL 쿼리를 작성합니다.
- namespace는 위에서 만든 매퍼 인터페이스의 전체 경로와 일치해야 합니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.MemberMapper">
<insert id="insertMember" parameterType="Member" useGeneratedKeys="true" keyProperty="id">
INSERT INTO members (username, email)
VALUES (#{username}, #{email})
</insert>
<select id="findMemberById" parameterType="int" resultType="Member">
SELECT id, username, email
FROM members
WHERE id = #{id}
</select>
<select id="findAllMembers" resultType="Member">
SELECT id, username, email
FROM members
ORDER BY id ASC
</select>
<update id="updateMember" parameterType="Member">
UPDATE members
SET username = #{username},
email = #{email}
WHERE id = #{id}
</update>
<delete id="deleteMember" parameterType="int">
DELETE
FROM members
WHERE id = #{id}
</delete>
</mapper>
6. 서비스 계층 (Service Layer)
컨트롤러와 매퍼(DAO) 사이에서 비즈니스 로직을 처리하는 계층입니다. (지금 예제에서는 단순 호출만 합니다.)
src/main/java/com/example/demo/service/MemberService.java (인터페이스)
package com.example.demo.service;
import com.example.demo.dto.Member;
import java.util.List;
public interface MemberService {
Member createMember(Member member);
Member getMemberById(int id);
List<Member> getAllMembers();
Member updateMember(int id, Member member);
void deleteMember(int id);
}
src/main/java/com/example/demo/service/MemberServiceImpl.java (구현체)
- @Service 어노테이션을 붙여 비즈니스 로직을 담당하는 서비스 빈(Bean)으로 등록합니다.
- MemberMapper를 주입받아 사용합니다.
package com.example.demo.service;
import com.example.demo.dto.Member;
import com.example.demo.mapper.MemberMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service // 이 클래스가 서비스 레이어의 컴포넌트임을 선언
public class MemberServiceImpl implements MemberService {
private final MemberMapper memberMapper;
// 생성자 주입 (Constructor Injection)
// @Autowired를 생략할 수 있습니다 (Spring 4.3 이상).
public MemberServiceImpl(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
@Override
public Member createMember(Member member) {
memberMapper.insertMember(member);
// insertMember 호출 시 useGeneratedKeys="true" keyProperty="id" 설정 덕분에
// member 객체에 생성된 id가 자동으로 채워집니다.
return member;
}
@Override
public Member getMemberById(int id) {
return memberMapper.findMemberById(id);
}
@Override
public List<Member> getAllMembers() {
return memberMapper.findAllMembers();
}
@Override
public Member updateMember(int id, Member member) {
// 전달받은 id를 member 객체에 설정합니다.
member.setId(id);
memberMapper.updateMember(member);
// 수정된 객체를 다시 조회하여 반환하거나, member 객체를 그대로 반환할 수 있습니다.
// 여기서는 수정된 member 객체를 반환합니다.
return member;
}
@Override
public void deleteMember(int id) {
memberMapper.deleteMember(id);
}
}
7. REST 컨트롤러 (Controller)
클라이언트(웹 브라우저, 모바일 앱 등)의 HTTP 요청을 받아 서비스 계층으로 전달하고, 그 결과를 HTTP 응답으로 반환하는 관문입니다.
src/main/java/com/example/demo/controller/MemberController.java
- @RestController: 이 클래스의 모든 메서드가 @ResponseBody를 포함하도록 하여, 뷰 템플릿이 아닌 데이터(주로 JSON)를 반환하도록 합니다.
- @RequestMapping("/api/members"): 이 컨트롤러의 모든 메서드는 /api/members 라는 공통 URL 경로를 가집니다.
package com.example.demo.controller;
import com.example.demo.dto.Member;
import com.example.demo.service.MemberService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController // REST API를 위한 컨트롤러임을 선언
@RequestMapping("/api/members") // 이 컨트롤러의 기본 URL 경로
public class MemberController {
private final MemberService memberService;
// 생성자 주입
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
// (C)reate: 회원 생성 (POST /api/members)
// @RequestBody: HTTP 요청 본문(body)에 담긴 JSON 데이터를 Member 객체로 변환
@PostMapping
public ResponseEntity<Member> createMember(@RequestBody Member member) {
Member createdMember = memberService.createMember(member);
// 생성된 리소스(createdMember)와 함께 HTTP 201 Created 상태 코드를 반환
return new ResponseEntity<>(createdMember, HttpStatus.CREATED);
}
// (R)ead: 모든 회원 조회 (GET /api/members)
@GetMapping
public ResponseEntity<List<Member>> getAllMembers() {
List<Member> members = memberService.getAllMembers();
return ResponseEntity.ok(members); // (단축형) return new ResponseEntity<>(members, HttpStatus.OK);
}
// (R)ead: 특정 회원 조회 (GET /api/members/{id})
// @PathVariable: URL 경로에 포함된 값(id)을 파라미터로 받음
@GetMapping("/{id}")
public ResponseEntity<Member> getMemberById(@PathVariable int id) {
Member member = memberService.getMemberById(id);
if (member != null) {
return ResponseEntity.ok(member);
} else {
// 해당 ID의 회원이 없으면 404 Not Found 반환
return ResponseEntity.notFound().build();
}
}
// (U)pdate: 회원 정보 수정 (PUT /api/members/{id})
@PutMapping("/{id}")
public ResponseEntity<Member> updateMember(@PathVariable int id, @RequestBody Member member) {
Member updatedMember = memberService.updateMember(id, member);
if (updatedMember != null) {
return ResponseEntity.ok(updatedMember);
} else {
return ResponseEntity.notFound().build();
}
}
// (D)elete: 회원 삭제 (DELETE /api/members/{id})
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMember(@PathVariable int id) {
memberService.deleteMember(id);
// 삭제 성공 시 HTTP 204 No Content 반환
return ResponseEntity.noContent().build();
}
}
8. 실행 및 테스트
- DemoApplication.java (메인 애플리케이션)을 실행합니다.
- Postman, Insomnia 같은 API 테스트 도구나 curl 명령어를 사용하여 API를 테스트합니다.
- 생성 (POST): POST http://localhost:8080/api/members
- Body (JSON): {"username": "testuser", "email": "test@example.com"}
- 전체 조회 (GET): GET http://localhost:8080/api/members
- 단건 조회 (GET): GET http://localhost:8080/api/members/1 (ID가 1인 회원)
- 수정 (PUT): PUT http://localhost:8080/api/members/1
- Body (JSON): {"username": "updatedUser", "email": "updated@example.com"}
- 삭제 (DELETE): DELETE http://localhost:8080/api/members/1
