[Spring] REST API 구현 (Spring Boot, JPA, H2)과 Content Negotiation(콘텐츠 협상)
by 우와한개발자
[REST API 구현]
- Spring Boot, Spring JPA, h2-console 적용
1. pom.xml
<dependencies>
<!-- Spring Web (REST API) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 인메모리 DB -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- XML 응답 지원 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. application.properties
# H2 콘솔 활성화
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# DataSource 설정
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA 설정
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
3. Entity
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private int age;
}
4. DTO
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberDto {
private Long id;
private String name;
private String email;
private int age;
}
5. Repository
- JpaRepository를 상속하면 기본 CRUD 메서드가 자동으로 제공됨
- 별도 쿼리 없이 save(), findById(), findAll(), deleteById() 등 사용 가능
public interface MemberRepository extends JpaRepository<Member, Long> {
}
6. Service
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 전체 조회
public List<MemberDto> findAll() {
return memberRepository.findAll().stream()
.map(m -> MemberDto.builder()
.id(m.getId())
.name(m.getName())
.email(m.getEmail())
.age(m.getAge())
.build())
.collect(Collectors.toList());
}
// 단건 조회
public MemberDto findById(Long id) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다: " + id));
return MemberDto.builder()
.id(member.getId())
.name(member.getName())
.email(member.getEmail())
.age(member.getAge())
.build();
}
// 등록
public MemberDto save(MemberDto dto) {
Member member = Member.builder()
.name(dto.getName())
.email(dto.getEmail())
.age(dto.getAge())
.build();
Member saved = memberRepository.save(member);
dto.setId(saved.getId());
return dto;
}
// 수정
public MemberDto update(Long id, MemberDto dto) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다: " + id));
member.setName(dto.getName());
member.setEmail(dto.getEmail());
member.setAge(dto.getAge());
memberRepository.save(member);
dto.setId(id);
return dto;
}
// 삭제
public void delete(Long id) {
memberRepository.deleteById(id);
}
}
7. Controller
- @RestController : @Controller + @ResponseBody 조합.
- 모든 메서드가 JSON/XML 등으로 직렬화되어 응답됨
- produces 속성으로 JSON, XML 모두 지원 (Content Negotiation)
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
// 전체 조회
@GetMapping(produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public ResponseEntity<List<MemberDto>> findAll() {
return ResponseEntity.ok(memberService.findAll());
}
// 단건 조회
@GetMapping(value = "/{id}", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public ResponseEntity<MemberDto> findById(@PathVariable Long id) {
return ResponseEntity.ok(memberService.findById(id));
}
// 등록
@PostMapping(produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public ResponseEntity<MemberDto> save(@RequestBody MemberDto dto) {
return ResponseEntity.status(HttpStatus.CREATED).body(memberService.save(dto));
}
// 수정
@PutMapping(value = "/{id}", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public ResponseEntity<MemberDto> update(@PathVariable Long id, @RequestBody MemberDto dto) {
return ResponseEntity.ok(memberService.update(id, dto));
}
// 삭제
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
memberService.delete(id);
return ResponseEntity.noContent().build();
}
}
| 메서드 | URL | HTTP 메서드 | 상태코드 |
| findAll() | /members | GET | 200 OK |
| findById() | /members/{id} | GET | 200 OK |
| save() | /members | POST | 201 Created |
| update() | /members/{id} | PUT | 200 OK |
| delete() | /members/{id} | DELETE | 204 No Content |
[Content Negotiation (컨텐츠 협상)]
1. XML 형식 응답 구현
- 클라이언트에서 Content-Type 요청헤더를 application/xml로 설정한 경우 xml 반환하도록 함
1) pom.xml
- 기본적으로 Spring Boot는 JSON 응답만 지원하기 때문에 XML 응답을 위해 의존성 추가 필요
- 해당 의존성 추가 시 Spring Boot가 MappingJackson2XmlHttpMessageConverter를 자동으로 등록
<!-- XML 직렬화를 위한 Jackson XML 모듈 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2) Controller 핸들러 메서드
- @RequestMapping 계열 어노테이션에 produces 속성으로 지원할 응답 형식 명시
// JSON만 응답
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
// XML만 응답
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_XML_VALUE)
// JSON, XML 모두 응답 가능 (Content Negotiation)
@GetMapping(value = "/{id}", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
3) DTO
- XML 형식으로 직렬화될 수 있도록 @JacksonXmlRootElement 어노테이션 추가
@JacksonXmlRootElement(localName = "member") // XML 루트 태그 이름 지정
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member{
private Long id;
private String name;
private String email;
private int age;
}
(1) localName 속성이란?
- @JacksonXmlRootElement의 속성으로 XML 루트 태그의 이름을 직접 지정하는 것
- 필수 아닌 선택
<!-- localName 속성 지정한 경우 : localName에 지정한 값이 태그 이름이 됨-->
<member>
<id>1</id>
<name>홍길동</name>
</member>
<!-- localName 속성 지정하지 않은 경우 : 클래스명이 그대로 태그 이름이 됨-->
<Member>
<id>1</id>
<name>홍길동</name>
</Member>`
2. Content Negotiation(컨텐츠 협상)
- 서버가 클라이언트의 요청에 따라 적절한 형식의 콘텐츠를 자동으로 제공하는 메커니즘
- 클라이언트가 선호하는 콘텐츠 형식(예: JSON, XML)을 서버에 전달하면, 서버는 이를 기반으로 적합한 응답을 반환함.
| 헤더 | 방향 | 의미 |
| Accept | 클라이언트 → 서버 | 클라이언트가 받고 싶은 응답 형식 |
| Content-Type | 클라이언트 → 서버 | 클라이언트가 보내는 요청 body의 형식 |
| Content-Type | 서버 → 클라이언트 | 서버가 응답하는 body의 형식 |
1) 내부 동작
- jackson-dataformat-xml 의존성을 추가하면 XML 컨버터도 자동으로 등록됨
클라이언트 요청 (Accept: application/json)
↓
ContentNegotiationManager // Accept 헤더를 읽고 응답 형식 결정
↓
HttpMessageConverter // 결정된 형식으로 실제 객체를 변환
(MappingJackson2HttpMessageConverter → JSON)
(MappingJackson2XmlHttpMessageConverter → XML)
↓
Java 객체 → JSON/XML 직렬화 후 응답
| 담당 | 역할 |
| ContentNegotiationManager | Accept 헤더를 읽고 응답 형식 결정 |
| HttpMessageConverter | 결정된 형식으로 실제 객체를 변환 |
| 컨버터 | 처리 형식 | 자동 등록 조건 |
| MappingJackson2HttpMessageConverter | application/json | spring-boot-starter-web에 Jackson 기본 포함(자동 등) |
| MappingJackson2XmlHttpMessageConverter | application/xml | jackson-dataformat-xml 별도 추가 필요 |
| StringHttpMessageConverter | text/plain | spring-boot-starter-web에 기본 포함(자동 등록) |
'프레임워크 > Spring' 카테고리의 다른 글
블로그의 정보
우와한개발자 님의 블로그
우와한개발자