반응형
Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

배움과 기록의 장

[Spring] MVC - API 계층 본문

backend/spring

[Spring] MVC - API 계층

chaeunii 2025. 4. 10. 11:23

2023.2.13 작성

 

🔸 API 계층 구현

계층형 아키텍처

Spring MVC에서 클라이언트 요청의 최종목적지인 API계층, 즉 Controller 클래스를 설계하고 구현한다.

 

 

🔸 프로젝트 환경 구성

1. 샘플 프로젝트 소개

1) 애플리케이션 이름: 커피주문 웹 애플리케이션 (커피 주문을 위해 필요한 정보를 제공하기 위한 서버용 웹애플리케이션)

2) 애플리케이션 경계(앱 기능을 특정 범위로 제한하는 것) : 커피만 주문할 수 있도로 기능 제한

3)  커피 주문을 위해 필요한 정보: Coffee , Member, Order

4)  커피 주문 앱에서 제공할 기능: 주인이 커피정보를 등록,수정,조회,삭제하는 기능 등

 

2. 샘플 프로젝트 환경 구성

- Project: Gradle-Groovy

- Language: Java

- Spring Boot: 2.7.8

- Packaging: Jar

- Java: 11

- Dependencies:

   Lombok(애너테이션을 통해 자주 사용하는 자바코드를 자동으로 구성해주는 라이브러리),

   Spring Web(’Spring Web’ Spring 기반의 웹 애플리케이션을 개발하는데 필요한 의존 라이브러리들을 자동으로 설정해주는 모듈들을 포함)

 

 

 

🔸 Controller 클래스 설계 및 구조 생성

 

1) 샘플 프로젝트의 Java 패키지 구조 잡기

    - 주로 사용되는 구조에는,

  • 기능기반 패키지 구조(package-by- feature): coffee 기능, member 기능 등.. 각 기능 안에 계층별 클래스들이 모여있음
  • 계층 기반 패키지 구조(package-by-layer): API계층(controller, dto), 서비스계층(model, service), 데이터계층(repository)

   - 앱의 요구사항이나 특서에 따라 상황에 맞게 사용하면 되지만, Spring Boot 팀에서는 테스트와 리팩토링이 용이하고, 향후에 마이크로 서비스 시스템으로의 분리가 상대적으로 용이한 기능 기반 패키지 구조 사용을 권장

 

   - 샘플 프로젝트 패키지 구조 (대략)

  • coffee
    • coffeeController
    • coffeeService
    • coffeeRepository
  • member
    • memberController
    • memberService
    • memberRepository
  • order
    • orderController
    • orderService
    • orderRepository

 

2) 앤트리 포인트 클래스 작성 (Entrypoint, 애플리케이션 시작점)

  - Spring Boot 기반의 애플리케이션이 정상적으로 실행되기 위해서 가장 먼저 해야될 일

  - ‘Spring Initializr’를 통해 생성한 프로젝트에는 main() 메서드가 포함된 엔트리포인트 클래스가 이미 작성

  - 엔트리 포인트 클래스

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication   // (a)
public class Section3Week1Application {

	public static void main(String[] args) {
    // (b)
		SpringApplication.run(Section3Week1Application.class, args);
	}

}

- (a) @SpringBootApplication 

  • 자동 구성을 활성화
  • 애플리케이션 패키지 내에서 @Component가 붙은 클래스를 검색한 후(scan), Spring Bean으로 등록하는 기능을 활성화
  • @Configuration 이 붙은 클래스를 자동으로 찾아주고, 추가적으로 Spring Bean을 등록하는 기능을 활성화

- (b) SpringApplication.run(Section3Week1Application.class, args);

  • Spring 애플리케이션을 부트스트랩하고, 실행하는 역할
  • 부트스트랩이란? 앱 실행 전 여러가지 설정 작업을 수행하여 실행 가능한 애플리케이션으로 만드는 단계를 의미

 

3) 샘플 프로젝트의 Controller 구조 작성

- member 패키지 안의 memberController (coffee, order 도 마찬가지)

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController   // (a)
@RequestMapping("/v1/members")   // (b)
public class MemberController {
}

- (a) @RestController

  • 특정 클래스에 @RestController 를 추가하면, Spring MVC에서는 해당 클래스가 REST API의 리소스를 처리하기 위한 API 엔드포인트로 동작함을 정의
  • @RestController 가 추가된 클래스는 애플리케이션 로딩 시, Spring Bean으로 등록

- (b) @RequestMapping("/v1/members")   

  • 클라이언트의 요청과 클라이언트 요청을 처리하는 핸들러 메서드(Handler Method)를 매핑해주는 역할
  • Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL(Base URL) 설정

 

🔸 핸들러 메서드

1) 핸들러 메서드란? Controller 클래스 내 클라이언트의 요청을 처리하는 메서드

2) 핸들러 메서드 적용

- member 패키지 안의 memberController (coffee, order 도 마찬가지)

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/v1/members", produces = {MediaType.APPLICATION_JSON_VALUE}) //(a)
public class MemberController {
    @PostMapping //(b)
    public String postMember(@RequestParam("email") String email, //(c)
                             @RequestParam("name") String name,
                             @RequestParam("phone") String phone) {
        
        // 서비스로직은 아직 고려 x
        
        String response =  //(d)
                "{\"" + 
                   "email\":\""+email+"\"," + 
                   "\"name\":\""+name+"\",\"" + 
                   "phone\":\"" + phone+ 
                "\"}"; 
        return response;
    }

    @GetMapping("/{member-id}") //(e)
    public String getMember(@PathVariable("member-id")long memberId) { //(f)
      
        // 서비스로직은 아직 고려 x
        
        return null;
    }

    @GetMapping
    public String getMembers() {
        
        // 서비스로직은 아직 고려 x
        
        return null;
    }
}

- (a) @RequestMapping(value = "/v1/members", produces = {MediaType.APPLICATION_JSON_VALUE})

  • produces애트리뷰트(Attribute)는 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정
  • 여기서는 JSON 형식의 응답 데이터를 전송하겠다는 의미
  • 이 값을 설정하지 않으면 여기서 설정한 JSON 형식의 데이터가 아닌 문자열 자체를 전송

- (b) @PostMapping

  • 클라이언트의 요청 데이터(request body)를 서버에 생성(POST)할 때 사용하는 애너테이션
  • 요청 전송 시, HTTP Method 타입을 동일하게 맞춰(POST) 전송해야 한다

- (c) @RequestParam("~~")

  • 핸들러 메서드의 파라미터 종류 중 하나
  • 주로 클라이언트로부터 쿼리 파라미터(Query Parmeter 또는 Query String), 폼 데이터(form-data), x-www-form-urlencoded 형식으로 전송된 데이터를 받을 때 사용하는 애너테이션

- (d) String response = "~~~(JSON형식)~~~"

  • REST API 기반 애플리케이션이기 때문에 JSON 형식에 맞게 작성한 후, JSON 형식의 응답데이터 전송
  • 지금처럼 일일이 작성하면 번거롭고 오타로 인한 에러가 발생할 가능성이 높다 -> 개선예정

- (e) @GetMapping("/{member-id}")

  • 클라이언트가 서버에 리소스를 조회(GET)할 때 사용하는 애너테이션
  • 애너테이션의 괄호 안 애트리뷰트로, 전체 HTTP URI의 일부를 지정 (사용할 수 있는 애트리뷰트 더 있음)
  • {member-id}는 회원 식별자를 의미하며, 클라이언트가 어떤 값을 지정하느냐에 따라 동적으로 바뀌는 값

- (f) @PathVariable("member-id")

  • @RequestParam과 마찬가지로 핸들러 메서드의 파라미터 종류 중 하나
  • 괄호 안의 값은 @GetMapping("/{member-id}")의 중괄호{}안의 문자열과 동일해야 한다.  => “member-id”
  • 만약 두 문자열이 다르다면 MissingPathVariableException이 발생한다.

3) 개선이 필요한 곳

- 수작업으로 만들어준 JSON문자열

- @RequestParam 애너테이션을 통한 요청 파라미터 수신 (파라미터가 늘어날 경우 비효율적임)

 

🔸 응답데이터에 ResponseEntity 적용 ("수작업으로 만들어준 JSON문자열" 개선!!)

1) 개선된 코드

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/v1/members") // (a)
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@RequestParam("email") String email,
                                     @RequestParam("name") String name,
                                     @RequestParam("phone") String phone) {
        // (b)
        Map<String, String> map = new HashMap<>();
        map.put("email", email);
        map.put("name", name);
        map.put("phone", phone);

        // (c)
        return new ResponseEntity<>(map, HttpStatus.CREATED);
    }

    @GetMapping("/{member-id}")
    public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
        System.out.println("# memberId: " + memberId);

        // 서비스로직은 아직 고려 x

        // (d)
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getMembers() {
        System.out.println("# get Members");

        // 서비스로직은 아직 고려 x

        // (e)
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

- (a) 기존에 있던 produces 애트리뷰트 없애줬다

  • (b)에서 보면 Map을 던져줄 건데, Map 을  던져주면 내부적으로 JSON형식의 응답데이터로 자동변환 해주기 때문이다.

- (b) JSON 문자열을 직접 작성하는 수작업을 Map객체로 대체하였다.

- (c) 리턴 시에 map과 HTTP응답상태를 ResponseEntity로 한번 감싸서 리턴해주었다.

- (d), (e)도 마찬가지로 수정해주었다.

 

반응형