최근의 서비스/애플리케이션의 개발 흐름:
2000년 로이 필딩이 제안한 아키텍처 스타일
| 기존 방식 | REST 방식 |
|---|---|
| /getUserInfo?id=123 | /users/123 (자원 중심) |
| 주로 GET과 POST만 사용 | GET, POST, PUT, DELETE 등 활용 |
| 서버에 세션 상태 유지 경향 | Stateless: 각 요청은 독립적 |
| HTML | JSON, XML 등 표준화된 형식 |
| - | 클라이언트-서버가 명확히 분리됨 |
| 확장성 제한(높은 결합도) | 확장성 높음(분산 시스템에 적합) |
| - | URI에 버전정보 포함 가능 (예: /api/v1/users) |
members/{mno}/addresses 자원의 관계를 URL에 반영하여 계층 구조 표현 /members, /members/{mno}같은 URL에 다양한 요청 방식 사용 예시:
http://example.com/api/members/123에 대한URL 상의 변수를 처리하기 위한 어노테이션, 자동 형 변환 지원
@GetMapping("/members/${mno}")
@ResponseBody
public Map<String, ?> getMember(@PathVariable int mno) {
// ...
}
어노테이션 구조:
@Target(ElementType.PARAMETER)
public @interface PathVariable {
@AliasFor("value")
String name() default "";
boolean required() default true;
}
일반 @Controller에서 REST 서비스를 위해 사용
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ResponseBody {}
@Controller이면서 @ResponseBody 기능을 함께 제공
@Target(ElementType.TYPE)
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
body로 전송된 JSON 데이터를 객체로 변환
@Target(ElementType.PARAMETER)
public @interface RequestBody {
boolean required() default true;
}
@RestController
@RequestMapping("/api/v1/members")
@RequiredArgsConstructor
@Slf4j
public class MemberRestController {
private final BasicMemberService mService;
@PostMapping
private Map<String, ?> registMember(@ModelAttribute Member member) {
try {
mService.registMember(member);
return Map.of("status", "SUCCESS", "member", member);
} catch (DataAccessException e) {
e.printStackTrace();
return Map.of("status", "FAIL", "error", e.getMessage());
}
}
}
Talend API Tester를 통한 테스트:
REST API에 대한 자동 문서 생성/관리 시스템
http://server:port/context-path/swagger-ui.html@Configuration
// 문서에 대한 설명
@OpenAPIDefinition(info = @Info(title = "API 명세서 TEST", description = "API 명세서 테스트 입니다.", version = "v1"))
public class SwaggerConfig {
// 관련된 API들의 grouping
@Bean
GroupedOpenApi memberOpenApi() {
String[] paths = { "/api/v1/members/**" };
return GroupedOpenApi.builder().group("Member 관련 API").pathsToMatch(paths).build();
}
@Bean
GroupedOpenApi otherOpenApi() {
String[] paths = { "/api/etc/**" };
return GroupedOpenApi.builder().group("기타 API").pathsToMatch(paths).build();
}
}
@RestController
@RequestMapping("/api/v1/members")
@Tag(name = "AuthRestController", description = "회원 관리를 위한 기능 제공")
public class AuthRestController implements RestControllerHelper {
@GetMapping
@Operation(summary = "회원 목록 조회", description = "모든 회원의 정보를 반환한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원 목록 조회 성공"),
@ApiResponse(responseCode = "500", description = "회원 목록 조회 실패")
})
private ResponseEntity<?> memberList(@ModelAttribute SearchCondition condition, Model model) {
try {
Page<Member> page = mService.search(condition);
return handleSuccess(Map.of("result", page));
} catch (DataAccessException e) {
return handleFail(e);
}
}
}
@Schema(title = "Address(주소)", description = "회원의 주소 정보")
public class Address {
@Schema(description = "주소번호로 저장 시 자동 채번", requiredMode = RequiredMode.NOT_REQUIRED)
private int ano;
@Schema(description = "회원번호", requiredMode = RequiredMode.REQUIRED)
private int mno;
}
크로스 도메인 요청 처리를 위한 도구
SOP(Same origin Policy : 동일 근원 정책)
CORS: 서로 다른 domain 간의 리소스 공유 설정
@CrossOrigin: 컨트롤러 별 설정 WebMvcConfigurer를 통한 전역 설정JavaScript를 통한 요청이 문제이며 일반 요청은 문제되지 않음
Controller에서 REST API를 호출하기 위한 template
주요 메서드 (xxx는 get/post)
xxxForObject: 간단한 요청에 최적화, HTTP 응답 본문만 반환하며 응답 헤더나 상태 코드는 제공하지 않음xxxForEntity: 간단한 요청을 이용하는데, HTTP 응답의 모든 정보를 포함한 ResponseEntity 객체 반환, 응답 상태코드, 헤더, 본문을 포함exchange: 좀 더 복잡한 HTTP 요청을 처리할 때 사용, get/post 뿐 아니라 put, delete 등을 지원, 요청 헤더와 본문 설정 가능@RestController
@RequestMapping("/api/v1/etc")
@RequiredArgsConstructor
@Tag(name = "EtcRestController", description = "기타 유틸리티성 기능 제공")
public class EtcRestController implements RestControllerHelper {
private final RestTemplate restTemplate;
@Value("${key.vworld}")
private String vworldKey;
@GetMapping("/sidos")
@Operation(summary = "시/도조회", description = "국토교통부 디지털트윈 국도에서 시/도 정보 조회")
private ResponseEntity<?> getSido() {
try {
StringBuilder url = new StringBuilder("https://api.vworld.kr/ned/data/admCodeList?");
url.append("format=json&").append("numOfRows=10&domain=localhost&").append("key=").append(vworldKey);
Map<String, Object> result = restTemplate.getForObject(new URI(url.toString()), Map.class);
return handleSuccess(result);
} catch (Exception e) {
return handleFail(e);
}
}
}