우와한 개발자

[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에 기본 포함(자동 등록)

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기