Ver3.0 자바로 웹 서비스 만들기: SOAP부터 REST, GraphQL까지 완벽 가이드 🚀

자바로 웹 서비스 만들기: SOAP부터 REST, GraphQL까지 완벽 가이드 🚀
백엔드 개발자라면 꼭 알아야 할 API 구현 방식 총정리 ✨
요즘 웹 개발하면서 API 안 만드는 사람 있어요? ㅋㅋㅋ 프론트엔드든 백엔드든 API는 이제 필수템이 되어버렸죠 💻
특히 자바로 백엔드 개발하시는 분들이라면 SOAP, REST, GraphQL 이 세 가지 방식은 반드시 알아둬야 하는데요!
오늘은 이 세 가지 API 구현 방식을 완전 정복해보는 시간을 가져볼게요 🎯
솔직히 처음에는 "API가 뭐 그렇게 복잡해?" 했는데 막상 실무에서 써보니까 각각의 장단점이 확실하더라구요 ㅎㅎ
프로젝트 성격에 따라 어떤 방식을 선택하느냐가 개발 생산성과 유지보수에 엄청난 영향을 미치거든요!
재능넷 같은 플랫폼에서도 다양한 개발자들이 이런 기술 스택으로 프로젝트를 진행하고 있답니다 😊
🎭 SOAP: 전통의 강자, 아직도 현역인 이유
SOAP(Simple Object Access Protocol)는 이름과 달리 전혀 Simple하지 않아요 ㅋㅋㅋ
2000년대 초반부터 사용되어온 프로토콜인데, XML 기반으로 메시지를 주고받는 방식이죠 📨
요즘 트렌디한 개발자들은 "SOAP? 그거 레거시 아니야?" 하는데, 실제로는 아직도 금융권이나 공공기관에서 엄청 많이 쓰이고 있어요!
WSDL(Web Services Description Language)이라는 명세서로 API를 정의하는데, 이게 좀 복잡하긴 해도 명확하게 계약을 정의할 수 있다는 장점이 있어요 ✅
자바로 SOAP 웹 서비스 구현하기
자바에서 SOAP 서비스를 만들 때는 주로 JAX-WS(Java API for XML Web Services)를 사용해요.
스프링 부트에서는 spring-boot-starter-web-services 의존성을 추가하면 되구요!
// SOAP 웹 서비스 엔드포인트 예제
@Endpoint
public class UserServiceEndpoint {
private static final String NAMESPACE_URI = "http://example.com/users";
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "getUserRequest")
@ResponsePayload
public GetUserResponse getUser(@RequestPayload GetUserRequest request) {
GetUserResponse response = new GetUserResponse();
User user = new User();
user.setId(request.getId());
user.setName("홍길동");
user.setEmail("hong@example.com");
response.setUser(user);
return response;
}
}
// WSDL 설정
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
@Bean
public ServletRegistrationBean messageDispatcherServlet(
ApplicationContext applicationContext) {
MessageDispatcherServlet servlet =
new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean(servlet, "/ws/*");
}
@Bean(name = "users")
public DefaultWsdl11Definition defaultWsdl11Definition(
XsdSchema usersSchema) {
DefaultWsdl11Definition wsdl11Definition =
new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("UsersPort");
wsdl11Definition.setLocationUri("/ws");
wsdl11Definition.setTargetNamespace("http://example.com/users");
wsdl11Definition.setSchema(usersSchema);
return wsdl11Definition;
}
@Bean
public XsdSchema usersSchema() {
return new SimpleXsdSchema(
new ClassPathResource("users.xsd"));
}
}
Maven 플러그인인 jaxb2-maven-plugin을 사용하면 XSD에서 자동으로 자바 클래스를 생성할 수 있답니다 🛠️
SOAP 메시지는 이렇게 생겼어요. XML이라 좀 장황하긴 한데, 구조가 명확하죠:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:usr="http://example.com/users">
<soapenv:Header/>
<soapenv:Body>
<usr:getUserRequest>
<usr:id>12345</usr:id>
</usr:getUserRequest>
</soapenv:Body>
</soapenv:Envelope>
SOAP를 선택해야 하는 경우
• 금융 거래, 결제 시스템처럼 트랜잭션 보장이 중요한 경우
• 엄격한 보안 요구사항이 있는 기업 환경
• 레거시 시스템과의 통합이 필요한 경우
• ACID 트랜잭션이 필요한 분산 시스템
• 공공기관이나 대기업의 표준 프로토콜로 지정된 경우
개발 복잡도가 높고 러닝커브가 있어서 요즘 신규 프로젝트에서는 잘 안 쓰는 추세예요 ㅠㅠ
🌐 REST: 웹의 언어, 가장 대중적인 선택
REST(Representational State Transfer)는 지금 가장 핫한 API 스타일이죠! 🔥
Roy Fielding이 2000년 박사 논문에서 제안한 아키텍처 스타일인데, HTTP 프로토콜의 장점을 최대한 활용하는 방식이에요.
JSON 형식으로 데이터를 주고받고, URL로 리소스를 표현하고, HTTP 메서드(GET, POST, PUT, DELETE)로 작업을 정의하죠 📡
2. 무상태성(Stateless): 각 요청은 독립적
3. 캐시 가능: 응답을 캐싱해서 성능 향상
4. 계층화 시스템: 중간 서버 추가 가능
5. 인터페이스 일관성: 통일된 인터페이스
6. 주문형 코드(선택): 실행 가능한 코드 전송
스프링 부트로 RESTful API 구현하기
스프링 부트에서 REST API 만드는 건 진짜 쉬워요 ㅋㅋㅋ
@RestController 어노테이션 하나면 끝이거든요!
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// 전체 사용자 조회
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> users = userService.findAll();
return ResponseEntity.ok(users);
}
// 특정 사용자 조회
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
UserDTO user = userService.findById(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
// 사용자 생성
@PostMapping
public ResponseEntity<UserDTO> createUser(
@Valid @RequestBody UserCreateRequest request) {
UserDTO createdUser = userService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(createdUser.getId())
.toUri();
return ResponseEntity.created(location).body(createdUser);
}
// 사용자 수정
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
UserDTO updatedUser = userService.update(id, request);
if (updatedUser == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(updatedUser);
}
// 사용자 삭제
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
boolean deleted = userService.delete(id);
if (!deleted) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
// 사용자 검색
@GetMapping("/search")
public ResponseEntity<List<UserDTO>> searchUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email) {
List<UserDTO> users = userService.search(name, email);
return ResponseEntity.ok(users);
}
}
REST API 설계 베스트 프랙티스
REST API를 잘 설계하려면 몇 가지 원칙을 지켜야 해요! 실무에서 정말 중요한 부분이에요 💪
| 원칙 | 좋은 예 | 나쁜 예 |
|---|---|---|
| 명사 사용 | /api/users | /api/getUsers |
| 복수형 사용 | /api/products | /api/product |
| 계층 구조 | /api/users/123/orders | /api/userOrders?userId=123 |
| 필터링 | /api/users?status=active | /api/activeUsers |
| 버전 관리 | /api/v1/users | /api/users (버전 없음) |
• 201 Created: 리소스 생성 성공 (POST)
• 204 No Content: 성공했지만 반환할 내용 없음 (DELETE)
• 400 Bad Request: 잘못된 요청
• 401 Unauthorized: 인증 필요
• 403 Forbidden: 권한 없음
• 404 Not Found: 리소스 없음
• 500 Internal Server Error: 서버 오류
HATEOAS로 진정한 REST 구현하기
HATEOAS(Hypermedia As The Engine Of Application State)는 REST의 마지막 퍼즐 조각이에요!
응답에 다음 가능한 액션들의 링크를 포함시켜서 클라이언트가 API를 탐색할 수 있게 하는 거죠 🗺️
@RestController
@RequestMapping("/api/users")
public class UserHateoasController {
@GetMapping("/{id}")
public EntityModel<UserDTO> getUserWithLinks(@PathVariable Long id) {
UserDTO user = userService.findById(id);
EntityModel<UserDTO> resource = EntityModel.of(user);
// 자기 자신 링크
resource.add(linkTo(methodOn(UserHateoasController.class)
.getUserWithLinks(id)).withSelfRel());
// 전체 사용자 목록 링크
resource.add(linkTo(methodOn(UserHateoasController.class)
.getAllUsers()).withRel("users"));
// 사용자 주문 목록 링크
resource.add(linkTo(methodOn(OrderController.class)
.getUserOrders(id)).withRel("orders"));
return resource;
}
}
// 응답 예시
{
"id": 123,
"name": "홍길동",
"email": "hong@example.com",
"_links": {
"self": {
"href": "http://localhost:8080/api/users/123"
},
"users": {
"href": "http://localhost:8080/api/users"
},
"orders": {
"href": "http://localhost:8080/api/users/123/orders"
}
}
}
REST API 문서화: Swagger/OpenAPI
API 만들었으면 문서화는 필수죠! Swagger(OpenAPI)를 사용하면 자동으로 예쁜 API 문서를 만들 수 있어요 📚
// Swagger 설정
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.api"))
.paths(PathSelectors.ant("/api/**"))
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("사용자 관리 API")
.description("RESTful API 예제")
.version("1.0.0")
.contact(new Contact("개발팀", "https://www.jaenung.net",
"dev@example.com"))
.build();
}
}
// 컨트롤러에 문서화 추가
@RestController
@RequestMapping("/api/users")
@Api(tags = "사용자 관리")
public class UserController {
@ApiOperation(value = "사용자 조회", notes = "ID로 특정 사용자를 조회합니다")
@ApiResponses({
@ApiResponse(code = 200, message = "조회 성공"),
@ApiResponse(code = 404, message = "사용자를 찾을 수 없음")
})
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(
@ApiParam(value = "사용자 ID", required = true)
@PathVariable Long id) {
// 구현...
}
}
🎯 GraphQL: 데이터 페칭의 혁명
GraphQL은 페이스북(현 Meta)에서 2015년에 오픈소스로 공개한 쿼리 언어예요! 🚀
REST의 단점들을 해결하기 위해 만들어졌는데, 특히 "오버페칭"과 "언더페칭" 문제를 완벽하게 해결했죠.
클라이언트가 필요한 데이터만 정확하게 요청할 수 있다는 게 가장 큰 장점이에요 ✨
• 쿼리(Query): 데이터 조회
• 뮤테이션(Mutation): 데이터 변경
• 서브스크립션(Subscription): 실시간 데이터 구독
• 리졸버(Resolver): 실제 데이터를 가져오는 함수
REST vs GraphQL: 실제 차이 체감하기
예를 들어볼게요! 사용자 정보와 그 사용자의 최근 게시글 3개를 가져온다고 해봅시다 🤔
// 1. 사용자 정보 조회
GET /api/users/123
// 2. 사용자의 게시글 조회
GET /api/users/123/posts?limit=3
// 3. 각 게시글의 댓글 수 조회
GET /api/posts/1/comments/count
GET /api/posts/2/comments/count
GET /api/posts/3/comments/count
// 총 5번의 HTTP 요청 필요! 😱
// 단 1번의 요청으로 모든 데이터 조회!
query {
user(id: 123) {
id
name
email
posts(limit: 3) {
id
title
content
commentsCount
}
}
}
// 딱 필요한 필드만 요청하고 한 번에 받아옴! 🎉
자바에서 GraphQL 구현하기
자바에서는 주로 graphql-java나 Spring for GraphQL을 사용해요.
스프링 부트 3.0부터는 GraphQL이 공식 지원되어서 더 쉬워졌어요! 😊
// 1. 의존성 추가 (build.gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
// 2. GraphQL 스키마 정의 (schema.graphqls)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
commentsCount: Int!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
}
// 3. 컨트롤러 구현
@Controller
public class UserGraphQLController {
@Autowired
private UserService userService;
@QueryMapping
public User user(@Argument Long id) {
return userService.findById(id);
}
@QueryMapping
public List<User> users() {
return userService.findAll();
}
@MutationMapping
public User createUser(@Argument CreateUserInput input) {
return userService.create(input);
}
@MutationMapping
public User updateUser(@Argument Long id,
@Argument UpdateUserInput input) {
return userService.update(id, input);
}
@MutationMapping
public Boolean deleteUser(@Argument Long id) {
return userService.delete(id);
}
// 중첩된 필드 리졸버
@SchemaMapping(typeName = "User", field = "posts")
public List<Post> userPosts(User user) {
return postService.findByUserId(user.getId());
}
}
DataLoader로 N+1 문제 해결하기
GraphQL의 가장 큰 함정이 바로 N+1 쿼리 문제예요! 😱
예를 들어 10명의 사용자와 각자의 게시글을 조회하면, 1번(사용자 목록) + 10번(각 사용자의 게시글) = 11번의 쿼리가 발생하죠.
이걸 해결하는 게 바로 DataLoader예요!
@Configuration
public class DataLoaderConfig {
@Bean
public DataLoader<Long, Post> postDataLoader(PostService postService) {
return DataLoader.newDataLoader((List<Long> postIds) -> {
// 한 번에 모든 게시글을 조회
List<Post> posts = postService.findByIds(postIds);
// ID를 키로 하는 맵으로 변환
Map<Long, Post> postMap = posts.stream()
.collect(Collectors.toMap(Post::getId, p -> p));
// 요청된 순서대로 반환
return CompletableFuture.supplyAsync(() ->
postIds.stream()
.map(postMap::get)
.collect(Collectors.toList())
);
});
}
}
@Controller
public class PostGraphQLController {
@Autowired
private DataLoader<Long, Post> postDataLoader;
@SchemaMapping(typeName = "User", field = "posts")
public CompletableFuture<List<Post>> userPosts(
User user,
DataFetchingEnvironment env) {
DataLoader<Long, Post> loader =
env.getDataLoader("postDataLoader");
// 배치로 로딩
return loader.loadMany(user.getPostIds());
}
}
10명의 사용자 게시글을 조회할 때 10번의 쿼리 대신 1번의 배치 쿼리로 처리하는 거죠 🎯
실시간 데이터: Subscription 구현
GraphQL의 숨겨진 보석, Subscription! 웹소켓 기반으로 실시간 데이터를 푸시할 수 있어요 🔔
// 스키마에 Subscription 추가
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
// 구현
@Controller
public class SubscriptionController {
private final Sinks.Many<Post> postSink =
Sinks.many().multicast().onBackpressureBuffer();
@SubscriptionMapping
public Flux<Post> postCreated() {
return postSink.asFlux();
}
@MutationMapping
public Post createPost(@Argument CreatePostInput input) {
Post newPost = postService.create(input);
// 구독자들에게 알림
postSink.tryEmitNext(newPost);
return newPost;
}
}
// 클라이언트에서 구독
subscription {
postCreated {
id
title
author {
name
}
}
}
GraphQL 보안 고려사항
2. 쿼리 복잡도 계산: 과도한 리소스 사용 차단
3. Rate Limiting: 요청 횟수 제한
4. 인증/인가: 필드 레벨 권한 체크
5. 쿼리 화이트리스트: 허용된 쿼리만 실행
@Configuration
public class GraphQLSecurityConfig {
@Bean
public GraphQLInstrumentation maxQueryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation(10); // 최대 깊이 10
}
@Bean
public GraphQLInstrumentation maxQueryComplexityInstrumentation() {
return new MaxQueryComplexityInstrumentation(100); // 복잡도 100
}
}
// 필드 레벨 인가
@Controller
public class SecureUserController {
@QueryMapping
@PreAuthorize("hasRole('ADMIN')")
public List<User> allUsers() {
return userService.findAll();
}
@SchemaMapping(typeName = "User", field = "email")
public String userEmail(User user,
@AuthenticationPrincipal UserDetails currentUser) {
// 본인 또는 관리자만 이메일 조회 가능
if (user.getId().equals(currentUser.getId()) ||
currentUser.getAuthorities().contains("ROLE_ADMIN")) {
return user.getEmail();
}
return null; // 권한 없으면 null 반환
}
}
⚖️ 세 가지 방식 비교: 어떤 걸 선택할까?
자, 이제 SOAP, REST, GraphQL을 다 살펴봤는데요! 실전에서 어떤 걸 선택해야 할까요? 🤔
정답은... "상황에 따라 다르다"예요 ㅋㅋㅋ (개발자들이 제일 좋아하는 답변 아니겠어요?)
| 비교 항목 | SOAP | REST | GraphQL |
|---|---|---|---|
| 프로토콜 | 엄격한 프로토콜 | 아키텍처 스타일 | 쿼리 언어 |
| 데이터 형식 | XML | JSON, XML 등 | JSON |
| 엔드포인트 | 단일 | 다중 (리소스별) | 단일 |
| 학습 곡선 | 높음 😰 | 낮음 😊 | 중간 🤔 |
| 성능 | 무거움 | 가벼움 | 최적화 가능 |
| 캐싱 | 어려움 | 쉬움 (HTTP 캐시) | 복잡함 |
| 보안 | WS-Security 등 강력 | HTTPS, OAuth 등 | 커스텀 구현 필요 |
| 실시간 | 지원 안 함 | WebSocket 별도 | Subscription 내장 |
프로젝트별 추천 선택
• 공공기관 시스템 통합
• 엄격한 보안과 트랜잭션이 필요한 경우
• 레거시 시스템과의 호환성이 중요한 경우
• ACID 트랜잭션이 필수인 분산 시스템
• 모바일 앱 백엔드
• 마이크로서비스 아키텍처
• 공개 API (외부 개발자에게 제공)
• 캐싱이 중요한 시스템
• 빠른 개발이 필요한 MVP 프로젝트
• 다양한 클라이언트 (웹, 모바일, 태블릿)를 지원
• 실시간 기능이 필요한 경우
• 네트워크 대역폭이 제한적인 환경
• 프론트엔드 팀이 백엔드 의존도를 줄이고 싶을 때
• 데이터 페칭 최적화가 중요한 경우
하이브리드 접근: 함께 사용하기
사실 실무에서는 하나만 고집할 필요 없어요! 상황에 맞게 섞어 쓰는 게 현명하죠 💡
예를 들면 이런 식으로요:
// 스프링 부트에서 REST와 GraphQL 동시 지원
@SpringBootApplication
public class HybridApiApplication {
public static void main(String[] args) {
SpringApplication.run(HybridApiApplication.class, args);
}
}
// REST 컨트롤러
@RestController
@RequestMapping("/api/v1")
public class RestUserController {
// 간단한 CRUD는 REST로
@GetMapping("/users")
public List<UserDTO> getUsers() {
return userService.findAll();
}
}
// GraphQL 컨트롤러
@Controller
public class GraphQLUserController {
// 복잡한 쿼리는 GraphQL로
@QueryMapping
public UserWithDetails userDetails(@Argument Long id) {
return userService.findDetailedUser(id);
}
}
// 레거시 SOAP 서비스 통합
@Service
public class LegacyIntegrationService {
// 외부 레거시 시스템은 SOAP로
@Autowired
private SoapClient legacySoapClient;
public PaymentResult processPayment(PaymentRequest request) {
return legacySoapClient.callPaymentService(request);
}
}
예를 들어 Spring Cloud Gateway나 Kong, AWS API Gateway 같은 걸 사용하면 REST, GraphQL, SOAP를 모두 통합 관리할 수 있어요 🎪
성능 최적화 전략
어떤 방식을 선택하든 성능 최적화는 필수죠! 각 방식별 최적화 팁을 알려드릴게요 🚀
// 1. HTTP 캐싱 활용
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userService.findById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.eTag(String.valueOf(user.hashCode()))
.body(user);
}
// 2. 페이징과 필터링
@GetMapping("/users")
public Page<UserDTO> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String filter) {
Pageable pageable = PageRequest.of(page, size);
return userService.findAll(filter, pageable);
}
// 3. 압축 활성화 (application.yml)
server:
compression:
enabled: true
mime-types: application/json,application/xml
min-response-size: 1024
// 1. DataLoader로 N+1 문제 해결 (위에서 설명함)
// 2. 쿼리 복잡도 제한
@Configuration
public class GraphQLConfig {
@Bean
public GraphQLInstrumentation maxComplexityInstrumentation() {
return new MaxQueryComplexityInstrumentation(100);
}
}
// 3. 지속 쿼리 (Persisted Queries)
// 클라이언트가 쿼리 해시만 보내고 서버에서 캐시된 쿼리 사용
@Configuration
public class PersistedQueryConfig {
@Bean
public PersistedQueryCache persistedQueryCache() {
return new InMemoryPersistedQueryCache();
}
}
// 4. 필드 레벨 캐싱
@SchemaMapping
@Cacheable("users")
public User user(@Argument Long id) {
return userService.findById(id);
}
모니터링과 디버깅
API를 운영하면서 모니터링은 정말 중요해요! 문제가 생겼을 때 빠르게 파악하고 대응해야 하니까요 📊
// Spring Boot Actuator로 모니터링
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}
// application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
// 커스텀 메트릭 추가
@Component
public class ApiMetrics {
private final Counter apiCallCounter;
private final Timer apiResponseTime;
public ApiMetrics(MeterRegistry registry) {
this.apiCallCounter = Counter.builder("api.calls")
.description("Total API calls")
.tag("type", "rest")
.register(registry);
this.apiResponseTime = Timer.builder("api.response.time")
.description("API response time")
.register(registry);
}
public void recordApiCall() {
apiCallCounter.increment();
}
public void recordResponseTime(long milliseconds) {
apiResponseTime.record(milliseconds, TimeUnit.MILLISECONDS);
}
}
// GraphQL 쿼리 로깅
@Component
public class GraphQLLoggingInstrumentation
extends SimpleInstrumentation {
@Override
public InstrumentationContext<ExecutionResult>
beginExecution(InstrumentationExecutionParameters parameters) {
long startTime = System.currentTimeMillis();
return new SimpleInstrumentationContext<>() {
@Override
public void onCompleted(ExecutionResult result, Throwable t) {
long duration = System.currentTimeMillis() - startTime;
log.info("GraphQL query executed in {}ms: {}",
duration, parameters.getQuery());
}
};
}
}
🛡️ 보안: API를 안전하게 지키기
API 보안은 선택이 아니라 필수예요! 특히 외부에 공개되는 API라면 더더욱 중요하죠 🔒
인증, 인가, 데이터 암호화, Rate Limiting 등 다양한 보안 레이어가 필요해요.
JWT 기반 인증 구현
// JWT 토큰 생성 및 검증
@Service
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long validityInMilliseconds;
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public String getUsername(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
// JWT 필터
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsername(token);
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Rate Limiting으로 남용 방지
API 남용을 막기 위해 요청 횟수를 제한하는 건 필수예요! Bucket4j 라이브러리를 많이 사용하죠 🪣
// Rate Limiting 설정
@Configuration
public class RateLimitConfig {
@Bean
public Bucket createBucket() {
// 1분에 100개 요청 허용
Bandwidth limit = Bandwidth.classic(100,
Refill.intervally(100, Duration.ofMinutes(1)));
return Bucket.builder()
.addLimit(limit)
.build();
}
}
// Rate Limiting 인터셉터
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = getClientKey(request);
Bucket bucket = resolveBucket(key);
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.addHeader("X-Rate-Limit-Remaining",
String.valueOf(probe.getRemainingTokens()));
return true;
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.addHeader("X-Rate-Limit-Retry-After-Seconds",
String.valueOf(waitForRefill));
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(),
"Too many requests");
return false;
}
}
private Bucket resolveBucket(String key) {
return cache.computeIfAbsent(key, k -> {
Bandwidth limit = Bandwidth.classic(100,
Refill.intervally(100, Duration.ofMinutes(1)));
return Bucket.builder().addLimit(limit).build();
});
}
private String getClientKey(HttpServletRequest request) {
// IP 주소 또는 API 키로 클라이언트 식별
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null) {
return apiKey;
}
return request.getRemoteAddr();
}
}
CORS 설정
웹 브라우저에서 다른 도메인의 API를 호출할 때 CORS 설정이 필요해요! 이것도 보안의 일부죠 🌐
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.jaenung.net", "https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
// 또는 어노테이션 방식
@RestController
@CrossOrigin(origins = "https://www.jaenung.net")
@RequestMapping("/api/users")
public class UserController {
// ...
}
📚 실전 프로젝트 예제: 종합 구현
자, 이제 배운 걸 종합해서 실전 프로젝트를 만들어볼까요? 간단한 블로그 시스템을 만들어보겠습니다! ✍️
REST와 GraphQL을 모두 지원하는 API를 구현해볼게요.
프로젝트 구조
blog-api/
├── src/main/java/com/example/blog/
│ ├── BlogApiApplication.java
│ ├── config/
│ │ ├── SecurityConfig.java
│ │ ├── GraphQLConfig.java
│ │ └── SwaggerConfig.java
│ ├── domain/
│ │ ├── Post.java
│ │ ├── Comment.java
│ │ └── User.java
│ ├── repository/
│ │ ├── PostRepository.java
│ │ ├── CommentRepository.java
│ │ └── UserRepository.java
│ ├── service/
│ │ ├── PostService.java
│ │ ├── CommentService.java
│ │ └── UserService.java
│ ├── dto/
│ │ ├── PostDTO.java
│ │ ├── CommentDTO.java
│ │ └── UserDTO.java
│ ├── controller/
│ │ └── rest/
│ │ ├── PostRestController.java
│ │ └── CommentRestController.java
│ └── graphql/
│ ├── PostGraphQLController.java
│ └── CommentGraphQLController.java
└── src/main/resources/
├── application.yml
└── graphql/
└── schema.graphqls
도메인 모델
@Entity
@Table(name = "posts")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private User author;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>();
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@Entity
@Table(name = "comments")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private User author;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
서비스 레이어
@Service
@Transactional
public class PostService {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
public PostDTO createPost(CreatePostRequest request, String username) {
User author = userRepository.findByUsername(username)
.orElseThrow(() -> new UserNotFoundException(username));
Post post = new Post();
post.setTitle(request.getTitle());
post.setContent(request.getContent());
post.setAuthor(author);
Post savedPost = postRepository.save(post);
return convertToDTO(savedPost);
}
@Transactional(readOnly = true)
public Page<PostDTO> getAllPosts(Pageable pageable) {
return postRepository.findAll(pageable)
.map(this::convertToDTO);
}
@Transactional(readOnly = true)
public PostDTO getPostById(Long id) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
return convertToDTO(post);
}
public PostDTO updatePost(Long id, UpdatePostRequest request,
String username) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
if (!post.getAuthor().getUsername().equals(username)) {
throw new UnauthorizedException("Not the author");
}
if (request.getTitle() != null) {
post.setTitle(request.getTitle());
}
if (request.getContent() != null) {
post.setContent(request.getContent());
}
Post updatedPost = postRepository.save(post);
return convertToDTO(updatedPost);
}
public void deletePost(Long id, String username) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
if (!post.getAuthor().getUsername().equals(username)) {
throw new UnauthorizedException("Not the author");
}
postRepository.delete(post);
}
private PostDTO convertToDTO(Post post) {
PostDTO dto = new PostDTO();
dto.setId(post.getId());
dto.setTitle(post.getTitle());
dto.setContent(post.getContent());
dto.setAuthorName(post.getAuthor().getUsername());
dto.setCommentsCount(post.getComments().size());
dto.setCreatedAt(post.getCreatedAt());
dto.setUpdatedAt(post.getUpdatedAt());
return dto;
}
}
REST 컨트롤러
@RestController
@RequestMapping("/api/v1/posts")
@Api(tags = "게시글 관리")
public class PostRestController {
@Autowired
private PostService postService;
@GetMapping
@ApiOperation("게시글 목록 조회")
public ResponseEntity<Page<PostDTO>> getAllPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt,desc") String sort) {
Pageable pageable = PageRequest.of(page, size,
Sort.by(Sort.Direction.fromString(sort.split(",")[1]),
sort.split(",")[0]));
Page<PostDTO> posts = postService.getAllPosts(pageable);
return ResponseEntity.ok(posts);
}
@GetMapping("/{id}")
@ApiOperation("게시글 상세 조회")
public ResponseEntity<PostDTO> getPost(@PathVariable Long id) {
PostDTO post = postService.getPostById(id);
return ResponseEntity.ok(post);
}
@PostMapping
@ApiOperation("게시글 작성")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<PostDTO> createPost(
@Valid @RequestBody CreatePostRequest request,
@AuthenticationPrincipal UserDetails userDetails) {
PostDTO createdPost = postService.createPost(
request, userDetails.getUsername());
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(createdPost.getId())
.toUri();
return ResponseEntity.created(location).body(createdPost);
}
@PutMapping("/{id}")
@ApiOperation("게시글 수정")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<PostDTO> updatePost(
@PathVariable Long id,
@Valid @RequestBody UpdatePostRequest request,
@AuthenticationPrincipal UserDetails userDetails) {
PostDTO updatedPost = postService.updatePost(
id, request, userDetails.getUsername());
return ResponseEntity.ok(updatedPost);
}
@DeleteMapping("/{id}")
@ApiOperation("게시글 삭제")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<Void> deletePost(
@PathVariable Long id,
@AuthenticationPrincipal UserDetails userDetails) {
postService.deletePost(id, userDetails.getUsername());
return ResponseEntity.noContent().build();
}
}
GraphQL 스키마 및 컨트롤러
# schema.graphqls
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
commentsCount: Int!
createdAt: String!
updatedAt: String!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: String!
}
type User {
id: ID!
username: String!
email: String!
posts: [Post!]!
}
type Query {
posts(page: Int = 0, size: Int = 20): PostPage!
post(id: ID!): Post
comments(postId: ID!): [Comment!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
addComment(input: AddCommentInput!): Comment!
}
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
input CreatePostInput {
title: String!
content: String!
}
input UpdatePostInput {
title: String
content: String
}
input AddCommentInput {
postId: ID!
content: String!
}
type PostPage {
content: [Post!]!
totalElements: Int!
totalPages: Int!
number: Int!
size: Int!
}
// GraphQL 컨트롤러
@Controller
public class PostGraphQLController {
@Autowired
private PostService postService;
@Autowired
private CommentService commentService;
@QueryMapping
public PostPage posts(@Argument int page, @Argument int size) {
Pageable pageable = PageRequest.of(page, size);
Page<PostDTO> postPage = postService.getAllPosts(pageable);
return PostPage.builder()
.content(postPage.getContent())
.totalElements(postPage.getTotalElements())
.totalPages(postPage.getTotalPages())
.number(postPage.getNumber())
.size(postPage.getSize())
.build();
}
@QueryMapping
public PostDTO post(@Argument Long id) {
return postService.getPostById(id);
}
@MutationMapping
@PreAuthorize("isAuthenticated()")
public PostDTO createPost(
@Argument CreatePostInput input,
@AuthenticationPrincipal UserDetails userDetails) {
CreatePostRequest request = new CreatePostRequest();
request.setTitle(input.getTitle());
request.setContent(input.getContent());
return postService.createPost(request, userDetails.getUsername());
}
@SchemaMapping(typeName = "Post", field = "comments")
public List<CommentDTO> postComments(PostDTO post) {
return commentService.getCommentsByPostId(post.getId());
}
// Subscription 구현
private final Sinks.Many<PostDTO> postSink =
Sinks.many().multicast().onBackpressureBuffer();
@SubscriptionMapping
public Flux<PostDTO> postCreated() {
return postSink.asFlux();
}
@MutationMapping
@PreAuthorize("isAuthenticated()")
public PostDTO createPostWithNotification(
@Argument CreatePostInput input,
@AuthenticationPrincipal UserDetails userDetails) {
PostDTO newPost = createPost(input, userDetails);
// 구독자들에게 알림
postSink.tryEmitNext(newPost);
return newPost;
}
}
실무 경험이 풍부한 개발자들에게 직접 배울 수 있는 기회가 많답니다 💪
🎬 마무리: 당신의 선택은?
와... 여기까지 오느라 고생 많으셨어요! ㅋㅋㅋ 진짜 긴 여정이었죠? 🏃♂️
SOAP, REST, GraphQL 세 가지 API 구현 방식을 모두 살펴봤는데요, 각각의 장단점이 명확하죠?
핵심 정리를 해볼게요:
• SOAP는 엄격하고 안전하지만 무겁고 복잡해요. 금융권이나 레거시 시스템에 적합하죠 🏦
• REST는 가볍고 직관적이며 캐싱이 쉬워요. 대부분의 웹/모바일 앱에 최적이에요 🌐
• GraphQL은 유연하고 효율적이지만 러닝커브가 있어요. 복잡한 데이터 구조에 강력하죠 🚀
실무에서는 "이것만 써야 한다"는 없어요! 프로젝트 요구사항, 팀의 역량, 기존 시스템과의 호환성 등을 종합적으로 고려해서 선택하면 돼요 🎯
심지어 하나의 프로젝트에서 여러 방식을 혼용하는 것도 전혀 이상하지 않아요!
REST에 익숙해지면 GraphQL을 시도해보고, 필요하다면 SOAP도 배워보세요.
중요한 건 "왜 이 방식을 선택했는지" 설명할 수 있어야 한다는 거예요! 🎓
자바 웹 서비스 개발, 생각보다 재미있지 않나요? ㅎㅎ
이 글이 여러분의 API 개발 여정에 도움이 되었으면 좋겠어요!
궁금한 점이 있다면 재능넷 커뮤니티에서 질문하거나, 전문가에게 멘토링을 받아보는 것도 추천드려요 😊
그럼 모두 해피 코딩하세요! 화이팅! 💪✨
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.