HTTP 메서드 이해: 멱등성, 안전성 및 사용자 권한 검증까지
📌 서론
REST API에서 사용되는 HTTP 메서드들은 클라이언트가 서버에게 어떤 작업을 요청하는 방식을 정의한다. 이 메서드들은 특정 리소스에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행하는데 사용된다. 여기에는 주로 사용되는 여섯 가지 HTTP 메서드가 있으며, 각각의 특성과 멱등성 여부, 그리고 몇몇 메서드에 대해서 사용자 권한 검증을 아래에서 설명하겠다.
멱등성(Idempotency)이란?
동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태도 동일하게 남을 때, 해당 HTTP 메서드가 멱등성을 가졌다고 말한다.
다른 말로는, 멱등성 메서드에는 통계기록 등을 제외하면 어떠한 부수효과(side effect)도 존재해서는 안된다. 예를 들어, 서버는 모든 요청을 받은 시간과 함께 기록할 수 있다. 멱등성(Idempotency)는 클라이언트가 의도한 효과에만 적용된다. 예를 들어, POST 요청은 데이터를 서버로 보내려고 하거나, DELETE 요청은 서버에서 리소스를 삭제하려고 한다.
멱등성을 따질 땐 실제 서버의 백엔드 상태만 보면 되며, 각 요청에서 반환하는 응답 코드는 다를 수 있다. 첫 번째 DELETE 요청이 200을 반환한다면, 그 이후는 아마 404를 반환할 것이다. DELETE가 멱등성을 가진다는 것은, REST API에서 개발자는 DELETE 메서드를 사용해 "목록의 마지막 항목 제거" 기능을 구현해서는 안 된다는 것이다.
(출처: https://developer.mozilla.org/ko/docs/Glossary/Idempotent)
safe도 멱등성과 같이 자주 사용되는 용어다. "안전하다"는 HTTP 메서드가 서버의 리소스를 변경하지 않는다는 것을 의미한다.
즉, 이러한 요청은 리소스의 상태나 서버의 데이터를 읽는 데 사용되며, 서버에 어떠한 변화도 주지 않아야 한다. GET 메서드가 대표적인 예시로, 데이터를 조회하는 용도로 사용되며 서버의 상태를 변경하지 않는다. 안전한 메서드는 서버의 데이터에 영향을 주지 않기 때문에, 캐싱이나 미리 로드하기 같은 목적으로 자유롭게 사용될 수 있다.
HTTP 메서드
1. GET
- 설명: 서버로부터 정보를 조회하기 위해 사용된다. GET 요청은 데이터를 가져오는 데 사용되며, 서버의 상태를 변경하지 않아야 한다.
- 멱등성: 있음. 같은 GET 요청을 여러 번 보내도 서버의 상태가 바뀌지 않으므로 멱등하다.
- 예시 코드: 사용자 정보 조회를 위한 API 요청
GET /api/users/1
이 요청은 ID가 1인 사용자의 정보를 조회한다. GET 요청은 서버의 데이터를 변경하지 않으므로 안전하며, 같은 요청을 여러 번 해도 동일한 사용자 정보를 반환하기 때문에 멱등성을 가진다.
2. POST
- 설명: 서버에 데이터를 전송하여 새로운 리소스를 생성하기 위해 사용된다. 예를 들어, 새 사용자 등록, 게시글 작성 등에 사용된다.
- 멱등성: 없음. 동일한 POST 요청을 여러 번 보내면 동일한 작업이 여러 번 수행되어 서로 다른 결과를 생성할 수 있다.
- 예시 코드: 새로운 사용자 등록을 위한 API 요청
POST /api/users
Content-Type: application/json
{
"name": "홍길동",
"email": "hong@example.com"
}
이 요청은 새로운 사용자를 생성한다. POST는 서버의 상태를 변경하므로 안전하지 않으며, 같은 요청을 여러 번 수행하면 동일한 데이터를 가진 사용자가 여러 명 생성될 수 있어 멱등성이 없다.
3. PUT
- 설명: 서버에 존재하는 리소스를 대체하는 데 사용된다. 예를 들어, 사용자의 정보를 갱신할 때 사용할 수 있다.
- 멱등성: 있음. 동일한 PUT 요청을 여러 번 보내도 마지막 요청의 결과만이 유지되므로, 서버의 상태는 동일하게 유지된다.
- 예시 코드: 사용자 정보 업데이트를 위한 API 요청
PUT /api/users/1
Content-Type: application/json
{
"name": "김철수",
"email": "kim@example.com"
}
이 요청은 ID가 1인 사용자의 정보를 주어진 데이터로 전체 업데이트한다. PUT은 요청된 리소스의 전체를 대체하므로, 같은 요청을 여러 번 수행해도 최종적으로는 동일한 상태를 유지하게된다. 따라서 멱등성이 있다.
4. DELETE
- 설명: 서버의 특정 리소스를 삭제하는 데 사용된다.
- 멱등성: 있음. 동일한 DELETE 요청을 여러 번 보내도 한 번 삭제된 리소스는 없어지므로, 추가적인 요청은 상태 변화를 일으키지 않는다.
- 예시 코드: 사용자 삭제를 위한 API 요청
DELETE /api/users/1
이 요청은 ID가 1인 사용자를 삭제한다. DELETE 요청은 해당 리소스를 서버에서 제거하며, 이미 삭제된 리소스에 대해 동일한 DELETE 요청을 수행해도 서버의 상태는 변하지 않는다. 이로 인해 멱등성을 가진다.
5. PATCH
- 설명: 리소스의 일부를 업데이트하는 데 사용된다. PUT과 다르게 전체 리소스를 대체하지 않고, 일부분만 변경한다.
- 멱등성: 경우에 따라 다름. PATCH 요청이 항상 멱등하지는 않지만, 요청이 항상 동일한 변경을 적용한다면 멱등할 수 있다.
- 예시 코드: 사용자 일부 정보 업데이트를 위한 API 요청
PATCH /api/users/1
Content-Type: application/json
{
"email": "newemail@example.com"
}
이 요청은 ID가 1인 사용자의 이메일 주소만을 새 값으로 업데이트한다. PATCH는 리소스의 일부만을 수정하는 경우에 사용되며, 같은 요청을 여러 번 수행하면 최종적으로는 동일한 결과를 유지한다. 그러나 구현 방식에 따라 멱등성이 보장되지 않을 수도 있다.
6. OPTIONS
- 설명: 대상 리소스가 지원하는 메서드를 확인하기 위해 사용된다. 주로 CORS(Cross-Origin Resource Sharing)의 사전 요청(pre-flight request)으로 사용된다.
- 멱등성: 있음. OPTIONS 요청은 서버의 상태를 변경하지 않으므로 멱등하다.
- 예시 코드: 리소스가 지원하는 메서드 확인을 위한 API 요청
OPTIONS /api/users/1
이 요청은 클라이언트가 /api/users/1 리소스에 대해 수행할 수 있는 HTTP 메서드를 서버로부터 조회한다. OPTIONS 요청은 서버의 상태를 변경하지 않으므로 멱등하며, 리소스에 대해 어떤 HTTP 메서드들이 허용되는지 알려주는 용도로 사용된다.
GET과 POST의 차이
- 용도
- GET: 데이터를 조회하는 데 사용된다. 서버의 데이터나 상태를 변경하지 않고, 리소스를 가져오기만 한다.
- POST: 서버에 데이터를 전송하여 새로운 리소스를 생성하거나, 데이터를 처리하기 위한 일반적인 용도로 사용된다.
- 데이터 전송 방식
- GET: 요청 데이터가 URL의 일부로 전송된다. 예를 들어, /api/search?query=example와 같이 쿼리 스트링을 통해 데이터를 전달한다.
- POST: 요청 데이터가 HTTP 메시지의 바디에 포함되어 전송된다. 이 방식은 GET 방식보다 많은 양의 데이터를 안전하게 전송할 수 있다는 장점이 있다.
- 안전성과 멱등성
- GET: 안전하고 멱등하다. 같은 GET 요청을 여러 번 해도 서버의 상태를 변경하지 않는다.
- POST: 안전하지 않고 멱등하지 않다. 같은 POST 요청을 여러 번 하면 같은 작업이 여러 번 수행될 수 있으며, 이로 인해 서버의 상태가 변경될 수 있다.
PUT과 PATCH의 차이
- 용도
- PUT: 지정된 URI에 리소스를 생성하거나, 이미 존재하는 경우 전체 리소스를 대체한다. PUT은 전체 리소스를 클라이언트에서 서버로 전송한다.
- PATCH: 기존 리소스의 일부만 수정하기 위해 사용된다. PATCH는 전체 리소스 대신 변경할 부분의 데이터만 전송한다.
- 멱등성
- PUT: 멱등하다. 동일한 PUT 요청을 여러 번 수행해도 마지막 요청의 결과만 유지되므로, 서버의 상태는 동일하게 유지된다.
- PATCH: 원칙적으로 멱등성을 가질 수 있으나, 구현에 따라 멱등하지 않을 수도 있다. PATCH 요청의 멱등성은 요청이 항상 동일한 변경을 적용할 때만 보장된다.
PATCH의 멱등성 상세 설명
PATCH의 멱등성은 요청의 본문과 서버에서의 적용 방식에 따라 달라진다. 예를 들어, 리소스의 특정 필드를 새 값으로 변경하는 PATCH 요청은 여러 번 수행해도 리소스의 해당 필드가 마지막으로 지정된 값으로 유지되므로 멱등하다고 할 수 있다. 그러나, 값을 증가시키는 연산과 같이 현재 리소스의 상태에 따라 결과가 달라지는 경우(예: "사용자의 포인트를 10 증가시킨다"), 같은 PATCH 요청을 여러 번 수행하면 결과가 달라질 수 있으므로 멱등하지 않다.
멱등성은 요청을 재시도하는 과정에서 중요한 개념이다. 네트워크 지연이나 오류로 인해 요청의 성공 여부가 불확실한 경우, 멱등한 요청은 안전하게 여러 번 재시도할 수 있지만, 멱등하지 않은 요청은 부작용을 일으킬 수 있어 주의가 필요하다.
사용자 권한 확인
PUT, DELETE, PATCH와 같은 HTTP 메서드들은 서버의 리소스를 변경하거나 삭제할 수 있기 때문에, 이러한 요청을 처리하기 전에 사용자의 권한을 확인하는 것이 중요하다. 이를 위해 사용자 인증 및 권한 확인을 담당하는 미들웨어를 API 서버에 구현하는 것이 일반적인 방법이다. 이러한 미들웨어는 요청이 처리되기 전에 요청을 보낸 사용자가 해당 작업을 수행할 권한이 있는지를 검사한다.
사용자 권한 확인 미들웨어 구현의 중요성
- 보안 강화: 무분별한 데이터 수정, 삭제를 방지하여 시스템의 보안을 강화할 수 있다.
- 데이터 무결성 유지: 적절한 권한을 가진 사용자만이 데이터를 변경할 수 있도록 함으로써 데이터의 무결성을 유지할 수 있다.
- 사용자 경험 개선: 권한이 없는 사용자에게 적절한 오류 메시지를 반환함으로써 사용자 경험을 개선할 수 있다.
Spring Boot에서 사용자 권한 확인 미들웨어 구현
1. AOP(관점 지향 프로그래밍) 사용
AOP는 애플리케이션의 핵심 로직에서 분리하여 공통 관심사를 모듈화하는 프로그래밍 패러다임이다. 예를 들어, 로깅, 트랜잭션 관리, 보안 검사(권한 검사 포함) 등이 이에 해당된다. 스프링 부트에서는 주로 @Aspect 어노테이션을 사용하여 AOP를 구현한다. AOP를 사용하면 권한 검사 로직을 한 곳에 정의하고, 어노테이션 등을 통해 필요한 컨트롤러나 서비스 메서드에 적용할 수 있다. 이렇게 하면 코드의 중복을 줄이고, 유지 보수를 용이하게 할 수 있다.
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.project.controller.*.*(..)) && @annotation(com.example.project.security.AdminOnly)")
public void checkAdminRole(JoinPoint joinPoint) {
// 권한 검사 로직을 실행
// 현재 사용자가 ADMIN 권한을 가지고 있는지 검사
}
}
이 예시에서 @Aspect는 AOP의 Aspect를 정의하며, @Before 어드바이스는 타겟 메서드가 실행되기 전에 권한 검사 로직을 실행하도록 한다. execution 표현식은 어떤 메서드에 이 로직을 적용할지를 정의하고, @annotation은 특정 어노테이션이 붙은 메서드에만 이 로직을 적용하게 한다.
AOP를 사용하면, 권한 검사와 같은 공통 관심사를 애플리케이션의 다른 부분에 간섭하지 않고 독립적으로 관리할 수 있다. 이는 코드의 재사용성과 가독성을 높이며, 애플리케이션의 유지 보수성을 개선하는 데 도움이 된다.
2. Spring Security 사용
Spring Boot와 같은 프레임워크를 사용할 때, @PreAuthorize 어노테이션 또는 Spring Security의 커스텀 미들웨어를 통해 요청 처리 전 사용자의 권한을 확인할 수 있다.
// Spring Security를 사용한 권한 확인 예시
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/api/users")
public ResponseEntity<?> createUser(@RequestBody User user) {
// 사용자 생성 로직
}
@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
@PutMapping("/api/users/{userId}")
public ResponseEntity<?> updateUser(@PathVariable Long userId, @RequestBody User user) {
// 사용자 정보 업데이트 로직
}
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/api/users/{userId}")
public ResponseEntity<?> deleteUser(@PathVariable Long userId) {
// 사용자 삭제 로직
}
이 코드 예시에서 @PreAuthorize 어노테이션은 메서드가 호출되기 전에 실행되는 표현식을 기반으로 한 권한 검사를 정의한다.
예를 들어, hasRole('ADMIN')은 사용자가 'ADMIN' 역할을 가지고 있어야 한다는 것을 의미한다. 또한, #userId == principal.id 표현식은 메서드 파라미터로 전달된 userId가 현재 인증된 사용자의 ID와 같아야 한다는 것을 검사한다.
이러한 미들웨어 구현을 통해, API 서버는 민감한 작업을 수행하기 전에 사용자의 권한을 효과적으로 검사할 수 있으며, 서버의 리소스를 안전하게 관리할 수 있다. 사용자 권한 검사는 REST API의 보안 및 데이터 무결성을 유지하는 데 필수적인 부분이다.
'Spring > Spring 기초 지식' 카테고리의 다른 글
XSS 공격 방어: 입력 데이터 이스케이프, CSP, HTTPOnly 설정 (0) | 2023.12.17 |
---|---|
SQL 인젝션 방어: PreparedStatement 기본 사용부터 MyBatis, JPA 적용까지 (0) | 2023.12.16 |