공부/JAVA

[1회차 01] 자바 간단 정리

junani0v0 2024. 5. 9. 00:21

자바 핵심 개념

선언부와 구현부

메서드는 다음과 같이 정의할 수 있다.

public static int multiply(int a, int b) {
}

제어자 반환타입 메서드이름(매개변수 목록) {
  구현부
}

선언부

메서드에서 구현부를 제외한 나머지를 선언부라고 한다. 선언부에는 다음과 같은 종류가 있다.

  • 제어자: public, static과 같은 부분이다.

    접근 제어자에는 다음과 같은 종류가 있다.

    • private: 같은 클래스에서만 접근 가능하다.

    • default: 같은 패키지에서만 접근 가능하다.

    • protected: 상속 관계에서만 접근 가능하다.

    • public: 어디서든 접근 가능하다.

  • 반환 타입: 메서드가 실행되고 반환하는 데이터의 타입이다.

  • 메서드 이름: 메서드를 호출하는 메서드의 이름이다.

  • 매개 변수: 메서드 내에서만 사용할 수 있는 지역 변수이다.

매개 변수 (Parameter) vs. 인수(Argument)

메서드 정의와 호출

다음과 같이 곱셈 연산을 하는 메서드가 정의되어 있고 multiply 메서드를 호출했다고 하자.

public class ParamAndArg {
  public static void main(String[] args) {
    // 2. 메서드 호출
    int multiplication = multiply(3, 8);

    // 3. 결과 출력
    System.out.println("결과 출력: "+ multiplication);
  }

  // 1. 메서드 정의
  public static int multiply(int a, int b) {
    System.out.printf("%d * %d 연산 수행%n", a, b);
    return a * b;
  }
}

인수 (Argument)

위 예시에서 3, 8과 같이 메서드를 호출할 때 넘기는 값을 인수, 인자, 또는 아규먼트라고 한다.

매개변수 (Parmeter)

위 예시에서 int a, int b과 같이 메서드 선언부에 있는 변수를 매개변수 또는 파라미터라고 한다.

참조에 의한 호출 (Call by Reference) vs. 값에 의한 호출 (Call by Value)

메서드를 호출할 때 값을 전달하는 방법에는 다음과 같은 두 가지 방식이 있다.

참조에 의한 호출

  • 주소 값을 인자로 전달하는 호출 방식.

  • 원본 값 변경 가능.

  • 기본형과 관련.

    예) 아파트 주소

값에 의한 호출

  • 값 자체를 복사해 인자로 전달하는 호출 방식.

  • 원본 값 변경 불가.

  • 참조형과 관련.

    예) 아파트 주소에 사는 가구원 수

멤버 변수와 지역 변수

클래스를 구성하는 변수에는 멤버 변수와 지역 변수가 있다. 멤버 변수에는 인스턴스 변수와 클래스 변수가 있다.

변수의 종류 선언 위치 사용 가능 시점
인스턴스 변수 (iv) 클래스 영역 객체 생성 이후
클래스 변수 (static + iv) 클래스 영역 메모리에 로딩 될 때 = 객체 생성 이전 (항상)
지역 변수 (lv) 메서드 영역 메서드 호출 시

자바의 메모리 영역

자바의 메모리 영역은 다음과 같이 크게 세 가지 영역으로 구분된다.

클래스 영역 (Class Area)

프로그램을 실행하는데 필요한 공통 데이터를 관리하는 영역이다.

  • 클래스 정보: 클래스(.class)와 관련된 모든 정보를 보관한다.

  • static 영역: static 변수들을 보관한다.

  • 상수 풀: 공통 리터럴 상수를 보관한다.
    예) "java", 123

스택 영역 (Stack Area)

  • 메서드 실행을 위한 영역이다.

  • 메서드의 지역 변수를 위한 공간이 생성되며 메서드 수행이 끝나면 메모리 공간을 반환한다.

힙 영역 (Heap Area)

  • new 연산자를 통해 생성된 객체(배열 포함)를 위한 영역이다.

  • 참조 되지 않는 객체는 가비지 컬렉터(GC)에 의해 제거되는 가비지 컬렉션이 일어나는 영역이기도 하다.

클래스와 인스턴스

class 붕어빵 {
  String filling;

  붕어빵(String filling, String topping) {
    this.filling = filling;
  }
}

public class Main {
  public static void main(String[] args) {
    붕어빵 붕어빵1 = new 붕어빵("참치");
    붕어빵 붕어빵2 = new 붕어빵("초콜릿");
  }
}

클래스

  • 사용자 정의 타입을 만드는 설계도
  • 예) 붕어빵

인스턴스 (객체)

  • 클래스를 이용해 만든 실제 메모리(힙 영역)에 만든 실체
  • 예) 붕어빵1, 붕어빵2가 참조하고 있는 것

객체 지향 핵심 개념

abstract class 붕어빵 {
  private filling;

  abstract void 속_채우기();

  protected void 굽기() {
    System.out.println("굽기!");
  }
}
class 피자_붕어빵 extends 붕어빵 {
  private String filling;

  @Override
  void 속_채우기() {
    this.filling = "피자";
  }

  void 속_채우기(boolean 재료_소진) {
    if (재료_소진) {
      this.filling = "팥";
    }
  }

  void 굽기() {
    System.out.println("굽기!");
  }
}

위와 같은 예를 이용해 객체 지향 개념을 설명하겠다.

상속 (Inheritance)

재사용 & 확장과 구체화

  • 기존 클래스의 필드와 메서드를 재사용하고 확장 가능하게 한다.
    예) 굽기()

  • 추상 클래스의 구체화이다.
    예) 속_채우기()

상속 관계

  • 두 클래스가 is-a 관계일 때 두 클래스는 상속 관계에 있다고 할 수 있다.
    예) 피자 붕어빵은 붕어빵이다. 팥 붕어빵은 붕어빵이다.

오버로딩과 오버라이딩

  • 오버로딩: 선언부가 같고 매개변수가 다른 메서드를 정의
    예) 속_채우기(boolean 재료_소진)

  • 오버라이딩: 부모의 메서드를 재정의, 상속 관계
    예) 속_채우기()

캡슐화

  • setter나 getter 등을 통해 클래스의 필드에 접근하게 하는 방식이다.
  • 필드의 위변조를 방지한다.

다형성

핵심 기능

public class Polymorphism {
  public static void main(String[] args) {
    피자_붕어빵 피붕 = new 피자_붕어빵();
    팥_붕어빵 팥붕 = new 팥_붕어빵();

    붕어빵[] 붕어빵들 = {피붕, 팥붕};

    for (붕어빵 붕어빵_하나 : 붕어빵들) {
      System.out.println("붕어빵 속 채우기 시작");
      붕어빵_하나.속_채우기();
      System.out.println("붕어빵 속 채우기 종료");
    }
  }
}
  • 다형적 참조: 부모 타입의 변수가 자식 인스턴스를 참조할 수 있다.
    예) 붕어빵[] 붕어빵들 = {피붕, 팥붕};

  • 오버라이딩 우선: 자식 클래스에서 오버라이딩된 메서드가 우선 호출된다.
    예) 붕어빵_하나.속_채우기();: 붕어빵 타입이 피자_붕어빵일 때 붕어빵속_채우기()가 아닌 피자_붕어빵의 오버라이드된 메서드가 호출된다.

[참고] 두 객체가 서로 상속 관계에 있는지 확인할 때는 instanceof를 사용한다.

그렇다면 이와 같은 다형성을 사용하는 이유는 무엇일까?

사용 이유

앞서 본 예와 같이 추상화의 정도에 따라 추상 클래스, 인터페이스 순서로 추상화의 정도가 극대화된다. 인터페이스 사용해야 좋은 설계를 할 수 있다고 하는데 그 이유는 다음과 같다.

  • 제약: 인터페이스의 모든 메서드는 추상 메서드이기 때문에 해당 인터페이스를 구현하는 자식 클래스는 모든 기능을 구현해야만 하는 강제성을 가진다.

  • 다중 구현 가능: 상속과 달리 implements를 사용하는 인터페이스는 여러 인터페이스를 구현할 수 있다.

하지만 가장 중요한 다형성의 사용 이유는 바로 역할과 구현의 분리이다.

class Driver {
  private Model1 model1 = new Model1();

  void drive() {
    model1.시동_켜기();
    model1.엑셀_밟기();
    model1.시동_끄기();
  }
}

class Model1 {
  void 시동_켜기() {};
  void 엑셀_밟기() {};
  void 시동_끄기() {};
}

위와 같이 운전자가 차를 운전하는 상황을 가정해보자. 해당 운전자가 차를 Model2로 바꾸는 경우 Driver 클래스의 대부분을 수정해야 한다. 결국 역할과 구현의 분리가 이루어지지 않아 운전자 자신을 고쳐야 하는 경우가 발생하는 것이다. 이를 방지하기 위해 Car(역할)라는 인터페이스가 필요하며 이를 Driver에 도입하게 되면 차를 Model2(구현)로 바꾸는 상황에서 Driver 클래스를 바꿔야 하는 경우가 없어진다.

class Driver {
  private Car car;

  Driver(Car car) {
    this.car = car;
  }

  void drive() {
    car.시동_켜기();
    car.엑셀_밟기();
    car.시동_끄기();
  }
}

interface Car {
  void 시동_켜기();
  void 엑셀_밟기();
  void 시동_끄기();
}

class Model1 implements Car {
  void 시동_켜기() {};
  void 엑셀_밟기() {};
  void 시동_끄기() {};
}

이와 같이 어떤 운전해도 운전자의 코드는 변하지 않는 설계를 확장에는 열려있고 수정에는 닫혀 있는 OCP (Open-Closed Principle) 원칙이라고 한다.