TIL - 0802

서버 성능 및 안정성 향상을 위해 고려해볼 수 있는 아키텍쳐

리버스 프록시

  • 서버 앞 단에 위치하여 요청을 몸빵 하는 역할 : 단순 몸빵만 하는 것이 아니라 효율적인 처리와 보안성을 높여줄 수 있음
  • 캐시 가능 : 정적인 자원에 대해 캐시를 해뒀다가 이후 구성요소로 가지않고도 바로 응답해줄 수 있는 장점이 있음
  • 서버 보안 : WS, WAS 보안(감추기 가능)
    • Nginx의 경우 리버스 프록시 역할을 할 수 있음 (Nginx가 아니더라도 다른 웹서버에서도 지원)

로드밸런싱

  • 처리를 분산하기위해 N대의 웹서버, 톰캣을 도입함, 균등한 트래픽 분산을 위해 앞 단에 로드밸런서를 위치시킴
  • 로드밸런서 종류
    • 하드웨어 L4/L7 : 로드밸런스 기능을 하드웨어로 구현해낸 것, L은 로드밸랜스이며, 4와 7은 OSI 7계층에서 몇계층까지 로드밸런서가 들여다볼 수 있냐를 나타냄, 7계층의 경우 어플리케이션 계층(URI, 쿠키 등 HTTP), 4계층의 경우 TCP/IP(IP, Port)에 따라 로드밸런싱, L7이 당연히 비쌈
    • 소프트웨어 : HAProxy, 오픈소스 로드밸런싱 소프트웨어

캐시

  • 데이터의 성격에 따라 캐시해뒀다가 곧바로 응답을 줄 수 있음 : 데이터를 받아내기위한 내부적인 처리를 하지않고 이전에 했던 응답을 저장했다가 곧바로 응답으로 돌려줌
  • 웹서버 / 웹어플리케이션서버 모두 할 수 있음
    • 웹서버 : static 자원
    • 웹어플리케이션 서버 : 자주 사용되는(캐시 히팅율이 높은), 주로 최신의 것 - 자주 변경되는지 체크해야함(1시간이라도 큰 성능상의 이점을 가져옴), 데이터베이스 서버에 요청하지않아도 곧바로 응답을 내려줄 수 있음
  • local/global 캐시 : Nginx(WS), 톰캣(WAS)도 내부적으로 캐시(local)를 해둘 수 있으나 메모리를 사용해야하므로 메모리가 가득찰 경우 프로세스 종료될 위험이 생김(안정성 측면에서 좋지않음) 그렇기때문에 외부 캐시 서버를 두는 것을 고려해봐야함(global), 주로 캐시서버로는 redis를 쓰는 추세(인 메모리, 먼저 메모리에 캐시를 해두고 하드디스크에 씽크를 맞추는 형식) - 같은 네트워크에 묶어두고 빠르게 응답 받을 수 있도록(평균 2ms, latency가 짧아 로컬 메모리보다는 속도가 느릴지 몰라도 굉장히 빠른 속도)
    • 스프링 어노테이션으로 @Cacheable(local) 제공함 : 매번 데이터베이스에 요청하여 데이터를 응답받지않아도 처리할 수 있게됨, 데이터 성격에 따라 잘 고려해야함
  • 주의할 점은 만료기간이 되지않았는데 변경되었을 경우 static 자원은 응답해주는 리소스의 이름을 변경시켜주면 됨(클라이언트는 패스 기준 캐시)

메세지큐(MQ)

  • 동기적으로 처리하지않아도 되는 작업을 MQ 서버에 요청시킴 : 비동기적으로 처리를 함
    • 예를 들어 회원가입을 개발할 경우 유저에 회원가입 후 이메일을 보냈습니다 화면은 동기적으로 보여져야하지만, 이메일을 보내는 작업 자체는 동기적으로 처리되지 않아도 될 수 있으므로 해당 부분을 비동기 처리 고려해볼 수 있음
    • latency가 낮아도 상관없는 처리의 경우 톰캣이 쓰레드 점유를 해가면서까지 동기적으로 처리하지않고 비동기 처리를 할 수 있는 MQ 서버를 구성해서 요청하는 것이 어떨까?
  • 메세지큐에 작업을 요청하면 메세지큐에서 비동기적으로 처리 : 큐에 작업이 풀로 쌓이면 큐에 잠시 대기시켜뒀다가 처리
    • kafka를 사용하는 추세라고 함

데이터베이스 master - slave 리플리케이션

  • 데이터를 복제해서 master 서버가 장애 발생하더라도 빠르게 서비스할 수 있도록 하는 아키텍쳐 : 이중화
  • 빠르게 서비스를 재개 할 수 있도록 하기위해서 주기적으로 데이터 복제를 해야함
    • 주기적으로 해야하는데, 해당 시기를 설정할 수 있음 : 너무 자주하면 성능 저하, 너무 간격을 길게 둘 경우 1번 복제할 때 소요 시간 증가, 마스터 서버 장애 발생 시 데이터 손실이 커짐
  • 구성요소 장애 감지에 대한 시스템도 갖춰져 있어야함

  • 이런식으로 구성할 수 있음
    • master(읽기, 쓰기) - active / slave(읽기) - stand by : master 장애 발생 시 스탠바이 하고 있던 slave 서버로 전환되어 서비스를 재개함
    • master(쓰기) - active / slave(읽기) - active : 하나의 데이터베이스만 일하는 것이 아니라 각각 일하게하여 부하를 분산시켜 안정성을 확보함

[프로젝트 개발] 인증 포함 리뷰 글쓰기 과정 만들기

기능이 추가될 것으로 예상될 때 예상으로 코딩하지말고 필요할 때 만들고, 분리하자

  • 확장가능하며, 다른 객체에 변경을 유도하지않는 구조로 코딩할 수는 있지만, 코드 자체를 그에 맞춰 미리 짜는 것은 좋지않음
    • 사용하지도 않는 메소드, 객체를 미리 만들어두고 프로젝트를 더럽히지 말 것
    • 테스트 주도 개발을 한다면 메소드부터 만들어가면서 스텝 바이 스텝으로 구조 나누기도 좋은 방법 : 구조 자체도 처음부터 예상에 따라 먼저 개발하는 것도 오히려 코드 복잡도를 높일 수 있음
  • 예를 들어 필요한 인프라로직이 없음에도 불구하고 UserService와 같이 서비스 계층을 미리 만들어 UserRepository를 곧바로 사용하지못해 단계만 하나 더 만드는 상황과 같은 코딩을 하지말자
    • 인프라로직 : 핵심 로직은 아니지만 서비스 운영에 있어서 필요한 로직(예를 들어 트랜잭션)
/* 가짜 코드 */
public class XXX {
	
	@Autowired
	private UserService userService;
	
	public void example() {
	    userService.findByUserName(userName);
	}
}

@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;
    
	public User findByUserName(String userName) {
   	    return userRepository.findByUserName(userName).orElseThrow(NoSuchElement::new);
	}
}

테스트 환경에서 인증 관련 쿠키설정하기

HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", "Auth=" + jwt);

JWT 인증 후 정보(Account)를 컨트롤러에서 사용하기

  • 메소드 인자에서 Principal를 설정하면 가져올 수는 있으나 매번 캐스팅해야하는 문제가 있음
    • 같은 처리를 반복해야할 때 중복을 줄일 수 있음 : 커스텀 어노테이션을 통해서
    • Principal은 인증 후 객체 Authentication(PostAuthAuthentication)에서 get할 수 있음 - principal로 설정도 인증 과정에서 우리가 했음
@PostMapping("")
@PreAuthorize("hasRole('ROLE_USER')")
public String create(Principal principal) {
	Account account = (Account) principal.getName();
	return "redirect:/";
}
  • 커스텀 어노테이션을 만들어 어노테이션을 만나면 HandlerMethodArgumentsResolver 동작하도록 함 : Account로 캐스팅해서 인자로 설정되도록 함
    • 리졸버 생성(빈) 후 등록해줘야함
@PostMapping("")
@PreAuthorize("hasRole('ROLE_USER')")
public String create(@Login Account account, ReviewDto reviewDto) {

	return "redirect:/";
}

/* 리졸버 : 반복 작업을 하나로 */
public class LoginRequiredMethodArgumentsResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Login.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return authentication.getPrincipal();
    }
}

/* 리졸버 스프링빈 등록 - 리졸버 등록 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public LoginRequiredMethodArgumentsResolver loginRequiredMethodArgumentsResolver() {
        return new LoginRequiredMethodArgumentsResolver();
    }
  
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginRequiredMethodArgumentsResolver());
    }
}

인증과 인가 구분

  • JWT 프로바이더를 통해 인증을 하고, 메소드 별로 인가를 할 수 있는 구조로 개발함 : 인증이 되었다고 해서 인가까지 되었다고 생각하면 안됨
    • 인증은 이미 필터에 걸려 프로바이더를 거쳐 인증을 하고 옴, 이후 메소드 별로 인가를 하는 것(이미 인증된 정보에 대해 또다시 권한 검증)