728x90

반효경 교수님의 운영체제 강의를 정리하는 포스팅
System Structure & Program Execution 1

📌 컴퓨터 시스템 구조

컴퓨터 시스템의 구조는 아래와 같다.

이미지 출처 - https://asfirstalways.tistory.com/115

컴퓨터 시스템에는 크게 중앙 처리장치인 CPU, 메모리, 그리고 외부 장치들인 디스크, 키보드 등등
으로 구분된다.
컴퓨터는 외부에서부터 데이터를 읽어와 연산을 한 후에 다시 출력해주는 방식으로 처리한다.

📌 CPU

CPUMemory에 올라간 프로그램들의 명령들을 하나하나 읽어들여 수행하는 역할을 담당한다.

I/O마저도 이 CPU가 관리하게 되면, 너무나도 많은 인터럽트가 발생하게 된다.

이런 경우에는 CPU가 효율적이지 못하다고 할 수 있다. (오버헤드가 너무 큼)
그래서 이것을 방지하게 나오는 것이 바로 DMA(Direct Memory Access) Controller를 사용한다.

접근 범위

CPU가 접근 가능한 곳은 메모리와 Local Buffer이다.

Interrupt Line

CPU는 자기가 처리하던 연산 중간에 인터럽트가 발생하게 되면, 하던일을 두고
인터럽트에 관련된 업무를 먼저 처리한다.

Mode Bit

운영체제에는 두가지의 모드가 존재하는데,

  1. 유저모드
  2. 커널모드

두가지 모드가 존재하는 이유는, I/O장치들을 보호하기 위해서이다.
모든걸 조작해서 악의적인 프로그램을 만들어서 I/O 장치에 접근할 수 없게하고, 운영체제를 통해서만
I/O를 수행할 수 있게 하는것.

  • 유저모드
    • 어플리케이션이 실행되는 영역
  • 커널모드
    • 프로그램들이 잘 수행되다가 인터럽트가 발생되어 운영체제가 호출되어 수행되는 영역

프로세스가 사용자 모드에서 작업을 수행하다 중요한 작업을 수행해야 할 경우에는 System Call(소프트웨어 인터럽트)을 통해 운영체제에게 서비스를 대신해 줄 것을 요청하게 된다.
그러면 CPU의 제어권은 다시 운영체제로 넘어가게 되고 인터럽트가 발생할 때에는 모드 비트가 자동적으로 0(커널모드)으로 세팅되어 필요한 작업을 수행하고 요청된 작업이 끝나게 되면 모드 비트는 다시 1(유저모드)로 만들어 사용자 프로그램에게 CPU를 넘겨주게 된다.

📌 DMA (Direct Memory Access) Controller

DMA 컨트롤러는 Local Buffer에 저장된 데이터들을 메모리로 복사하는 작업이 완료 되면,
그때만 CPU에게 인터럽트를 발생시킨다.

📌 Memory Controller

메모리 컨트롤러는 현재 위의 구조대로면, CPU와 DMA 컨트롤러가 서로 메모리에 접근이 가능하다.

그래서 만약 CPU, DMA가 동시에 접근하는 경우 데이터의 일관성이 깨질 수 있기 때문에

서로의 사용을 분배해주는게 바로 이 메모리 컨트롤러이다.

📌 Device Controller

이 컨트롤러는 해당 I/O를 관리하는 작은 CPU개념이다.
제어 정보를 위해 control, status register를 가진다.
Local Buffer(실제 데이터 저장)를 가진다.
I/O는 Device와 Local Buffer 사이에서 일어난다.
I/O가 끝난 경우에는 CPU에게 인터럽트로 알리게 된다. (DMA가 존재하면 DMA Controller)

Local Buffer

디바이스 컨트롤러가 데이터를 임시로 저장하기 위한 작업 공간.

Device Driver도 있는데 이는 CPU가 실행하는 각 디바이스들에 접근하기 위한 소프트웨어이다.

📌 Timer

무한 루프문을 돌게되는 어떤 프로그램만 CPU를 독점하는 상황이 생길 수 있다.
이럴 때를 대비해서 만든 것이 바로 타이머이다.
타이머는 특정 프로그램이 CPU를 독점하는 것을 막아주는 역할을 수행한다.
컴퓨터를 처음 시작하면 운영 체제가 CPU를 가지고 있다가 사용자 프로그램에게 CPU를 넘겨준다.
이 때, 그냥 넘겨 주지 않고 타이머 값을 설정하고 넘겨준다.
어떤 프로그램이 설정된 Timer의 값이 0이 되었을 때 타이머 인터럽트가 발생하여 다른 프로그램에게 CPU를 넘겨준다.

📌 Interrupt

CPU가 한개의 작업밖에 수행할 수 없는데,
하나의 작업을 수행중에 I/O가 발생하거나, 다른 우선 순위가 급한일이 생기게 되면 이 인터럽트가 발생된다.
키보드에서 'a'라는 키를 누르게 되면, 이 키의 코드값이 Local Buffer에 저장되고 인터럽트가 발생해서
처리하고 있던 작업을 인터럽트가 발생하기 직전까지의 정보를 저장(여기가 바로 PCB) 하는 인터럽트 처리 루틴을 수행한다.
인터럽트는 소프트웨어, 하드웨어 인터럽트 2개가 존재한다.

소프트웨어 인터럽트

돌다가 운영체제에게 대신 해달라고 요청할 경우 인터럽트를 발생시킬 수 있음
종류 - 예외 상황, System Call

하드웨어 인터럽트

하드웨어가 발생시키는 인터럽트로, CPU가 아닌 다른 하드웨어 장치가 cpu에 어떤 사실을 알려주거나 cpu 서비스를 요청해야 할 경우 발생시킨다.
ex - I/O완료 인터럽트 발생 (하드웨어 인터럽트)

인터럽트 벡터

인터럽트 처리 루틴 주소를 알고 있다. 종류마다 그 인터럽트가 발생하면 어디있는 함수를 실행하는지

인터럽트 처리 루틴

인터럽트 처리 루틴을 통해 해당하는 인터럽트 처리를 완료하고 나면 원래 수행하던 작업으로 돌아갈 위치를 알아야 하고,
인터럽트 처리 전에 수행 중이던 작업이 무엇이었는지 반드시 저장해야 한다.
그래서 운영 체제는 PCB라는 공간을 별도로 가지고 있다.

📌 System call

사용자 프로그램이 운영체제의 서비스를 받기 위해 커널 함수를 호출
모든 입출력 명령은 운영 체제만 사용할 수 있는 특권 명령으로만 가능하다. 그래서 사용자 프로그램은 이 시스템 콜을 활용한다.

728x90

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

운영체제 3강  (0) 2022.08.22
운영체제 2강 - 2  (0) 2022.08.18
운영체제 2강 - 1  (2) 2022.08.11
프로세스 상태  (0) 2022.08.10
728x90

자바 변성 (Variance)

자바의 가변성에는 크게 공변, 무공변, 반공변이 존재한다.
제네릭을 잘 사용하려면 이 가변성에 대한 이해가 필요하다.

변성을 제대로 이해하려면 "타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입인가?" 라는 질문에서 시작하는게 좋다.

배열은 공변, 제네릭은 무공변이 기본이라고 다들 알고 있을 것이다.

무공변 (Invariance) or 불공변

기본적으로 제네릭은 무공변이다.

무공변이라고 하니 헷갈리는것 같다. 사전적으로 번역해보면 불공변으로 나오게 된다.

타입 S가 T의 하위 타입일 때, Box[S]와 Box[T] 사이에 상속 관계가 없는 것

쉽게 말하면 너는너, 나는 나 인 느낌이다.
그래서 선언한 유형만 들어갈 수 있게 코드를 구성할 수 있다.

Object에는 Object만, String에는 String만 들어갈 수 있단 얘기이다.

void invariance() {
    // 제네릭은 기본적으로 무공변
    List<Object> objectList = new ArrayList<>();
    List<String> stringList = new ArrayList<>();

    objectList.add(1);
    objectList.add(1.0);

    stringList.add("aaaaa");
}

공변

공변(covariance)는 타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입 임을 나타내는 개념

@Test
void arrayTest() {
    Object[] arr = new Long[5]; //배열에서는 공변이고, Long은 Object의 하위타입이기에 할당이 가능하다.
    arr[0] = "arr"; //공변으로 인해 선언한 arr은 Object로 참조가 된상태라 String도 할당 가능.
    // 여기서 런타임에 ArrayStoreException 발생
}

자바에서 이 배열을 공변으로 열어두지 않았다면, 다형성의 이점을 살릴 수 없게 됐을 수 있다.

Arrays.swap()

Arrays의 메소드를 하나를 가져와봤는데,
만약 공변이 아니었다면, 이 배열 스왑 메소드는 객체별로 전부 구현해주어야 했을 것이다.
제네릭이 있기전엔 형변환에 대한 에러가 나더라도,
다형성의 장점으로 얻을 수 있는 이득이 많았을 것 같다.

리스트의 공변

void variance() {
    List<? extends Object> list = new ArrayList<Number>();
    list.add(1); //컴파일 에러
    list.add(1.0); //컴파일 에러
    list.get(0); // 정상 로직
}

이처럼 선언을 했을때 add는 선언된 제네릭으로 변수를 넣게 되어있는데,

무공변으로 만들었을 경우

공변인 경우

위와 같은 경우에는 capture of ? extends Object e Object의 하위타입은 맞지만,

어떤 타입인지는 모른다? 라는 뜻이라고 생각된다.

그래서 list.get(0)이 Object로 형변환은 가능하지만, 반대로 add()를 통해 null을 제외한 무언가를 추가해줄 수는 없다는 소리이다. 안에 들어가는 객체가 정확하게 뭔지 모르기 때문이다.

그래서 정확한 타입이 어떤건지는 모르기 때문에 개발자가 null을 제외하고는 아무것도 추가하지 못하게 막을 수 있다라고 봐도 될 것 같다.
그래서 자주쓰던 Collections 클래스의 UnmodifiableList를 찾아보게 되었다.

생성자에 이런식으로 공변을 이용해서 막아주고 있는것을 볼 수 있었다.
그러면서 List의 구현체이기 때문에 밖에서는 add에 어떤 값을 넣어줄 수는 있기에, 그대로

Override로 재정의 한 뒤에 Exception을 던져주게 만든것을 확인할 수 있었다.

반공변(Contravariance)

반공변 처음 봤을때 반만 된다 이런생각을 했었다.ㅋㅋㅋㅋㅋㅋㅋ

그게 아니라 공변의 반대

타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 상위 타입 임을 나타내는 개념입니다.

@Test
void contravariance() {
    List<? super Number> list = new ArrayList<>();
    list.add(1.0);

    final Number number = (Number) list.get(0);
    final Object object = list.get(0);
}

Number를 포함한 Number의 상위 타입들만 들어갈 수 있게 설정한 상태이다.

아까는 하위타입이 뭔지 알 수가 없다는 것이었는데,

이 코드는 Number 상위인건 알겠는데 상위 누구인지를 알 수 없는 상태이다.

super키워드 다음에 붙은 클래스까지의 형은 전부 넣을 수 있다는 소리와도 같다.

다시말하면, 최소 Number 타입은 보장이 된다는 소리와 같다.
그래서 list.get(0); 에서 최상 타입인 Object로 꺼내서

형에 맞는 캐스팅 or instanceof를 통해 값을 읽어오는게 가능하다.

마무리

이렇게 자바의 가변성에 대해 알아보았다.
얼추 정리되면서 감은 잡은것 같다.
PECS(Producer Extends Consumer Super)를 보면서,
일반적으로 소비(Consume)라는게 스타크래프트의 디파일러가 저글링을 컨슘해서 저글링을 잡아먹기때문에,

스타크래프트의 컨슘

어떤 컬렉션이 만들어지는 과정이 컨슘이라고 생각하고 값을 빼내는 과정(get)이 동작한다고 알고 있었다.
반대로 생산자(Producer)는 말그대로 생산이기에 값을 생성해주는(new) or 더해주는(add) 것이 생산자로 알고 있었다.
반대로 알고있던 것이다.

올바른 내용

컬렉션을 뒤져서 어떤 작업들을 처리 해주어야 한다면 그게 바로 컬렉션 값을 빼내(get) 뭔가를 만들기 때문에 생산자가 되어 extends를 사용해야 한다는 것이고,

컬렉션에 값을 추가해야되면 매개변수로 주어진 값이 소비되어 컬렉션에 들어가니(add) 소비자 관점이라고 보는것 같다.

그래서 이 경우에는 super를 사용해주면 되겠다.

휴..되게 어렵다 😇😇😇

아무튼 읽기전용으로 만들고 싶을때에는 extends를 사용하는것.

좀더 안전하게 데이터 삽입을 하고싶다면 super를 사용하는 것만 기억하면 될 것 같다.

728x90

'Java' 카테고리의 다른 글

참조 유형  (0) 2022.09.12
Checked Exception, Unchecked Exception  (0) 2022.09.07
일급 컬렉션  (0) 2022.08.09
변수  (0) 2022.08.07
728x90

Jenkins

위키백과에서 발췌한 내용에 따른다.
젠킨스(Jenkins)는 소프트웨어 개발 시 지속적 통합(continuous integration) 서비스를 제공하는 툴이다. 다수의 개발자들이 하나의 프로그램을 개발할 때 버전 충돌을 방지하기 위해 각자 작업한 내용을 공유 영역에 있는 Git등의 저장소에 빈번히 업로드함으로써 지속적 통합이 가능하도록 해 준다. MIT 라이선스를 따른다.

발생 시점

현재의 회사에서 배포를 젠킨스를 이용하여 배포를 진행한다.
신규 기능개발과 레거시를 청산하는 작업을 주로 해왔었어서 이쪽을 고치는게 우선은 아니었다.
그래서 모르고 있었던 것일수 있다.
에러 상황을 확인해보자

에러 상세

[Pipeline] End of Pipeline
java.lang.InterruptedException
    at java.base/java.lang.Object.wait(Native Method)
    at java.base/java.lang.Thread.join(Thread.java:1300)
    at java.base/java.lang.Thread.join(Thread.java:1375)
    at java.base/jdk.internal.reflect.GeneratedMethodAccessor774.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at org.codehaus.groovy.runtime.InvokerHelper.invokePojoMethod(InvokerHelper.java:913)
    at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:904)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodN(ScriptBytecodeAdapter.java:168)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodNSafe(ScriptBytecodeAdapter.java:176)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethodNSpreadSafe(ScriptBytecodeAdapter.java:183)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeMethod0SpreadSafe(ScriptBytecodeAdapter.java:198)
    at org.hidetake.groovy.ssh.interaction.Interactions.waitForEndOfStream(Interactions.groovy:97)
    at org.hidetake.groovy.ssh.interaction.Interactions$waitForEndOfStream$2.call(Unknown Source)
    at org.hidetake.groovy.ssh.operation.Command.execute(Command.groovy:83)
    at org.hidetake.groovy.ssh.operation.Operation$execute$0.call(Unknown Source)
    at org.hidetake.groovy.ssh.session.execution.Command$Helper.execute(Command.groovy:50)
    at jdk.internal.reflect.GeneratedMethodAccessor951.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite$StaticMetaMethodSiteNoUnwrapNoCoerce.invoke(StaticMetaMethodSite.java:151)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.call(StaticMetaMethodSite.java:91)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:141)
    at org.hidetake.groovy.ssh.session.execution.Command$Trait$Helper.execute(Command.groovy:30)
    at org.hidetake.groovy.ssh.session.execution.Command$Trait$Helper$execute$0.call(Unknown Source)
    at org.hidetake.groovy.ssh.session.SessionHandler.execute(SessionHandler.groovy)
    at jdk.internal.reflect.GeneratedMethodAccessor949.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoCachedMethodSite.invoke(PogoMetaMethodSite.java:169)
    at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.call(PogoMetaMethodSite.java:71)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:133)
    at org.hidetake.groovy.ssh.session.execution.Command$Trait$Helper.execute(Command.groovy)
    at org.hidetake.groovy.ssh.session.execution.Command$Trait$Helper$execute.call(Unknown Source)
    at org.hidetake.groovy.ssh.session.SessionHandler.execute(SessionHandler.groovy)
    at jdk.internal.reflect.GeneratedMethodAccessor948.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:384)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:69)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:166)
    at org.jenkinsci.plugins.sshsteps.SSHService$_executeCommand_closure3$_closure13.doCall(SSHService.groovy:182)
    at org.jenkinsci.plugins.sshsteps.SSHService$_executeCommand_closure3$_closure13.doCall(SSHService.groovy)
    at jdk.internal.reflect.GeneratedMethodAccessor947.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
    at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:42)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:57)
    at org.hidetake.groovy.ssh.util.Utility.callWithDelegate(Utility.groovy:17)
    at jdk.internal.reflect.GeneratedMethodAccessor427.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.invoke(StaticMetaMethodSite.java:46)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.callStatic(StaticMetaMethodSite.java:102)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:214)
    at org.hidetake.groovy.ssh.session.SessionTask.wetRun(SessionTask.groovy:64)
    at jdk.internal.reflect.GeneratedMethodAccessor6430.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoCachedMethodSiteNoUnwrapNoCoerce.invoke(PogoMetaMethodSite.java:210)
    at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.callCurrent(PogoMetaMethodSite.java:59)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:158)
    at org.hidetake.groovy.ssh.session.SessionTask.call(SessionTask.groovy:48)
    at java_util_concurrent_Callable$call.call(Unknown Source)
    at org.hidetake.groovy.ssh.core.Service.run(Service.groovy:81)
    at org.hidetake.groovy.ssh.core.Service$run$1.call(Unknown Source)
    at org.jenkinsci.plugins.sshsteps.SSHService.executeCommand(SSHService.groovy:177)
    at org.jenkinsci.plugins.sshsteps.steps.CommandStep$Execution$CommandCallable.execute(CommandStep.java:84)
    at org.jenkinsci.plugins.sshsteps.util.SSHMasterToSlaveCallable.call(SSHMasterToSlaveCallable.java:32)
    at hudson.remoting.LocalChannel.call(LocalChannel.java:46)
    at org.jenkinsci.plugins.sshsteps.steps.CommandStep$Execution.run(CommandStep.java:72)
    at org.jenkinsci.plugins.sshsteps.util.SSHStepExecution.lambda$start$0(SSHStepExecution.java:84)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:829)
Finished: ABORTED

정확히 이 에러 윗부분까지는 클라이언트의 요청을 받아서 처리해주고 있다가
한방에 서버가 다운되어버렸다.
젠킨스에서의 배포 스크립트에도 문제가 있었다.
아래의 쉘 스크립트를 젠킨스가 도커 컨테이너 내부에서 실행해주게 만들었다.

#!/bin/bash
export JAVA_TOOL_OPTIONS="-Dfile.encoding='UTF8' -Duser.timezone=Asia/Seoul"
kill -9 $(lsof -t -i:8080)
cd /root/server/
git pull
git checkout $1
git pull origin $1
kill -9 $(lsof -t -i:8080)
mvn clean package -P dev

java -jar -Dspring.profiles.active=dev target/server-0.0.1-SNAPSHOT.jar

이러한 구성으로 된 쉘 스크립트를 통해 실행을 진행했기 때문에
jar를 즉각 실행하게 만들어서 로그가 그대로 젠킨스에 전부 찍히고,

왜? 🤔

그야 당연할것인데, 로그를 실시간으로 젠킨스가 배포 과정을 찍을텐데,

java -jar 명령어를 백그라운드로 실행시켜주지를 않았다.

젠킨스의 배포는 항상 finished상태가 나오질 않는 상태였다.

항상 이상태였다 ㅋㅋㅋ

이미지

gif 처음만들어봤는데 재밌네..
이부분에서 로그가 과다하게 많이 쌓이게 되어 에러를 내뱉고
was가 죽어버린 상태가 되어버렸다. (비정상적 셧다운)

해결

JENKINS-45150 large console logging can take Jenkins down or affecting performance - Jenkins Jira
검색을 진행해보니 위와같은 내용들도 얻을 수 있었다.
그래서 해결과정의 순서를 생각한 방식은 다음과 같다.

  1. 젠킨스는 배포를 끝내서
    이러한 화면을 만들어주어야 한다.

  2. 스프링 애플리케이션의 배포 스크립트를 바꿔주어야 한다. (java -jar를 백그라운드로)

이렇게 하면 되겠다!

그래서 전부 바꿔주게 된다.

#!/bin/bash
export JAVA_TOOL_OPTIONS="-Dfile.encoding='UTF8' -Duser.timezone=Asia/Seoul"
kill -15 $(lsof -t -i:8080)
cd /root/server/
git pull
git checkout $1
git pull origin $1
kill -15 $(lsof -t -i:8080)
mvn clean package -P dev

nohup java -jar -Dspring.profiles.active=dev target/server-0.0.1-SNAPSHOT.jar > ~/app.log 2>&1 &

echo "Deploy Success"

nohup을 이용한 중단없이 실행해주고 젠킨스는 밖으로 빠져나와야 했기 때문에
이 명령어를 선택하고 실행해주었다.

그와 동시에 간단하게 라이브로 볼수있게끔 기본적으로 생성되는 nohup.out을 혹시몰라 만들어둔채로 마무리를 해놓았다.

그리고 kill명령어를 15로 바꾸었는데,
9는 강제종료기 때문에 진행중이던 작업을 즉시 종료하고 데이터도 저장하지 않는다.
15는 자신이 하던 작업을 모두 안전하게 종료하는 절차를 밟는다.
메모리상에 있는 데이터와 각종 설정/환경 파일을 안전하게 저장한 후 프로세스를 종료한다.
15로 한다고 한들, 종료 명령어를 주게되면, 어떤 클라이언트가 요청을 보내서 작업중인 데이터도 끊어질 것이다.
그래서 spring에서 제공하는 graceful shutdown을 적용하고 kill -15를 같이 붙여주었다.
graceful shutdown은 지금 포스팅에서 다루지 않겠다.
이렇게 해서 젠킨스의 로그 과다 적재로 서버가 죽는 현상을 제거시키게 되었다.

정리

이렇게 되어 젠킨스에 기존에 (내가 건드리지 않은) 잘못되게 설정되어 있던 것을 고치게 되니
괜찮다.
그러면서 동시에 툴도 툴마다의 각자의 할일이 있는 것인데,
CI/CD를 위한 툴에서 로그 모니터링까지 하고 있었으니 과다적재로 에러를 뱉는다는 것은
어찌보면 당연한 것이었을 수 있다고 나는 생각한다.🔥

728x90

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

FeignClient Logging level 디버깅  (0) 2022.12.17
@Async 사용시 에러 해결  (0) 2022.11.04
AWS SNS 토큰 에러  (0) 2022.08.10
YAML 파일을 읽어보자  (0) 2022.08.09
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

+ Recent posts