Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

끈기 있는 개발 공간

자바와 절차적/구조적 프로그래밍 [스프링 입문을 위한 자바 객체지향의 원리와 이해 Ch2] 본문

Java

자바와 절차적/구조적 프로그래밍 [스프링 입문을 위한 자바 객체지향의 원리와 이해 Ch2]

tenacy 2022. 8. 19. 23:22

자바 즉, 객체 지향 언어 이전에 절차적/구조적 프로그래밍이 존재했습니다. 절차적/구조적 프로그래밍은 자바에 어떤 것을 유산으로 남겨 줬는지 이 장에서 살펴보겠습니다.

 

JVM, JRE, JDK?

 

  • JVM(Java Virtual Machine)
    • 가상의 컴퓨터
  • JRE(Java Runtime Environment)
    • JVM용 운영체제
    • 자바 프로그램 실행기인 java.exe 포함
  • JDK(Java Development Kit)
    • JVM용 소프트웨어 개발 도구
    • 소스 컴파일러인 javac.exe 포함

 

편의를 위해 JDK가 JRE를 포함하고 다시 JRE는 JVM을 포함하는 형태로 배포됩니다. 자바 프로그램의 개발과 구동 과정을 살펴봅시다.

 

 

위 그림에서도 알 수 있듯이 자바는 OS에 독립적입니다. 자바 소스 파일을 윈도우에서 컴파일해도 유닉스나 리눅스에서도 실행 가능합니다. 이런 자바의 특성을 WORA(Write Once Run Anywhere)라고 합니다.

 

프로그램이 메모리를 사용하는 방식

 

  • 코드 실행 영역
    • 이 영역을 공부하면(ex. 어셈블리어) 컴퓨터의 작동 원리 이해에 큰 도움이 됨
    • 로우 레벨(운영체제나 언어 자체 등) 개발자가 아니면 깊게 알 필요 없음
  • 데이터 저장 영역
    • 스태틱 영역
    • 스택 영역
    • 힙 영역

 

 

데이터 저장 영역은 위와 같이 그림으로 그려보면 T처럼 보이기에 앞으로는 T 메모리 구조라고 지칭하겠습니다.

 

Use not goto but function(method)

goto는 사용할 필요가 없으며, 사용해서도 안 됩니다. 이유가 뭘까요?

 

  • 프로그램의 실행 순서가 너무 복잡해질 가능성이 존재하기 때문→ 치명적인 단점
  • 프로그램을 논리적으로 잘 구성하면 완전히 대체할 수 있기 때문→ 100% 대체 가능

 

대신, 구조적 프로그래밍에서는 함수를 사용하면 됩니다.

함수를 사용하면 좋은 점은 다음과 같습니다.

 

  • 중복 코드를 한 곳에 모아 관리 가능
  • 논리를 함수 단위로 분리해서 이해하기 쉬운 코드 작성 가능
  • 지역 변수 사용
    • 공유 사용 시 문제가 발생하기 쉬운 전역 변수의 사용 지양

 

객체 지향 언어에서 절차적/구조적 프로그래밍의 유산은 메서드입니다.

프로그램의 모든 요소인 순서와 제어문이 바로 메서드 안에 전부 담겨 있기 때문입니다. 참고로, 객체 지향 언어에서의 메서드는 절차적/구조적 프로그래밍에서의 함수와 동일합니다. 차이가 있다면 메서드는 클래스 정의 안에만 존재한다는 겁니다.

 

T 메모리로 살펴 보는 main() 메서드

 

Start.class

public class Start {
	public static void main(String[] args) {
		System.out.println("으악!!!");
	}
}

 

위 코드를 실행하면 어떤 일이 일어나는지 알아봅시다. 우선, 다음은 실행하기까지의 과정입니다.

 

  1. JRE가 Start.class에서 main() 메서드의 존재를 확인합니다.
  1. main() 메서드가 존재하면 JRE는 프로그램 실행을 위해 JVM에 전원을 넣어 부팅합니다.
  1. JVM은 목적 파일을 받아 실행합니다.
    1. 전처리 과정을 수행합니다.
      1. JVM은 java.lang 패키지를 T 메모리 스태틱 영역에 가져다 놓습니다.
      1. JVM은 개발자가 작성한 모든 클래스와 임포트 패키지들을 스태틱 영역에 가져다 놓습니다.
    1. 스택 프레임을 스택 영역에 할당합니다({ 을 만날 때마다 스택 프레임이 하나씩 생깁니다. 클래스 정의를 시작하는 여는 중괄호만 빼고).
      1. main 스택 프레임을 스택 영역에 할당합니다.
    1. 메서드 인자들의 변수 공간을 할당합니다.
      1. 메서드의 인자 args를 저장할 변수 공간을 스택 프레임의 맨 밑에 확보합니다.
    1. main() 메서드 안의 첫 명령문을 실행합니다.

 

다음은 [과정 3-d]까지의 그림입니다.

 

 

다음은 실행 후부터의 과정입니다.

 

  1. JVM은 목적 파일을 받아 실행합니다.
    1. main() 메서드의 끝을 나타내는 닫는 중괄호와 만나 main 스택 프레임이 소멸됩니다(여는 괄호로 스택 프레임이 만들어지고 닫는 중괄호로 스택 프레임이 소멸됩니다).
  1. main() 메서드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 운영체제 상의 메모리에서 사라집니다.
  1. T 메모리도 사라집니다.

 

다음은 [과정 3-e]까지의 그림입니다.

 

 

T 메모리의 모든 영역에 존재하는 변수

 

  • 스택 영역 - 지역 변수
    • 스택 프레임 안에서 일생을 보냄
    • 따라서, 스택 프레임이 사라지면 함께 사라짐
  • 스태틱 영역 - 클래스 멤버 변수
    • JVM이 종료될 때 까지 고정된(static) 상태로 그 자리를 지킴
  • 힙 영역 - 객체 멤버 변수
    • 객체와 함께 가비지 컬렉터(힙 메모리 회수기)에 의해 일생을 마치게 됨

 

자바에서 T 메모리 안에 존재하는 서로 다른 메서드의 지역 변수는 참조할 수 없습니다.

짐작하건대, 다음과 같은 이유 때문일 겁니다.

 

  • 메서드는 서로의 고유 공간이니까
  • 자바에서는 개발자가 메모리 관리를 하지 않으니까 즉, C/C++의 포인터가 없으니까

 

전역 변수는 좀… 그렇지만!

전역 변수는 스택 프레임에 독립적입니다.

전역 변수는 스택 영역이 아닌 스태틱 영역에 위치하기 때문이죠. 이는 프로젝트 규모가 커지면 커질 수록 여러 곳에서 전역 변수 값의 변경 가능성이 커진다는 것을 의미합니다. 어느 곳에서 전역 변수 값이 변경되었는지 추적하기 어렵기 때문에 프로젝트 유지 보수도 쉽지 않게 되는 것이죠.

 

그렇다면 값의 변경만 막으면 전역 변수를 사용해도 괜찮을까요? 물론입니다. 앞에서 문제시했던 건 여러 스택 프레임에서의 값의 변경 가능성이었습니다. 하지만 애초에 값이 변경되지 않는 변수라면 굳이 값의 변경을 추적할 필요도 없기 때문에 유지 보수 면에서도 걱정할 필요가 없게 되는 것이죠.

 

멀티 스레드와 멀티 프로세스에서의 T 메모리 (부제: 전역 변수 사용이 위험한 이유)

 

  • 멀티 스레드 환경
    • T 메모리 모델의 스택 영역을 스레드 개수만큼 분할해서 사용
    • 스레드에서 다른 스레드의 스택 영역은 참조할 수 없지만 스태틱 영역과 힙 영역은 공유

 

 

  • 멀티 프로세스 환경
    • 다수의 데이터 저장 영역, 즉 다수의 T 메모리를 가짐
    • 하나의 프로세스가 다른 프로세스의 T 메모리 영역을 절대 침범할 수 없음
    • 메모리 사용량이 큼

 

 

public class Start6 extends Thread {
    static int share;

    public static void main(String[] args) {
        Start6 t1 = new Start6();
        Start6 t2 = new Start6();

        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        for (int count = 0; count < 10; count++) {
            System.out.println(share++);

            try { sleep(1000); }
            catch (InterruptedException e) {}
        }
    }
}

 

전역 변수 share를 스레드 t1에서 10번, 스레드 t2에서 10번 증가시키므로 마지막으로 출력된 share는 19이어야 할 겁니다. 과연 그럴까요?

 

1
0
2
3
4
5
6
7
9
8
10
11
12
13
14
14
15
16
17
18

Process finished with exit code 0

 

틀렸습니다. 전역 변수를 사용하게 되면 이와 같이 예측할 수 없는 결과를 초래하게 됩니다. T 메모리 관점에서 이와 같은 결과가 나오게 된 이유를 스스로 짐작해봅시다.

 

정리

객체 지향은 절차적/구조적 프로그래밍과 완전히 다른 프로그래밍 패러다임이라고 생각할 수 있지만 연산자, 제어문, 메모리 관리 체계 등등 많은 부분을 차용하고 있습니다. 그래서 객체 지향 프로그래머라고 할 지라도 절차적/구조적 프로그래밍 기법도 잘 알고 있어야 합니다. 그 중심에는 함수가 있다는 걸 기억합시다.

 

Comments