JWT (JSON Web Token)
JSON 객체로 안전하게 전송하기 위한 방식
  • JWT를 암호화하여 당사자간 비밀 제공
  • 서명의 용도로도 사용
  • RSA, HS256

 

구조

 

  • Header : 어떤 알고리즘으로 서명했는지
  • Payload : 정보
    • 등록된 클레임: iss(발행자), exp(만료시간), sub(주제), aud(청중) 등
    • 개인 클레임: 유저 ID
  • Signature : Header + Payload + 키값 -> 암호화

각각이 Base64로 암호화

 

 

1) 알고리즘 - HS256 (HMAC-SHA256 기반 인증)

JWT 발급 과정

JWT 인증 검증 과정

 

 

2) 알고리즘 - RS256 (RSA SHA-256 기반 서명)

JWT 발급 과정

Signature를 생성할때
RSA(Header, Payload, 서버 개인키) 로 서명 생성

 

JWT 인증 검증 과정

서버는 클라이언트의 Signature를  서버의 공개키로 검증

 

 


 

세션 방식의 한계

 

  • 서버별로 세션 정보를 별도 저장해야 함
  • 서버 간 세션 공유를 위한 추가 인프라 필요 (Redis 등)

https://gamza-devlog.tistory.com/54

 

[Spring Security] Session

Session: 서버가 사용자를 기억하게 해주는 메모리HTTP 서버는 무상태(Stateless)이기 때문에 이전 요청을 기억하지 못하므로, 세션을 통해 사용자의 상태 정보를 지속적으로 유지세션이 사라지는 경

gamza-devlog.tistory.com

 

JWT를 사용하면
서버1, 서버2, 서버3 모두 같은 시크릿키만 보유 어떤 서버든 JWT 검증 가능 → 별도 세션 저장소 불필요

 

 

💡 A가 B에게 CIP를 지켜 메시지를 안전하게 전달하기 위해 암호화 키와 함께 메세지 전달

CIP
: 기밀성(Confidentiality), 무결성(Integrity), 인증(Authentication)

 

 

‼️ 발생할 수 있는 문제

1. 암호키 탈취 위험

C가 암호키를 탈취하여 메세지 해독 가능

A → [메시지1 + 암호화키] → C(해커) → B
     ↓
C가 암호키를 탈취하여 메시지 해독 가능

 

2. 인증 문제 (송신자 위조)

C가 암호키를 몰라도 메세지를 가로채고 다른 메세지로 교체 가능

B는 실제 송신자가 누구인지 확인할 수 없음

A → [메시지1] → C(해커) 
C → [메시지2] → B (B는 A가 보낸 줄 알고 받음)

 

 

RSA로 보안문제 해결

RSA
· Public Key (공개키): 누구나 알 수 있는 키
· Private Key (개인키): 본인만 알고 있는 키

핵심원리
1. 공개키로 암호화 → 개인키로 복호화 (기밀성, 암호화)
2. 개인키로 암호화 → 공개키로 복호화 (인증, 전자서명)

 

1. 암호키 탈취 위험 해결 : 기밀성 확보

1. A가 B의 공개키로 메세지 암호화
2. A → [암호화된 메시지] → 해커C → B
3. 해커C가 가로채지만 읽을 수 없음
4. B만이 자신의 개인키로 복호화 가능
  • 해커가 메시지를 가로채도 내용을 알 수 없음
  • B의 개인키 없이는 절대 복호화 불가능

2. 인증 문제 (송신자 위조) 해결 : 인증 확보

1. A가 자신의 개인키로 메시지 암호화 (디지털 서명)
2. A → [서명된 메시지] → 해커C → B
3. 해커C가 내용을 볼 수는 있음 (A의 공개키로 복호화 가능)
4. 하지만 B는 A의 공개키로 복호화되므로 A가 보낸 것임을 확신
  • 송신자가 A임을 확실히 증명
  • 해커가 메시지를 위조하면 A의 공개키로 복호화되지 않음
  • 부인방지: A는 자신이 보냈다는 것을 부인할 수 없음
 
 
🤔 만약 해커C가 B의 개인키가 없어 해독할 수 없는 메세지를 날려버리고 새로운 데이터를 B에게 보낸다면?
  • 기밀성 필요 (해커가 내용을 보면 안 됨)
  • 인증 필요 (A가 보낸 것임을 증명)
1. A가 "1234"를 B의 공개키로 암호화 (기밀성)
   → 결과: [B공개키로 암호화된 "1234"]

2. 1번 결과를 A의 개인키로 한 번 더 암호화 (인증)
   → 최종: [A개인키로 암호화된 [B공개키로 암호화된 "1234"]]

3. B가 받아서 복호화:
   - 먼저 A의 공개키로 복호화 → 송신자 A 확인 ✅
   - 그 다음 B의 개인키로 복호화 → 내용 "1234" 확인 ✅
 

Session

: 서버가 사용자를 기억하게 해주는 메모리

HTTP 서버는 무상태(Stateless)이기 때문에 이전 요청을 기억하지 못하므로, 세션을 통해 사용자의 상태 정보를 지속적으로 유지

세션이 사라지는 경우

  1. 서버 쪽에서 세션 값을 날릴 때
  2. 사용자가 브라우저를 종료할 때
  3. 특정 시간(보통 30분)이 지났을 때

 

세션 단점

만약 동시접속자 수가 많은 서비스여서 로드밸런서 + 서버 3대로 부하를 분산할 때,

사용자 A의 요청 흐름:
1차 요청 → 로드밸런서 → 서버1 → 세션ID: ABC123 생성
2차 요청 → 로드밸런서 → 서버2 → 세션ID: ABC123 모름 😱

서버1 메모리: { ABC123: {user: "홍길동", loginTime: "10:00"} }
서버2 메모리: { } ← 빈 상태
서버3 메모리: { }

 

세션은 서버 메모리에 저장되기 때문에 여러 서버 환경에서 사용자 상태 정보를 공유할 수 없어 확장성에 제약이 있음

URL 레벨 보안 적용

@Configuration
@EnableWebSecurity
public class SecurityConfig{

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user/**").authenticated()
                        .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().permitAll()
                )

 

 

메서드 레벨 보안 적용

 

1. @Secure

단순 권한 체크

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true) // 추가! - secured 어노테이션 활성화
public class SecurityConfig{
	...
}
    @Secured("ROLE_ADMIN")
    @GetMapping("/info")
    public String info() {
        return "개인정보";
    }

    @Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
    @GetMapping("/update")
    public String update() {
        return "정보 수정";
    }

 

2. @PreAuthorize

복잡한 조건 체크

메서드가 실행되기 직전에 검사

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 추가! - preAuthorize 어노테이션 활성화
public class SecurityConfig{
	...
}
    @PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
    @GetMapping("/data")
    public String data() {
        return "아주 중요한 정보;";
    }

비밀번호를 암호화 하지 않고(평문으로) 저장하면 Security로 로그인 할 수 없음

→ Spring Security는 기본적으로 비밀번호가 암호화되어 있다고 가정

 

Spring Security는 {암호화방식}암호화된비밀번호 형식으로 저장함

// 내부적으로 이런 과정이 일어남
passwordEncoder.matches("1234", "1234")

// java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
// : 암호화 방식을 식별할 수 없다

 

비밀번호 저장 시

  1. BCrypt 암호화 사용
  2. 임시로 평문 사용 (비밀번호 앞에 접두사 {noop} 사용) → user.setPassword("{noop}1234");

 

 

BCryptPasswordEncoder

    @Bean // -> 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해줌
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
// Service
...
private final PasswordEncoder passwordEncoder;

...
    @Transactional
    public void saveUser(User user) {
    	String rawPassword = user.getPassword();
        String encPassword = passwordEncoder.encode(rawPassword);
        user.setPassword(encPassword);
        userRepository.save(user);
    }

 

 

로그인

@Configuration
@EnableWebSecurity // 스프링시큐리티 필터가 스프링 필터체인에 등록됨
public class SecurityConfig{

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable()) // CSRF 비활성화
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user/**").authenticated() // 인증 필요
                        .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") // 권한 확인
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().permitAll() // 나머지는 허용
                )
                .formLogin(form -> form
                        .loginPage("/loginForm") // 권한,인증이 필요한 요청은 /loginForm으로 리다이렉트 됨
                        .loginProcessingUrl("/login") // POST /login이 호출되면 시큐리티가 낚아채서 대신 로그인 진행
                        .defaultSuccessUrl("/")); // 성공하면 "/" 메인페이지로 리다이렉트

        return http.build();
    }
}
.loginProcessingUrl("/login")

POST  /login이 호출되면 Spring Security가 낚아채서 로그인을 진행해줌

/login 컨트롤러 만들지 않아도 됨

 

 

로그인 후 세션 생성

로그인 성공 → Security Session 생성
Security ContextHolder라는 키값에 세션 정보를 저장

[객체 계층 구조]
Security Session
ㅤㅤㅤㅤㅤㅤ└── Authentication (인증 객체)
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ└── UserDetails (사용자 상세 정보 객체)

 

1. 로그인 처리

시큐리티가 /login 주소 요청을 낚아채서 로그인을 진행

 

2. UserDetailsService 호출

Spring Security → PrincipalDetailsService.loadUserByUsername() 호출

 

3. 사용자 조회 및 UserDetails 생성

User userEntity = userRepository.findByUsername(username);

return new PrincipalDetails(userEntity);

 

4. Authentication 객체 생성

반환된 UserDetails가 Authentication 객체 내부에 저장

 

5. Security Session 저장

Authentication이 SecurityContextHolder에 저장

 

//UserDetails 구현

public class PrincipalDetails implements UserDetails {

    private User user;
    
    public PrincipalDetails(User user) {
        this.user = user;
    }

    // 해당 유저의 권한을 리턴
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();

        authorities.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });

        return authorities;
    }
    
    ...

 

@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if (userEntity != null) {
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
    // 반환된 UserDetails는 Authentication 내부에 들어감
    // Security Session에는 저 Authentication이 들어감
}

'Back-End > ↳ Spring Security' 카테고리의 다른 글

[Spring Security] JWT  (0) 2025.07.27
[Spring Security] RSA  (1) 2025.07.27
[Spring Security] Session  (1) 2025.07.27
[Spring Security] 권한 설정  (3) 2025.07.27
[Spring Security] 기본 설정 - URL별 접근 권한 설정  (0) 2025.07.26
implementation 'org.springframework.boot:spring-boot-starter-security'

 

build.gradle에 Spring Security 의존성을 추가하면 SpringBoot의 Auto Configuration에 의해 모든 URL이 자동 보호

→ 기본 사용자(user)와 랜덤 패스워드가 생성되어 로그인이 필요한 상태가 됨

 

 

또한 url을 /login으로 갖는 컨트롤러가 있을 때,

    @GetMapping("/login")
    public String login() {
        return "로그인 페이지!";
    }

 

Spring Security의 기본 로그인 폼으로 이동 ("로그인 페이지!" x)

 

  • Spring Security가 모든 경로를 자동으로 보호
  • 기본 formLogin() 설정이 활성화됨
  • /login 경로를 Spring Security가 가로채서 기본 로그인 폼 제공
  • 컨트롤러는 실행되지 않음

 

 

 

 

커스텀 보안 설정 구성

  • /user/** : 인증 필요
  • /manager/** : 인증 + ADMIN or MANAGER 권한 필요
  • /admin/** : 인증 + ADMIN 권한 필요
  • 나머지 URL은 모든 사용자 접근 가능
@Configuration
@EnableWebSecurity // 스프링시큐리티 필터가 스프링 필터체인에 등록됨
public class SecurityConfig{

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable()) // CSRF 비활성화
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user/**").authenticated() // 인증 필요
                        .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") // 권한 확인
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().permitAll() // 나머지는 허용
                );

        return http.build();
    }

}

 

.anyRequest().permitAll()

 

기본 보안이 비활성화 됨 → /login 경로도 일반적인 컨트롤러로 처리 → GET /login : "로그인 페이지!" (컨트롤러 응답)

 

 

인증이 필요한 경로로 들어갔을 때, 로그인페이지 컨트롤러가 실행되어야 함

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable()) // CSRF 비활성화
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user/**").authenticated() // 인증 필요
                        .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") // 권한 확인
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().permitAll() // 나머지는 허용
                )
                .formLogin(form -> form
                        .loginPage("/login"));

        return http.build();
    }

 

/user 경로 접근 시 동작 과정

  1. 요청: GET /user → Spring Security Filter Chain
  2. Spring Security 인증 상태 확인 → 로그인 안한 상태
  3. Spring Security 설정 확인: authenticated() → 인증 필요
  4. .loginPage("/login") 확인 → 리다이렉트 준비
  5. Spring Security 내부적으로 원래 가려던 곳 저장: /user
  6. 로그인페이지로 리다이렉트 → 로그인 컨트롤러 실행 (/login)
  7. 로그인 성공 → /user로 리다이렉트

 

 

기존 디폴트 로그인폼으로 가려면

.formLogin(Customizer.withDefaults());

'Back-End > ↳ Spring Security' 카테고리의 다른 글

[Spring Security] JWT  (0) 2025.07.27
[Spring Security] RSA  (1) 2025.07.27
[Spring Security] Session  (1) 2025.07.27
[Spring Security] 권한 설정  (3) 2025.07.27
[Spring Security] 회원가입 / 로그인  (2) 2025.07.27

 

 

Page객체를 DTO 대신 가져올 경우

  @GetMapping
  public ResponseEntity<Page<ProductEntity>> getProductList( ...
  • 결과값이 이상하게 나오거나
  • JSON으로 변경시 오류가 나거나 (HttpMessageNotReadableException)
  • Redis 등에 직렬화/역직렬화 시 문제가 발생
  • 필요없는 필드도 많음 

 

PagedModel

Spring Web에서 권장하는 Page용 DTO 객체

직렬화, 역직렬화 시 발생하는 문제 최소화

new PageModel<>(page)

 

 

 

❗️ @Transactional(readOnly=true) 어노테이션 필수

 

 

'Back-End > Spring' 카테고리의 다른 글

API 테스트를 통한 문서 자동화  (1) 2025.04.13
스케줄러 서비스 분리  (0) 2025.04.04
Custom exception  (0) 2025.03.26
Feign Client LoadBalancer 의존성 누락  (1) 2025.03.21

실리콘 맥

https://jmeter.apache.org/download_jmeter.cgi

Binaries 압축 파일 받아서 압축 해제

 

Apache JMeter - Download Apache JMeter

Download Apache JMeter We recommend you use a mirror to download our release builds, but you must verify the integrity of the downloaded files using signatures downloaded from our main distribution directories. Recent releases (48 hours) may not yet be ava

jmeter.apache.org

 

해당 폴더안에 bin 폴더에서 jmeter.sh 를 실행

cd apache-jmeter-5.6.2
./bin/jmeter.sh

 

 

 

 

100명의 사용자가 10번씩 반복하면서 총 1000번 실행됨

Ramp-up시간 : 사용자가 모두 시작하는 데 걸리는 시간

 

 

 

결과

 

처리량 = TPS

초당 1047.1개를 처리할 수 있다

(DB가 아니라 메모리에 저장하고 로드가 걸릴만한 작업이 없음^^)

유광열 튜터님 특강

 

웹 테스트 기본 - API 테스트

보통 포스트맨, 인텔리제이 HTTP, Talend, Insomnia 등을 이용 → 가내수공업

단위테스트도 중요하지만 웹 개발자는 API테스트를 가장 많이 함

 


 

API 테스트 클래스 어노테이션

@SpringBootTest
  • 스프링부트 애플리케이션 전체 컨텍스트를 로드하여 통합테스트 수행 - 일부만 가져올 수도 있지만 귀찮으니까..
  • 실제 애플리케이션 환경과 유사하게 테스트 진행 가능
@AutoConfigureMockMvc
  • 실제 서버를 실행하지 않고도 MVC 관련 기능 (컨트롤러, 필터, 인터셉터 등)을 테스트 할 수 있도록 MockMvc 객체 자동으로 구성
  • HTTP 요청과 응답을 모의로 검증
@Transactional
  • 각 테스트 메서드를 트랜잭션 내에서 실행
  • 테스트가 끝난 후 데이터가 롤백 (메서드마다)
@ActiveProfiles("dev")
  • 테스트 실행시 사용할 설정파일 적용 (ex. application-dev.yml)

 

 

의존성 주입

// 필수

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;
  • MockMvc : HTTP 요청과 응답 모의 검증 객체
  • ObjectMapper: 자바 - JSON 변경, 요청이나 응답 body 가공

 

 

더미 데이터 추가

ㅇㅇ

 

 


Given : 주어진 값 (ex. RequestDto)
When : 어떤 작업을 수행
Then : 기대



GET 

  @Test
  @DisplayName("단일 상품 조회 성공")
  public void testProductGetByProductIdSuccess() throws Exception {

    UUID productId = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
    mockMvc.perform( // When
            MockMvcRequestBuilders.get("/v1/products/{product_id}",
                    productId)
                .header("X-USER-ID", 1L)
                .header("X-USER-ROLE", "ROLE_COMPANY")
        )
        .andExpectAll( // Then
            MockMvcResultMatchers.status().isOk(),
            MockMvcResultMatchers.jsonPath("$.code").value(0),
            MockMvcResultMatchers.jsonPath("$.message").value("단일 상품 조회 성공"),
            MockMvcResultMatchers.jsonPath("$.data").doesNotExist()
        )
        .andDo(MockMvcResultHandlers.print());
  }

 

perform (When)

API 테스트에서 통신을 Mocking

 

andExpect, andExpectAll (Then)

andExpectAll을 사용할 경우 여러 조건을 포함할 수 있음

 

 

POST

  @Test
  @DisplayName("상품 생성 성공")
  public void testProductPostSuccess() throws Exception {
    UUID shopId = UUID.randomUUID();

    // Request DTO Mocking
    ReqProductPostDtoApiV1 dto = ReqProductPostDtoApiV1.builder()
        .product(Product.builder()
            .name("testProduct1")
            .price(BigDecimal.valueOf(12000))
            .stock(100)
            .shopId(shopId)
            .build())
        .build();

    // DI한 ObjectMapper를 이용해 DTO를 String으로
    // Given
    String dtoJson = objectMapper.writeValueAsString(dto);

    
    mockMvc.perform( // When
            MockMvcRequestBuilders.post("/v1/products")
                .content(dtoJson)
                .contentType(MediaType.APPLICATION_JSON)
                .header("X-USER-ID", 1L)
                .header("X-USER-ROLE", "ROLE_COMPANY")
        )
        .andExpectAll( // Then
            MockMvcResultMatchers.status().isOk(),
            MockMvcResultMatchers.jsonPath("$.code").value(0),
            MockMvcResultMatchers.jsonPath("$.message").value("상품 등록 성공"),
            MockMvcResultMatchers.jsonPath("$.data").doesNotExist()
        )
        .andDo(MockMvcResultHandlers.print());
  }

 

 

💡 로그인 유지

로그인은 테스트 할때 중복이 있음.

@Test 붙이지 않고 메서드로 만들어 놓고 사용

  private MvcResult login() throws Exception {
    ReqAuthPostLoginDtoApiV1 reqDto = ReqAuthPostLoginDtoApiV1.builder()
        .user(ReqAuthPostLoginDtoApiV1.User.builder()
            .username("testuser1")
            .password("1234")
            .build())
        .build();
    
    String reqDtoJson = objectMapper.writeValueAsString(reqDto);
    
    return mockMvc.perform(MockMvcRequestBuilders.post("/v1/auth/login")
                            .content(reqDtoJson)
                            .contentType(MediaType.APPLICATION_JSON)
        )
        .andExpectAll(
            MockMvcResultMatchers.status().isOk()
        )
        .andReturn();
  }
  @Test
  @DisplayName("상품 생성 성공")
  public void testProductPostSuccess() throws Exception {

    ApiResponse<ResAuthPostLoginDtoApiV1> resLoginDto = objectMapper.readValue(
        login().getResponse().getContentAsString(),
        
        new TypeReference<>() {}
    );
        
        ...
     
    mockMvc.perform(
            MockMvcRequestBuilders.post("/v1/products")
                .header(HttpHeaders.AUTHORIZATION,
                    "Bearer " + resLoginDto.getData().getAccessJwt())
                .content(dtoJson)
                
       ...

 

'Back-End > Spring' 카테고리의 다른 글

[Spring] Page 대신 PagedModel  (0) 2025.04.26
스케줄러 서비스 분리  (0) 2025.04.04
Custom exception  (0) 2025.03.26
Feign Client LoadBalancer 의존성 누락  (1) 2025.03.21

 

아키텍처 설계: 클라이언트-서비스 관계

클라이언트 - 컨트롤러 - 서비스

컨트롤러와 서비스가 1:1 관계를 가져야 함

 

그런데 스케줄러가 개입한다면?

스케줄러(클라이언트) - 서비스

스케줄러가 클라이언트 역할을 하기 때문에, 스케줄러가 접근하는 서비스는 컨트롤러가 접근하는 서비스와 별개로 구성하는 것이 바람직함

 

기존 코드

- 스케줄러가 컨트롤러를 통해 접근하는 서비스에 접근하고 있음

- 배송 담당자 할당 기능은 컨트롤러와 직접 연관이 없음에도 DeliveryService에 포함되어 있음

- 스케줄러가 여러 서비스에 의존하여 의존성이 복잡함

public interface DeliveryService {
    ...
    void assignPendingDeliveries();
    ...
}


public interface DeliveryRouteService {
    ...
    void assignPendingDeliveries();
    ...
}

 

@Component
public class DeliveryAssignmentScheduler {
    private final DeliveryService deliveryService;
    private final DeliveryRouteService deliveryRouteService;
    
    ...
    
    @Scheduled(fixedRate = 60000)
    public void runAssignmentScheduler() {
    	...
        deliveryService.assignPendingDeliveries();
        deliveryRouteService.assignPendingDeliveries();
    }
}

 

 

 

 

 

개선 

: 스케줄러 전용 서비스 분리

- 스케줄러 관련된 코드를 모아둔 별도의 schedule 패키지 생성

- 배송 담당자 할당만 전담하는 DeliveryAssigmentService 인터페이스/구현체 생성

- 기존 배송 담당자 할당 로직을 새 서비스로 이동

- 스케줄러의 의존성을 새로운 서비스로 단일화

// schedule 패키지 내의 스케줄러
@Component
public class DeliveryAssignmentScheduler {
    private final DeliveryAssignmentService deliveryAssignmentService;
    
    ...
    
    @Scheduled(fixedRate = 60000)
    public void runAssignmentScheduler() {
    	...
        deliveryAssignmentService.assignCompanyDeliveryManager();
        deliveryAssignmentService.assignHubDeliveryManager();
    }
}


// schedule 패키지 내의 할당 서비스
public class DeliveryAssignmentService {
    void assignCompanyDeliveryManager();
    void assignHubDeliveryManager();
}

 

이상적으로는 스케줄러(클라이언트) - 워커(컨트롤러) - 서비스 구조가 좋지만, 이번 리팩토링에서는 워커 없이 진행

 

 

효과

- 클라이언트 - 서비스 관계 명확

  • 웹 요청: 클라이언트 - 컨트롤러 - 서비스
  • 스케줄러: 스케줄러 - 서비스

- 컨트롤러와 연관된 서비스는 컨트롤러 관련 기능만 포함하게 됨

- 스케줄러가 단일 서비스에만 의존하게 되어 의존성이 단순해짐

- 스케줄링 기능이 새롭게 추가되어도 기존 서비스에 영향을 주지 않음

'Back-End > Spring' 카테고리의 다른 글

[Spring] Page 대신 PagedModel  (0) 2025.04.26
API 테스트를 통한 문서 자동화  (1) 2025.04.13
Custom exception  (0) 2025.03.26
Feign Client LoadBalancer 의존성 누락  (1) 2025.03.21

+ Recent posts