지금까지 살펴본 부분은 항상 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

1. 문제 설명

휴대폰의 자판은 컴퓨터 키보드 자판과는 다르게 하나의 키에 여러 개의 문자가 할당될 수 있습니다. 키 하나에 여러 문자가 할당된 경우, 동일한 키를 연속해서 빠르게 누르면 할당된 순서대로 문자가 바뀝니다.

예를 들어, 1번 키에 "A", "B", "C" 순서대로 문자가 할당되어 있다면 1번 키를 한 번 누르면 "A", 두 번 누르면 "B", 세 번 누르면 "C"가 되는 식입니다.

같은 규칙을 적용해 아무렇게나 만든 휴대폰 자판이 있습니다. 이 휴대폰 자판은 키의 개수가 1개부터 최대 100개까지 있을 수 있으며, 특정 키를 눌렀을 때 입력되는 문자들도 무작위로 배열되어 있습니다. 또, 같은 문자가 자판 전체에 여러 번 할당된 경우도 있고, 키 하나에 같은 문자가 여러 번 할당된 경우도 있습니다. 심지어 아예 할당되지 않은 경우도 있습니다. 따라서 몇몇 문자열은 작성할 수 없을 수도 있습니다.

이 휴대폰 자판을 이용해 특정 문자열을 작성할 때, 키를 최소 몇 번 눌러야 그 문자열을 작성할 수 있는지 알아보고자 합니다.

1번 키부터 차례대로 할당된 문자들이 순서대로 담긴 문자열배열 keymap과 입력하려는 문자열들이 담긴 문자열 배열 targets가 주어질 때, 각 문자열을 작성하기 위해 키를 최소 몇 번씩 눌러야 하는지 순서대로 배열에 담아 return 하는 solution 함수를 완성해 주세요.

단, 목표 문자열을 작성할 수 없을 때는 -1을 저장합니다.

 

- 제한 사항

  • 1 ≤ keymap의 길이 ≤ 100
    • 1 ≤ keymap의 원소의 길이 ≤ 100
    • keymap[i]는 i + 1번 키를 눌렀을 때 순서대로 바뀌는 문자를 의미합니다.
      • 예를 들어 keymap[0] = "ABACD" 인 경우 1번 키를 한 번 누르면 A, 두 번 누르면 B, 세 번 누르면 A 가 됩니다.
    • keymap의 원소의 길이는 서로 다를 수 있습니다.
    • keymap의 원소는 알파벳 대문자로만 이루어져 있습니다.
  • 1 ≤ targets의 길이 ≤ 100
    • 1 ≤ targets의 원소의 길이 ≤ 100
    • targets의 원소는 알파벳 대문자로만 이루어져 있습니다.

2. 풀이 코드

#include <string>
#include <vector>
#include <map>
using namespace std;

vector<int> solution(vector<string> keymap, vector<string> targets) {
    vector<int> answer;

    map<char, int> keyNumber;


    for (string keyInfo : keymap)
    {
        for (int i = 0; i < keyInfo.length(); i++)
        {
            if (keyNumber.find(keyInfo[i]) == keyNumber.end())
                keyNumber[keyInfo[i]] = i + 1;
            else if(keyNumber[keyInfo[i]] > i)
                keyNumber[keyInfo[i]] = i+1;

        }

    }


    for (string target : targets)
    {
        int count = 0;
        for (int i = 0; i < target.length(); i++)
        {
            if (keyNumber.find(target[i]) == keyNumber.end())
            {
                count = -1;
                break;
            }
            else       
                count += keyNumber[target[i]];


        }
        answer.push_back(count);
    }



    return answer;
}

 

3. 정리

K:  keymap 문자열 수
L₁ : 각 keymap  문자열의 길이
T  : target 문자열 수
L₂ : 각 target 문자열의 길이

 

1. map에 각 문자별로 가장 작은 횟수로 도달할 수 잇는 횟수는 저장한다.

2. targets의 배열의 문자열을 순회하면서 1번에서 저장한 도달할 수 있는 최소 횟수를 더한다. 

 

전체 시간 복잡도는 문자열 길이만큼 순회하기 때문에 O(K × L₁ + T × L₂)이다.

 

출처 :https://school.programmers.co.kr/learn/courses/30/lessons/160586

'Algorithm' 카테고리의 다른 글

에라토스테네스의 체  (0) 2025.05.28
BOJ - 13909번 - 창문 닫기  (0) 2025.05.28
[프로그래머스] 덧칠하기  (0) 2025.04.14
[프로그래머스] 바탕화면 정리  (0) 2025.04.10
[프로그래머스] 공원 산책  (0) 2025.04.09

1. 문제 설명

어느 학교에 페인트가 칠해진 길이가 n미터인 벽이 있습니다. 벽에 동아리 · 학회 홍보나 회사 채용 공고 포스터 등을 게시하기 위해 테이프로 붙였다가 철거할 때 떼는 일이 많고 그 과정에서 페인트가 벗겨지곤 합니다. 페인트가 벗겨진 벽이 보기 흉해져 학교는 벽에 페인트를 덧칠하기로 했습니다.

넓은 벽 전체에 페인트를 새로 칠하는 대신, 구역을 나누어 일부만 페인트를 새로 칠 함으로써 예산을 아끼려 합니다. 이를 위해 벽을 1미터 길이의 구역 n개로 나누고, 각 구역에 왼쪽부터 순서대로 1번부터 n번까지 번호를 붙였습니다. 그리고 페인트를 다시 칠해야 할 구역들을 정했습니다.

벽에 페인트를 칠하는 롤러의 길이는 m미터이고, 롤러로 벽에 페인트를 한 번 칠하는 규칙은 다음과 같습니다.

롤러가 벽에서 벗어나면 안 됩니다.
구역의 일부분만 포함되도록 칠하면 안 됩니다.
즉, 롤러의 좌우측 끝을 구역의 경계선 혹은 벽의 좌우측 끝부분에 맞춘 후 롤러를 위아래로 움직이면서 벽을 칠합니다. 현재 페인트를 칠하는 구역들을 완전히 칠한 후 벽에서 롤러를 떼며, 이를 벽을 한 번 칠했다고 정의합니다.

한 구역에 페인트를 여러 번 칠해도 되고 다시 칠해야 할 구역이 아닌 곳에 페인트를 칠해도 되지만 다시 칠하기로 정한 구역은 적어도 한 번 페인트칠을 해야 합니다. 예산을 아끼기 위해 다시 칠할 구역을 정했듯 마찬가지로 롤러로 페인트칠을 하는 횟수를 최소화하려고 합니다.

정수 n, m과 다시 페인트를 칠하기로 정한 구역들의 번호가 담긴 정수 배열 section이 매개변수로 주어질 때 롤러로 페인트칠해야 하는 최소 횟수를 return 하는 solution 함수를 작성해 주세요.

 

- 제한 조건

  • 1 ≤ m ≤ n ≤ 100,000
  • 1 ≤ section의 길이 ≤ n
    • 1 ≤ section의 원소 ≤ n
    • section의 원소는 페인트를 다시 칠해야 하는 구역의 번호입니다.
    • section에서 같은 원소가 두 번 이상 나타나지 않습니다.
    • section의 원소는 오름차순으로 정렬되어 있습니다.

 

#include <string>
#include <vector>

using namespace std;

int solution(int n, int m, vector<int> section) {
    int answer = 0;

	// 첫번째원소부터 페인트 길이만큼 한번 먼저 칠한다
    int paintedIdx = section[0] + m -1;
    answer++;

    for (int i = 1; i < section.size(); i++)
    {
    	// 페인트 길이만큼 새로 칠한다
        if (paintedIdx < section[i] )
        {
            paintedIdx = section[i] + m -1;
            answer++;
        }
            
    }
 
    
    return answer;
}

 

3. 정리

section 배열의 첫 원소부터 페인트 길이만큼 벽을 한번 칠하고 어디까지 칠했는지 기억 해놓는다. ex) paintedIdx

section 2번째 원소부터 순회를 하는데 paintedIdx(페인트가 칠해진 벽의 최대 번호) 이하는 아무런 행동을 하지 않는다. 그 외에 paintedIdx보다 큰 원소가 들어오면 그 원소의 idx부터 페인트 길이까지 다시 칠한다. 

 

제약사항 중에 " 롤러가 벽에서 벗어나면 안 됩니다." 내용이 있지만 위 내용처럼 최대한 안겹치게 칠하면 실제로는 paintedIdx가 벽의 최대 범위를 벗어나지만 이 경우는 가장 마지막 한번 밖에 존재하지 않는다. 따라서 마지막은 칠해야 하는 벽을 포함한 최대 벽의 번호까지만 칠하는 경우로 생각하고 넘어간다.

 

 

 

전체 시간 복잡도는 section 원소의 수 만큼 순회하므로 O(N) 이다.

 

출처 : https://school.programmers.co.kr/learn/courses/30/lessons/161989

 

'Algorithm' 카테고리의 다른 글

BOJ - 13909번 - 창문 닫기  (0) 2025.05.28
[프로그래머스] 제출 내역  (0) 2025.04.14
[프로그래머스] 바탕화면 정리  (0) 2025.04.10
[프로그래머스] 공원 산책  (0) 2025.04.09
[프로그래머스] 추억 점수  (0) 2025.04.08

1. 문제 설명

코딩테스트를 준비하는 머쓱이는 프로그래머스에서 문제를 풀고 나중에 다시 코드를 보면서 공부하려고 작성한 코드를 컴퓨터 바탕화면에 아무 위치에나 저장해 둡니다. 저장한 코드가 많아지면서 머쓱이는 본인의 컴퓨터 바탕화면이 너무 지저분하다고 생각했습니다. 프로그래머스에서 작성했던 코드는 그 문제에 가서 다시 볼 수 있기 때문에 저장해 둔 파일들을 전부 삭제하기로 했습니다.

컴퓨터 바탕화면은 각 칸이 정사각형인 격자판입니다. 이때 컴퓨터 바탕화면의 상태를 나타낸 문자열 배열 wallpaper가 주어집니다. 파일들은 바탕화면의 격자칸에 위치하고 바탕화면의 격자점들은 바탕화면의 가장 왼쪽 위를 (0, 0)으로 시작해 (세로 좌표, 가로 좌표)로 표현합니다. 빈칸은 ".", 파일이 있는 칸은 "#"의 값을 가집니다. 드래그를 하면 파일들을 선택할 수 있고, 선택된 파일들을 삭제할 수 있습니다. 머쓱이는 최소한의 이동거리를 갖는 한 번의 드래그로 모든 파일을 선택해서 한 번에 지우려고 하며 드래그로 파일들을 선택하는 방법은 다음과 같습니다.

드래그는 바탕화면의 격자점 S(lux, luy)를 마우스 왼쪽 버튼으로 클릭한 상태로 격자점 E(rdx, rdy)로 이동한 뒤 마우스 왼쪽 버튼을 떼는 행동입니다. 이때, "점 S에서 점 E로 드래그한다"고 표현하고 점 S와 점 E를 각각 드래그의 시작점, 끝점이라고 표현합니다.

점 S(lux, luy)에서 점 E(rdx, rdy)로 드래그를 할 때, "드래그 한 거리"는 |rdx - lux| + |rdy - luy|로 정의합니다.

점 S에서 점 E로 드래그를 하면 바탕화면에서 두 격자점을 각각 왼쪽 위, 오른쪽 아래로 하는 직사각형 내부에 있는 모든 파일이 선택됩니다.

예를 들어 wallpaper = [".#...", "..#..", "...#."]인 바탕화면을 그림으로 나타내면 다음과 같습니다.


이러한 바탕화면에서 다음 그림과 같이 S(0, 1)에서 E(3, 4)로 드래그하면 세 개의 파일이 모두 선택되므로 드래그 한 거리 (3 - 0) + (4 - 1) = 6을 최솟값으로 모든 파일을 선택 가능합니다.


(0, 0)에서 (3, 5)로 드래그해도 모든 파일을 선택할 수 있지만 이때 드래그 한 거리는 (3 - 0) + (5 - 0) = 8이고 이전의 방법보다 거리가 늘어납니다.

머쓱이의 컴퓨터 바탕화면의 상태를 나타내는 문자열 배열 wallpaper가 매개변수로 주어질 때 바탕화면의 파일들을 한 번에 삭제하기 위해 최소한의 이동거리를 갖는 드래그의 시작점과 끝점을 담은 정수 배열을 return하는 solution 함수를 작성해 주세요. 드래그의 시작점이 (lux, luy), 끝점이 (rdx, rdy)라면 정수 배열 [lux, luy, rdx, rdy]를 return하면 됩니다.

 

 

- 제한 조건

  • 1 ≤ wallpaper의 길이 ≤ 50
  • 1 ≤ wallpaper[i]의 길이 ≤ 50
    • wallpaper의 모든 원소의 길이는 동일합니다.
  • wallpaper[i][j]는 바탕화면에서 i + 1행 j + 1열에 해당하는 칸의 상태를 나타냅니다.
  • wallpaper[i][j]는 "#" 또는 "."의 값만 가집니다.
  • 바탕화면에는 적어도 하나의 파일이 있습니다.
  • 드래그 시작점 (lux, luy)와 끝점 (rdx, rdy)는 lux < rdx, luy < rdy를 만족해야 합니다.

 

#include <string>
#include <vector>

using namespace std;

vector<int> solution(vector<string> wallpaper) {
    vector<int> answer;
    vector<pair<int, int>> info = {};
    for (int y = 0; y < wallpaper.size(); y++)
    {
        for (int x = 0 ;x < wallpaper[0].length(); x++)
        {
            if (wallpaper[y][x] == '#')
            {
                info.push_back({ y,x });
            }
        }
    }

    int Sy = info[0].first;
    int Ey = info[0].first;
    int Sx = info[0].second;
    int Ex = info[0].second;


    for (auto p : info)
    {
        if (Sy > p.first)
            Sy = p.first;

        if (Sx > p.second)
            Sx = p.second;

        if (Ey < p.first)
            Ey = p.first;

        if (Ex < p.second)
            Ex = p.second;
    }
    answer.push_back(Sy);
    answer.push_back(Sx);
    answer.push_back(Ey+1);
    answer.push_back(Ex+1);


    return answer;
}

 

3. 정리

시작점 : ( y의 가장 낮은 수, x의 가장 낮은 수)

끝점 :  ( y의 가장 높은 수, x의 가장 높은 수)

 

위와 같이 정의 하면 최단 거리로 모든 파일을 드래그 할 수 있다. 추가적으로 #의 좌표들은 파일의 시작지점(좌상단)이므로 끝점의 y+1,x+1 을 하여 마지막 파일을 포함하도록 한다.

 

N : wallpaper 배열의 원소의 수

W : wallpaper 안의 각 문자열의 길이

M : 파일의 시작지점(좌상단)의 갯수

 

전체 시간 복잡도는  O( N × W + M)

 

출처 : https://school.programmers.co.kr/learn/courses/30/lessons/161990

'Algorithm' 카테고리의 다른 글

[프로그래머스] 제출 내역  (0) 2025.04.14
[프로그래머스] 덧칠하기  (0) 2025.04.14
[프로그래머스] 공원 산책  (0) 2025.04.09
[프로그래머스] 추억 점수  (0) 2025.04.08
[프로그래머스] 달리기 경주  (0) 2025.04.08

+ Recent posts