728x90

쿼리 속도 개선
1.6초 -> 0.4초
메타테이블과의 조인을 해서 정보를 얻어오는 과정
서로 논리적인 데이터베이스는 다른데 mysql에서 뷰 테이블을 사용하고 있음
그런데 유저 개개인의 데이터에서 매칭시키는 메타테이블의 idx 칼럼이 인덱스가 정해져있지 않았다.
그래서 유저 개개인의 데이터를 전부 조회해서 매칭되는 데이터를 추리고 있었다.

스크린샷 2022-06-21 오후 4 03 44


개선안

스크린샷 2022-06-21 오후 4 04 49

100개 이상의 row를 탐색하던 것이 25개만 탐색하는것으로 바뀐 실행이 나오게 된다.
그러면서 Extra가 제외되고 참조 정보가 인덱스로 가게 된다.
지금은 100개인 데이터였지만, 만약에 조회할 데이터가 10만 건, 100만 건 이렇게 늘어날 수록
시간은 증가했을 것이다.
진행중인 mysql 독서 스터디가 이런 생각을 하는데 도움을 많이 줬다.

728x90

'CS > 데이터베이스' 카테고리의 다른 글

Mysql 인덱스  (1) 2023.12.21
쿼리 작성 및 최적화  (0) 2022.08.11
728x90

업무에서 Spring Batch로 세미나를 진행하고, 앱 푸시 기능을 배치로 전환하는 작업을 진행했다.
여기에 저장하면서 글로만 보던 것들을 직접 경험해보면서 겪었던 일들을 기록하려고한다.

첫번째 에러

우리 푸시 배치 서버의 구조는 스프링 스케줄러 서버에서
푸시 서버의 api를 호출해서 해당 job들을 돌려주는 방식으로 구성이 되어있다.

물론 이 부분을 새롭게 개편해야 하는것은 맞다ㅋㅋㅋ

그래서 특정 시간이 되면 해당 job api로 호출을 하는데
여기서 대략 총 데이터가 100,000건 정도 되는데 전부 동기 + 블록킹처리로 진행했다.
그래서 스케줄러가 api를 쏘고 요청값이 최대 오래걸려도 limit을 30분을 잡았었다.
그런데 100,000건의 데이터를 여러 로그를 쌓고, 푸시를 하는데까지 1시간이 넘게 걸렸었다.
그래서 1시간이 지나도 받지 못하는 상황에 에러가 나서 해당 스케줄러가 돌다가 실패가 되었다. (근데 뒤에서의 푸시 서버는 계속 돌고있었다)

그러니 다시 정리해보면 1시간이 넘는 시간동안 작업을 하고 있던것이다.

-> 이부분에서 나도 대기중인 데이터만 뽑는 쿼리를 작성해야했는데 실수로 보낸 데이터까지 조회하게끔 만들었다.

Mono 객체를 block()을 사용해서 푸시를 진행했기 때문에, 블록킹 방식으로 동작하여

제어권도 아예 넘겨버려서 끝이나야 다음 작업을 수행하는 형태로 진행되서 굉장히 느렸었는데,

이방식을 subscribe() 방식으로 바꿔서 논블록킹으로 푸시 발송 명령만주고 다음 작업을 진행하게끔 해서

많이 속도를 줄일 수 있었다.
-> 이부분은 조만간 다른 포스팅에서 자세하게 다룰 예정이다.

찾은 부분

SEND, WAIT 두 상태의 데이터를 모두 가져오고 나서 SEND로 업데이트를 치고 있던 것이다.

이부분이 일단 성능 저하의 첫번째 원인이라 생각했고,
그리고 스케줄러는 다른 스케줄링들도 가지고 있으니, 푸시 서버에서 요청을 바로 돌려주고 해당 Job은 뒷편에서 실행해주는 것이 맞다고 생각했다.
그래서 이 두부분을 고쳐보았다.

  • 쿼리는 WAIT상태만 추출하여 갖고 있는다.
  • 푸시서버는 기본적으로 제공해주는 JobLauncher를 배제한다.
@RestController
@RequiredArgsConstructor
public class Demo {
    private final BasicBatchConfigurer basicBatchConfigurer;

    @PostMapping("/demo/push")
    public String appPushJobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = (SimpleJobLauncher) basicBatchConfigurer.getJobLauncher();
        jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());

        // jobLauncher 실행 로직....
    }
}

이렇게해서 요청을 바로 수행후 return값을 먼저 돌려주었다.
이렇게 해서 스케줄러의 동기처리로 늦어졌던 것에 대해서 일단락 짓게 되었고,
1시간 이내로 처리가 되게 되었다.

두번째 에러

두번째 에러는 페이징 처리에 대한 부분이었다.
참고 블로그 링크
예를 들어서, 총 10페이지로 구성되어있고, 1페이지당 10개의 데이터가 있다고 가정한다.
총 100개의 데이터가 업데이트가 되어야 한다.
근데 커밋이 한번 일어나게 되면,

1페이지 10 -> 2페이지 10 -> 3페이지 10 -> ... 이 될줄 알았었다.

애초에 도입하기 이전부터 해당 참고 블로그를 보면서 이런 에러가 있구나 하면서 개발을 진행했다.
인덱스 기준으로
1 ~ 10 번까지의 데이터가 작업 완료되면 해당 데이터들은 Update가 진행이 될 것이다.
그래서 11 ~ 20을 원하던 다음 데이터는 21 ~ 30을 조회하게 되는것.
이것은 배치의 문제가 아니라 그냥 페이징 쿼리 자체의 문제라고 한다.

그래서 유저들에게 푸시를 보낼때 50%의 유저만 푸시를 받았을 것이다.😇

이렇게 해서는 안됐다.
방법은 우선 2가지가 있었다.
그렇지만 나는 2번째 방법을 사용했다.

그렇다면 왜?

우리 회사의 배치 서버는 JPA로 구성하기로 했었고, 그래서 JPA를 사용한 배치 동작을 구현해서
Cursor대신 JpaPaging을 사용했다.

커서 사용

커서(Cursor)란??

쿼리문에 의해서 반환되는 결과값들을 저장하는 메모리공간
Fetch => 커서에서 원하는 결과값을 추출하는 것
커서는 한번 커넥션을 맺은 후 커서만을 다음으로 가기 때문에 조회하고 Update되어도 갱신되는 일이 없이 적용 가능하다.

@Bean
@StepScope
public JpaPagingItemReader<Pay> payPagingReader() {

    private final int chunkSize = 1000;

    JpaPagingItemReader<PushAlarm> reader = new JpaPagingItemReader<PushAlarm>() {
        @Override
        public int getPage() {
            return 0;
        }
    };

    reader.setQueryString("SELECT p FROM PushAlarm p WHERE p.sendStatus = :sendStatus");
    reader.setParameterValues(Map.of("sendStatus", "WAIT"));
    reader.setPageSize(chunkSize);
    reader.setEntityManagerFactory(entityManagerFactory);
    reader.setName("payPagingReader");

    return reader;
}

이런식으로 page를 0으로 고정시켜줘서 update가 일어나도 다시 0페이지만 계속 조회하는 것이다.
이렇게 해서 문제를 해결했다.

마무리

배치 세미나를 진행하면서 공식문서를 읽고 참고 블로그까지 더해서 학습해서
적용해본 결과 그래도 역시 글로 보는것보다 맞으면서 배우는게 좀 더 빠르게 습득이 가능하다는걸 느낀다.
지금은 데이터가 작아서 내가 일을 제대로 처리했을지 모르겠다.
그래서 강의를 하나 더 들으면서 좀더 뿌리를 깊게 내려야겠다...

728x90
728x90

도메인을 제대로 만들지 못하면 요구사항을 충족하는 소프트웨어를 만들기란 힘들다는 것을 잘 알고있다.
도메인 영역은 기본 패시브로 잘 구현하되,
거기에 도메인에 활력을 불어넣어줄 표현영역, 응용영역도 잘 구현이 되어야 한다.

표현 영역

표현 영역은 사용자의 요청을 해석한다.

스프링으로 따져 생각해본다면 Controller로 생각하면 될 것 같다.

DDD에서 말하는 패키지 구조로 보면 interfaces가 될 것이다.

표현 영역은 URL, 요청 파라미터, 쿠키, 헤더 등등을 이용해서 클라이언트에서 원하는 작업을 받아서
응용 영역에 처리를 위임시킨다.

응용 영역

응용 영역은 표현 영역의 요청을 받아 처리를 하는 Service로 생각해볼 수 있다.


표현 영역에서 전달 받은 데이터는 일단 신뢰할 수 없는 데이터이므로,
값에 대한 검증이 있을 수 있고, 또 응용 영역에서 필요로 하는 데이터 타입으로 변환을 시켜주는 동작이 들어가야 한다.
그 후 응용 영역이 요구하는 객체를 생성하고 응용 서비스의 메소드를 호출한다.
그리고 작업이 완료되었을 때 반환되는 값을 토대로
응답 객체를 만들어서 알맞는 형식으로 응답을 내려준다.

응용 서비스의 역할

보편적인 응용 서비스의 구조는 이런식이다.

public Response applicationMethod(final long id) {
    //1.인프라 스트럭쳐에서 애그리거트를 불러온다.
    Domain domain = domainRepository.findById(id);

    //2. 애그리거트의 도메인 기능을 실행한다.
    domain.doSomething();

    //3. 결과를 반환해준다.
    return domain.toResponse();
}

조회해서 내려받는건 보통 이런식일거라고 생각한다.

그리고 생성이나 수정같은 경우에는 Request 요청 객체가 들어올때 유효성 검증을 실행하고,

그 후에 조건에 부합하는 경우에 생성, 수정을 해주면 된다.


단순 조회인 경우 서비스 레이어가 필요하지 않은 경우에는

Controller -> Repository 로만 구성해도 무방하다.

기존 Layered Architecture 로 구성했을 때, jpa 구현기술에 대한 의존이 있는 경우에

도메인이 인프라 스트럭쳐에 의존하게 된다.
DDD를 연습해보면서 느끼는 점은, 도메인에 대한 리포지토리를 인터페이스로 도출한 후에
구현체들을 인프라 스트럭쳐에 위치시키면 도메인이 구현 기술에 대한 의존이 없어지게 구성이 된다.
JpaRepository를 만든다고 한다면

DomainRepository <- DomainJpaRepository 로 의존이 반대로 흐르게 구성이 된다.

값 검증

값 검증은 표현 영역, 응용 영역 두곳에서 모두 수행이 가능하다.

  • 표현 영역
    • 필수 값, 값 형식, 범위 등등을 검증한다. (@Valid@Validated)
  • 응용 서비스
    • 데이터의 존재 유무와 같은 논리적 오류를 검증한다. (findById()orElseThrow 같은것들을 생각해보면 될듯????)

@RestControllerAdvice@ExcepitonHandler를 사용해서

요청 값에 대한 검증을 먼저 진행해준다.

728x90

'아키텍처' 카테고리의 다른 글

DDD 도메인  (0) 2022.08.11
Monolithic vs MSA  (0) 2022.08.10
728x90

도메인

도메인은 구현해야할 소프트웨어의 대상이다.
쇼핑몰을 생각해보면 쇼핑몰은 대상, 그리고 상품조회, 주문, 배송, 결제 등등이
하위 도메인이 된다.
그리고 도메인이라고 해서 고정된 하위 도메인이 존재하는건 아니다.
결제같은것을 PG사에 위임하니까 말이다.

도메인 모델 패턴

기존의 나는 흔히 말하는 MVC 레이어 아키텍처를 사용했다.

이미지 출처 바로가기

단순히 Controller, Service, Repository세개를 사용해서 구현했었다.

이것은 내가 넥스트스텝에서 TDD와 클린 코드를 수강했어도 변하지 못했다.
근데 이 방식이 아니라 DDD책을 보면서 주워들었던 도메인 모델 패턴을 봤다.
그래도 말만 바뀌었지 느낌은 전과 같았다.

의존은 화살표 방향으로 주입이 된다.

  • Presentation
    • 사용자의 요청을 처리하고 사용자에게 정보를 보여준다.
    • 사용자는 사람이 아니라 외부 시스템일 수 있다.
  • Application
    • 사용자가 요청한 기능을 실행
    • 비즈니스 로직을 구현하지는 않고 도메인의 계층을 조합해주기만 함
  • Domain
    • 이 설계의 가장 핵심
    • 시스템이 제공하는 도메인들의 규칙을 구현
  • InfraStructure
    • DB나 메시징 시스템 같은 외부 시스템의 연동을 처리함

핵심 규칙들은 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 확장해야 된다면
다른 코드에 영향을 끼치지않고 모델들에 반영해주어야 한다.
도출한 모델들은 엔티티와 값 타입으로 구분할 수 있다.

엔티티와 값타입을 제대로 구분하여야 도메인을 잘 설계하고 구현할 수 있다. 👍

엔티티

엔티티는 식별자를 가진다. 이 식별자는 엔티티 객체마다 고유해야 한다.

그래서 회원의 경우 회원번호가 서로 다르다.
이것이 바로 식별자가 되는것!!
다른 값이 바뀌어도 이 엔티티 객체가 생성되거나 삭제되기 전까지 유지된다.

그렇기 때문에 이 식별자 값이 같다면? 같은 객체❗️

그렇다면 이 식별자 값으로 equals(), hashCode()를 재정의 할 수 있다.

식별자 생성

그러면 식별자를 생성해야 하는데
엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.

  1. 특정 규칙에 따라 생성
  2. UUID같은 고유 식별자 사용
  3. 값 직접 입력 (개인적으로 제일 안좋다 생각)
  4. 일련번호 사용 (시퀀스나 DB의 auto-increment)

모델

배송지의 집주소가 우편번호 + 도로명주소 + 상세주소 이렇게 같이 사용하는게 대부분인데,

세 가지 데이터는 다르지만 개념적으로는 하나로 일맥상통한다. 바로 주소

값 타입은 바로 이럴때 사용하는 것이다.
이 값타입의 장점은 이 값들만의 기능을 추가할 수 있다.
이 값객체의 값을 바꾸는건 기존 데이터를 두기보단 변경이 되면 새로 생성하는
불변 객체 를 생성하는 방식을 선호한다.
불변으로 만드는 이유는 안전한 코드를 작성할 수 있다는 곳에 있다.
setter 메소드로 값을 바꾼다면 다른 로직에서 잘못 반영이 되는 불상사를 초래할 수 있다.
그래서 두 값 객체를 비교할 때에는 모든 인자가 같은지를 체크해주어야 한다.

무지성 Getter/Setter 남용금지

getter는 어쩔수 없는 경우가 있다고 할지라도,
setter는 값이 깨지는것을 막을수는 없다. + 핵심 개념이나 로직을 망가뜨릴수 있다.

728x90

'아키텍처' 카테고리의 다른 글

DDD 표현 영역과 응용 영역  (0) 2022.08.11
Monolithic vs MSA  (0) 2022.08.10
728x90

Real Mysql

real mysql 책을 읽으면서 스터디 한 내용을 정리하고자 한다.
일단 11.1 부터 11.3까지의 내용만을 정리했다.
내가 사용하는 애플리케이션에서 특정 데이터를 테이터베이스에 저장하거나 조회를 할 때
SQL이라는 문장을 사용해야 한다.

데이터베이스의 테이블이나 구조를 변경할때는 DDL(데이터 정의 언어)

테이블의 데이터를 조작을 위한 언어는 DML(데이터 조작 언어) 이다.

SQL작성 규칙은 mysql의 서버 시스템 정책에 따라 바뀔 수 있다.
이 정책은 데이터베이스에 어떤 테이블의 데이터들이 들어가지 않았을 때 설정하는 것이 중요하다.

영문 대소문자 구분

Mysql에서는 설치된 운영체제에 따라서 대소문자를 구분하는데,
DB의 테이블이 디스크의 디렉토리나 파일로 매핑이 되기 때문이다.
윈도우의 명령 프롬프트에서는 대충 디렉토리를 대소문자 구분하지 않고
Tap키를 눌러 자동완성을 시키면 그냥 그 알파벳에 맞는 디렉토리를 자동완성 시킨다.
반면에 유닉스 계열에서는 대소문자를 구분해서 대문자로 시작하는 디렉토리를 소문자부터 눌러서 Tap키를 누르면 찾지를 못한다.
그래서 운영체제를 옮기면서 db를 이관할 경우 문제가 생길 수 있기 때문에

Mysql 설정 파일에 lower_case_table_name 시스템 변수를 설정해주면 된다.

Mysql 예약어

데이터베이스 테이블에 예약어와 겹치는 키워드로 생성하는 경우
역따옴표나 큰따옴표로 감싸주어야 한다.
근데 이 감싸주는 행동 때문에 애를 먹을 수 있다.
단순 조회에서도 에러가 나올텐데 이 에러가 상세 정보를 나타내주는 것이 아니라 문법 오류라고만 띄워준다고 한다.
테이블을 생성해주어야 할 때에도 역따옴표를 넣지않고 생성을 해보다가 에러를 맞는 방법이 좋을것 같다.
그리고 무엇보다 최선의 방법은 예약어 키워드와 같은 테이블을 만들지 않는것이
가장 좋은 방법이다.

문자열

나는 이 문자열이 좀 신기했는데,
자동으로 다른 칼럼으로 형변환해서 비교한다는게 조금 신기했었다.

SELECT * FROM Member
WHERE number = '123';

위와 같이 정수형인 컬럼에 문자열로 데이터를 조회하면
조건에 해당하는 저 문자열만 숫자로 자동 형변환이 들어가게 되니까 성능상 문제는 존재하지 않는다.
하지만 역으로 문자열 컬럼이지만 숫자데이터만 저장되어있는 경우에

SELECT * FROM Member
WHERE zipcode = 10001;

우편번호를 형식만 숫자인 문자열로 저장했다고 했을 때 숫자형으로 조건을 검색하면

zipcode에 해당하는 값을 전부 형변환하면서 하나씩 탐색하기 때문에

형변환에 대한 리소스를 많이 잡아먹는다. 이렇게 비교하는건 좋지 않다.

그리고 zipcode가 보편적으로 숫자가 99% 이겠지만, 만약 문자가 들어간게 하나라도 있었다면 위의 조건은 에러를 뱉게 될 것이다.

DATE

이부분은 따로 뗴어져서 있었지만, 마찬가지로 위에서 봤던것 처럼
이 날짜부분도 자동으로 형변환이 된다.
그래서 문자를 Date형식으로 치환하는 어떤 함수를 쓰지 않아도 된다.
그리고 문자열로 조회한다고 해서 인덱스를 못타는 것도 아니다.

Boolean

나는 이부분을 보자마자 바로 tinyint(1) 을 떠올렸다.
true는 1, false는 0으로 나타내주지만,
이것을 정수형 변수에 넣어도 동작한다.
대신 false는 딱 0만 표현이 되는데,
true라고 해서 1 이상의 값들을 표현해주지는 못한다.
그래서 사용할거라면 tinyint(1)로 제한해서 쓰는게 좋을것 같다고 봤다.

더 많은 상태가 필요하다면 Enum을 사용하는게 바람직하다고 생각한다.

Like 연산자

이 연산자를 통해서 정규표현식을 사용하는 연산자보다는 좀 넓은 범위로 검색할 수 있는데 대신 인덱스를 사용할 수가 있다.

  • Like에서 사용하는 와일드카드
    • % : 0 또는 1개 이상의 모든 문자에 일치하는지
    • _ : 정확히 1개의 문자 일치

이 와일드카드들을 직접 문자열에 넣어서 탐색하고 싶다면

ESCAPE를 추가해서 검색하면 된다.

~로 시작하는 칼럼을 찾는 데에는 인덱스 레인지 스캔을 적용해서 탐색하는게 빠르지만,
~로 끝나는 칼럼을 찾는곳에서는 인덱스의 left-most 특성으로 인덱스 풀스캔을 진행하게 된다.
mysql의 B-Tree 인덱스를 이용한 검색은 100% 일치 또는 값의 앞부분(Left-most)만 일치하는 경우에 사용할 수 있다.

Between 연산자

위의 이미지는 real mysql에서 가져온 이미지이다.
왼쪽이 Between 연산, 오른쪽이 In 연산이다.
특정 조건이 명확하게 보이는경우엔 In 연산자를 적용해주는 것이 훨씬 빠를것이다.
둘다 같은 데이터를 조회할 수는 있지만 범위를 지정하기 때문에 인덱스를 타지 않고 해당 조건을 쭉 조회하게 될 것이다.

다시 설명하면, 값이 불분명한 범위내에서 검색을 해야하면 Between을 사용해야 하지만, 명확한 경우라면 In절을 사용하는것이 훨씬좋다는것

Mysql 내장함수

여기서 다른 JSON에 대한 특정 문법들에 대한 내용도 나오지만,
쓸일이 많이 없을것 같아서 읽기만 했고 제대로 봤던건

NOW, SYSDATE의 차이

이 두개는 나는 자바를 엮어서 생각했다.
아마 그 부분이 맞을거라고 생각한다.

SELECT NOW(), SLEEP(2), NOW();
SELECT SYSDATE(), SLEEP(2), SYSDATE();

NOW는 한 명령에 대해 동일한 시간을 가지고 2초를 지나서 데이터를 출력해주니까 값이 같다.
반면, SYSDATE는 한 명령이 아니라 그 자체의 함수가 있을때마다 즉각적으로 실행을 하기 때문에 두 값에 차이가 있다.
이게 이해가 잘 안된다면 아래의 자바 코드로 생각해보면 될 것 같다.

public class Demo {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);
        Thread.sleep(2000);
        System.out.println(now);

        System.out.println(LocalDateTime.now());
        Thread.sleep(2000);
        System.out.println(LocalDateTime.now());
    }
}

이 두개 방식의 차이이다.

그래서 조건식에 현재 시간을 여러번 넣어야하는 경우라면 NOW()를 쓰고

자바에서는 위쪽에 한번 선언한것으로 전부 넣어서 조회를 해주어야 조건이 제대로 동작할 것이다.

정리

11.3장이 MYSQL의 내장함수 설명부분이라 특정 함수들이 많아서
읽는데에 조금 분량이 많았던 것 같다.
다 같이 같은 공간에서 한번에 읽고 토론하는 시간을 가지니까
몰랐던부분도 이해하게 되고 집단지성으로 이게 이런의미구나! 라는걸 가져갈 수 있는 장점이 있다고 생각한다.
개발자에게 있어서 이 책은 2장이 더 괜찮을거라는 추천들 때문에 2장부터 보지만, 더 나아가서는 1장도 봐야 이해가 더 쉬울거라고 본다.

728x90

'CS > 데이터베이스' 카테고리의 다른 글

Mysql 인덱스  (1) 2023.12.21
쿼리 개선 2  (0) 2022.08.11
728x90

사용자가 프로그램을 실행시키면 메모리에 올라가며 프로세스가 실행된다.
지금의 컴퓨터는 수많은 프로세스들이 실행된다.
시분할 시스템이 사용되는 운영체제에서는 여러개의 프로세스를 돌아가면서 실행한다.
cpu가 병렬적으로 여러 프로세스를 막 실행시키는것이 아니라,
한 순간에 하나의 프로세스만 처리가 가능하다.
그렇지만, 그 속도가 우리가 눈치챌 수도 없을 만큼의 속도로 분할되어 실행되기
때문에 여러 프로세스가 동시에 실행되는것 처럼 느끼게 한다.
프로세스는 시분할 시스템 처리를 위한 5가지의 상태가 존재한다.

  • 생성

    • 이 상태는 PCB를 생성하고, 메모리에 프로그램 적재를 요청한 상태
    • 메모리에 프로그램 적재를 승인받으면 준비상태로 넘어간다.
  • 준비

    • CPU를 사용하기 위해 기다리고 있는 상태
    • 준비 상태의 프로세스는 CPU 스케줄러에 의해 CPU가 할당
    • 대부분의 프로세스가 이 상태에 존재함.
  • 실행

    • CPU스케줄러에 의해 CPU를 할당 받아 실행되는 상태
    • 실행 상태에 있는 프로세스의 수는 CPU의 개수만큼
    • 이 상태에 있는 프로세스도 CPU를 무한정 쓸 수 있는것이 아니라 할당된 시간만큼만 사용이 가능하다.
    • CPU스케줄러는 할당된 시간을 초과하면 할당했던 CPU를 강제로 뺏는다.
      • 이 때, 프로세스는 다시 준비 상태로 되돌아간다.
  • 완료

    • 프로세스가 종료된 상태
    • 프로세스가 사용했던 데이터들을 메모리에서 제거한 후 생성된 PCB도 제거한다.
  • 대기

    • 프로세스가 입출력요청이 있으면, 완료될 때까지 기다리는 상태
    • CPU는 굉장히 빠른데 비해 입출력은 굉장히 느린작업에 속함.
    • 특정 프로세스가 입출력 요청을 한다면 요청이 완료될 때까지 CPU를 기다리게 하는것은 굉장히 비효율적이기 떄문에 입출력 요청을 한 프로세스를 이 상태에 두고 다른 프로세스에게 CPU를 할당함.
    • 시간이 지나, 입출력이 완료되면 이 대기상태에게 CPU할당 기회를 준다.
    • 이렇게 되면, CPU에게는 미안하지만 쉬는 시간을 주지 않고 빡세게 굴릴 수 있다.
  • 출처

728x90

'CS > 운영체제' 카테고리의 다른 글

운영체제 3강  (0) 2022.08.22
운영체제 2강 - 2  (0) 2022.08.18
운영체제 2강 - 1  (2) 2022.08.11
운영체제 1강  (0) 2022.08.11
728x90
Unable to load credentials from any of the providers in the chain 
AwsCredentialsProviderChain(credentialsProviders=
SystemPropertyCredentialsProvider(), EnvironmentVariableCredentialsProvider(), WebIdentityTokenCredentialsProvider(), ProfileCredentialsProvider(), ContainerCredentialsProvider(), InstanceProfileCredentialsProvider()]) : SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set., ProfileCredentialsProvider(): Profile file contained no credentials for profile default: ProfileFile(profiles=[]), ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set., InstanceProfileCredentialsProvider(): Unable to load credentials from service endpoint.

라는 에러가 발생했다.
앱푸시를 보내려는 토큰을 받아오는 과정인데
회사의 사수분께서 도커 안쪽에서 aws의 accessKey, secretKey 설정에 대해 들은게 없다고 하셨었는데
이 에러가 나서 운영중인 서비스 애플리케이션의 컨테이너로 들어가보니까

경로는 ~/.aws 이었다.

안에 보니까 config, credentials 파일이 있었는데

config

credentials

이 두개가 존재했다. 이게 없어서 상단의 오류 메세지를 뿜어냈던 것이다.

728x90

'디버깅' 카테고리의 다른 글

@Async 사용시 에러 해결  (0) 2022.11.04
Jenkins 에러  (0) 2022.08.11
YAML 파일을 읽어보자  (0) 2022.08.09
AbstractMessageConverter  (0) 2022.08.09
728x90

@Valid @Validated 차이

@Valid

@Valid는 JSR-303표준 스펙이다.

org.hibernate.validator.internal.constraintvalidators 안에 구현된

여러 Validator 구현체들로 인해 값을 검증해준다.

이의 핵심은 LocalValidatorFactoryBean 이며, 나는 스프링 부트를

사용하였기 때문에 자동으로 구성이 된다.

동작 원리

기본적으로 컨트롤러에서 @Valid가 없더라도

유효성 검증을 처리하는 로직을 지나간다.
이유는??

InvocableHandlerMethod는 적절한 파라미터 처리기를 찾으려고

HandlerMethodArgumentResolverComposite로 보낸다.

HandlerMethodArgumentResolverComposite

얘가 처리해줄 resolver를 찾는데 getArgumentResolver();

private final Map<MethodParameter, HandlerMethodArgumentResolver>argumentResolverCache = new ConcurrentHashMap<>(256);

이 인자에서 들어있는 RequestResponseBodyMethodProcessor를 통해

RequestResponseBodyMethodProcessor

validation을 진행한다.

RequestResponseBodyMethodProcessor

AbstractMessageConverterMethodArgumentResolver 를 상속받고 있는데

상속받는 이 클래스의 validateIfApplicable에서 어노테이션 for 루프를 돌면서

AbstractMessageConverterMethodArgumentResolver

@Valid가 있는지 검색한다.

있으면 DataBinder객체에 넘겨서 validate를 수행한다.

여기서 검증에 오류가 있으면 MethodArgumentNotValidException이 발생하고,

이는 스프링 ExceptionResolverDefaultHandlerExceptionResolver덕분에

400 에러를 뱉게된다.

@Validated


@Validated (전역 컨트롤러에 붙임)
위의 @Valid와 다르게 cglib 그러니까 AOP기반으로 메소드 요청을

MethodValidationInterceptor가 받아서 처리해준다.

왜 cglib이냐면 SampleController는 일반 클래스이므로

인터페이스처럼 JDK 동적 프록시가 아닌 Cglib proxy를 사용한다.

그리고선 이 프록시가 요청을 가로채서 유효성 검증을 진행해준다.

검증을 수행하고서는 Set<ConstraintViolation<Object>>result;

가 비어있는 값이 아니라면 ConstraintViolationException을 던져주는데

에러 메시지의 기본값은 javax.validation.constraints.XXX.message properties에 정의되어있다.

이는 위처럼 DefaultHandlerExceptionResolver에 등록되어 있는 객체가 아니기에

500에러와 함께 밖으로 뱉어주게 된다. 별도의 ExceptionHandler를 같이 구현해주어야 할것이다.

아래는 내가 구현한 예제 소스이다.

SampleController.java

import javax.validation.Valid;
import javax.validation.constraints.Min;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@Validated
public class SampleController {

    @PostMapping("/hello")
    public String hello(@Valid @RequestBody MessageRequest messageRequest) {
        log.info(messageRequest.getMessage());
        return "hello";
    }

    @GetMapping("/hi")
    public String hi(@Min(value = 1) int value) {
        log.info(String.valueOf(value));
        return "hi";
    }

}

MessageRequest.java

import javax.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class MessageRequest {

    @NotNull(message = "message는 null일 수 없습니다.")
    private String message;

}

SampleControllerTest.java

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.lsj8367.web.request.MessageRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@WebMvcTest(SampleController.class)
class SampleControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext ctx;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
            .alwaysDo(print())
            .build();
    }

    @Test
    @DisplayName("Post @Valid 테스트")
    void test() throws Exception {
        final String obj = objectMapper.writeValueAsString(new MessageRequest());

        mockMvc.perform(post("/hello")
                .content(obj)
                .contentType(MediaType.APPLICATION_JSON_VALUE))
            .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("Get @Validated 테스트")
    void hiTest() throws Exception {
        mockMvc.perform(get("/hi")
                .param("value", "0")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
            )
            .andExpect(status().isInternalServerError());
    }
728x90

'Spring' 카테고리의 다른 글

분산 락  (0) 2023.04.15
@ModelAttribute, @RequestBody 커맨드 객체  (3) 2022.09.15
AOP  (0) 2022.08.10
@ExceptionHandler  (0) 2022.08.10
728x90

예제는 깃허브에 있다.

AOP (Aspect Oriented Programming)

AOP는 스프링의 핵심 구성요소중 하나이다.
관점지향 프로그래밍은 프로그램 구조에 대한 또 다른 사고방식을 제공하며
객체 지향 프로그래밍을 보완해준다.

  • OOP의 모듈화 핵심 단위
    • 클래스
  • AOP의 모듈화 단위
    • 관점 (aspect)

AOP는 횡단 관심사의 분리를 허용해주어 모듈성을 높이는 것을 목표로 하는 패러다임이다.
코드 자체를 수정하지 않고 기존 코드에 추가 동작을 추가해서 수행한다.

개념 그리고 용어

  • Aspect
    • 여러 클래스에 중복되어 있는 관심사의 모듈화
    • 대표적인 예로 트랜잭션 관리가 있다.
    • Spring AOP 에서는 @Aspect를 사용한다.
  • JoinPoint
    • 메소드 실행이나 예외 처리와 같은 프로그램 실행중인 지점
    • AOP에서의 JoinPoint는 항상 메소드 실행을 나타냄.
  • Advice
    • 언제 공통 관심 기능을 핵심 로직에 적용할지를 정의
    • around, before, after 등이 있음.
    • AOP 프레임워크는 관점을 인터셉터로 모델링하고 유지한다.
    • 타깃 오브젝트에 종속되지 않는 순수한 부가기능을 담은 오브젝트
  • PointCut
    • Advice가 이 포인트컷 표현식과 연관되고, 일치하는 모든 조인 포인트에서 실행되게 한다.
    • JoinPoint의 상세 스펙을 정의한 것이다.
    • 스프링은 기본적으로 AspectJ의 pointcut 표현식을 사용한다.
  • Advisor
    • PointCutAdvice를 하나씩 가지고 있는 오브젝트
    • 어떤 기능을 어디에 전달할 것인지를 알고있는 가장 기본이 되는 모듈
    • Spring AOP에서만 사용되는 용어

스프링 AOP의 특징

프록시 패턴 기반의 AOP 구현체, 프록시 객체를 사용하는 이유는 여러개의 부가 기능들을 추가하기 위해서 사용한다.
스프링 빈에만 AOP를 적용할 수 있다.
스프링 IoC와 연동해서 중복 코드, 프록시 패턴 구현의 번거로움, 객체간 복잡도 해결을 진행한다.
결국 프록시 패턴, 데코레이터 패턴에 대한 중복도도 제거하려고 나온것이 스프링 AOP라고 생각한다.

스프링 프록시 방식의 AOP 적용

프록시 방식의 AOP를 적용하려면 최소 아래의 네가지 빈을 등록해야 한다.

  • AutoProxyCreator
  • Advice
  • PointCut
  • Advisor

일반 스프링 프레임워크에서는 설정을 해주려면 xml에 여러가지 설정들을 해주어야 하지만,

부트에서는 build.gradle에 의존성 하나만 추가해주면 자동으로 설정이 된다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop
}

추가적으로 PointCut을 정의할 때에는 위에서 설명했던 것 처럼
AspectJ 표현식을 통해 정의해준다. 자세한건 여기를 통해서 확인할 수 있다.

AOP 어노테이션

모든 어노테이션 뒤에는 AspectJ 표현식을 사용해서 적용할 부분을 정의해준다.

  • @Pointcut
    • AspectJ를 적용할 타겟을 정의해준다.
    • 전체 컨트롤러의 함수대상, 특정 어노테이션을 설정한 함수대상, 특정 메소드 대상 등 적용하기를 원하는 범위를 정의하는 어노테이션
  • @Before
    • 조건 표현식에 정의한 메소드들이 실행되기 전에 수행
  • @AfterReturning
    • 적용된 타깃 메소드가 실행된 후에 수행
  • @Around
    • 타깃 메소드 실행 전, 후 처리 둘다 수행이 가능
    • 사용해줄 때 해당 메소드를 ProceedingJoinPoint로 받아준다.

프록시 팩토리의 기술 선택 방법

  • 대상에 인터페이스가 있으면
    • JDK 동적 프록시, 인터페이스 기반 프록시
  • 대상에 인터페이스가 없으면
    • cglib, 구체 클래스 기반 프록시
  • ProxyFactory의 setProxyTargetClass(true);
    • cglib, 구체클래스 기반 프록시, 인터페이스 여부 상관없음
728x90

'Spring' 카테고리의 다른 글

@ModelAttribute, @RequestBody 커맨드 객체  (3) 2022.09.15
@Valid, @Validated 차이  (0) 2022.08.10
@ExceptionHandler  (0) 2022.08.10
Spring Rest Docs 테스트로 문서화를 해보자!  (0) 2022.08.10

+ Recent posts