본문 바로가기
카테고리 없음

객체와 클래스1

by 경자꿈사 2024. 7. 16.

오늘부터 몇 차례에 걸쳐 '객체' 와 '클래스' 에 대해 되도록 쉽게 설명을 해보려고 한다.
컴퓨터 프로그래밍에서 말하는 '객체' 는 속성(데이터)과 메서드를 포함하는 일종의 데이터 구조인데 '클래스'라는 것을 사용해 원하는 객체를 표현하고 그 '클래스'의 인스턴스(instance : 사례, 경우) 생성을 통해 실제 프로그램에서 사용할 수 있는 '객체'를 만들게 된다.

h = Hash.new
위 코드는 Hash 클래스의 생성자 메서드(new)를 통해 인스턴스 하나를 생성하는 코드인데 이를 통해 생성된 인스턴스는 변수 h에 담기게 된다.
이후 코드에서는 h를 통해 방금 생성한 Hash 클래스의 인스턴스 즉, 해시 객체에 접근할 수 있게 된다.
조금 더 자세히 설명하면 실제 변수 h가 가지고 있는 것은 해시 객체 자체가 아니라 해시 객체를 참조(Reference)할 수 있는 '참조 값'인데 '참조 값'은 객체가 실제 저장된 메모리 위치와 관련된 값이라고 생각하면 된다.
따라서 h2 = h 라고 코드를 작성하게 되면 h 와 h2 모두 동일한 해시 객체의 '참조 값'을 갖게 되므로 이제 h 또는 h2 어느 것을 사용하더라도 동일한 객체를 참조하게 된다.
아래 코드 실행 결과를 보면 이것을 확인할 수 있다. 여러분도 irb에서 직접 해보길 바란다.

>> h = Hash.new
=> {}
>> h2 = h
=> {}
>> h[:lang] = "Ruby"
=> "Ruby"
>> h
=> {:lang=>"Ruby"}
>> h2
=> {:lang=>"Ruby"}

그러면 이번엔 '사람'이라는 객체를 표현하는 클래스를 하나 만들어 보면서 클래스와 객체에 대해 좀 더 살펴보도록 하자.
위에서 객체는 '속성'과 '메서드'를 포함하는 일종의 데이터 구조라고 했는데 그러면 '사람'이라는 객체는 어떠한 '속성'과 '메서드'를 가져야 할까?
그것을 알기 위해서는 먼저 내가 현재 작성하는 프로그램에서 '사람'이라는 객체가 왜 필요한지를 생각해 봐야 한다.
예를 들어 주소록 관리 프로그램을 만든다고 할 때 필요한 정보가 이름, 연락처, 주소라고 한다면 그것들이 바로 '사람' 객체의 '속성'이 될 수 있다.
그리고 '사람' 객체의 '메서드' 예로는 각 속성 정보를 돌려 주거나 변경 할 수 있게 하는 메서드와 해당 속성 정보들을 보기 좋게 하나의 문자열로 표현하여 돌려주는 메서드 등이 있을 수 있다.
이제 '사람' 객체가 가져야 하는 '속성'과 '메서드'가 어떤 것인지 대략 정리가 되었으니 실제 클래스를 작성하여 '사람' 객체를 표현해 보자.

class Person
  def initialize(name, phone, address)
    @name = name
    @phone = phone
    @address = address
  end
  
  def name
    return @name
  end
  
  def phone
    return @phone
  end
  
  def address
    return @address
  end  
  
  def set_phone(phone)
    @phone = phone
  end
  
  def set_address(address)
    @address = address
  end
  
  def info
    return "#{name} / #{phone} / #{address}"
  end
end
>> require './person'
=> true
>> p = Person.new("홍길동", "010-1111-1111", "서울 특별시 강동구")
=> #<Person:0x000001c13e645f90 @name="홍길동", @phone="010-1111-1111", @address="서울 특별시 강...
>> p.name
=> "홍길동"
>> p.phone
=> "010-1111-1111"
>> p.address
=> "서울 특별시 강동구"
>> p.set_phone("010-2222-2222")
=> "010-2222-2222"
>> p.info
=> "홍길동 / 010-2222-2222 / 서울 특별시 강동구"
>>

Person 클래스를 작성하여 person.rb 라는 이름으로 저장하였고 테스트를 위해 해당 파일이 있는 폴더 위치에서 irb 를 실행하였다. 'require' 메서드를 사용하면 파일에 작성된 코드를 현재 프로그램 안으로 로드할 수 있는데 irb를 실행한 위치와 같은 경로 상에 있는 루비 파일을 로드하기 위해서는 앞에 './' 을 붙여서 로드 대상 파일이 현재 디렉토리('.') 밑에 ('/') 있다는 것을 명시해 줘야 한다. '.rb' 확장자는 생략이 가능하다.

 

위의 코드에서 보는 것처럼 클래스는 아래와 같은 형식으로 작성하면 된다.
class 클래스이름
  내용
end

내용에는 필요한 메서드들을 작성해 주면되고 속성은 보통 initialize 메서드에서 인스턴스 변수를 초기화해 주면서 정의한다고 생각하면 된다.
위의 Person 클래스에서 @name, @phone, @address 이 세개가 인스턴스 변수이고(@ 가 변수이름 앞에 붙어있다.) 각각 이름, 연락처, 주소에 해당하는 속성을 저장하는 역할을 한다.
클래스의 생성자를 통해 동일한 클래스의 인스턴스를 필요한 만큼 여러 개 생성할 수 있고 그러면 각각의 인스턴스마다 서로 다른 속성 데이터를 저장할 수 있는 고유의 저장소가 필요한데 그게 바로 인스턴스 변수이다.
initialize 메서드는 객체를 생성할 때 초기 설정 역할을 담당하며 클래스의 생성자 메서드인 new를 호출할 때 new에 전달된 인수와 함께 자동으로 호출된다.
지금까지 봐왔던 대로 특정 객체의 메서드를 호출하는 방법은 객체(의 참조 값)를 담고 있는 변수 뒤에 '.' 을 붙이고 이어서 메서드 명을 적어 주면 된다.
만약 인수가 있으면 메서드 명 뒤에 괄호를 적고 그 괄호 안에 인수를 적어주면 된다. 
Ruby에서는 메서드 호출 시 인수가 있더라도 괄호를 생략하는 것이 가능한데 괄호를 생략한 코드가 더 이해하기 쉽다고 생각된다면 괄호를 생략하는 것도 괜찮은 방법이다.
다만 전체 프로그램 코드에서 그러한 코딩 스타일을 일관되게 유지하는 것이 좋다.
다시 위의 Person 클래스의 테스트 코드를 보면 연락처를 수정할 때 p.set_phone("010-2222-2222") 처럼 작성했는데 변수에 값을 할당할 때 처럼 p.phone = "010-2222-2222" 이런 식으로 작성할 수 있다면 더 좋을 것이다.  
Ruby에서는 이와 같이 속성 값을 변경하는 메서드(보통 setter 라고 부름.)에 대해 "객체.속성명 = 값" 의 형식으로 메서드를 호출할 수 있도록 해준다.
그렇게 하기 위해서는 다음 코드 처럼 클래스 정의에서 Set 메서드의 이름을 수정해야 한다.

class Person
  ...생략

  def phone=(phone)
    @phone = phone
  end
  
  def address=(address)
    @address = address
  end
  
  ...생략
end
>> require './person'
=> true
>> p = Person.new("홍길동", "010-1111-1111", "서울 특별시 강동구")
=> #<Person:0x0000023f09709668 @name="홍길동", @phone="010-1111-1111", @address="서울 특별시 강...
>> p.phone = "010-2222-2222"
=> "010-2222-2222"
>> p.info
=> "홍길동 / 010-2222-2222 / 서울 특별시 강동구"

이제는 원하는 대로 Person 객체의 phone 속성을 변경하기 위해 Set 메서드 호출을 p.phone = "010-2222-2222" 이렇게 작성 할 수 있게 되었다.
위의 info 메서드에서는 세 개의 문자열 인터폴레이션(#{...})을 사용했는데 이들 각각은 name, phone, address 메서드를 호출한 결과 값을 문자열 안에 삽입시킨다.
클래스에 정의된 메서드 안에서도 자신의 메서드를 호출할 수 있는데 이때는 '객체.' 을 생략하고 '메서드 명' 으로만 호출이  가능하다.

그런데 만약 메서드 이름과 동일한 이름의 변수를 메서드 안에서 이미 사용하고 있다면 그 변수를 참조하게 된다.

아래 코드 실행 결과를 보면 name 메서드를 호출한 게 아니라 메서드 안에서 정의한 name 변수의 값을 참조한 걸 알수 있다.

?> class Person
?>   def initialize(name)
?>     @name = name
?>   end
?>
?>   def name
?>     @name
?>   end
?>
?>   def info
?>     name = "??"
?>     return "My name is #{name}"
?>   end
>> end
=> :info
>>
>> p = Person.new("홍길동")
=> #<Person:0x000001c4225c2438 @name="홍길동">
>> p.info
=> "My name is ??"
>>

이러한 경우에 내가 원하는 게 name 변수의 값이 아니라 name 메서드를 호출하는 거라면 name() 또는 self.name 으로 작성해 주면 된다. 즉 괄호나 self. 을 적어 메서드 호출임을 명확히 알려 주는 것이다.

여기서 self 는 현재 메서드 호출의 대상인 객체 자신에 대한 참조 값을 갖는 내부 변수라고 생각하면 된다.

아래 그림을 보면 서로 다른 객체의 object_id 값이 서로 다르고 각 객체의 self 값을 통해 확인한 object_id 값 역시 서로 다른 것이 보일 것이다. 그리고 우리가 예상한 대로 같은 객체 끼리는 object_id 값을 바로 확인할 때와 self 를 통해 확인할 때가 서로 같음을 알 수 있다.

?> class Person
?>   def get_self
?>     return self
?>   end
>> end
=> :get_self
>>
>> p1 = Person.new
=> #<Person:0x0000022ec466e168>
>> p2 = Person.new
=> #<Person:0x0000022ec40bb630>
>> p1.object_id
=> 260
>> p1.get_self.object_id
=> 260
>> p2.object_id
=> 280
>> p2.get_self.object_id
=> 280
>>

프로그래밍 언어에는 키워드(keyword)라 부르는 특정한 용도로 사용하기 위해 미리 예약해 놓은 단어들이 있는데 self 도 바로 Ruby 에서 사용하는 여러 키워드(keyword)들 중 하나이고 지금까지 사용했던 if, elsif, else, for, while, class, def 등도 모두 Ruby 의 키워드이다. 키워드와 같은 이름으로 변수나 메서드 명을 짓는 것은 피해야 한다.

 

메서드를 작성할 때 return 을 생략할 수 있는데 return 을 생략하면 메서드 실행 시 마지막으로 평가된 표현식의 값을 결과로 돌려주게 된다. 그게 어떤 의미인지 먼저 다음 코드를 통해 확인해 보자.

?> def test_return(val)
?>   if val
?>     "Y"
?>   else
?>     "N"
?>   end
>> end
=> :test_return
>>
>> test_return(true)
=> "Y"
>> test_return(false)
=> "N"
>>

앞의 코드를 보면 메서드 안에 if / else 문 하나가 있는데 메서드 호출 시 전달한 인수 값에 따라 메서드 반환 값이 달라지는 걸 볼 수 있다. 인수를 true 로 해서 호출하게 되면 조건절이 참이되어 if 문의 내용이 실행되는데 if 문의 내용은 단순히 문자열 "Y" 이고 이 표현식을 평가하면 그냥 문자열 "Y" 가 되어 최종 반환값은 문자열 "Y" 가 된다. false 를 인수로 주면 당연히  조건절이 거짓이 되어 else 문이 실행되고 최종 반환값은 "N" 이 된다.

 

return 을 생략해도 코드를 이해하는 문제가 없다면 return 을 생략하는 것도 괜찮은 방법이다. 이 또한 코딩 스타일이므로  전체 프로그램 소스에서 일관되게 유지하는 것이 좋다.

 

끝으로 return 을 생략한 Person 클래스의 코드를 보며 이번 글을 마무리하자.

class Person
  def initialize(name, phone, address)
    @name = name
    @phone = phone
    @address = address
  end
  
  def name
    @name
  end
  
  def phone
    @phone
  end
  
  def address
    @address
  end  
  
  def phone=(phone)
    @phone = phone
  end
  
  def address=(address)
    @address = address
  end
  
  def info
    "#{name} / #{phone} / #{address}"
  end
end

 

See you again~~