본문 바로가기

Study/CS

Java 의 개발 철학과 특징

반응형

Java 가 등장하기 전까지, 대부분의 프로그래밍 언어는 특수 OS 나 아키텍쳐에 국한되어 컴파일되도록 설계된다는 문제를 안고 있었다. 이는 플랫폼이 다양해지면서 해당하는 CPU 에 맞는 컴파일러를 구축하게끔 만들었으며, 서로 다른 환경에 따른 컴파일러를 구축하는 문제는 당연히 시간과 비용이 수반된다. 따라서 더 나은 방법을 찾기 위해 Java 개발자들은 Cross Platform 언어를 개발하고자 하였다.

Write Once, Run Anywhere

Java 는 Write Once, Run Anywhere 을 개발 철학으로 두었다. 다시 말해 플랫폼 독립적인 언어를 지향점으로, 같은 코드를 어느 운영체제에서나 실행시킬 수 있게 하자는 컨셉을 담고 있다.

Java 가 한번 개발된 프로그램을 어디서나 실행시키겠다는 철학을 실현시키기 위한 방법에는 4가지 상호 연관된 기술이 있다.

 

  1. Java Programming language
  2. Java class file format
  3. Java API (ex : String.class, Object.class ...)
  4. JVM

Java 프로그래밍 언어로 작성한 소스코드 파일은 단순히 코드만을 담고 있는 파일일 뿐, 이 자체로 무언가가 수행되는 건 아니다. 따라서 이 파일을 실행하기 위해서는 어떤 과정이 필요하다. 이 과정을 컴파일 이라 하며, 소스 파일은 컴파일을 통해 .class 확장자를 가진 바이너리 파일로 만들어주어야 한다. 

 

Java 의 컴파일은 보통 JDK 에 내장되어 있는 javac 컴파일러를 사용하게 된다. 컴파일 후 얻게 된 .class 포맷의 바이트코드는 수행이 가능한 형태의 파일이다. 그리고 이를 수행하기 위해서는 JRE(Java Runtime Environment) 가 필요하다. JRE 는 Java 애플리케이션을 실행하기 위한 Java Virtual Machine 을 구현하는 환경인데, JRE 가 드러내는 특징이 곧 Java 가 모든 권한과 환경에서 실행이 가능하다는 의미이다.

 

JRE 를 수행하기 위해서는 'java' 라는 프로그램을 호출하여, 컴파일한 바이너리 파일을 인수로 제공하면 된다. 이 때 java 프로그램은 java 실행환경에 컴파일된 파일을 들고 들어가는 역할을 한다. 즉, java 프로그램은 JVM 을 OS 위에 하나의 프로세스로 올리는 작업과 함께 바이너리 파일의 로딩도 수행한다. 이렇게 컴파일 타임을 거쳐 생성된 바이너리 파일을 수행하게 되는 시점이 런타임 이다. 이 시점에는 바이너리 파일을 분석하여 JRE 에 포함된 Java API 와 더불어 Java 프로그램을 수행하게 된다.

 

Java 프로그램 실행 과정

 

따라서 Java 는 이 같은 과정을 가지고 실행되므로, 다음 두가지 대표적인 특징이 있다.

  • 이식성이 높다.
    • Java 는 이식성이 강하기 때문에 서로 다른 플랫폼의 특성을 고려할 필요 없이, 다른 운영체제나 CPU 에서도 같은 코드를 사용할 수 있다.
  • 아키텍쳐 중립적이다.
    • Java 는 Java 소스코드를 컴파일하여 바이트 코드를 만들어내고, 이 바이트 코드는 각 환경에 설치된 Java 인터프리터에 의해 해석되기 때문에 어떤 플랫폼에서도 실행이 가능하다. 다시 말하면 Java 인터프리터만 가지고 있다면 Java 애플리케이션을 실행시킬 수 있다.

이 뿐 아니라 Java 는 객체 지향적 언어 라는 대표적인 특징을 가지고 있는데, 이 부분은 별도로 설명이 필요할 것 같다.

 

객체 지향 언어

객체 지향적인 특성을 설명하려는 수단에는 먼저 객체 지향의 5가지 원칙이 있다. 여기에는 단일 책임 원칙, 개방 폐쇄 원칙, 리스코프 치환 원칙, 인터페이스 분리의 원칙, 의존의 역전 원칙이 포함된다.

 

먼저, 단일 책임 원칙이란 객체나 메소드가 각각 오직 하나씩의 책임을 가져야 한다는 원칙이다. 따라서 하나의 책임만 수행하게끔 객체나 메소드의 분리를 수행하여야 한다. 또, 접근제어자를 활용하여 그 책임을 강제시키는 방법도 있을 수 있다. 특정 객체 내부 정보에 대해서 다른 클래스의 메소드에서 자유롭게 접근하여 정보를 수정할 수 없어야 하고, 대신 그 객체 스스로가 자신의 값을 수정하도록 설계하여야 한다. 이는 아래에 설명되어 있는 객체 지향의 특징인 캡슐화와도 관련이 깊다. 다른 객체의 메소드는 public 생성자나 getter 를 통해 정보에 접근시켜야 한다.

 

두번째로, 개방 폐쇄 원칙이란 객체의 확장에는 열려있고, 수정에는 닫혀있어야 한다는 원칙이다. 기존 코드를 변경하지 않고도 기능을 수정하거나 추가할 수 있도록 설계하여야 한다. 이 원칙이 지켜지기 위해서는 변경되는 것이 무엇인지에 초점을 맞추어야 한다. 자주 변경되는 내용은 수정하기 쉽게 설계하고, 아닌 것은 수정되는 내용에 영향받지 않도록 분리해서 설계하는 것이 중요하다. 나는 개인적으로 이를 위해 인터페이스를 많이 활용한다.

 

세번째로, 리스코프 치환 법칙이란 변수 타입이 선언됐을 때 하위 타입은 상위 타입의 기능을 문제없이 모두 수행할 수 있어야 한다는 것이다. 즉, 상속관계에 놓인 두 클래스에서, 잣식 클래스는 부모 클래스에서 가능한 행위를 모두 수행할 수 있어야 한다.

 

네번째로, 인터페이스 분리의 원칙이란 인터페이스를, 그걸 사용하는 클라이언트를 기준으로 분리해야한다는 원칙이다. 즉, 한 클래스는 자신이 사용하지 않는 인터페이스를 구현하지 않아야 한다는 뜻이다. 하나의 제너럴한 인터페이스보다는, 여러개의 구체적인 인터페이스가 적합하다. 이 법칙으로 시스템의 내부 의존성을 약화시켜서 리팩토링도 비교적 수월하게 수행할 수 있게 된다.

 

다섯번째로, 의존의 역전 원칙을 만족한다는 것은 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺는다는 것이다. 즉, 변화하기 쉬운 것보단 변하기 어려운 것에 의존해야한다는 원칙이다.

 

이런 특성을 가지고 있는 객체 지향은 4가지 키워드를 통해 다시 설명할 수 있다. 여기에는 캡슐화, 상속, 추상화, 다형성이 있다.

 

캡슐화란 비슷한 역할을 하는 속성과 메소드들을 하나의 클래스로 모은 것을 말한다. 캡슐화에는 정보 은닉 개념이 속해 있는데, 캡슐 내부의 로직이나 변수들은 감추고 외부에는 api 만을 제공하는 것을 의미한다. 그리고 클래스 내부를 외부에 공개하지 않음으로서 public 접근 제어자를 가지고 있지 않다면 외부에서 이 클래스의 정보를 마음대로 수정하지 못하게 한다.

 

상속을 통해서는 특정 클래스의 재사용이 가능하다. 상위 클래스를 하위 클래스에서 상속받게 되면 상위 클래스의 멤버변수나 메소드를 그대로 물려받을 수 있다. 따라서 코드의 재사용성과 생산성이 증가한다.

 

추상화란 어떤 실체로부터 공통적인 부분이나 관심있는 특성들을 한 곳에 모은 것을 의미한다. 객체 지향에서 추상화는, 어떤 하위 클래스들에 존재하는 공통적인 메소드를 인터페이스로 정의하는 것을 예로 들 수 있다.

 

다형성은 같은 모양의 메소드가 상황에 따라 다르게 동작하는 것을 의미한다. 오버로딩과 오버라이딩이 있는데, 오버로딩이란 함수 이름은 같지만 파라미터 수, 타입을 다르게 해서 다른 용도로 사용하는 것이다. 오버로딩 개념으로 정의된 경우, 컴파일 시점에 어떤 메소드가 사용될 지 결정된다. 반면 오버라이딩이란 상위 클래스 메소드를 하위 클래스에서 같은 이름과 형식으로 재정의하는 것을 의미한다. 오버라이딩 개념으로 정의된 경우, 런타임 시점에 어떤 메소드가 사용될 지 결정된다.

 

객체 지향 프로그래밍 : OOP

위 특징들을 가지고 프로그램을 개발하고자 할 때, 우리는 객체 지향 프로그래밍이라는 말을 많이 사용한다. 객체 지향 프로그래밍은, 프로그래밍에서 필요한 데이터를 추상화 시켜서 상태와 행위를 가진 객체로 만들고, 그 객체간 상호작용을 통해 로직을 구성하는 방법이다. 따라서 클래스 단위의 모듈화가 필요하며 재사용성을 증가시킬 수 있지만 이에 대해 설계 시 많은 노력과 시간이 필요하다.

객체 지향 프로그래밍 vs 함수형 프로그래밍

Java 에서는 함수형 프로그래밍을 돕기 위해 JDK 8 부터 Stream API 를 지원하고 있다. 객체 지향 프로그래밍에 비교하여, 함수형 프로그래밍은 어떻게(How) 가 아닌 무엇을(What) 에 대해 정의한다. 로직 자체를 작성하지 않고 목적 위주로 작성하며, 데이터의 입력이 주어지고 그 데이터의 흐름을 추상적으로 정의해나가는 방식으로 구현된다. 

 

객체 지향 프로그래밍은 동작하는 부분을 캡슐화해서 이해를 돕지만, 함수형 프로그래밍은 동작하는 부분을 최소화해서 이해를 돕는다. Java 에서 이 무엇(What) 에 대해 작성할 수 있도록 돕는게 바로 Collection 의 Stream API 라고 할 수 있다. Stream API 는 스트림을 생성하는 최초 연산과 중간에 로직을 구성하는 중간 연산, 그리고 결과물을 처리하게 되는 최종 연산으로 나뉜다. 이 중 중간연산은 Stream 타입을 리턴하며 계속 메서드 체이닝이 가능한 형태이다. 

 

반응형