본문 바로가기

자바

자바의 클래스(Class)란 무엇일까? - 스터디 5차

최근에 생소한 게임을 만드는 프로젝트를 맡게 되어 업무에 치이다보니, 일과 관련된 공부만 했었던 것 같네요. ㅎㅎ

CBT가 거의 끝나가고 있어서, 개인 공부를 좀 해야겠다라는 생각이 들어요.

블로그에 새글도 쓰면 좋겠다 싶기도 하고 ㅎㅎ
그래서 가볍게 시작하면 어떨까 합니다.

개발 커뮤니티 들렸다가, 굉장히 좋아하는 백기선님이 온라인으로 진행했던 라이브 스터디를 클론하여 발표하고 공유하는 모임이 있어서, 자바를 다시 복습하면서 자바를 사용하고 느꼈던 부분을 공유하면 좋겠다 싶어서 참여 하기로 마음먹었어요.
 
스터디는 5주차 과제부터 15주차 까지 진행하기로 했는데 일단 필수 과제 내용을 보고 제가 알고 있는 내용 끄적여보고 찾아보면서 좀 더 깊게 정리 해볼까 합니다.

 

1. 클래스가 무엇이고, 어떻게 정의하지?

 

먼저 제가 아는 내용 주저리 해볼게요.
저는 클래스를 객체지향 언어에서 객체를 만들기 위해 프로그래밍적으로 정의하는 걸로 알고 있습니다.
 
파일 한개당 이너클래스를 제외하고는 하나의 클래스만 정의하는게 일반적인 정의 하는 방법이구요.
그안에 객체의 속성을 정의 하는 인스턴스 변수들이 들어갈 수 있고, 기능을 정의하는 메서드로 이루어져 있죠.
 
특징을 더 생각해보면 Class는 접근제어자가 붙을 수 있는데, 모든 클래스에서 접근 가능하게 하는 public, 같은 패키지안에서 접근 할 수 있는 default만 지정할 수 할 수 있다는 특징이 기억이 나네요.
 

public class Study {

  private static long nextId = 0;
  
  private final Map<Long, Person> persons;

  public Study(){
    this.persons = new ConcurrentHashMap<>();
  }

  public Person getPersonById(long id){
    Person person = persons.get(id);
    return person;
  }
  
  public Person add(Person person){
    this.persons.put(nextId, person);
    nextId++;  
  }
  
}

코드로 간단히 클래스 정의 해봤어요. 스터디에 가입했으니 Study 클래스를 ㅎㅎ

속성을 나타내는 인스턴스 변수들을 정의해주고, 객체를 생성할 때 사용되는 생성자 만들어줍니다.

아무 것도 없는 기본생성자는 다른 생성자가 없다면 기본으로 생성되는데 다른 생성자가 존재할때 만약 기본 생성자가 필요하면 기본생성자 정의 해줘야 합니다. 그리고 toString이나 equals, hashcode 오버라이드해서 재정의 할 수도 있어요.
 
인스턴스변수는 외부에서 직접 접근하지 못하도록 Getter, Setter 만들어 줄 수 있었고, 안에 필요한 정적 팩터리 메서드나, 메서드를 정의 해서 기능을 만들면 Study 클래스 대충 완성. ㅎㅎ
아는 내용 의식의 흐름대로 써봤는데 진짜 주저리 주저리 했네요 ㅋㅋ
 
이제 좀 더 살펴볼 게 무엇이 있을지 리서치 해볼게요.
 

좀 더 다양한 시선에서 본 것들

요즘 제가 학습방법이 조금 바뀌었는데, 공유하자면 공식문서부터 살펴보면서 컨셉, 왜 만들어졌을까? 무슨 의도가 담겨있을까? 부터 찾아 보게 되었어요. 그러고 이해가 잘안가면 유명한 개발자들이 정리한 글이나 유명 블로그들을 훓어 보면서 개념을 이해하는 방법입니다.

더 자세히 보고 싶으면 책으로 공부합니다 ㅎㅎ 귀찮지만 빠르게 본질적인 아이디어를 살펴보게 되는 것 같아요.
 
자바는 오라클이죠. 오라클 컨셉 문서부터 봅시다.

 
 
공식문서에서 말하는 클래스의 컨셉은 무엇이냐?

해석하자면 현실세계에서 많은 객체들의 종류 있는데, 자전거를 예로 들면서 같은 제조사와 모델이 동일한 수많은 자전거가 존재할 수 있고, 각 자전거는 blueprint 세트로 제작되었다고 합니다. blueprint는 도면? 이런 뜻이 있죠.

객체 지향 용어로 자전거라고 알려진 객체의 인스턴스라고 하면서 클래스는 각 객체가 생성되는 blueprint라고 말하는데
쉽게 풀이하면 클래스는 각각에 객체를 만들수 있는 blueprint 도면, 틀 이라는 설명입니다.
 

제 생각에는 클래스는 각각의 객체를 편하게 만들기 위해 정의 해놓은 것이라는게 핵심인 것 같습니다.
클래스로 각각의 양산형 객체를 만드는 거죠. ㅎㅎ

 
밑에 좀 더 자세하게 된 문서를 볼게요.

 
Classes and Object 제가 객체를 만들기 위한 프로그래밍적 정의라고 말했었는데,
항상 묶여서 다루는 것 보니 틀린말은 아닌거 같네요 ㅎㅎ
 
뭐가 더 있을 줄 알았는데, 일반적인 사용법에 대한 내용으로 짧게 다루네요.
그래도 다행히 여기서 건질 내용은 있었어요.
 

 
중첩된 Nested Classes는 내부에서만 사용할 수 있는 private으로 지정할 수 있다고 하네요.
뭐 이너 클래스를 사용하면 당연한건가?

그리고 컨벤션과 관련있는 중요한 첫문자를 대문자로 시작하는 관례라는 내용
상위클래스에게 상속받을 때는 하나만 상속받을 수 있다.

둘 이상의 인터페이스를 구현할 수 있다는 내용이 있네요. ㅎㅎ
클래스를 이야기 할때 쫌 더 말할게 생겼어요. 🙃
 
밑에 과제들이 많은 관계로 클래스는 이정도만 다루겠습니다 .ㅎㅎ

 

2. 객체 만들기 & new 키워드

 

객체는 클래스라는 도면으로 만들 수 있어요. 자바에서는 "new 키워드를 이용해서 객체를 생성하면서 메모리에 올린다"라고 알고 있는데요. 참조형 데이터타입을 갖는 객체는 JVM에는 Heap 메모리 영역에 저장되어서 존재하는 것처럼 관리되고
따로 참조 하지 않게 되면 GC에 의해서 메모리에서 제거될꺼에요.
 
그러면 Study 클래스를 이용해서 객체(인스턴스)화하는 코드를 작성해볼게요.
테스트에 사용할 Test 클래스를 만들고, main 메서드를 하나 정의해서 new 키워드로 Study 객체를 생성했어요.

앞부분에 객체를 참조하기 위한 변수를 선언해줍니다. Study study;

new 키워드로 객체를 생성하면, Study 클래스에 정의한 생성자를 통해서 객체를 메모리에 올라가게 됩니다.

= 연산자로 생성된 객체의 메모리 주소를 참조 변수에 넣어주면 됩니다.

public class Test {

  public static void main(String[] args) {
    // 객체 생성
    Study study = new Study();
  }
  
}

 

좀 더 다양한 시선에서 본 것들

객체라는 말이 애매하죠? 객체를 조금 더 살펴볼까요?

객체는 객체지향 기술을 이해하는 데 핵심이라고 하네요.
실제 객체는 두 가지 특성인 상태 와 동작이 있다고 하구요. 상태와 동작을 식별하는게 객체지향 프로그래밍을 사고하는데 좋은 방법이라고 합니다. 각 개체에 대해 두 가지 질문을 해보라고 합니다. "이 객체는 어떤 상태에 있을 수 있나요?" 그리고 "이 객체가 수행할 수 있는 동작은 무엇입니까? ㅎㅎ

앞서 말했던 인스턴스 변수가 상태(속성), 메서드가 동작(기능)이라고 이해하시면 좋을 것 같네요.
더 밑에 내려보면 객체를 활용하면 몇가지 이점이 있다고 명확하게 나와있어요.

  1. 모듈성: 객체의 소스 코드는 다른 객체의 소스 코드와 독립적으로 작성되고 유지 관리될 수 있다. 생성된 객체는 시스템 내부에서 쉽게 전달될 수 있다.
  2. 정보 숨김: 객체의 메서드와만 상호 작용함으로써 내부 구현이 외부 세계로부터 숨겨진 상태로 유지된다.
  3. 코드 재사용: 이미 존재하는 객체의 경우(다른 소프트웨어 개발자가 작성한 경우) 프로그램에서 해당 객체를 재사용이 가능하다.
  4. 연결성 및 디버깅 용이성: 특정 객체에 문제가 발생하면, 쉽게 제거 하고 다른 객체로 바꿀 수 있다고 이야기 하네요. 볼트가 부러지면 기계 전체를 교체하는 것이 아니라 볼트를 교체한다? ㅎㅎ

문서에서는 new 키워드는 클래스로 객체(인스턴스)를 만들면서 생성자를 같이 호출한다고 만 나와있어요.

 
좀 더 new 키워드에 대해 자세하게 나와있는게 없을까요?

 

리서치하던 중에 medium 사이트의 유료게시물 중에 Understanding the new Keyword in Java라는 글이 보이더라구요.

무료 발췌부분인데, 자세한 내용은 맨 밑에 링크를 걸어두었으니 확인하시면 좋을 것 같네요.

 

런타임중에 객체를 초기화하고 메모리를 동적으로 할당 할 수 있게 한다.

자세하게는 new 키워드는 객체를 메모리에 할당하기 위한 Java JVM의 명령어 라고 써놨네요.

 

내부적으로 일어나는 일은?

  1. 클래스 로딩: 클래스 로더가 클래스명.class 바이트코드를 아직 로드하지 않은 경우 로드 (처음 클래스 객체를 만들때만 로딩)
  2. 메모리 할당: JVM의 힙 메모리 공간에 객체에 대한 메모리를 할당
  3. 생성자 실행: 클래스의 생성자가 호출되어 새 객체를 초기화
Study study = new Study();

 

참조 변수 메모리 할당 -> 인스턴스(객체) 메모리 할당 -> 초기화 -> 생성자 실행 -> 참조 변수에 메모리 주소값 저장

의 과정을 거치는구나 정도로 넘어가시면 될 것 같네요. ㅎㅎ

 

 

3. 메서드 정의하는 방법은?

 

프로그래밍에서 메서드는 클래스안에 정의할 수 있는 행동(기능)입니다. 편히 함수라고 불리죠?

접근 제어자 지정하고 메모리에 고정적으로 올릴 것인지 여부에 따라서 static 키워드는 선택

void는 리턴타입이 없다는 거고, 리턴타입이 있으면 리턴 타입 지정해주고, 메서드 명명규칙에 의해서 메서드명 이름 지어주고

매개변수는 뭐가 들어올지 적어주면 됩니다.

// id 값을 가지고 person은 조회하는 메서드
public Person getPersonById(long id) {
    Person person = persons.get(id);
    return person;
}

// main 메서드
public void static main(String[] args) {

}

위에 간단히 작성한 Study 클래스에서 id값을 가지고 Person을 조회하는 메서드 사용 예시 입니다.

 

좀 더 다양한 시선에서 본 것들

메서드는 같은 메서드명을 가졌지만 매개변수가 다른 경우인 오버로딩,

상위 타입의 클래스를 상속 받았을때, 메서드를 재정의 하는 오버라이드

그리고 메서드를 구분하는 메서드 시그니처에 대해서 좀 찾아보면 어떨까 싶네요.

 

오버로딩

이름은 같지만 매개변수의 개수가 다르거나 타입이 다른 경우 이를 메서드 오버로딩이라고 하는데요.

하나의 작업만 수행해야 하는 경우 동일한 메서드 이름을 사용하면 프로그램의 가독성이 높아지는 장점이 있어요.

 

대표적으로 출력할때 사용하는 System.out.print(); 가 존재하는데, PrintStream 구현 클래스를 살펴볼게요.

 

같은 이름의 print가 여러개 존재하고, boolean, char, int 등 데이터 타입이 다른 매개변수로 만들어져 있죠.

이게 바로 오버로딩의 전형적인 예시 입니다. 이름은 갖지만 그냥 편하게 다른 메서드라고 알고 계시면 좋을 것 같네요.

 

오버로딩이 되는 조건을 정리해보면

  1. 메서드 이름이 같아야 한다.
  2. 매개변수의 개수 또는 데이터 타입이 달라야 한다.
  3. 매개변수는 같아도 리턴타입이 다른 경우는 오버로딩이 성립되지 않는다.
    (리턴타입은 관계 없음)

 

메서드 시그니처

오버로딩의 조건을 보면 다른 메서드인지 java가 어떻게 구분하는거야? 생각이 들지 않나요?

나는 System.out.print(); 에 타입이 다른 데이터를 넣을 때 알아서 각기 다른 메서드가 호출된다는거 잖아요?

 

 

위키피디아 인데요.

JVM은 메서드 시그니처라는 메커니즘을 이용하여 메서드를 식별한다고 합니다.

 

In Java, a method signature is a part of the method declaration. It’s the combination of the method name and the parameter list

 

여러 블로그에서는 메서드 이름과 매개변수 목록의 조합으로 이루어져있다는 설명이 있는데, "메서드이름" 혹은 "매개변수"를 보고 메서드를 구분하는 구나 라고 이해하시면 될 것 같네요.

 

오버라이드

상위 타입의 클래스를 상속 받은 클래스가 있다고 할때, 상위 클래스의 메서드를 좀 변경해서 사용하고 싶을때 재정의해서 사용할 수 있습니다. 아래는 geeksforgeeks 사이트에서 발췌했습니다. 더 자세하게 보시고 싶으시면 맨 아래의 링크를 확인해주세요. 

 

가장 핵심은 객체지향언어의 특징인 다형성을 사용하는 방법이라고 하는데요.

부모 클래스를 상속받았을때 자식 클래스는 부모의 기능을 사용할 수 있습니다. 그러나, 부모의 기능을 쓰고 싶지 않다.

자식 클래스는 같은 메서드이름이고 매개변수도 같은데, 다른 기능으로 동작하게 하고 싶을 때 사용 합니다.

 

Study를 부모 클래스로 하고, SubStudy는 Study를 상속받는 서브 모임이라고 생각하고 만들어봤어요.

모임 시간을 가져오는 메서드(getMeetingTime) 를 만들었고,

new SubStudy로 만든 객체에서 getMeetingTime 메서드를 호출하면 재정의된 메서드를 호출 하게 되어서, 모임시간이 다른것을 볼 수 있습니다.

class SubStudy extends Study {

  @Override
  public LocalDateTime getMeetingTime() {
    
    // SubStudy의 미팅시간은 Study(부모 클래스)의 미팅 시간 1시간 이후로
    return super.getMeetingTime().plusHours(1);
  }
}

public class Study {

  private final LocalDateTime meetingTime;

  public Study() {
    // 오늘 오전 10시
    this.meetingTime = LocalDate.now().atTime(10, 0);
  }

  public LocalDateTime getMeetingTime() {
    return meetingTime;
  }

  public static void main(String[] args) {
    Study study = new Study();
    System.out.println(study.getMeetingTime());

    Study subStudy = new SubStudy();
    System.out.println(subStudy.getMeetingTime());
  }
  
}

> Task :user-core:Study.main()
2024-02-03T10:00
2024-02-03T11:00

 

4. 생성자 정의 하는 방법

생성자는 객체가 생성될때, 딱 1번 호출되어서 인스턴스화 된 객체에 속성을 채워주거나 어떠한 프로세스를 진행해주는 녀석인데요.

저는 생성자에는 그 클래스를 만든 개발자가 이렇게 생성되어야해.!! 라는 의도를 담을 수 있다고 생각합니다.

 

생성자는 클래스명에 매개변수가 전달될 수 있게 ()가 붙는 것이 특징이죠.

또한 일반적인 메서드와 다르게 반환타입을 지정하지 않아요. 

클래스에는 생성자가 무조건 1개 이상 있어야 하기때문에, 만약 생성자를 만들지 않는다면 컴파일러 자동으로 아무 매개변수도 없는 생성자를 제공합니다.

public class Study {

  private LocalDateTime meetingTime;
  
  private Person studyHead;
  
  // 다른 생성자가 없다면 생략 가능 (컴파일러가 자동으로 만들어 줌)
  public Study(){
  }
  
  public Study(Person person){
    this.studyHead = person;
  }
  
}

 

 

생성자는 객체가 생성될때 1번만 실행되고, 그 클래스를 만든 개발자가 이렇게 생성되었으면 좋겠다는 의도가 담을 수 있는 메서드이다.
저는 요렇게 정리 하고 싶네요.

 

5. this 키워드 이해하기

this는 클래스 파일 안에서 자기 자신을 지칭 합니다.

제 생각에는 자기 자신안에 있는 것을 더 명확히 지칭하기 위한 것 아닐까 하네요.

예를들어 Study 생성자를 만들때 매개변수명이 Study의 속성인 studyHead와 네이밍이 같다면, 혼란스러우니깐 ㅎㅎ

this.studyHead는 객체안의 속성을 가르키게 되고, studyHead는 매개변수로 들어온 Person을 지칭하도록이요.

public class Study {

  private LocalDateTime meetingTime;
  
  private Person studyHead;
  
  public Study(Person studyHead){
    // Study 객체의 studyHead에 접근
    this.studyHead = studyHead;
    // Study 객체의 meetingTime 접근
    this.meetingTime = LocalDateTime.of(2024, Month.FEBRUARY, 3, 10, 0);
  }
  
}

 

좀 더 다양한 시선에서 본 것들

공식문서에 this를 사용하는 일반적인 이유가 명확히 나와 있네요.

 

this 필드가 메서드나 생성자 매개변수에 의해 숨겨지기 때문이라고 나오네요.

 

다른 생성자를 호출할 때도 쓰일수 있구요. 

이렇듯 'this' 키워드 덕분에 읽기 쉬운 코드를 유지할 수 있는것 같네요.

명확하게 구분하여 모호성을 줄이고 다른 개발자가 코드를 더 쉽게 이해할 수 있도록 말이에요.

 

저는 기본적인 자바 문법을 정리하면서, 생각보다 쓸 내용이 많네? 계속 어떻게 쓰지 ㅋㅋ 라는 생각과
기본적인 문법인데도, 진짜 많은 고민이 담겨있는데? 왜 자바가 아직도 사랑받는 언어인가를 느끼게 되는 것 같아요.
부족한 글 읽어 주셔서 감사합니다.
아직 참고자료를 보며 제 생각을 정리하는 주니어 개발자입니다. 잘못된 내용 있으면 지적해주시면 감사하겠습니다. 🙏

 

Reference

[오라클 자바 클래스 문서 참고 링크]
https://docs.oracle.com/javase/tutorial/java/concepts/class.html 

https://docs.oracle.com/javase/tutorial/java/javaOO/classes.html

https://docs.oracle.com/javase/tutorial/java/concepts/object.html

https://docs.oracle.com/javase/tutorial/java/javaOO/objectcreation.html

 

[daeldung 스택, 힙 메모리 참고 링크]

https://www.baeldung.com/java-stack-heap

 

[new 키워드의 이해 블로그]

https://medium.com/codimis/understanding-the-new-keyword-in-java-be571dec090bhttps://www.prepbytes.com/blog/java/new-keyword-in-java/

 

[메서드 문서 참고 링크]

https://www.javatpoint.com/method-overloading-in-java

https://codechacha.com/ko/java-method-signature/

https://en.wikipedia.org/wiki/Type_signature#Java

 

https://www.simplilearn.com/tutorials/java-tutorial/overriding-in-java

https://www.geeksforgeeks.org/overriding-in-java/

 

[this 키워드의 이해]

https://docs.oracle.com/javase/tutorial/java/javaOO/thiskey.html

https://ioflood.com/blog/this-java/

 

하얀종이 개발자