객체 지향 프로그래밍
객체(Object)란 무엇인가?
객체는 데이터와 그 데이터에 관련된 함수(동작)를 포함하는 하나의 단위입니다. 예를 들어, 숫자 5는 하나의 객체입니다. 이 숫자 5는 데이터(숫자 값)를 가지고 있으며, 그 데이터에 적용할 수 있는 함수(예: 더하기, 빼기)가 있습니다.
R에서 사용되는 대부분의 데이터(벡터, 리스트, 데이터 프레임 등)는 객체라고 생각할 수 있습니다.
클래스(Class)란 무엇인가?
클래스는 객체의 청사진입니다. 즉, 특정 유형의 객체들이 어떻게 생기고, 어떤 동작을 할 수 있는지를 정의하는 틀입니다. 예를 들어, 사람(Person)이라는 클래스를 정의하면, 이 클래스를 기반으로 여러 사람 객체(예: John, Alice)를 만들 수 있습니다.
객체는 어떤 클래스로 만들어졌는지 "소속"을 가지고 있습니다. 예를 들어, John이라는 객체는 Person 클래스에 속합니다.
객체 지향 프로그래밍이란?
객체지향 프로그래밍(OOP)은 데이터를 객체로 묶고, 그 객체들이 서로 상호작용하도록 하는 프로그래밍 방식입니다.
OOP에서는 클래스와 객체를 중심으로 프로그램을 설계합니다.
R은 함수형 프로그래밍 언어이지만, 객체지향 프로그래밍을 지원하기 위해 S3, S4, R6과 같은 세 가지 주요 시스템을 제공합니다:
R에서의 객체 지향 프로그래밍 : S3 시스템
S3 시스템은 R에서 가장 기본적이고 단순한 객체지향 시스템입니다.
다른 객체지향 시스템에 비해 구조가 간단하여, 빠르고 유연하게 클래스를 정의하고 사용할 수 있습니다.
S3는 다른 언어의 객체지향 시스템보다 덜 엄격하며, 객체와 메소드의 정의가 매우 자유롭습니다.
S3 클래스 지정
S3 시스템에서 클래스는 특별히 정의된 구조체를 갖지 않습니다.
대신, 객체에 클래스 이름을 단순히 할당하는 방식으로 클래스를 지정할 수 있습니다.
예시
먼저, 리스트를 만들고 그 리스트에 "Person"이라는 클래스를 지정해보겠습니다.
# 리스트 생성
my_object <- list(name = "John", age = 25)
# 클래스 지정
class(my_object) <- "Person"
이 코드에서 my_object는 리스트입니다.
class(my_object) <- "Person"을 통해 이 리스트에 "Person"이라는 클래스를 지정했습니다.
이제 my_object는 "Person" 클래스의 객체로 간주됩니다.
S3 메소드(Method)
S3 시스템에서 메소드는 특정 클래스에 대해 특별한 동작을 수행하는 함수입니다.
함수 이름 뒤에 클래스 이름을 붙여 메소드를 정의할 수 있습니다.
예를 들어, "Person" 클래스에 대한 print 메소드를 정의해보겠습니다.
예시
우리는 print 함수를 "Person" 클래스에 맞게 커스터마이징할 수 있습니다.
# "Person" 클래스에 대한 print 메소드 정의
print.Person <- function(obj) {
cat("Name:", obj$name, "\\nAge:", obj$age, "\\n")
}
# 객체 출력
print(my_object)
여기서 print.Person 함수는 "Person" 클래스에 속한 객체를 출력할 때 사용되는 메소드입니다. 이 함수는 obj라는 인수를 받아, obj$name과 obj$age를 출력합니다.
위의 예시에서 my_object는 "Person" 클래스에 속해 있으므로, print(my_object)를 호출하면 print.Person 메소드가 자동으로 사용됩니다. 결과적으로 my_object의 name과 age 속성이 출력됩니다.
메소드 디스패치
S3 시스템에서는 메소드 디스패치가 중요한 역할을 합니다.
메소드 디스패치란, 특정 객체에 대해 호출된 함수가 해당 객체의 클래스에 맞는 메소드를 자동으로 선택하여 실행하는 것을 의미합니다.
예를 들어, R에서 print() 함수를 호출할 때, R은 객체의 클래스를 확인하고 그 클래스에 맞는 print 메소드를 찾습니다.
만약 객체의 클래스가 "Person"이라면, R은 print.Person 메소드를 호출합니다.
메소드의 추가 예시
다른 함수들도 특정 클래스에 대해 메소드를 정의할 수 있습니다. 예를 들어, "Person" 클래스에 대해 summary 메소드를 정의할 수 있습니다.
# "Person" 클래스에 대한 summary 메소드 정의
summary.Person <- function(obj) {
cat("This is a summary of a Person object:\\n")
cat("Name:", obj$name, "\\n")
cat("Age:", obj$age, "\\n")
}
# summary 함수 호출
summary(my_object)
이 메소드는 summary(my_object)를 호출할 때 실행되며, "Person" 객체의 요약 정보를 출력합니다.
S3의 유연성
S3 시스템은 매우 유연합니다. 클래스와 메소드를 간단히 정의하고 사용할 수 있으며, 객체에 여러 클래스를 지정할 수도 있습니다.
# 여러 클래스 지정
class(my_object) <- c("Employee", "Person")
# 여러 클래스에 대한 메소드 정의
print.Employee <- function(obj) {
cat("Employee ID:", obj$id, "\\n")
}
# Employee 클래스 메소드 호출
print(my_object)
이 코드에서 my_object는 "Employee"와 "Person" 두 클래스에 속합니다. R은 print(my_object)를 호출할 때, 먼저 "Employee" 클래스를 확인하고, 그에 맞는 메소드를 사용합니다.
S3의 한계
S3 시스템은 단순하고 유연하지만, 구조적으로 엄격하지 않기 때문에 복잡한 객체지향 프로그래밍에는 적합하지 않을 수 있습니다. 예를 들어, 클래스의 속성을 명확하게 정의하거나, 상속 구조를 복잡하게 설정하는 기능이 부족합니다. 이런 경우에는 S4나 R6 같은 더 강력한 객체지향 시스템을 사용하는 것이 좋습니다.
R에서의 객체 지향 프로그래밍 : S4 시스템
S4 시스템은 R에서 좀 더 구조화된 객체지향 프로그래밍을 지원하는 시스템입니다. S3 시스템보다 엄격하며, 클래스와 메소드를 명확하게 정의해야 합니다. S4는 더 복잡한 객체지향 설계를 필요로 하는 경우에 적합합니다.
S4 클래스 정의
S4 클래스는 setClass() 함수를 사용하여 정의합니다. S4 클래스는 슬롯(slot)이라고 불리는 필드(속성)를 가지고 있습니다. 슬롯은 클래스의 객체가 가지는 속성을 정의하는 역할을 합니다.
예시
setClass(
"Person",
slots = list(
name = "character", # 이름은 문자형(character)
age = "numeric" # 나이는 숫자형(numeric)
)
)
이 예시에서, Person 클래스는 두 개의 슬롯(name과 age)을 가지며, 각각의 데이터 타입도 지정합니다.
S4 객체 생성
S4 객체는 new() 함수를 사용하여 생성됩니다. new() 함수는 객체를 만들고, 클래스에 정의된 슬롯들을 초기화합니다.
예시
john <- new("Person", name = "John", age = 25)
여기서 john은 Person 클래스의 객체입니다. name 슬롯에는 "John"이, age 슬롯에는 25가 저장됩니다.
S4 메소드 정의
S4 시스템에서는 메소드도 명확하게 정의해야 합니다. 메소드는 특정 클래스와 연관된 함수로, setGeneric()과 setMethod() 함수를 사용해 정의합니다.
예시) print 메소드 정의
# print 함수의 일반적인 형태 정의
setGeneric("print", function(x) standardGeneric("print"))
# "Person" 클래스에 대한 print 메소드 정의
setMethod("print", "Person",
function(x) {
cat("Name:", x@name, "\\nAge:", x@age, "\\n")
}
)
- setGeneric("print", ...): R에 print라는 제네릭 함수를 정의합니다.
- setMethod("print", "Person", ...): "Person" 클래스에 대한 print 메소드를 정의합니다. 이 메소드에서 객체의 name과 age 슬롯을 출력합니다.
S4 슬롯 접근
S4 객체의 슬롯에 접근하려면 @ 연산자를 사용합니다. 이는 S3의 $ 연산자와 유사하지만, S4에서 슬롯에 접근할 때는 반드시 @를 사용해야 합니다.
예시)
john@name # "John"
john@age # 25
상속
S4 시스템에서는 클래스 상속을 지원합니다. 새로운 클래스는 기존 클래스의 슬롯과 메소드를 상속받을 수 있습니다.
예시)
# Employee 클래스는 Person 클래스를 상속받음
setClass(
"Employee",
contains = "Person",
slots = list(
employeeID = "character"
)
)
이 예시에서 Employee 클래스는 Person 클래스를 상속받고, 추가로 employeeID라는 슬롯을 가지고 있습니다.
R에서의 객체 지향 프로그래밍 : R6 시스템
R6는 캡슐화, 상속, 다형성 등의 객체지향 프로그래밍 개념을 지원하며, 특히 상태를 가지는 객체(즉, 내부 상태를 유지하면서 작동하는 객체)를 만들기에 적합합니다. R6 클래스는 함수형이 아닌 레퍼런스형 객체를 사용합니다. 이는 객체의 복사본이 아닌 참조(reference)를 사용하여 메모리에서 객체를 직접 수정할 수 있음을 의미합니다.
R6 클래스 정의
R6 클래스를 정의하려면 R6::R6Class() 함수를 사용합니다. 이 함수는 클래스를 정의하고, 그 클래스의 공용(public) 및 비공용(private) 멤버(속성 및 메소드)를 설정하는 데 사용됩니다.
예시)
library(R6)
Person <- R6Class("Person",
public = list(
name = NULL,
age = NULL,
initialize = function(name = NA, age = NA) {
self$name <- name
self$age <- age
},
print = function() {
cat("Name:", self$name, "\\nAge:", self$age, "\\n")
}
)
)
이 예시에서:
- Person <- R6Class("Person", ...)는 Person이라는 R6 클래스를 정의합니다.
- public 리스트는 공용 멤버들을 정의하는 데 사용됩니다. 이는 객체 외부에서 접근할 수 있는 속성과 메소드를 포함합니다.
- initialize()는 생성자 메소드로, 객체가 생성될 때 호출됩니다. 객체의 초기 속성 값을 설정하는 데 사용됩니다.
- self는 객체 자신을 가리키며, self$name, self$age와 같이 객체 내부에서 자신의 속성이나 메소드를 참조할 때 사용됩니다.
R6 객체 생성
R6 객체는 $new() 메소드를 사용하여 생성됩니다. 이 메소드는 객체를 만들고 initialize() 메소드를 호출하여 객체의 속성을 초기화합니다.
예시)
john <- Person$new(name = "John", age = 25)
john$print()
- john <- Person$new(name = "John", age = 25)는 Person 클래스의 새로운 객체 john을 생성합니다.
- john$print()는 john 객체의 print 메소드를 호출하여 객체의 이름과 나이를 출력합니다.
R6 속성 및 메소드
R6 클래스는 속성과 메소드를 가지며, 이를 통해 객체의 상태를 관리하고 행동을 정의할 수 있습니다.
- 속성: public 리스트에 정의된 변수는 객체의 속성입니다. R6 객체는 이러한 속성 값을 저장하고 유지합니다.
- 메소드: public 리스트에 정의된 함수는 객체의 메소드입니다. R6 객체는 이 메소드를 통해 행동할 수 있습니다.
예시) 속성 수정
john$name <- "Johnny"
john$age <- 30
john$print()
- 이 코드에서는 john 객체의 name과 age 속성을 직접 수정할 수 있습니다.
- R6 객체는 레퍼런스형이므로, 객체를 수정하면 복사본이 아니라 원본 객체가 변경됩니다.
R6 클래스 상속
R6 시스템은 상속을 지원하여, 하나의 클래스를 기반으로 새로운 클래스를 정의할 수 있습니다.
예시) "Employee" 클래스 정의
Employee <- R6Class("Employee",
inherit = Person,
public = list(
employeeID = NULL,
initialize = function(name = NA, age = NA, employeeID = NA) {
super$initialize(name, age)
self$employeeID <- employeeID
},
print = function() {
super$print()
cat("Employee ID:", self$employeeID, "\\n")
}
)
)
- inherit = Person: Employee 클래스는 Person 클래스를 상속받습니다.
- super: 상위 클래스의 메소드(예: initialize, print)를 호출할 때 사용됩니다.
- Employee$new(name = "Alice", age = 28, employeeID = "E123")로 객체를 생성할 수 있으며, print() 메소드는 상위 클래스의 출력에 추가로 employeeID를 출력합니다.
캡슐화
R6 시스템에서는 private 리스트를 사용하여 비공용 멤버를 정의할 수 있습니다. 이는 클래스 외부에서 접근할 수 없으며, 오직 클래스 내부에서만 사용할 수 있습니다.
예시) 캡슐화 사용
Person <- R6Class("Person",
private = list(
secret = NULL
),
public = list(
set_secret = function(value) {
private$secret <- value
},
get_secret = function() {
private$secret
}
)
)
john <- Person$new()
john$set_secret("This is a secret")
john$get_secret()
- private$secret: 비공용 멤버로, 클래스 외부에서는 접근할 수 없습니다.
- set_secret과 get_secret: 비공용 멤버에 접근하기 위한 공용 메소드입니다.
R에서의 클래스 선택
- S3: 간단하고 유연한 시스템이 필요한 경우. 대부분의 R 패키지와 호환성이 높습니다.
- S4: 더 엄격한 클래스 구조와 메소드 정의가 필요한 경우. 예를 들어, 여러 슬롯을 가진 복잡한 객체를 다룰 때 적합합니다.
- R6: 캡슐화와 메소드 상속이 필요한 경우. 특히, 객체가 자체 상태를 가지거나 지속적으로 변하는 프로그램에 유용합니다.
객체 지향 프로그래밍은 데이터와 동작을 하나의 객체로 묶어 더 효율적이고 유연한 코드를 작성할 수 있게 해줍니다. R은 S3, S4, R6 등 다양한 객체지향 시스템을 제공하여 사용자의 필요에 맞는 방법을 선택할 수 있게 합니다. 각 시스템은 유연성과 구조적 엄격성 측면에서 차이를 보이며, 상황에 따라 적합한 시스템을 사용하는 것이 중요합니다. 이를 통해 더 나은 코드 관리와 확장성을 확보할 수 있습니다.
감사합니다.
'프로그래밍 언어 > R' 카테고리의 다른 글
18. R을 활용한 기본 통계 2️⃣ (가설 검정 및 회귀 분석) (1) | 2024.09.16 |
---|---|
17. R을 활용한 기본 통계 1️⃣ (기초 통계 분석 및 확률 분포) (0) | 2024.09.13 |
15. R 함수를 이용한 데이터 시각화 3️⃣ (기타 시각화 기법 및 그래프 저장) (0) | 2024.09.07 |
14. R 함수를 이용한 데이터 시각화 2️⃣ (ggplot2 패키지) (0) | 2024.09.05 |
13. R 함수를 이용한 데이터 시각화 1️⃣ (기본 그래픽 시스템) (1) | 2024.09.03 |
데이터 분석을 공부하고 카페를 열심히 돌아다니는 이야기
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!