일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- spring
- MariaDB
- Swagger
- 서버
- server
- 쿼리
- 스프링오류
- 스웨거
- API
- Static
- 스프링RESTAPI
- 스프링시큐리티
- 인텔리제이오류
- application.yml
- restAPI
- SQL
- JWT
- Java
- 오버라이딩
- 스프링
- RDBMS
- 상태코드
- HTTP
- 어노테이션
- JPA
- SpringSecurity
- HTTP상태코드
- 시큐리티
- 자바
- 의존성주입
- Today
- Total
취뽀몽
[Java] 컴포지션 (Composition) 본문
자바에는 부모 클래스의 메소드를 재사용 할 수 있는 상속 기능을 제공한다.
상속은 코드의 재사용성이 뛰어나고 다형성을 구현하는 주요한 방법 중 하나이지만, 코드의 유연성이 떨어지고 클래스간 강한 의존도가 생기며 캡슐화를 위반한다는 큰 단점이 있다.
여기서 캡슐화는 행위와 속성을 하나의 묶음으로 가지고 있는 것을 말한다.
캡슐화의 효과에는 은닉화가 있다. 은닉화란 관련된 데이터와 동작을 하나로 묶고, 외부에는 필요한 정보만 노출시키는 것을 말한다.
즉, 데이터와 그 데이터를 다루는 메소드를 함께 묶어 외부에서의 접근을 제한하여 데이터의 무결성와 보안을 유지하는 것이다.
그렇다면 상속이 캡슐화를 위반하는 경우는 어떤 경우가 있을까?
public class Car {
public void move() {
System.out.println("달린다");
}
}
class SuperCar extends Car {
@Override
public void move() {
System.out.println("빠르게 달린다");
}
}
위와 같이 오버라이딩을 통해 메소드를 재선언하는 경우이다.
SuperCar는 Car의 move 메소드를 오버라이딩하여 "빠르게 달린다" 라는 새로운 기능을 제공한다.
이렇게 되는 경우에 Car 클래스는 캡슐화가 유지되지 않는다.
부모 Car 클래스의 move() 메소드를 가져다가 쓰기만 해야하는데, 오버라이딩을 통해 재정의하기 때문에 은닉화의 개념에서 캡슐화를 위반했다고 볼 수 있다.
상위 클래스의 구현이 하위 클래스에 노출 되는 것과 같기 때문이다.
이펙티브 자바 item 18장을 공부하다보면 '상속은 캡슐화를 깨트린다' 라는 문장을 여러 블로그에서 볼 수 있다.
https://stackoverflow.com/questions/40321009/inhertitance-breaks-encapsulation/40321880#40321880
Inhertitance breaks encapsulation
I have seen many articles where it says inheritance breaks encapsulation http://igstan.ro/posts/2011-09-09-how-inheritance-violates-encapsulation.html But I am unable to understand the concept be...
stackoverflow.com
위의 글이나, 이펙티브 자바 item 18장의 원문을 살펴보면 상속은 캡슐화를 깨는 것이 아닌 위반한다고 적혀있다.
상속 자체가 캡슐화를 깬다고 표현하는 것은 객체 지향에서 중요한 캡슐화를 깨버리는 상속을 아예 사용하지 말자는 강한 의미로 들려서 (개인적으로), 상속은 캡슐화를 위반할 가능성이 있다고 표현하는 것이 좋다고 생각한다.
상속은 위에 언급한 것처럼 여러 단점이 있기 때문에, 이러한 단점을 극복하기 위해 사용하는 것이 Composition 이다.
상속 : 하위 클래스가 상위 클래스의 특성을 재정의 한 것. is-a 관계이다.
컴포지션 : 기존 클래스가 새로운 클래스의 구성요소가 되는 것. has-a 관계이다.
Compositon은 기존 클래스를 확장하는 것이 아닌, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방법을 사용하여 기능을 확장시킨다.
새로운 클래스의 인스턴스 메소드들은 private 필드로 참조하는 기존 클래스의 대응하는 메소드(forwarding method)를 호출하여 그 결과를 반환하고, 이를 forwarding(전달) 이라고 한다.
Composition 방식을 사용하면 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향을 받지 않으며, 기존 클래스에 새로운 메소드가 추가되더라도 영향을 받지 않는다.
코드를 통해 예시를 알아보자.
public class Person {
private String name; // 이름
private int age; // 나이
public Person(String name, int age) { // 생성자
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Student extends Person { // 상속
private int studentId;
public Student(String name, int age, int studentId) {
super(name, age);
this.studentId = studentId;
}
public int getStudentId() {
return studentId;
}
}
위의 예시는 Student 클래스가 Person 클래스를 상속받아 확장하는 형태이다.
Student 클래스는 Person 클래스의 멤버 변수(이름, 나이)와 메소드를 상속받아 사용할 수 있다.
이 경우, Person 클래스의 모든 멤버를 상속받기 때문에 Person 클래스의 변경이 Student 클래스에 직접적인 영향을 미칠 수 있다.
즉 클래스간 강한 의존도가 생기는 것이므로, 유연성과 재사용성이 감소할 수 있다.
그렇다면 Composition을 적용해보자.
public class Person {
private String name; // 이름
private int age; // 나이
public Person(String name, int age) { // 생성자
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Student { // Student 클래스
private Person person; // Compostion 방식 사용
private int studentId;
public Student(String name, int age, int studentId) {
person = new Person(name, age);
this.studentId = studentId;
}
public String getName() {
return person.getName();
}
public int getAge() {
return person.getAge();
}
public int getStudentId() {
return studentId;
}
}
Student 클래스에 Person 클래스를 private로 참조하여 컴포지션 방식을 사용하고 있다.
생성자를 통해 Student 객체가 생성될 때, Person 객체를 생성하고 해당 객체를 person 멤버에 할당함으로써 Student 클래스가 Person 클래스의 기능을 활용할 수 있게 했다.
Student 클래스는 Person 클래스의 인스턴스를 멤버 변수로 가지고 있다.
Student 클래스는 Person 클래스의 기능을 활용하기 위해 내부적으로 Person 인스턴스를 생성하고, Person 클래스의 메서드를 호출하여 데이터를 가져온다.
Student 클래스는 Person 클래스의 기능을 선택적으로 사용할 수 있다.
즉, 필요한 경우에만 Person 클래스의 메소드를 호출하여 데이터를 가져온다고 할 수 있다.
Student 클래스는 Person 클래스와 느슨한 결합을 갖게 된다.
즉, Person 클래스의 내부 구현에 대해 상대적으로 독립적이며, Person 클래스의 변경이 Student 클래스에 큰 영향을 주지 않는다.
컴포지션을 사용하면 클래스 간 has-a 관계(Student has a Person 관계)를 형성하고, 코드를 재사용하면서 유연한 구조로 생성할 수 있다.
그럼 무조건 상속 대신 컴포지션을 써야 할까?
상속과 컴포지션은 특정 상황에 맞게 설계해서 사용해야 한다.
상속은 is-a 관계를 표현하기에 적합하기에 "사람은 동물이다" 와 같은 표현은 상속을 사용해야 하고,
"자동차는 엔진을 가지고 있다" 와 같은 표현은 컴포지션을 통해 표현할 수 있다.
따라서 관계를 정확히 표현하고자 할 때에는 적절한 방식을 선택해야 한다.
또한 컴포지션은 다형성을 직접적으로 지원하지 않기 때문에 다형성을 구현하고자 한다면 상속을 사용해야 한다.
클래스 상속은 기능 재사용에 있어서 강력한 도구임이 분명하지만, 문제점이 많기 때문에 추상 메소드나 인터페이스 상속을 통해 역할과 책임을 정제하는 용도로 사용하는 것이 좋다.
컴포지션을 오남용한다면 복잡성이 증가할 수 있고, 여러 클래스에서 유사한 코드가 중복될 수 있고 코드 길이가 길어지는 등 문제가 발생할 수 있다.
따라서, 무조건 컴포지션을 사용하기 보다는 적절한 상황에 맞춰 설계하는 것이 중요하다!
'java' 카테고리의 다른 글
[Java] Static 키워드 (0) | 2023.07.20 |
---|---|
[Java] Java Virtual Machine (0) | 2023.07.13 |
[Java] 에러(error)와 오류(exception) 차이 (0) | 2023.07.06 |
[Java] 동기와 비동기 방식 (0) | 2023.07.01 |
[JAVA] 불변 클래스 (0) | 2023.06.18 |