지금까지 살펴본 부분은 항상 Server의 입장에서 API를 제공하는 방법에 대하여 학습

Back-end에서 Client로 다른 Server와의 연결은 필수!!!

Port 설정은 resource 하위의 application.properties 파일에서

server.port = {사용할 포트 번호} 로 변경할 수 있다.

1. Server to Server - GET

Client

package com.example.client.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResponse {
    private String name;
    private int age;
}

package com.example.client.service;

import com.example.client.dto.UserResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    // <http://localhost/api/server/hello>
    // response
    public UserResponse hello(){
        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/hello")
                .queryParam("name","jun")
                .queryParam("age",30)
                .encode()
                .build()
                .toUri();

        System.out.println(uri.toString());

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<UserResponse> result = restTemplate.getForEntity(uri, UserResponse.class);
        System.out.println("getStatusCode : "+ result.getStatusCode());
        System.out.println("getBody : "+ result.getBody());

        return result.getBody();
    }
}

RestTemplate 클래스를 이용하여 다른 서버로 요청(GET, POST, DELETE, …) 을 보낼 수 있다.

서버와 API 표준 스펙을 정하여 DTO 클래스를 설계하여 응답을 받을 때 사용한다.

Server

package com.example.server.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private int age;
}

package com.example.server.controller;

import com.example.server.dto.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/server")
public class ServerApiController {

    @GetMapping("/hello")
    public User hello(@RequestParam String name ,@RequestParam int age){
        User user = new User();
        user.setName(name);
        user.setAge(age);

        return user;
    }
}

 

 

2. Server to Server - POST

Client

package com.example.client.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserRequest {
    private String name;
    private int age;
}
package com.example.client.service;

import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    public UserResponse post(){
        // <http://localhost:9090/api/server/user/{userId}/name/{userName}>

        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/user/{userId}/name/{userName}")
                .encode()
                .build()
                .expand(100,"jun")
                .toUri();

        System.out.println(uri);

        // http body -> object -> object mapper -> json -> rest template -> http body json
        UserRequest req = new UserRequest();
        req.setAge(30);
        req.setName("jun");

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<UserResponse> response = restTemplate.postForEntity(uri, req, UserResponse.class);

        System.out.println("getStatusCode : "+ response.getStatusCode());
        System.out.println("getHeaders : "+ response.getHeaders());
        System.out.println("getBody : "+ response.getBody());

        return response.getBody();

    }
}

RestTemplate의 postForEntity 함수를 이용하여 requstBody의 내용을 추가하여 다른 서버로 요청할 수 있다.

Server

package com.example.server.controller;

import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {

    @PostMapping("/user/{userId}/name/{userName}")
    public User post(@RequestBody User user, @PathVariable int userId, @PathVariable String userName){
        log.info("userId : {} , userName : {}",userId,userName);
        log.info("user : {}",user);

        return user;
    }
}

 

 

3. Server to Server - exchange

Client

package com.example.client.service;

import com.example.client.dto.Req;
import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    public UserResponse exchange(){
        // <http://localhost:9090/api/server/user/{userId}/name/{userName}>

        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/user/{userId}/name/{userName}")
                .encode()
                .build()
                .expand(100,"jun")
                .toUri();

        System.out.println(uri);

        // http body -> object -> object mapper -> json -> rest template -> http body json
        UserRequest req = new UserRequest();
        req.setAge(30);
        req.setName("jun");

        RequestEntity<UserRequest> requestEntity = RequestEntity
                                                .post(uri)
                                                .header("x-authorization","asdf")
                                                .header("custom-header","112233")
                                                .body(req);

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<UserResponse> response = restTemplate.exchange(requestEntity,UserResponse.class);

        System.out.println("getStatusCode : "+ response.getStatusCode());
        System.out.println("getHeaders : "+ response.getHeaders());
        System.out.println("getBody : "+ response.getBody());

        return response.getBody();

    }

}

RestTemplate의 exchange함수를 이용하여 RequestEntity를 추가할 수 있다.

→ RequestEntity 클래스에서 httpMethod, URI, Header, Body를 설정

Server

package com.example.server.controller;

import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {

    @GetMapping("/hello")
    public User hello(@RequestParam String name ,@RequestParam int age){
        User user = new User();
        user.setName(name);
        user.setAge(age);

        return user;
    }

    @PostMapping("/user/{userId}/name/{userName}")
    public User post(@RequestBody User user,
                     @PathVariable int userId,
                     @PathVariable String userName,
                     @RequestHeader(name ="x-authorization") String authorization ,
                     @RequestHeader(name ="custom-header") String customHeader){
        log.info("userId : {} , userName : {}",userId,userName);
        log.info("x-authorization : {} , custom-header: {}",authorization,customHeader);
        log.info("user : {}",user);

        return user;
    }
}

 

4. Server to Server - GenericExchange

{
	"header": {
		"response_code" : ""
	},
	"body" : {
		"name" : "jun",
		"age" : 30

	}

위 처럼 JSON 내용이 header,body 2가지로 나뉘고 상황에 따라 body의 내용만 바뀌는 상황이 있을 수 있다.

제네릭 dto클래스를 이용하여 간단하게 구현할 수 있다.

Client

package com.example.client.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Req<T> {
    private Header header;
    private T resBody;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Header{
        private String responseCode;
    }
}

package com.example.client.service;

import com.example.client.dto.Req;
import com.example.client.dto.UserRequest;
import com.example.client.dto.UserResponse;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service
public class RestTemplateService {

    public Req<UserResponse> genericExchange(){
        // <http://localhost:9090/api/server/user/{userId}/name/{userName}>

        URI uri = UriComponentsBuilder
                .fromUriString("<http://localhost:9090>")
                .path("/api/server/user/{userId}/name/{userName}")
                .encode()
                .build()
                .expand(100,"jun")
                .toUri();

        System.out.println(uri);

        // http body -> object -> object mapper -> json -> rest template -> http body json
        UserRequest userRequest = new UserRequest();
        userRequest.setAge(30);
        userRequest.setName("jun");

        Req<UserRequest> req = new Req<>();
        req.setHeader(new Req.Header());
        req.setResBody(userRequest);

        RequestEntity<Req<UserRequest>> requestEntity = RequestEntity
                .post(uri)
                .header("x-authorization","asdf")
                .header("custom-header","112233")
                .body(req);

        RestTemplate restTemplate = new RestTemplate();

        ResponseEntity<Req<UserResponse>> response = restTemplate
                                                    .exchange(requestEntity, new ParameterizedTypeReference<Req<UserResponse>>() {});

        System.out.println("getStatusCode : "+ response.getStatusCode());
        System.out.println("getHeaders : "+ response.getHeaders());
        System.out.println("getBody : "+ response.getBody());

        return response.getBody();

    }

}

Server

package com.example.server.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Req<T> {
    private Header header;
    private T resBody;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Header{
        private String responseCode;
    }
}
package com.example.server.controller;

import com.example.server.dto.Req;
import com.example.server.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {
    @PostMapping("/user/{userId}/name/{userName}")
    public Req<User> post(
                     @RequestBody Req<User> req,
                     @PathVariable int userId,
                     @PathVariable String userName,
                     @RequestHeader(name ="x-authorization") String authorization ,
                     @RequestHeader(name ="custom-header") String customHeader){
                     
        log.info("userId : {} , userName : {}",userId,userName);
        log.info("x-authorization : {} , custom-header: {}",authorization,customHeader);
        log.info("user : {}",req.getResBody());

        Req<User> response = new Req<>();
        response.setHeader(new Req.Header());
        response.setResBody(req.getResBody());

        return response;
    }
}

'Spring' 카테고리의 다른 글

Jacoco - 테스트 커버리지 확인하기  (1) 2025.04.21
JUnit  (0) 2025.04.21
비동기 처리하기  (1) 2025.04.15
Interceptor  (0) 2025.04.15
서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15

일반적으로 Spring MVC(동기)방식을 권장하지만 필요에 따라 비동기를 설정할 순 있다.

 

 

기본적인 비동기

package com.example.async;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class AsyncApplication {

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

}

@EnableAsync 어노테이션 추가

 

package com.example.async.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class AsyncService {

    @Async
    public void hello() {

        for(int i=0;i<10;i++){
            try {
                Thread.sleep(2000);
                log.info("thread sleep check");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

@Async aop 기반이기 때문에 프록시 패턴을 따른다. 때문에 public 메소드에만 적용이 가능하다.

package com.example.async.controller;

import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {

    private final AsyncService asyncService;

    @GetMapping("/hello")
    public String hello(){
        asyncService.hello();
        log.info("method end");
        return "hello";
    }
}

@Async 추가를 하게 되면 해당 메소드가 비동기 처리가 되어 쓰레드가 해당 메소드를 실행하고 클라이언트는 쓰레드의 작업이 끝나지 않았으나 응답을 받는다.

 

 

여러 API들의 요청을 조인하여 응답 내려주기 - CompletableFuture

package com.example.async.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class AsyncService {

    private int count =0;

    @Async
    public CompletableFuture run() {

        return new AsyncResult(hello()).completable();
    }



    public String hello() {
        int idx = ++count;
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(2000);
                log.info("thread sleep check {} : {}",idx,i+1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        return "async hello";
    }
}

CompletableFuture 반환하는 함수에 @Async 어노테이션을 추가

@Async로 설정한 메소드 내에서 같은 클래스의 같은 메소드를 호출할 때는 Async를 타지 않는다.

 

package com.example.async.controller;

import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CompletableFuture;

@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {

    private final AsyncService asyncService;

    @GetMapping("/hello")
    public CompletableFuture hello(){
        log.info("CompletableFuture init");
        CompletableFuture api1 =  asyncService.run();
        CompletableFuture api2 =  asyncService.run();
        CompletableFuture api3 =  asyncService.run();
        return api3;
    }
}

api1, api2, api3 결과가 모두 종료되고 클라이언트에게 결과를 반환한다.

위 내용들은 Spring에서 정한 쓰레드들을 사용하게 된다.

 

 

직접 쓰레드 만들기

package com.example.async.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class AppConfig {


    @Bean("async-thread")
    public Executor asyncThread(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setMaxPoolSize(100);
        threadPoolTaskExecutor.setCorePoolSize(10);
        threadPoolTaskExecutor.setQueueCapacity(10);
        threadPoolTaskExecutor.setThreadNamePrefix("Async-");
        return threadPoolTaskExecutor;
    }
}

쓰레드 풀을 지정하는 것은 환경, 리퀘스트 양에 따라 달라진다. 때문에 이 풀이 어떻게 동작하는지 정확하게 알고 있어야 설정을 잘할 수 있다.

 

package com.example.async.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
public class AsyncService {

    @Async("async-thread")
    public CompletableFuture run() {

        return new AsyncResult(hello()).completable();
    }



    public String hello() {
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(2000);
                log.info("thread sleep check ");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        return "async hello";
    }
}

@Async 어노테이션 인자에 Bean으로 설정한 쓰레드 풀의 이름을 설정하게 되면 해당 메소드를 사용할 때 지정한 쓰레드 풀을 사용한다.

  • 몽고DB, Nosql : Spring Webflux 사용 권장
  • rdb : 사실상 async가 의미가 없음 , 데이터베이스 단에서 트랜잭션 떄문에 동기 방식으로 동작하기 때문에 async를 적용시켜도 전체적인 플로우 자체가 async로 동작할 수 없음

'Spring' 카테고리의 다른 글

JUnit  (0) 2025.04.21
Server to Server - RestTemplate  (1) 2025.04.16
Interceptor  (0) 2025.04.15
서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15
Filter  (1) 2025.04.15

Filter와 매우 유사한 형태로 존재 하지만, 차이점은 Spring Context에 등록 된다.

→ 핸들러, 컨트롤러, 메서드 정보 접근 가능

 

AOP와 유사한 기능을 제공 할 수 있으며,

주로 인증 단계를 처리 하거나, Logging 하는 데에 사용

이를 선/후 처리 함으로써, Service business logic과 분리 시킴

 

package com.example.demo.annotation;

import java.lang.annotation.*;
import java.lang.reflect.Method;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}

@Auth 검증이 필요한 빈,메소드 등에 사용할 어노테이션을 만든다

 

 

package com.example.demo.controller;


import com.example.demo.annotation.Auth;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/private")
@Auth
public class PrivateController {


    @GetMapping("/hello")
    public String hello(){
        return "private hello";
    }
}

인터셉터단에서 검증이 필요한 빈이나 메소드등에 @Auth 어노테이션을 추가한다,

 

 

package com.example.demo.interceptor;

import com.example.demo.annotation.Auth;
import com.example.demo.exception.AuthException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;


@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURI();

        URI uri = UriComponentsBuilder.fromUriString(url)
                .query(request.getQueryString())
                .build()
                .toUri();


        log.info("request url : {}",url);
        boolean hasAnnotation = checkAnnotation(handler, Auth.class);
        log.info("has annotation : {}",hasAnnotation);

        // Auth 권한을 가진 요청에 대해서는 확인 ex) 세션, 쿠키
        if(hasAnnotation){
            // 권한체크
            String query = uri.getQuery();
            log.info("query : {}", query);
            if(query.equals("name=jun")){
                return true;
            }

            throw new AuthException();

        }

        return true;
    }

    private  boolean checkAnnotation(Object handler,Class clazz){

        // resource javascript, html, etc
        if (handler instanceof ResourceHttpRequestHandler){
            return true;
        }


        // annotation
        HandlerMethod handlerMethod = (HandlerMethod)handler;


        if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
            // Auth annotation 있을때 true
            return true;
        }

        return false;

    }
}

handler에서 @Auth 어노테이션이 붙는 요청을 확인하여 특정 로직(인증)를 수행하는 예제이다.

 

 

package com.example.demo.config;

import com.example.demo.interceptor.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
    }
}

 

Configuration에서 WebMvcConfigurer 인터페이스를 구현하고 addInterceptors 메소드에서 registry.addInterceptor 함수 인자에 추가하려는 인터셉터를 추가한다.

addPathPatterns 함수를 이용하여 특정 URI 패턴에만 인터셉터를 적용할 수 있다.

'Spring' 카테고리의 다른 글

Server to Server - RestTemplate  (1) 2025.04.16
비동기 처리하기  (1) 2025.04.15
서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15
Filter  (1) 2025.04.15
Spring Boot Exception 처리  (0) 2025.03.24

1. 서블릿 컨테이너(웹 어플리케이션) -  예: Tomcat

  • 정의:
    웹 서버에서 자바 웹 애플리케이션(웹사이트)을 실행할 수 있게 해주는 프로그램입니다.
  • 주요 역할:
    • 웹 브라우저에서 들어오는 요청을 받아서, 서블릿(자바로 만든 웹 프로그램)에게 전달합니다.
    • 서블릿, 필터, 리스너 등 웹 관련 자바 객체의 생성과 생명주기를 관리합니다.
    • 예시: Tomcat, Jetty, Undertow 1. 서블릿 컨테이너(예: Tomcat)
      • 정의:
        웹 서버에서 자바 웹 애플리케이션(웹사이트)을 실행할 수 있게 해주는 프로그램입니다.
      • 주요 역할:
        • 웹 브라우저에서 들어오는 요청을 받아서, 서블릿(자바로 만든 웹 프로그램)에게 전달합니다.
        • 서블릿, 필터, 리스너 등 웹 관련 자바 객체의 생성과 생명주기를 관리합니다.
        • 예시: Tomcat, Jetty, Undertow 

2. Spring 컨텍스트(스프링 IoC 컨테이너)

  • 정의:
    Spring 프레임워크가 제공하는 "객체 관리 시스템"입니다.
    쉽게 말해, 프로그램에서 사용할 여러 객체(Bean)를 만들고, 서로 연결해주고, 필요할 때 꺼내 쓸 수 있게 관리해줍니다.
  • 주요 역할:
    • 개발자가 직접 객체를 만들고 연결하지 않아도, 설정에 따라 자동으로 객체를 생성하고 주입(Dependency Injection)합니다.
    • 객체의 생명주기(생성~소멸)도 관리합니다.
    • 예시: 회원 서비스, 주문 서비스 등 비즈니스 로직 객체들을 관리

 

- 실생활 예시

서블릿 컨테이너

  • 비유:
    "건물 관리자"
    • 한 건물(=웹 애플리케이션)을 짓고, 그 안에 여러 방(=서블릿, 필터 등)을 만들어줍니다.
    • 외부 손님(=웹 요청)이 오면, 건물 입구에서 안내해 각 방으로 보내줍니다.
    • 방의 청소, 유지보수(=생명주기 관리)도 담당합니다.

Spring 컨텍스트

  • 비유:
    "방 안의 집사"
    • 각 방(=서블릿, 컨트롤러 등) 안에서 필요한 가구(=객체, Bean)를 알아서 준비하고, 필요한 곳에 배치해줍니다.
    • 가구가 고장나면 교체하고, 필요 없으면 치웁니다.
    • 즉, 방(서블릿)이 제대로 일할 수 있도록 내부를 관리합니다.

 

그림 설명

 

[외부 요청]
     │
     ▼
┌─────────────────────────────┐
│      서블릿 컨테이너         │  ← 건물 관리자 (Tomcat 등)
│ ┌───────────────────────┐   │
│ │   Spring 컨텍스트     │   │  ← 방 안의 집사 (ApplicationContext)
│ │  (ApplicationContext) │   │
│ │   ┌───────────────┐   │   │
│ │   │  Bean1        │   │   │
│ │   │  Bean2        │   │   │
│ │   │  ...          │   │   │
│ │   └───────────────┘   │   │
│ └───────────────────────┘   │
└─────────────────────────────┘

 

'Spring' 카테고리의 다른 글

비동기 처리하기  (1) 2025.04.15
Interceptor  (0) 2025.04.15
Filter  (1) 2025.04.15
Spring Boot Exception 처리  (0) 2025.03.24
Validation  (0) 2025.03.24

Filter란 Web Application에서 관리되는 영역으로 Spring Boot Framework 에서 Client로 부터 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 할 수 있다.

 

유일하게 ServletRequest, ServletResponse의 객체를 변환 할 수 있다.

 

Spring에서 주로 request / response 의 로깅 용도로 활용하거나, 인증과 관련된 로직들을 해당 필터에서 처리함으로써 서비스 비즈니스 로직과 분리 시킨다.

 

보통의 경우 로깅 용도로 많이 사용

만일 인증을 한다고 하면 다단계 인증이 아니면 보통 인터셉터에서 구현을 추천한다고 한다.

 

 

package com.example.filter.filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;

@Slf4j
//@Component
@WebFilter(urlPatterns = "/api/user/*")
public class GlobalFilter  implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        // 전처리
        // 바로 읽는 것이 아니라 ByteArray 길이만 초기화가 됨
        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper ((HttpServletRequest) servletRequest);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper ((HttpServletResponse) servletResponse);


        // 요청을 서블릿으로 전달
        filterChain.doFilter(httpServletRequest, httpServletResponse);


        // 후처리
        String url = httpServletRequest.getRequestURI();

        // req
        String reqContent = new String(httpServletRequest.getContentAsByteArray());
        log.info("request url : {}, requestBody : {}", url,reqContent);



        String resContent = new String(httpServletResponse.getContentAsByteArray());
        int httpStatus = httpServletResponse.getStatus();

        // Response 복사하여 클라이언트에서 다시 사용할 수 있게
        httpServletResponse.copyBodyToResponse();

        log.info("response status : {} , responseBody : {}",httpStatus,resContent);

    }
}

 

doFilter 함수 전에 미리 requset, response 한번 읽어버리면 에러가 발생한다. 따라서 내용을 확인할 때는 doFilter 이후에 확인한다.

Global filter 사용하기 위해선 @Component 사용

특정 url 컨트롤러에서 사용하기 위해선 @WebFilter 사용

 

 

package com.example.filter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class FilterApplication {

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

}

 

@ServletComponentScan

  • @WebServlet, @WebFilter, @WebListener`와 같은 서블릿 관련 어노테이션이 붙은 클래스를 찾아서 자동으로 서블릿 컨테이너에 등록합니다

'Spring' 카테고리의 다른 글

Interceptor  (0) 2025.04.15
서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15
Spring Boot Exception 처리  (0) 2025.03.24
Validation  (0) 2025.03.24
Java Bean 규약  (0) 2025.03.24

Exception 처리

Web Application 의 입장에서 바라 보았을 때, 에러가 났을 때 내려줄 수 있는 방법은 많지 않다.

1. 에러 페이지
2. 4XX Error or 5XX Error
3. Client가 200 외에 처리를 하지 못 할 때는 200을 내려주고 별도의 에러 Message 전달

Web Application 입장에서는 이런 에러는 try-catch로 그 때 마다 묶는 것이 아닌 한 곳에 모아서 처리하는게 제일 편리하다.

 

@ControllerAdvice Global 예외 처리 및 특정 package / Controller 예외 처리
@ExceptionHandler 특정 Controller의 예외처리

 

 

package com.example.exception.advice;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalControllerAdvice {

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity exception(Exception e) {
        System.out.println(e.getClass().getName());

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
    }

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }

}

@RestControllerAdvice 어노테이션을 설정 함으로써 Spring Web Application 안의 Exception 들을 Global하게 처리할 수 있다.  basePackages 값을 넣으면 원하는 패키지도 설정이 가능하다.

@ExceptionHandler 를 이용하여 특정 Exception에 대해 어떤 처리를 할 지 메소드를 정의할 수 있다.

 

package com.example.exception.controller;

import com.example.exception.dto.User;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class ApiController {
    @PostMapping("/post")
    public User post(@Valid @RequestBody User user) {
        return user;
    }

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }

}

컨트롤러 내부에  @ExceptionHandler가 정의되어 있는 경우 @RestControllerAdvice, @ControllerAdvice 등 보다 우선순위가 높아 컨트롤러 개별 Exception 처리를 할 수 있다.

 

 

추가 예제 (Validation & Exception)

package com.example.exception.controller;

import com.example.exception.dto.User;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@Validated
public class ApiController 

    @GetMapping("/get")
    public User get(
            @NotNull
            @Size(min=2)
            @RequestParam String name,

            @NotNull
            @Min(1)
            @RequestParam Integer age) {

        User user = new User();
        user.setName(name);
        user.setAge(age);

        int a = 10+age;

        return user;
    }


    @PostMapping("/post")
    public User post(@Valid @RequestBody User user) {
        return user;
    }

}

 

@Validated은 스프링에서 제공하는 어노테이션으로써 컨트롤러뿐만 아니라 서비스, 레포지토리 등 모든 계층의 그룹 기반 유효성 검증을 지원한다. 예외로 ConstraintViolationException 발생한다.

 

 

package com.example.exception.dto;

public class Error {
    private String field;
    private String message;
    private String invalidValue;

   // get/set , toString 메소드 생략

}

 

package com.example.exception.dto;

import java.util.List;

public class ErrorResponse {

    String statusCode;
    String requestUrl;
    String code;
    String message;
    String resultCode;

    List<Error> errorList;

 // get/set , toString 메소드 생략
 
}

 

 

package com.example.exception.advice;

import com.example.exception.controller.ApiController;
import com.example.exception.dto.Error;
import com.example.exception.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ElementKind;
import jakarta.validation.Path;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.ArrayList;
import java.util.List;

@RestControllerAdvice(basePackageClasses = ApiController.class)
public class ApiControllerAdvice {


    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest request) {
        List<Error> errorList = new ArrayList<>();

        String fieldName = e.getParameterName();
        String invalidValue = e.getMessage();

        Error errorElement = new Error();
        errorElement.setField(fieldName);
        errorElement.setMessage(invalidValue);
        errorList.add(errorElement);

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("missingServletRequestParameterException");
        errorResponse.setRequestUrl(request.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAILED");


        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }



}

컨트롤러에서 validation 단계에서 exception이 발생한 경우 위와 같이 클라이언트의 잘못된 요청에 대하여 원하는 응답을 내려줄 수 있다.

 

 

{
    "statusCode": "400 BAD_REQUEST",
    "requestUrl": "/api/get",
    "code": null,
    "message": "constraintViolationException",
    "resultCode": "FAILED",
    "errorList": [
        {
            "field": "age",
            "message": "1 이상이어야 합니다",
            "invalidValue": "0"
        }
    ]
}

'Spring' 카테고리의 다른 글

서블릿 컨테이너와 Spring 컨텍스트  (0) 2025.04.15
Filter  (1) 2025.04.15
Validation  (0) 2025.03.24
Java Bean 규약  (0) 2025.03.24
AOP 관점지향 프로그램  (0) 2025.03.23

Java에서는 null 값에 대해서 접근 하려고 할 때 null pointer exception이 발생 함으로, 이러한 부분을 방지 하지 위해서 미리 검증을 하는 과정

  1. 검증해야 할 값이 많은 경우 코드의 길이가 길어 진다
  2. 구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요
  3. 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용의 한계가 있음
  4. 구현에 따라 달라 질 수 있지만, 검증 Logic이 변경 되는 경우 테스트 코드 등 참조하는 클래스에서 Logic이 변경되어야 하는 부분이 발생 할 수 있음

 

어노테이션 의미 기타
@Size 문자 길이 측정 Int Type 불가
@NotNull null 불가  
@NotEmpty null, “” 불가  
@NotBlank null, “”,” “불가  
@Past 과거 날짜  
@PastOrPresent 오늘이거나 과거 날짜  
@Future 미래 날짜  
@FutureOrPresent 오늘이거나 미래 날짜  
@Pattern 정규식 적용  
@Max 최대값  
@Min 최소값  
@AssertTrue / False 별도 Logic 적용  
@Valid 해당 object valication 실행  

 

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation' // 추가
}

Gradle 디펜던시에 validation 추가

 

 

 

package com.example.validation.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public class User {

    @NotBlank
    private String name;

    @Min(value = 0)
    private int age;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phoneNumber;

    /* 이하 생략  */ 
}

@Valid 을 사용한 객체는 안에 Validation과 관련된 어노테이션이 붙은 멤버 변수(속성) 값을 검증한다.

각 어노테이션은 message 값 설정을 통해 에러 발생 시 원하는 메세지로 수정 가능하다.

 

package com.example.validation.controller;

import com.example.validation.dto.User;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult) {

        if(bindingResult.hasErrors()) {
            StringBuilder sb = new StringBuilder();
            bindingResult.getAllErrors().forEach(objectError -> {
                FieldError field = (FieldError) objectError;
                String message = field.getDefaultMessage();

                System.out.println("field : " + field.getField() + ", message : " + message);

                sb.append("field : ").append(field.getField()).append(", message : ").append(message);
            });

            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
        }

        // logic

        return ResponseEntity.ok(user);
    }

}

컨트롤러 단에서 검증 시에는 BindingResult 객체를 활용하여 에러 발생 및 메시지 확인이 가능하다.

 

 Spring Boot Custom Validation

 

1. AssertTrue / False 와 같은 method 지정을 통해서 Custom Logic 적용 가능
    메소드명을 is*로 시작해야 한다.

 

package com.example.validation.dto;

import jakarta.validation.constraints.*;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class User {

    @NotBlank
    private String name;

    @Min(value = 0)
    private int age;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phoneNumber;

    @Size(min = 6, max = 6)
    private String reqYearMonth; // yyyyMM


     /* 중간 생략 */

    public String getReqYearMonth() {
        return reqYearMonth;
    }

    public void setReqYearMonth(String reqYearMonth) {
        this.reqYearMonth = reqYearMonth;
    }

    @AssertTrue(message = "yyyyMM의 형식에 맞지 않습니다.")
    public boolean isReqYearMonthValidation() {

        try {
            LocalDate localDate = LocalDate.parse(getReqYearMonth()+"01", DateTimeFormatter.ofPattern("yyyyMMdd"));
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                ", reqYearMonth='" + reqYearMonth + '\'' +
                '}';
    }
}

 

2. ConstraintValidator 를 적용하여 재사용이 가능한 Custom Logic 적용 가능
    - message: 유효성 검사 실패 시 알릴 메시지 설정
    - groups: 유효성 검사를 상황별로 분리하여 적용할 때 사용.
    -payload: 검증 실패 시 추가 정보를 전달하거나 메타데이터를 활용할 때 사용.
    -pattern (커스텀):  YearMonthValidator에서 검증 시 사용할 패턴 설정

 

package com.example.validation.annotation;

import com.example.validation.validator.YearMonthValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = {YearMonthValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface YearMonth {
    String message() default "yyyyMM 형식에 맞지 않습니다.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String pattern() default "yyyyMMdd";

}

@Constraint 어떤 Custom Validator 클래스로 검증을 할 것 인지 설정한다.  Custom Validator 클래스는 ConstraintValidator 인터페이스를 구현한 클래스로 정의할 수 있다.
ConstraintValidator<어노테이션명, 타입명>

 

package com.example.validation.validator;

import com.example.validation.annotation.YearMonth;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {

    private  String pattern;

    @Override
    public void initialize(YearMonth constraintAnnotation) {
        this.pattern = constraintAnnotation.pattern();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {

        try {
            LocalDate localDate = LocalDate.parse(value+"01", DateTimeFormatter.ofPattern(this.pattern));
        } catch (Exception e) {
            return false;
        }
        return true;
    }
}

initialize에서 커스텀 어노테이션 사용 시 설정한 값을 가져온다. 그 후에 isValid 메소드에서 검증 로직을 추가한다.

 

 

package com.example.validation.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;

import java.util.List;

public class User {

    @NotBlank
    private String name;

    @Max(value = 90)
    private int age;

    @Valid
    private List<Car> cars;


	  /* 생략 */
}

Validation를 적용한 객체 안에 또 다른 객체가 있을 경우에 또 다른 객체 또한 Validator이 필요한 경우에는 반드시 @Valid 어노테이션을 추가해야한다.

'Spring' 카테고리의 다른 글

Filter  (1) 2025.04.15
Spring Boot Exception 처리  (0) 2025.03.24
Java Bean 규약  (0) 2025.03.24
AOP 관점지향 프로그램  (0) 2025.03.23
IoC와 DI  (0) 2025.03.19
규약 항목  설명
기본 생성자 매개변수가 없는 기본 생성자를 가져야 합니다.
멤버 변수 접근 제어자 멤버 변수는 private으로 선언되어야 합니다.
Getter/Setter 메서드 멤버 변수에 대한 getter와 setter 메서드를 제공해야 하며, 이는 public으로 선언되어야 합니다.

속성 값을 가져올 때는 getProperty() 또는 isProperty() 형식의 Getter를 사용. 속성 값을 설정할 때는 setProperty(value) 형식의 Setter를 사용.
직렬화 선택적으로 Serializable 인터페이스를 구현하여 직렬화할 수 있습니다.

 

'Spring' 카테고리의 다른 글

Spring Boot Exception 처리  (0) 2025.03.24
Validation  (0) 2025.03.24
AOP 관점지향 프로그램  (0) 2025.03.23
IoC와 DI  (0) 2025.03.19
ObjectMapper  (0) 2025.03.18

+ Recent posts