본문 바로가기

스터디/etc

스프링 입문을 위한 자바 객체지향의 원리와 이해

  • JDK(Java Development kit) - 자바 개발 도구(JVM용 소프트웨어 개발 도구)
  • JRE(Java Runtime Environment) - 자바 실행 환경(JVM용 OS)
  • JVM(Java Virtual Machine) - 자바 가상 기계(가상의 컴퓨터)

JDK를 이용해 개발된 프로그램은 JRE에 의해 가상의 컴퓨터인 JVM상에서 구동된다.
JDK는 자바 소스 컴파일러인 javac.exe를 포함
JRE는 자바 프로그램 실행기인 java.exe를 포함

이러한 구조를 택한 이유는 기존에 각 플랫폼용으로 배포되는 설치 파일을 따로 준비해야 했던 불편함을 없애기 위해서이다.

자바 개발자는 본인이 사용 중인 플랫폼에 설치된 JVM용으로 프로그램을 작성하고 배포하면 각 플랫폼에 맞는 JVM이 중재자로서 각 플랫폼에서 프로그램을 구동시키는데 문제없게 만들어준다.

이러한 자바의 특성을 WORA -> Write Once Run Anywhere 라고 한다.

    코드 실행 영역                         Static 영역
       Stack 영역        Heap 영역

객체 지향 프로그램의 메모리 사용 방식

스태틱 영역은 "클래스들의 놀이터"이다.

스택 영역은 "메서드들의 놀이터"이다.

힙 영역은 "객체들의 놀이터"이다.

public class Example1 {

    public static void main(String[] args) {
        System.out.println("Hello OOP!!!");
    }
}

코드 실행 시 메모리의 변화

  1. JRE는 먼저 main 메서드를 찾는다.
  2. Example1 클래스에서 main () 메서드를 발견하면 JRE는 프로그램 실행을 위한 사전 준비에 착수한다.
    1. JVM에 전원을 넣어 부팅하고 목적파일을 받아 목적 파일을 실행한다. Example1.class 파일
    2. 모든 자바 프로그램이 반드시 포함하게되는 java.lang 패키지를 스태틱영역에 가져다 놓는다.
    3. 다음으로 JVM은 개발자가 작성한 모든 클래스와 임포트 패키지 역시 스태틱영역에 가져다 놓는다.
  3. main () 메서드가 스택 프레임이 스택영역에 할당된다. 여는 중괄호 " { "를 만날 때마다 스택 프레임이 하나씩 생긴다. 클래스 정의를 시작하는 여는 중괄호는 제외한다.
  4. main 메서드의 인자 args를 저장할 변수 공간을 스택 프레임의 맨 밑에 확보한다. 즉 메서드 인자들의 변수 공간을 할당하는 것이다.
  5. 이후 Hello OOP!가 출력되고 main 메서드의 닫는 중괄호를 만나 스택 프레임이 소멸된다.
  6. 그리고 main 메서드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 운영체제 상의 메모리에서 사라진다.
public class Example2 {

    public static void main(String[] args) {
        int i = 10;
        double d = 20.0;
    }
}

다음과 같은 코드에서는 위에서 살펴본 실행과정을 거친후 i 변수를 위한 공간을 main 메서드 스택 프레임 안에 밑에서부터 차곡차곡 마련한다.

위의 코드를 실행하면

main 스택 프레임에는 다음과 같이 메모리가 할당된다

d 20.0 (8바이트)
i 10 (4바이트)

args

main () 메서드안에 if 문이 있고 그것이 참일 경우

main () 메서드의 스택 프레임 안에 if 블록 스택 프레임이 중첩되어 생성된다.

"외부 스택 프레임에서 내부 스택 프레임의 변수에 접근하는 것은 불가능하나 그 역은 가능하다"

지역 변수는 스택 영역에서 일생을 보내다가 해당 스택 프레임이 사라지면 함께 사라진다.

클레스 멤버 변수는 스태틱 영역에서 한번 자리 잡으면 JVM이 종료될 때까지 고정된(static) 상태로 자리를 지킨다.

객체 멤버 변수는 힙에서 일생을 보낸다. 객체 멤버 변수들은 객체와 함께 가비지 컬렉터라고 하는 힙 메모리 회수기에 의해 일생을 마친다.

아무도 참조해주지 않을 때 객체를 쓰레기로 인지하고 가비지 컬렉터가 수거해가는데 가비지 컬렉터가 언제 오는지는 신만이 아신다고들 말한다.

다른 메서드에서 main 메서드의 지역 변수를 왜 참조 할 수 없을까?

뚜렷한 이유는 모르지만 짐작하자면 아래와 같다.

첫째, 그것이 이치에 맞기 때문. 메서드는 서로의 고유 공간인데 서로 침범하면 무단침입으로 문제를 유발할 수 있다.

둘째, 포인터 문제. 다른 메서드의 지역변수에 접근하려면 그 지역변수의 위치를 명확히 알아야 하는데, 명확히 알기 위해서는 포인터를 사용해야 한다. 자바가 가장 환영받은 이유중 하나는 포인터가 없다는 것이다.

읽기 전용으로 값을 공유해서 전역 상수로 쓰는 것은 적극 추천한다.

멀티 스레드의 메모리 사용 방식은 스택 영역을 스레드 개수만큼 분할해서 쓰는 것이다.

스태틱 영역
스택 영역 힙 영역
스레드 스레드

멀티 프로세스는 다수의 데이터 저장 영역을 갖는 구조이다.

스태틱 영역
스택 
스태틱 영역
스택
스태틱
스택

멀티 프로세스는 각 프로세스 마다 각자의 메모리가 있고 각자 고유의 공간이므로 서로 참조할 수 없다.

멀티 스레드는 하나의 메모리만 사용하는데 스택 영역만 분할해서 사용하기 때문에 스태틱 영영과 힙 영역은 공유해서 사용하는 구조이다.

멀티 프로세스는 서로 참조할 수 없기 때문에 안전한 구조이지만 메모리 사용량은 그만큼 크다.

멀티 스레드는 멀티 프로세스 대비 메모리를 적게 사용할 수 있는 구조다.

서블릿은 요청당 스레드를 생성한다. 위의 내용을 이해 했다면 요청당 스레드를 생성하는 것이 요청당 프로세스를 생성하는 것이 더 효율적인지 이해할 수 있다.

멀티 스레드에서 전역 변수를 사용하면 스레드의 안전성이 깨진다. 물론 이를 보완하는 방법으로 lock을 거는 방법이 있지만 락을 거는 순간 멀티 스레드의 장점은 버린 것과 같다.

자바와 객체 지향

클래스와 객체의 관계는 붕어빵틀과 붕어빵이 아니다.
붕어빵틀 붕어빵 = new 붕어빵틀();

이는 클래스와 객체의 관계가 아니다.
붕어빵틀은 붕어빵을 만드는 팩터리이다.

객체 지향의 4대 특성 - 캡! 상추다

  • 캡: 캡슐화(Encapsulation): 정보은닉(infomation hiding)
  • 상: 상속(inheritance): 재사용
  • 추: 추상화(Abstraction): 모델링
  • 다: 다형성(Polymorphism): 사용 편의

추상화는 모델링이다.

지구본은 지구를 모델링 한 것이지만 지구의 크기 등을 정확하게 표현하고 있지는 않다.
이처럼 모델은 실제 사물을 정확히 복제하는 게 아니라 목적에 맞게 관심 있는 특성만을 추출해서 표현하는 것이다. 다시 말해 모델은 추상화를 통해 실제 사물을 단순하게 묘사하는 것이다.

이런 모델링(추상화)는 객체 지향에서 클래스를 설계할 때 필요한 기법이고 또한 데이터베이스 테이블을 설계할 때 필요한 기법이다.

클래스 설계를 위해서는 애플리케이션이 어디에서 쓰이는 지, 목적이 뭔지 부터 정해야 한다.

객체 지향에서 추상화의 결과는 클래스다.

추상화의 개념을 넓게 본다면 상속을 통한 추상화, 인터페이스를 통한 추상화, 다형성을 통한 추상화도 포함된다.

static 멤버 변수(클래스 멤버 변수)에 대해

쥐 미키마우스 = new 쥐();
쥐 제리 = new 쥐();

쥐 클래스의 객체인 미키마우스와, 제리를 선언했을 때 다음과 같은 질문을 해보자.

미키마우스의 꼬리는 몇 개인가?
제리의 꼬리는 몇 개인가?
쥐의 꼬리는 몇 개인가?

모든 질문의 답은 한 개다.

꼬리 개수는 객체의 속성이지만 모든 객체가 같은 값을 가지고 있기에 클래스를 통해 질문해도 하나라는 답을 알 수 있다.

모두 같은 값을 갖는 꼬리 속성을 인스턴스 멤버로 갖는다면 Mouse 객체 수만큼 아까운 메모리를 잡아 먹는다.

static 메소드는 언제 사용해야 할까?

일단 main 메서드는 당연히 정적 메서드여아 한다. 프로그램을 실행하자마자 바로 실행되어야 하는데 main 메서드가 static 메소드가 아니라면 바로 실행할 수 없다.

실무에서는 클래스의 인스턴스를 만들지 않고 사용하게 되는 유틸리티성 메서드를 주로 정적 메서드로 구성한다.

지역 변수는 값을 초기화해주지 않으면 쓰레기 값을 갖는데 멤버 변수는 값을 자동으로 초기화 해준다. 왜일까?

지역 변수는 한 지역에서만 쓰는 변수이지만 멤버 변수는 공유 변수의 성격을 가지고 있기 때문이다.

멤버 변수는 딱히 누가 초기화해야 한다고 규정할 수 는 없다. 하지만 지역 변수는 한 지역에서만 사용되고 소멸되는 변수이기에 그 지역에서 초기화하는 것이 논리적으로 맞다.

상속: 재사용 + 확장

상속은 상위 클래스의 특성을 하위 클래스에서 상속하고 거기에 더해 필요한 특성을 추가, 즉 확장해서 사용할 수 있다는 의미이다.

다중 상속의 문제점은 인어 클래스를 통해 생각해볼 수 있다.

인어 클래스가 사람 클래스와 물고기 클래스를 다중 상속 했다면 사람도 수영할 수 있고 물고기도 수영할수 있는데 인어는 사람처럼 수영해야 할까? 물고기처럼 수영해야 할까?

이와 같은 문제를 다중 상속의 다이아몬드 문제라고 한다.

상위 클래스는 물려줄 특성이 풍성할수록 좋고,인터페이스는 구현을 강제할 메서드의 개수가 적을수록 좋다

다형성: 사용 편의성

다형성의 기본은 오버라이딩(overriding)과 오버로딩(overloading)이다.

물론 상위 클래스와 하위 클래스에서도 다형성을 이야기할 수 있고 인터페이스와 구현 클래스 사이에서도 다형성을 이야기 할 수 있다.

자바가 확장한 객체 지향

instanceof 연산자는 만들어진 객체가 특정 클래스의 인스턴스인지 물어보는 연산자이다.

instanceof 연산자가 강력하기는 하지만 객체 지향 설계 5원칙 가운데 LSP(리스코프 치환 원칙)을 어기는 코드에서 주로 나타나는 연산자이기에 코드에 instanceof 연산자가 보인다면 냄새나는 코드가 아닌지, 즉 리팩토링의 대상이 아닌지 점검해 봐야 한다.

인터페이스에서 디폴트 메서드라고 하는 객체 구상 메서드와 정적 추상 메서드를 지원할 수 있게 바뀌었다.

디폴트 메서드가 추가됨으로써 추상 클래스와의 차이점은?

추상 클래스는 공통의 인스턴스 변수를 가질 수 있다. 상속은 한개 뿐이지만 인터페이스는 여러개 구현이 가능한데 디폴트 메소드를 포함하는 인터페이스를 여러개 구현이 가능하다는 것은 자바에서도 다중 상속을 지원하게 되었다는 뜻이다.

디폴트 메서드를 통해 비어있는 구현을 했다면 기존의 구현을 강제했던 인터페이스에 비해 쓸데없이 빈 코드가 사라진다.

동작 다중 상속 중복되지 않는 최소한의 인터페이스를 유지한다면 코드에서 동작을 쉽게 재사용하고 조합할 수 있다.

**객체 메서드를 호출할 때 객체명.객체메서드명()이 아닌 클래스명.객체메서드명(this.객체이름)로 호출된다.

하나의 클래스에 객체가 여러개 있을 때 그 객체의 메서드의 내용은 똑같고 객체 멤버의 속성값만 다를 뿐이다.

똑같은 객체 멤버 메서드를 힙영역에 객체 수대로 만든다는 것은 심각한 메모리 낭비이다.그래서 JVM은 지능적으로 객체 멤버 메서드를 스태틱 영역에 하나만 보유한다.

그리고 클래스명.메서드명에 인자로 this 객체 참조 변수를 넘긴다.

객체 지향 설계 5원칙 - SOLID

객체 지향 설계(OOD; Object Oriented Design) 5원칙 - SOLID

  • S - SRP(Single Responsibility Principle): 단일 책임 원칙
  • O - OCP(Open Closed Principle): 개방 폐쇄 원칙
  • L - LSP(Liskov Substitution Principle): 리스코프 치환 원칙
  • I - ISP(Interface Segreagation Principle): 인터페이스 분리 원칙
  • D - DIP(Dependency Inversion Principle): 의존 역전 원칙

SOLID는 응집도는 높이고 결합도는 낮추라는 고전 원칙을 객체 지향의 관점에서 재정립 한 것

참고할만한 자료

SRP - 단일 책임 원칙

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다." - 로버트 C. 마틴

메서드가 단일 책임 원칙을 지키지 않을 경우 나타나는 대표적인 냄새가 바로 분기 처리를 위한 if문이다.

강아지 클래스의 소변보다 메소드를 추상 메소드로 정의하고 수컷 강아지 클래스와 암컷 강아지 클래스를 만들어서 따로 재정의 하는것이 바람직하다.

단일 책임 원칙과 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화이다.

추상화를 통해 클래스들을 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들이자.

OCP - 개방 폐쇄 원칙

"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려있어야 하지만 변경에 대해서는 닫혀 있어야 한다" - 로버트 C. 마틴

위 문장을 조금 더 의역해보면 "자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다."이다.

위 그림 처럼 창문과 기어가 수동이던 마티즈에서 창문과 기어가 자동인 쏘나타를 운전하면운전에 영향을 받아야만 할까? 객체 지향 세계에는 다른 해법이 있다.

상위 클래스 또는 인터페이스를 중간에 둠으로써 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 운전 습관에 영향을 받지 않게 된다.

자동차의 입장에서는 자신의 확장에는 개방돼 있는 것이고, 운전자 입장에서는 주변의 변화에 폐쇄돼 있는 것이다.

데이터베이스 프로그래밍에서 JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분외에는 따로 수정할 필요가 없다. 개방 폐쇄 원칙의 예시이다.

마찬가지로 자바에서도 개방 폐쇄 원칙이 적용 돼 있다.

JVM과 목적파일이 있기에 다양한 구동 환경에 걱정하지 않아도 된다. 각 운영체제별 JVM은 확장에 열려 있는 구조가 되는 것이다. 개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장정인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.

스프링 프레임워크에서 개방 폐쇄 원칙을 교과서적으로 활용하고 있다.

LSP - 리스코프 치환 원칙

"서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다" - 로버트 C. 마틴

상속과 인터페이스의 조건

상속: 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다.

인터페이스: 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.

위 두 개의 문장대로 구현된 프로그램이라면 이미 리스코프 치환 원칙을 잘 지키고 있다.

아버지 춘향이 = new 딸() X

동물 뽀로로 = new 펭귄() O

위의 글을 토대로 로버트 C. 마틴의 말을 의역한다면

"하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다."가 된다.

그림의 리스코프 치환 원칙의 위반 사례와 적용 사례를 다시 한번 보자.

결국 리스코프 치환 원칙은 객체 지향의 상속이라는 특성을 올바르게

활용하면 자연스럽게 얻게 되는 것이다.

다음과 같은 내용도 포함한다.

  1. 하위형에서 선행 조건은 강화될 수 없다.
  2. 하위형에서 후행 조건은 약화될 수 없다.
  3. 하위형에서 상위형의 불변 조건은 반드시 유지돼야 한다.

ISP - 인터페이스 분리 원칙

"클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다" - 로버트 C. 마틴

인터페이스 책임 원칙은 단일 책임 원칙의 다른 방법이다.

즉, 단일 책임 원칙과 인터페이스 분할 원칙은 같은 문제에 대한 두 가지 다른 해결책이다.

하지만 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다.

단일 책임 원칙에서 봤던 남자 클래스를

인터페이스 분할 원칙으로 해결한 버전의 사진이다.

상위 클래스는 풍성할수록 좋고, 인터페이스는 작을수록 좋다.

그 이유를 조금 더 살펴보자.

리스코프 치환 원칙에 따라 하위 객체는 상위 객체 인 척을 할 수 있다.

빈약한 상위 클래스를 이용한 경우 다양한 객체들에서 여기저기 형변환이 발생하면서 상속의 혜택을 제대로 누리지 못한다. 상위 클래스형의 참조 변수를 이용해야 상속의 가장 큰 혜택을 볼 수 있다.

인터페이스는 그 역할에 충실한 최소한의 기능만 공개해라.

DIP - 의존 역전 원칙

"고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다", "추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.", "자주 변경되는 구체 클래스에 의존하지 마라." - 로버트 C. 마틴

자동차가 스노우타이어에 의존한다면 계절이 바뀔 때마다 타이어를 교체해야하는 영향에 자동차가 노출이 되게 된다.

하지만 자동차 클래스가 타이어 인터페이스에 의존하고 타이어 인터페이스를 스노우타이어, 일반타이어, 광폭타이어 등이 구현한다면 자동차는 그 영향을 받지 않는 형태로 구성된다.

이 설명은 개방 폐쇄 원칙을 설명할 때 나온 설명이다.

이렇게 하나의 해결책을 찾으면 그 안에 여러 설계 원칙이 녹아있는 경우가 많다.

자동차가 추상적인 타이어 인터페이스에 의존하게 됐다. 바로 의존의 방향이 역전된 것이다.

자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.

의존 역전 원칙을 의역해보면 다음과 같다. "자신보다 변하기 쉬운 것에 의존하지 마라."

SOLID 원칙을 적용함으로써 얻는 혜택에 비하면 늘어나는 소스파일 개수에 대한 부담은 충분히 감수하고도 남을 만하다.

'스터디 > etc' 카테고리의 다른 글

마이크로서비스 도입 이렇게 한다.  (0) 2021.12.11
코딩을 지탱하는 기술  (0) 2020.10.09
함께 자라기(애자일로 가는 길)  (1) 2020.08.04